diff --git a/.gitmodules b/.gitmodules index 18bab2916..13f51c225 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,5 +16,5 @@ commit = 1edc2ae004974ebf053f4eba26b45469937b9381 [submodule "lib/eigenlayer-contracts"] path = lib/eigenlayer-contracts - url = https://github.com/yieldnest/eigenlayer-contracts - branch = v1.1.1-v4.9.0 + url = https://github.com/yieldnest/eigenlayer-contracts.git + branch = v1.3.0-v4.9.0 diff --git a/deployments/ynETH-17000.json b/deployments/ynETH-17000.json index b846dcf08..4fe39d6e1 100644 --- a/deployments/ynETH-17000.json +++ b/deployments/ynETH-17000.json @@ -11,12 +11,12 @@ "STAKING_NODES_OPERATOR": "0x9Dd8F69b62ddFd990241530F47dcEd0Dad7f7d39", "STAKING_NODE_CREATOR": "0x9Dd8F69b62ddFd990241530F47dcEd0Dad7f7d39", "VALIDATOR_MANAGER": "0x9Dd8F69b62ddFd990241530F47dcEd0Dad7f7d39", - "implementation-stakingNodesManager": "0xC9cf6740282617f3B392f900De0449E687Ce05e3", + "implementation-stakingNodesManager": "0x99a108a79419c62F2Ff384cE2441b435b918a252", "implementation-ynETH": "0x090D67d3C97712f6C17a037515CbB8502561EE57", "implementation-executionLayerReceiver": "0x1fbedf3773418f20b9dfeafcd9d263030eb0e42f", "implementation-consensusLayerReceiver": "0xe7acc0533c650ad0cc11f57f81c38fa19634b1d7", "implementation-rewardsDistributor": "0xb6ec4d9f71e437c672147c576f1c70ba5da8d159", - "implementation-withdrawalsProcessor": "0x36a38AA91947DbE6539e19512E6FF26576015Bb2", + "implementation-withdrawalsProcessor": "0xAbE3b5bF154d6441C63Dc34691E4F51Dbfac3bB0", "implementation-withdrawalQueueManager": "0x28732d8061D35B77dC2997926e0449dAdc3Ef3DD", "implementation-ynETHRedemptionAssetsVault": "0x7eBE6EAC9AD9B5e4D637c8f05191b273b009461a", "implementation-ynViewer": "0xBf1035e71E4a770a08462B20C95dE14763E1D186", @@ -38,5 +38,5 @@ "proxyAdmin-withdrawalQueueManager": "0x26425968beb4bD9c09b02C433996Cd05B1Fd4f07", "proxyAdmin-ynETHRedemptionAssetsVault": "0xd4d8E80d1c959CdC4601Cc6A0cc1EC18aBeB1888", "proxyAdmin-ynViewer": "0x3ffa0c3fba4adfe2b6e4d7e2f8e6e6324be5305b", - "stakingNodeImplementation": "0xf07861349Ed0cB4603590B47D2269768Ed6E2821" + "stakingNodeImplementation": "0xdd8e2B6B4D71E0E1b077dFcaea3Ca85fF31803a6" } \ No newline at end of file diff --git a/lib/eigenlayer-contracts b/lib/eigenlayer-contracts index 93a6f3ebe..174b64f2c 160000 --- a/lib/eigenlayer-contracts +++ b/lib/eigenlayer-contracts @@ -1 +1 @@ -Subproject commit 93a6f3ebe86406535c4060dea6bc390d4ed85b52 +Subproject commit 174b64f2c3ce50411acc2fce4b68af5f6683b3f0 diff --git a/lib/forge-std b/lib/forge-std index 4f57c59f0..3b20d60d1 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 4f57c59f066a03d13de8c65bb34fca8247f5fcb2 +Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 1edc2ae00..acd4ff74d 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 1edc2ae004974ebf053f4eba26b45469937b9381 +Subproject commit acd4ff74de833399287ed6b31b4debf6b2b35527 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index 22489db15..3d5fa5c24 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 22489db15621b9a42ebddb1facade6962034e9b9 +Subproject commit 3d5fa5c24c411112bab47bec25cfa9ad0af0e6e8 diff --git a/script/ContractAddresses.sol b/script/ContractAddresses.sol index f75d8c9a2..17f8629b6 100644 --- a/script/ContractAddresses.sol +++ b/script/ContractAddresses.sol @@ -35,6 +35,7 @@ contract ContractAddresses { address STRATEGY_MANAGER_ADDRESS; address STRATEGY_MANAGER_PAUSER_ADDRESS; address REWARDS_COORDINATOR_ADDRESS; + address ALLOCATION_MANAGER_ADDRESS; } struct LSDAddresses { @@ -97,7 +98,8 @@ contract ContractAddresses { DELEGATION_PAUSER_ADDRESS: 0x369e6F597e22EaB55fFb173C6d9cD234BD699111, // TODO: remove this if unused STRATEGY_MANAGER_ADDRESS: 0x858646372CC42E1A627fcE94aa7A7033e7CF075A, STRATEGY_MANAGER_PAUSER_ADDRESS: 0xBE1685C81aA44FF9FB319dD389addd9374383e90, - REWARDS_COORDINATOR_ADDRESS: 0x7750d328b314EfFa365A0402CcfD489B80B0adda + REWARDS_COORDINATOR_ADDRESS: 0x7750d328b314EfFa365A0402CcfD489B80B0adda, + ALLOCATION_MANAGER_ADDRESS: 0x0000000000000000000000000000000000000000 // TODO: Update this with correct address after mainnet deployment }), lsd: LSDAddresses({ SFRXETH_ADDRESS: 0xac3E018457B222d93114458476f3E3416Abbe38F, @@ -158,7 +160,8 @@ contract ContractAddresses { DELEGATION_PAUSER_ADDRESS: 0x28Ade60640fdBDb2609D8d8734D1b5cBeFc0C348, // Placeholder address, replaced with address(1) for holesky STRATEGY_MANAGER_ADDRESS: 0xdfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6, // Placeholder address, replaced with address(1) for holesky STRATEGY_MANAGER_PAUSER_ADDRESS: 0x28Ade60640fdBDb2609D8d8734D1b5cBeFc0C348, - REWARDS_COORDINATOR_ADDRESS: 0xAcc1fb458a1317E886dB376Fc8141540537E68fE + REWARDS_COORDINATOR_ADDRESS: 0xAcc1fb458a1317E886dB376Fc8141540537E68fE, + ALLOCATION_MANAGER_ADDRESS: 0x78469728304326CBc65f8f95FA756B0B73164462 }), lsd: LSDAddresses({ SFRXETH_ADDRESS: 0xa63f56985F9C7F3bc9fFc5685535649e0C1a55f3, diff --git a/script/delegation/DelegateTransactionDataBuilder.sol b/script/delegation/DelegateTransactionDataBuilder.sol index 7d71f730f..670d27e2d 100644 --- a/script/delegation/DelegateTransactionDataBuilder.sol +++ b/script/delegation/DelegateTransactionDataBuilder.sol @@ -9,7 +9,7 @@ import {PooledDepositsVault} from "src/PooledDepositsVault.sol"; // Renamed from import {ActorAddresses} from "script/Actors.sol"; import {console} from "lib/forge-std/src/console.sol"; import {IStakingNode} from "src/interfaces/IStakingNode.sol"; -import {ISignatureUtils} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {ISignatureUtilsMixinTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtilsMixin.sol"; import {IStakingNodesManager} from "src/interfaces/IStakingNodesManager.sol"; import {ContractAddresses} from "script/ContractAddresses.sol"; @@ -52,7 +52,7 @@ contract DelegateTransactionBuilder is BaseScript { bytes memory delegateTxData = abi.encodeWithSelector( IStakingNode.delegate.selector, currentOperator, - ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), + ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); console.log("Node address:", stakingNodes[i]); diff --git a/script/ynETH/DeployStakingNode.s.sol b/script/ynETH/DeployStakingNode.s.sol index 1b9190862..ada93da43 100644 --- a/script/ynETH/DeployStakingNode.s.sol +++ b/script/ynETH/DeployStakingNode.s.sol @@ -33,3 +33,8 @@ contract DeployStakingNode is BaseYnETHScript { // Deployer Public Key: 0x445b64828683ae4B6D5f0542f9E97707d631A847 // Staking Node Implementation: 0x79388c8cc46069c0e3f285f053692D7397e65e1e // Deployment JSON file written successfully: /Users/parth/Desktop/coding/yieldnest/prod-code-repos/yieldnest-protocol-private/deployments/ynETH-1.json + + +// HOLESKY DEPLOYMENT +// Staking Node Implementation: 0xdd8e2B6B4D71E0E1b077dFcaea3Ca85fF31803a6 + diff --git a/script/ynETH/DeployStakingNodesManager.s.sol b/script/ynETH/DeployStakingNodesManager.s.sol index c812cbbc0..3213bb313 100644 --- a/script/ynETH/DeployStakingNodesManager.s.sol +++ b/script/ynETH/DeployStakingNodesManager.s.sol @@ -33,3 +33,6 @@ contract DeployStakingNodesManager is BaseYnETHScript { // Deployer Public Key: 0x445b64828683ae4B6D5f0542f9E97707d631A847 // StakingNodesManager Implementation: 0x8E0b49B4A4384D812Bc6F55fA6412547524D41Ab // Deployment JSON file written successfully: /Users/parth/Desktop/coding/yieldnest/prod-code-repos/yieldnest-protocol-private/deployments/ynETH-1.json + +// HOLESKY DEPLOYMENT +// StakingNodesManager Implementation: 0x99a108a79419c62F2Ff384cE2441b435b918a252 diff --git a/script/ynETH/DeployWithdrawalsProcessor.s.sol b/script/ynETH/DeployWithdrawalsProcessor.s.sol new file mode 100644 index 000000000..47e43592e --- /dev/null +++ b/script/ynETH/DeployWithdrawalsProcessor.s.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {BaseYnETHScript} from "script/ynETH/BaseYnETHScript.s.sol"; +import {WithdrawalsProcessor} from "src/WithdrawalsProcessor.sol"; +import {console} from "lib/forge-std/src/console.sol"; + +contract DeployWithdrawalsProcessor is BaseYnETHScript { + + function run() external { + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + address publicKey = vm.addr(deployerPrivateKey); + console.log("Deployer Public Key:", publicKey); + + address _broadcaster = vm.addr(deployerPrivateKey); + + vm.startBroadcast(deployerPrivateKey); + + console.log("Current Block Number:", block.number); + console.log("Current Chain ID:", block.chainid); + + WithdrawalsProcessor withdrawalsProcessorImplementation = new WithdrawalsProcessor(); + + console.log("Withdrawals Processor Implementation:", address(withdrawalsProcessorImplementation)); + + vm.stopBroadcast(); + + } +} + +// HOLESKY DEPLOYMENT +// Withdrawals Processor Implementation: 0xAbE3b5bF154d6441C63Dc34691E4F51Dbfac3bB0 diff --git a/script/ynETH/VerifyYnETH.s.sol b/script/ynETH/VerifyYnETH.s.sol index ff8901617..923729f77 100644 --- a/script/ynETH/VerifyYnETH.s.sol +++ b/script/ynETH/VerifyYnETH.s.sol @@ -639,12 +639,15 @@ contract Verify is BaseYnETHScript { function veryifySanityChecks() internal view { // Check that previewDeposit of 1 ETH is less than 1 ether uint256 previewDepositResult = deployment.ynETH.previewDeposit(1 ether); + console.log("previewDepositResult", previewDepositResult); require(previewDepositResult < 1 ether, "previewDeposit of 1 ETH should be less than 1 ether"); console.log("\u2705 previewDeposit of 1 ETH is less than 1 ether"); // Check that totalSupply is less than totalAssets uint256 totalSupply = deployment.ynETH.totalSupply(); uint256 totalAssets = deployment.ynETH.totalAssets(); + console.log("totalSupply", totalSupply); + console.log("totalAssets", totalAssets); require(totalSupply < totalAssets, "totalSupply should be less than totalAssets"); console.log("\u2705 totalSupply is less than totalAssets"); @@ -665,7 +668,8 @@ contract Verify is BaseYnETHScript { for (uint256 i = 0; i < stakingNodes.length; i++) { stakingNodesBalance += stakingNodes[i].getETHBalance(); console.log(string.concat("Balance for node ", vm.toString(i), ": ", vm.toString(stakingNodes[i].getETHBalance()), " wei (", vm.toString(stakingNodes[i].getETHBalance() / 1e18), " ETH)")); - + console.log(string.concat("Pre ELIP002 queued shares for node ", vm.toString(i), ": ", vm.toString(stakingNodes[i].preELIP002QueuedSharesAmount()), " wei (", vm.toString(stakingNodes[i].preELIP002QueuedSharesAmount() / 1e18), " ETH)")); + console.log(string.concat("Queued shares for node ", vm.toString(i), ": ", vm.toString(stakingNodes[i].queuedSharesAmount()), " wei (", vm.toString(stakingNodes[i].queuedSharesAmount() / 1e18), " ETH)")); } uint256 totalCalculatedBalance = ynETHBalance + redemptionVaultBalance + stakingNodesBalance; diff --git a/src/HoleskyStakingNodesManager.sol b/src/HoleskyStakingNodesManager.sol new file mode 100644 index 000000000..78f76913f --- /dev/null +++ b/src/HoleskyStakingNodesManager.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {StakingNodesManager} from "./StakingNodesManager.sol"; +import {IRewardsCoordinator} from "lib/eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol"; + +contract HoleskyStakingNodesManager is StakingNodesManager { + + function initializeV3( + IRewardsCoordinator _rewardsCoordinator + ) external override reinitializer(3) { + if (address(_rewardsCoordinator) == address(0)) revert ZeroAddress(); + rewardsCoordinator = _rewardsCoordinator; + } + +} \ No newline at end of file diff --git a/src/StakingNode.sol b/src/StakingNode.sol index ee9ea95be..2c53a6f2c 100644 --- a/src/StakingNode.sol +++ b/src/StakingNode.sol @@ -9,13 +9,14 @@ import {IDelegationManager, IDelegationManagerTypes} from "lib/eigenlayer-contra import {IEigenPodManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPodManager.sol"; import {IEigenPod} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPod.sol"; import {IRewardsCoordinator} from "lib/eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol"; -import {ISignatureUtils} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {ISignatureUtilsMixinTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtilsMixin.sol"; import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; import {IBeacon} from "lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; import {IEigenPodManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPodManager.sol"; import {IStakingNodesManager} from "src/interfaces/IStakingNodesManager.sol"; import {IStakingNode} from "src/interfaces/IStakingNode.sol"; import {IERC20} from "lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol"; +import {SlashingLib} from "lib/eigenlayer-contracts/src/contracts/libraries/SlashingLib.sol"; import {IERC20 as IERC20V4} from "lib/eigenlayer-contracts/lib/openzeppelin-contracts-v4.9.0/contracts/interfaces/IERC20.sol"; import {DEFAULT_VALIDATOR_STAKE} from "src/Constants.sol"; @@ -47,7 +48,7 @@ interface StakingNodeEvents { IDelegationManager.Withdrawal[] withdrawals, uint256 totalWithdrawalAmount, uint256 actualWithdrawalAmount ); event ClaimerSet(address indexed claimer); - + event QueuedSharesSynced(uint256 queuedSharesAmount); } /** @@ -58,6 +59,7 @@ interface StakingNodeEvents { contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradeable { using BeaconChainProofs for *; + using SlashingLib for *; //-------------------------------------------------------------------------------------- //---------------------------------- ERRORS ------------------------------------------ @@ -74,6 +76,7 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea error TransferFailed(); error InsufficientWithdrawnETH(uint256 amount, uint256 withdrawnETH); error NotStakingNodesWithdrawer(); + error NotSyncedAfterSlashing(); error NotSynchronized(); error AlreadySynchronized(); @@ -115,14 +118,28 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea /** * @dev Amount of shares queued for withdrawal (no longer active in staking). 1 share == 1 ETH. - * Increases when calling queueWithdrawals, and decreases when calling completeQueuedWithdrawals. + * Increases when calling queueWithdrawals, and decreases when calling completeQueuedWithdrawals and on slashing */ uint256 public queuedSharesAmount; + /** + * @dev The address of the operator that this staking node is delegated to. + */ address public delegatedTo; /** - * @dev Allows only a whitelisted address to configure the contract + * @dev The amount of shares queued for withdrawal that were queued before the eigenlayer ELIP-002 upgrade + */ + uint256 public preELIP002QueuedSharesAmount; + + /** + * @dev Maps a withdrawal root to the amount of shares that can be withdrawn and whether the withdrawal root is post ELIP-002 slashing upgrade. + * This is used to track the amount of withdrawable shares that are queued for withdrawal. + */ + mapping(bytes32 withdrawalRoot => WithdrawableShareInfo withdrawableShareInfo) public withdrawableShareInfo; + + /** + * @dev Allows only a whitelisted address to configure the contract */ modifier onlyOperator() { if (!stakingNodesManager.isStakingNodesOperator(msg.sender)) revert NotStakingNodesOperator(); @@ -174,7 +191,12 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea } function initializeV3() external onlyStakingNodesManager reinitializer(3) { - delegatedTo = IDelegationManager(address(stakingNodesManager.delegationManager())).delegatedTo(address(this)); + delegatedTo = stakingNodesManager.delegationManager().delegatedTo(address(this)); + } + + function initializeV4() external onlyStakingNodesManager reinitializer(4) { + preELIP002QueuedSharesAmount = queuedSharesAmount; + queuedSharesAmount = 0; } //-------------------------------------------------------------------------------------- @@ -267,10 +289,10 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea */ function delegate( address operator, - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry, + ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt ) public override onlyDelegator onlyWhenSynchronized { - IDelegationManager delegationManager = IDelegationManager(address(stakingNodesManager.delegationManager())); + IDelegationManager delegationManager = stakingNodesManager.delegationManager(); delegationManager.delegateTo(operator, approverSignatureAndExpiry, approverSalt); delegatedTo = operator; @@ -284,22 +306,22 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea * It emits an `Undelegated` event with the address of the operator from whom the delegation is being removed. */ function undelegate() public onlyDelegator onlyWhenSynchronized returns (bytes32[] memory withdrawalRoots) { - IDelegationManager delegationManager = IDelegationManager(address(stakingNodesManager.delegationManager())); + IDelegationManager delegationManager = stakingNodesManager.delegationManager(); address operator = delegationManager.delegatedTo(address(this)); - // Get current shares before undelegating - int256 shares = stakingNodesManager.eigenPodManager().podOwnerDepositShares(address(this)); + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = beaconChainETHStrategy; + + (uint256[] memory withdrawableShares, ) = delegationManager.getWithdrawableShares(address(this), strategies); + withdrawalRoots = delegationManager.undelegate(address(this)); - if (shares > 0) { - // Adjust queuedSharesAmount by shares - queuedSharesAmount += uint256(shares); - } + syncQueuedShares(); delegatedTo = address(0); - emit Undelegated(operator, shares); + emit Undelegated(operator, int256(withdrawableShares[0])); } /** @@ -315,20 +337,49 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea emit ClaimerSet(claimer); } + /** + * @notice Syncs the queuedSharesAmount with the actual withdrawable shares queued for withdrawal. + * @dev This is generally used when slashing is done on this staking node or operator is slashed + */ + function syncQueuedShares() public { + + IDelegationManager delegationManager = stakingNodesManager.delegationManager(); + // This is used to track the amount of withdrawable shares that are queued for withdrawal. + uint256 queuedWithdrawableShares = 0; + + (IDelegationManagerTypes.Withdrawal[] memory withdrawals, uint256[][] memory shares) = delegationManager.getQueuedWithdrawals(address(this)); + for(uint256 i = 0; i < withdrawals.length; i++) { + bytes32 withdrawalRoot = delegationManager.calculateWithdrawalRoot(withdrawals[i]); + uint256 withdrawableShares = shares[i][0]; + withdrawableShareInfo[withdrawalRoot] = WithdrawableShareInfo({ + withdrawableShares: withdrawableShares, + postELIP002SlashingUpgrade: true + }); + queuedWithdrawableShares += withdrawableShares; + } + + // updating queuedSharesAmount due to sync + queuedSharesAmount = queuedWithdrawableShares; + + emit QueuedSharesSynced(queuedWithdrawableShares + preELIP002QueuedSharesAmount); + } + //-------------------------------------------------------------------------------------- //---------------------------------- WITHDRAWALS ------------------------------------- //-------------------------------------------------------------------------------------- /** * @dev Queues a validator Principal withdrawal for processing. DelegationManager calls EigenPodManager.decreasesShares - * which decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. - * @param sharesAmount The amount of shares to be queued for withdrawals. + * which decreases the `podOwner`'s shares by `depositSharesAmount`, down to a minimum of zero. The actual shares withdrawable + * will be less than `depositSharesAmount` depending on the slashing factor + * @param depositSharesAmount The amount of deposit shares to be queued for withdrawals. * @return fullWithdrawalRoots An array of keccak256 hashes of each withdrawal created. */ function queueWithdrawals( - uint256 sharesAmount - ) external onlyStakingNodesWithdrawer onlyWhenSynchronized returns (bytes32[] memory fullWithdrawalRoots) { - IDelegationManager delegationManager = IDelegationManager(address(stakingNodesManager.delegationManager())); + uint256 depositSharesAmount + ) external onlyStakingNodesWithdrawer onlyWhenSynchronized returns (bytes32[] memory) { + + IDelegationManager delegationManager = stakingNodesManager.delegationManager(); IDelegationManagerTypes.QueuedWithdrawalParams[] memory params = new IDelegationManagerTypes.QueuedWithdrawalParams[](1); IStrategy[] memory strategies = new IStrategy[](1); @@ -337,21 +388,29 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea uint256[] memory shares = new uint256[](1); strategies[0] = beaconChainETHStrategy; - shares[0] = sharesAmount; + shares[0] = depositSharesAmount; // The delegationManager requires the withdrawer == msg.sender (the StakingNode in this case). params[0] = IDelegationManagerTypes.QueuedWithdrawalParams({ strategies: strategies, depositShares: shares, __deprecated_withdrawer: address(this) }); + uint256[] memory withdrawableShares; + // fullWithdrawalRoots will be of length 1 because there is only one strategy + bytes32[] memory fullWithdrawalRoots = delegationManager.queueWithdrawals(params); + + (, withdrawableShares) = delegationManager.getQueuedWithdrawal(fullWithdrawalRoots[0]); + + // After running queueWithdrawals, eigenPodManager.getWithdrawableShares(address(this)) decreases by `withdrawableShares`. + // Therefore queuedSharesAmount increase by `withdrawableShares`. + queuedSharesAmount += withdrawableShares[0]; + withdrawableShareInfo[fullWithdrawalRoots[0]] = WithdrawableShareInfo({ + withdrawableShares: withdrawableShares[0], + postELIP002SlashingUpgrade: true + }); + emit QueuedWithdrawals(depositSharesAmount, fullWithdrawalRoots); - fullWithdrawalRoots = delegationManager.queueWithdrawals(params); - - // After running queueWithdrawals, eigenPodManager.podOwnerShares(address(this)) decreases by `sharesAmount`. - // Therefore queuedSharesAmount increase by `sharesAmount`. - - queuedSharesAmount += sharesAmount; - emit QueuedWithdrawals(sharesAmount, fullWithdrawalRoots); + return fullWithdrawalRoots; } /** @@ -361,18 +420,16 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea * number of blocks have passed since withdrawal was queued. * @param withdrawals The Withdrawals to complete. This withdrawalRoot (keccak hash of the Withdrawal) must match the * the withdrawal created as part of the queueWithdrawals call. - * @param middlewareTimesIndexes The middlewareTimesIndex parameter has to do - * with the Slasher, which currently does nothing. As of M2, this parameter - * has no bearing on anything and can be ignored */ function completeQueuedWithdrawals( - IDelegationManager.Withdrawal[] memory withdrawals, - uint256[] memory middlewareTimesIndexes + IDelegationManager.Withdrawal[] calldata withdrawals ) external onlyStakingNodesWithdrawer onlyWhenSynchronized { - uint256 totalWithdrawalAmount = 0; + + IDelegationManager delegationManager = stakingNodesManager.delegationManager(); bool[] memory receiveAsTokens = new bool[](withdrawals.length); IERC20V4[][] memory tokens = new IERC20V4[][](withdrawals.length); + uint256 totalWithdrawableShares = 0; for (uint256 i = 0; i < withdrawals.length; i++) { if (withdrawals[i].scaledShares.length != 1 || withdrawals[i].strategies.length != 1 || withdrawals[i].strategies[0] != beaconChainETHStrategy) { revert InvalidWithdrawal(); @@ -387,12 +444,12 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea // tokens array must match length of the withdrawals[i].strategies // but does not need actual values in the case of the beaconChainETHStrategy tokens[i] = new IERC20V4[](1); + + bytes32 withdrawalRoot = delegationManager.calculateWithdrawalRoot(withdrawals[i]); - totalWithdrawalAmount += withdrawals[i].scaledShares[0]; + totalWithdrawableShares += _decreaseQueuedSharesOnCompleteQueuedWithdrawal(delegationManager, withdrawals[i]); } - IDelegationManager delegationManager = IDelegationManager(address(stakingNodesManager.delegationManager())); - uint256 initialETHBalance = address(this).balance; // NOTE: completeQueuedWithdrawals can only be called by withdrawal.withdrawer for each withdrawal @@ -406,27 +463,30 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea uint256 finalETHBalance = address(this).balance; uint256 actualWithdrawalAmount = finalETHBalance - initialETHBalance; - // NOTE: actualWithdrawalAmount may be < totalWithdrawalAmount in case of slashing ! - - // Shares are no longer queued; decrease what was queued for withdrawal - queuedSharesAmount -= totalWithdrawalAmount; + if (actualWithdrawalAmount != totalWithdrawableShares) { + revert NotSyncedAfterSlashing(); + } // Withdraw validator principal resides in the StakingNode until StakingNodesManager retrieves it. withdrawnETH += actualWithdrawalAmount; - emit CompletedQueuedWithdrawals(withdrawals, totalWithdrawalAmount, actualWithdrawalAmount); + emit CompletedQueuedWithdrawals(withdrawals, totalWithdrawableShares, actualWithdrawalAmount); } /** * @notice Completes queued withdrawals with receiveAsTokens set to false + * @dev Call updateTotalETHStaked after this function * @param withdrawals Array of withdrawals to complete - * @param middlewareTimesIndexes Array of middleware times indexes */ function completeQueuedWithdrawalsAsShares( - IDelegationManager.Withdrawal[] calldata withdrawals, - uint256[] calldata middlewareTimesIndexes + IDelegationManager.Withdrawal[] calldata withdrawals ) external onlyDelegator onlyWhenSynchronized { - uint256 totalWithdrawalAmount = 0; + + syncQueuedShares(); + + uint256 totalWithdrawableShares = 0; + + IDelegationManager delegationManager = stakingNodesManager.delegationManager(); // Create empty tokens array since we're not receiving as tokens IERC20V4[][] memory tokens = new IERC20V4[][](withdrawals.length); @@ -439,23 +499,45 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea } tokens[i] = new IERC20V4[](1); receiveAsTokens[i] = false; - totalWithdrawalAmount += withdrawals[i].scaledShares[0]; + + totalWithdrawableShares += _decreaseQueuedSharesOnCompleteQueuedWithdrawal(delegationManager, withdrawals[i]); } - IDelegationManager delegationManager = IDelegationManager(address(stakingNodesManager.delegationManager())); - // Complete withdrawals with receiveAsTokens = false delegationManager.completeQueuedWithdrawals( withdrawals, tokens, - // middlewareTimesIndexes, receiveAsTokens ); - // Decrease queued shares amount - queuedSharesAmount -= totalWithdrawalAmount; + emit CompletedQueuedWithdrawals(withdrawals, totalWithdrawableShares, 0); + } + + /** + * @notice Decreases the queued shares on complete queued withdrawals and returns the total withdrawable shares + * @param delegationManager The delegation manager + * @param withdrawal The withdrawal struct + * @return totalWithdrawableShare The total withdrawable shares for the withdrawal struct + */ + function _decreaseQueuedSharesOnCompleteQueuedWithdrawal( + IDelegationManager delegationManager, + IDelegationManager.Withdrawal calldata withdrawal + ) internal returns (uint256 totalWithdrawableShare) { - emit CompletedQueuedWithdrawals(withdrawals, totalWithdrawalAmount, 0); + bytes32 withdrawalRoot = delegationManager.calculateWithdrawalRoot(withdrawal); + WithdrawableShareInfo storage withdrawableShareInfo = withdrawableShareInfo[withdrawalRoot]; + + if (withdrawableShareInfo.postELIP002SlashingUpgrade) { + // If the withdrawal root queued after ELIP-002 slashing upgrade, we need to subtract the shares from queuedSharesAmount + // and set the withdrawableShares to 0 for the withdrawal root + totalWithdrawableShare = withdrawableShareInfo.withdrawableShares; + queuedSharesAmount -= totalWithdrawableShare; + withdrawableShareInfo.withdrawableShares = 0; + } else { + // If the withdrawal root queued was before ELIP-002 slashing upgrade, we need to subtract the shares from preELIP002QueuedSharesAmount + totalWithdrawableShare = withdrawal.scaledShares[0]; + preELIP002QueuedSharesAmount -= totalWithdrawableShare; + } } //-------------------------------------------------------------------------------------- @@ -468,50 +550,21 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea * @return True if the delegation state is synced, false otherwise. */ function isSynchronized() public view returns (bool) { - IDelegationManager delegationManager = IDelegationManager(address(stakingNodesManager.delegationManager())); + IDelegationManager delegationManager = stakingNodesManager.delegationManager(); return delegatedTo == delegationManager.delegatedTo(address(this)); } - function synchronize(uint256 queuedShares, uint32 undelegateBlockNumber) public onlyDelegator { - if (isSynchronized()) { - revert AlreadySynchronized(); - } - - IDelegationManager delegationManager = IDelegationManager(address(stakingNodesManager.delegationManager())); - - IStrategy[] memory strategies = new IStrategy[](1); - uint256[] memory shares = new uint256[](1); - strategies[0] = beaconChainETHStrategy; - shares[0] = queuedShares; - - address thisNode = address(this); - - // We can assume that the Withdrawal queued by the undelegate() call made by the operator - // is the LAST queued withdrawal since to call queueWithdrawals you require isSynchronized() == true. - IDelegationManagerTypes.Withdrawal memory withdrawal = IDelegationManagerTypes.Withdrawal({ - staker: thisNode, - delegatedTo: delegatedTo, - withdrawer: thisNode, - nonce: delegationManager.cumulativeWithdrawalsQueued(thisNode) - 1, // must be the last withdrawal - startBlock: undelegateBlockNumber, - strategies: strategies, - scaledShares: shares - }); - - // IMPORTANT: withdrawalRoot is not spoofable because nonce is a strictly increasing value that - // gets incremented for each new withdrawal. - // This function only works with this pre-condition - bytes32 withdrawalRoot = delegationManager.calculateWithdrawalRoot(withdrawal); - - // Withdrawal MUST exist and must be the last - if (!IDelegationManagerExtended(address(delegationManager)).pendingWithdrawals(withdrawalRoot)) { - revert WithdrawalMismatch(); - } + /** + * @notice Synchronizes the StakingNode's delegation state with the DelegationManager and queued shares. + * @dev This function should be called after operator undelegate to this StakingNode or there is slashing event. + */ + function synchronize() public onlyDelegator { - // queue shares - queuedSharesAmount += queuedShares; + syncQueuedShares(); - delegatedTo = address(0); + IDelegationManager delegationManager = stakingNodesManager.delegationManager(); + delegatedTo = delegationManager.delegatedTo(address(this)); + stakingNodesManager.updateTotalETHStaked(); } //-------------------------------------------------------------------------------------- @@ -549,21 +602,32 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea (bool success,) = address(stakingNodesManager).call{value: amount}(""); if (!success) revert TransferFailed(); } - + + /** + * @notice Calculates the total ETH balance of the StakingNode + * @dev This function aggregates all forms of ETH associated with this StakingNode: + * 1. withdrawnETH - ETH that has been withdrawn from Eigenlayer and is held by this contract + * 2. unverifiedStakedETH - ETH staked with validators but not yet verified with withdrawal credentials + * 3. queuedSharesAmount - Shares queued for withdrawal after ELIP-002 upgrade (1 share = 1 ETH) + * 4. preELIP002QueuedSharesAmount - Shares queued before the ELIP-002 upgrade (1 share = 1 ETH) + * 5. Active withdrawable shares in Eigenlayer - Representing staked ETH that can be withdrawn (1 share = 1 ETH) + * @return The total ETH balance in wei, or 0 if the calculation results in a negative value + */ function getETHBalance() public view returns (uint256) { - IEigenPodManager eigenPodManager = IEigenPodManager(IStakingNodesManager(stakingNodesManager).eigenPodManager()); + IDelegationManager delegationManager = stakingNodesManager.delegationManager(); + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = beaconChainETHStrategy; + (uint256[] memory withdrawableShares, ) = delegationManager.getWithdrawableShares(address(this), strategies); + uint256 beaconChainETHStrategyWithdrawableShares = withdrawableShares[0]; + // Compute the total ETH balance of the StakingNode - // This includes: - // 1. withdrawnETH: ETH that has been withdrawn from Eigenlayer and is held by this StakingNode - // 2. unverifiedStakedETH: ETH staked with validators but not yet verified - // 3. queuedSharesAmount: Shares queued for withdrawal (1 share = 1 ETH) - // 4. podOwnerDepositShares: Active shares in Eigenlayer, representing staked ETH - int256 totalETHBalance = int256(withdrawnETH + unverifiedStakedETH + queuedSharesAmount) - + eigenPodManager.podOwnerDepositShares(address(this)); - - if (totalETHBalance < 0) return 0; + int256 totalETHBalance = + int256(withdrawnETH + unverifiedStakedETH + queuedSharesAmount + preELIP002QueuedSharesAmount + beaconChainETHStrategyWithdrawableShares); + if (totalETHBalance < 0) { + return 0; + } return uint256(totalETHBalance); } diff --git a/src/StakingNodesManager.sol b/src/StakingNodesManager.sol index d8e508aae..18ae16eb4 100644 --- a/src/StakingNodesManager.sol +++ b/src/StakingNodesManager.sol @@ -70,7 +70,7 @@ contract StakingNodesManager is error InvalidRewardsType(RewardsType rewardsType); error ValidatorUnused(bytes publicKey); error ValidatorNotWithdrawn(bytes publicKey, IEigenPod.VALIDATOR_STATUS status); - error NodeNotSynchronized(); + error NodeNotSynchronized(address nodeAddress); //-------------------------------------------------------------------------------------- //---------------------------------- ROLES ------------------------------------------- @@ -247,9 +247,10 @@ contract StakingNodesManager is function initializeV3( IRewardsCoordinator _rewardsCoordinator - ) external reinitializer(3) { + ) external virtual reinitializer(3) { if (address(_rewardsCoordinator) == address(0)) revert ZeroAddress(); rewardsCoordinator = _rewardsCoordinator; + uint256 updatedTotalETHStaked = 0; IStakingNode[] memory _nodes = getAllNodes(); for (uint256 i = 0; i < _nodes.length; i++) { @@ -471,6 +472,11 @@ contract StakingNodesManager is initializedVersion = node.getInitializedVersion(); } + if (initializedVersion == 3) { + node.initializeV4(); + initializedVersion = node.getInitializedVersion(); + } + // NOTE: For future versions, add additional if clauses that initialize the node // for the next version while keeping the previous initializers. } @@ -666,9 +672,8 @@ contract StakingNodesManager is IStakingNode[] memory allNodes = getAllNodes(); for (uint256 i = 0; i < allNodes.length; i++) { if (!allNodes[i].isSynchronized()) { - revert NodeNotSynchronized(); - } - + revert NodeNotSynchronized(address(allNodes[i])); + } updatedTotalETHStaked += allNodes[i].getETHBalance(); } diff --git a/src/WithdrawalQueueManager.sol b/src/WithdrawalQueueManager.sol index 2ff0edb99..3d69c95e7 100644 --- a/src/WithdrawalQueueManager.sol +++ b/src/WithdrawalQueueManager.sol @@ -296,9 +296,7 @@ contract WithdrawalQueueManager is IWithdrawalQueueManager, ERC721EnumerableUpgr ); uint256 unitOfAccountAmount = calculateRedemptionAmount(request.amount, redemptionRate); - // TODO: check if this is correct - //// decrements pendingRequestedRedemptionAmount by the amount that is to be sent to the user - //// this should be decremented by the amount with which it was incremented when withdrawal was created + pendingRequestedRedemptionAmount -= unitOfAccountAmount; _burn(tokenId); diff --git a/src/WithdrawalsProcessor.sol b/src/WithdrawalsProcessor.sol index 445dc6dc8..559afe9ee 100644 --- a/src/WithdrawalsProcessor.sol +++ b/src/WithdrawalsProcessor.sol @@ -63,15 +63,13 @@ contract WithdrawalsProcessor is Initializable, AccessControlUpgradeable, IWithd * @notice Bundles the completion of queued withdrawals and processing of principal withdrawals for a single node * @param withdrawalAction The withdrawal action containing node ID and withdrawal amounts * @param withdrawals Array of withdrawals to complete - * @param middlewareTimesIndexes Array of middleware times indexes for the withdrawals */ function completeAndProcessWithdrawalsForNode( IStakingNodesManager.WithdrawalAction memory withdrawalAction, - IDelegationManager.Withdrawal[] memory withdrawals, - uint256[] memory middlewareTimesIndexes + IDelegationManager.Withdrawal[] memory withdrawals ) external onlyRole(WITHDRAWAL_MANAGER_ROLE) { // Complete queued withdrawals - stakingNodesManager.nodes(withdrawalAction.nodeId).completeQueuedWithdrawals(withdrawals, middlewareTimesIndexes); + stakingNodesManager.nodes(withdrawalAction.nodeId).completeQueuedWithdrawals(withdrawals); // Process principal withdrawal IStakingNodesManager.WithdrawalAction[] memory actions = new IStakingNodesManager.WithdrawalAction[](1); diff --git a/src/interfaces/IStakingNode.sol b/src/interfaces/IStakingNode.sol index 5a4993a33..5d6c01307 100644 --- a/src/interfaces/IStakingNode.sol +++ b/src/interfaces/IStakingNode.sol @@ -5,17 +5,9 @@ import {BeaconChainProofs} from "lib/eigenlayer-contracts/src/contracts/librarie import {IStakingNodesManager} from "src/interfaces/IStakingNodesManager.sol"; import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; import {IEigenPod} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPod.sol"; -import {ISignatureUtils} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {ISignatureUtilsMixinTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtilsMixin.sol"; import {IDelegationManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; -struct WithdrawalCompletionParams { - uint256 middlewareTimesIndex; - uint256 amount; - uint32 withdrawalStartBlock; - address delegatedAddress; - uint96 nonce; -} - interface IStakingEvents { /// @notice Emitted when a user stakes ETH and receives ynETH. /// @param staker The address of the user staking ETH. @@ -34,13 +26,19 @@ interface IStakingNode { uint256 nodeId; } + /// @notice Information about the withdrawable shares for the withdrawal root. + struct WithdrawableShareInfo { + uint256 withdrawableShares; // amount of shares that can be withdrawn for the withdrawal root + bool postELIP002SlashingUpgrade; // whether the withdrawal root is post ELIP-002 slashing upgrade + } + function stakingNodesManager() external view returns (IStakingNodesManager); function eigenPod() external view returns (IEigenPod); function initialize(Init memory init) external; function createEigenPod() external returns (IEigenPod); function delegate( address operator, - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry, + ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt ) external; function undelegate() external returns (bytes32[] memory withdrawalRoots); @@ -56,6 +54,10 @@ interface IStakingNode { /// @notice Returns the beaconChainETHStrategy address used by the StakingNode. function beaconChainETHStrategy() external view returns (IStrategy); + function queuedSharesAmount() external view returns (uint256); + + function preELIP002QueuedSharesAmount() external view returns (uint256); + /** * @notice Verifies the withdrawal credentials and balance of validators. * @param beaconTimestamp An array of oracle block numbers corresponding to each validator. @@ -77,13 +79,11 @@ interface IStakingNode { ) external returns (bytes32[] memory fullWithdrawalRoots); function completeQueuedWithdrawals( - IDelegationManager.Withdrawal[] memory withdrawals, - uint256[] memory middlewareTimesIndexes + IDelegationManager.Withdrawal[] memory withdrawals ) external; function completeQueuedWithdrawalsAsShares( - IDelegationManager.Withdrawal[] calldata withdrawals, - uint256[] calldata middlewareTimesIndexes + IDelegationManager.Withdrawal[] calldata withdrawals ) external; function getInitializedVersion() external view returns (uint64); @@ -95,10 +95,13 @@ interface IStakingNode { function initializeV2(uint256 initialUnverifiedStakedETH) external; function initializeV3() external; + function initializeV4() external; function isSynchronized() external view returns (bool); - function synchronize(uint256 queuedShares, uint32 lastQueuedWithdrawalBlockNumber) external; + function synchronize() external; + + function syncQueuedShares() external; function delegatedTo() external view returns (address); diff --git a/src/interfaces/IStakingNodesManager.sol b/src/interfaces/IStakingNodesManager.sol index 66bbb984e..a37719bf1 100644 --- a/src/interfaces/IStakingNodesManager.sol +++ b/src/interfaces/IStakingNodesManager.sol @@ -52,6 +52,8 @@ interface IStakingNodesManager { function totalDeposited() external view returns (uint256); + function updateTotalETHStaked() external; + function processPrincipalWithdrawals( WithdrawalAction[] memory actions ) external; diff --git a/src/interfaces/ITokenStakingNode.sol b/src/interfaces/ITokenStakingNode.sol index d2b7b02f9..b91c1ec5e 100644 --- a/src/interfaces/ITokenStakingNode.sol +++ b/src/interfaces/ITokenStakingNode.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {ISignatureUtils} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {ISignatureUtilsMixinTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtilsMixin.sol"; import {ITokenStakingNodesManager} from "src/interfaces/ITokenStakingNodesManager.sol"; import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; import {IDelegationManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; @@ -29,7 +29,7 @@ interface ITokenStakingNode { function getInitializedVersion() external view returns (uint64); - function delegate(address operator, ISignatureUtils.SignatureWithExpiry memory signature, bytes32 approverSalt) + function delegate(address operator, ISignatureUtilsMixinTypes.SignatureWithExpiry memory signature, bytes32 approverSalt) external; function undelegate() external returns (bytes32[] memory withdrawalRoots); diff --git a/src/ynEIGEN/TokenStakingNode.sol b/src/ynEIGEN/TokenStakingNode.sol index 8df83edea..3c94dd624 100644 --- a/src/ynEIGEN/TokenStakingNode.sol +++ b/src/ynEIGEN/TokenStakingNode.sol @@ -9,7 +9,7 @@ import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.so import {IERC20 as IERC20V4} from "lib/eigenlayer-contracts/lib/openzeppelin-contracts-v4.9.0/contracts/interfaces/IERC20.sol"; import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {ArrayLib} from "src/lib/ArrayLib.sol"; -import {ISignatureUtils} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {ISignatureUtilsMixinTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtilsMixin.sol"; import {IStrategyManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; import {IDelegationManager, IDelegationManagerTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; @@ -311,7 +311,7 @@ contract TokenStakingNode is ITokenStakingNode, Initializable, ReentrancyGuardUp * @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 signature, bytes32 approverSalt) + function delegate(address operator, ISignatureUtilsMixinTypes.SignatureWithExpiry memory signature, bytes32 approverSalt) public virtual onlyDelegator diff --git a/src/ynViewer.sol b/src/ynViewer.sol index 8da96f08d..e0f34cdbd 100644 --- a/src/ynViewer.sol +++ b/src/ynViewer.sol @@ -40,10 +40,7 @@ contract ynViewer is IynViewer { /// @inheritdoc IynViewer function withdrawalDelayBlocks(address _strategy) external view returns (uint256) { IDelegationManager _delegationManager = stakingNodesManager.delegationManager(); - uint256 _minDelay = _delegationManager.minWithdrawalDelayBlocks(); - // uint256 _strategyDelay = _delegationManager.__deprecated_strategyWithdrawalDelayBlocks(IStrategy(_strategy)); - // return _minDelay > _strategyDelay ? _minDelay : _strategyDelay; - return _minDelay; + return _delegationManager.minWithdrawalDelayBlocks(); } /// @inheritdoc IynViewer diff --git a/test/integration/RewardsReceiver.t.sol b/test/integration/RewardsReceiver.t.sol index 7ca2a9029..d2e96f1cf 100644 --- a/test/integration/RewardsReceiver.t.sol +++ b/test/integration/RewardsReceiver.t.sol @@ -19,8 +19,10 @@ contract RewardsReceiverTest is IntegrationBaseTest { address newReceiver = address(33); vm.deal(address(executionLayerReceiver), 100); vm.prank(address(rewardsDistributor)); + uint256 newReceiverBalanceBefore = address(newReceiver).balance; executionLayerReceiver.transferETH(payable(newReceiver), 100); - assertEq(compareWithThreshold(address(newReceiver).balance, 100, 1), true); + uint256 balanceReceived = address(newReceiver).balance - newReceiverBalanceBefore; + assertEq(compareWithThreshold(balanceReceived, 100, 1), true); } function testRevertIfTransferETHNotWithrdrawerRole() public { diff --git a/test/integration/StakingNode.t.sol b/test/integration/StakingNode.t.sol index 118fa1b22..212f55722 100644 --- a/test/integration/StakingNode.t.sol +++ b/test/integration/StakingNode.t.sol @@ -2,36 +2,29 @@ pragma solidity ^0.8.24; import {UpgradeableBeacon} from "lib/openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol"; -import {OwnableUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; -import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; import {IPausable} from "lib/eigenlayer-contracts/src/contracts/interfaces/IPausable.sol"; import {IDelegationManager, IDelegationManagerTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; import {IStakingNode} from "src/interfaces/IStakingNode.sol"; import {IStakingNodesManager} from "src/interfaces/IStakingNodesManager.sol"; -import {IEigenPod} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPod.sol"; import {IStrategyManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; import {StakingNode} from "src/StakingNode.sol"; import {stdStorage, StdStorage} from "forge-std/Test.sol"; -import {ISignatureUtils} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {ISignatureUtilsMixinTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtilsMixin.sol"; import {BytesLib} from "lib/eigenlayer-contracts/src/contracts/libraries/BytesLib.sol"; import {EigenPod} from "lib/eigenlayer-contracts/src/contracts/pods/EigenPod.sol"; import {EigenPodManager} from "lib/eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol"; -import {IETHPOSDeposit} from "lib/eigenlayer-contracts/src/contracts/interfaces/IETHPOSDeposit.sol"; -import {IEigenPodManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPodManager.sol"; -import {IEigenPod} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPod.sol"; -import {TransparentUpgradeableProxy} from - "lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import { - BeaconChainMock, - CheckpointProofs, - CredentialProofs -} from "lib/eigenlayer-contracts/src/test/integration/mocks/BeaconChainMock.t.sol"; +import {IEigenPodManager, IEigenPodManagerErrors} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPodManager.sol"; +import {IEigenPod, IEigenPodErrors} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPod.sol"; import {BeaconChainProofs} from "lib/eigenlayer-contracts/src/contracts/libraries/BeaconChainProofs.sol"; -import {ProofParsingV1} from "test/eigenlayer-utils/ProofParsingV1.sol"; -import {Utils} from "script/Utils.sol"; import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; import {StakingNodeTestBase, IEigenPodSimplified} from "./StakingNodeTestBase.sol"; +import {CheckpointProofs, CredentialProofs} from "lib/eigenlayer-contracts/src/test/integration/mocks/BeaconChainMock.t.sol"; import {IRewardsCoordinator} from "lib/eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol"; +import {SlashingLib} from "lib/eigenlayer-contracts/src/contracts/libraries/SlashingLib.sol"; +import {IAllocationManager, IAllocationManagerTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {OperatorSet} from "lib/eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {AllocationManagerStorage} from "lib/eigenlayer-contracts/src/contracts/core/AllocationManagerStorage.sol"; +import {MockAVS} from "test/mocks/MockAVS.sol"; contract StakingNodeEigenPod is StakingNodeTestBase { @@ -53,10 +46,12 @@ contract StakingNodeEigenPod is StakingNodeTestBase { // TODO: double check this is the desired state for a pod. // we can't delegate on mainnet at this time so one should be able to farm points without delegating assertEq(eigenPodInstance.withdrawableRestakedExecutionLayerGwei(), 0, "Restaked Gwei should be 0"); + // Rewards given to each validator during epoch processing assertEq(address(eigenPodManager), address(eigenPodInstance.eigenPodManager()), "EigenPodManager should match"); assertEq(eigenPodInstance.podOwner(), address(stakingNodeInstance), "Pod owner address does not match"); - + address payable eigenPodAddress = payable(address(eigenPodInstance)); + // Get initial pod owner shares int256 initialpodOwnerDepositShares = eigenPodManager.podOwnerDepositShares(address(stakingNodeInstance)); // Assert that initial pod owner shares are 0 @@ -137,7 +132,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { function testDelegateFailWhenNotAdmin() public { vm.expectRevert(); stakingNodeInstance.delegate( - address(this), ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + address(this), ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); } @@ -149,7 +144,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator1, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator1, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); address delegatedOperator = delegationManager.delegatedTo(address(stakingNodeInstance)); @@ -166,7 +161,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator1, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator1, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); // // Attempt to undelegate with the wrong role @@ -196,7 +191,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator1, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator1, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); // // Attempt to undelegate with the wrong role @@ -232,7 +227,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { vm.expectRevert(); vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator1, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator1, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); vm.expectRevert(); @@ -245,11 +240,11 @@ contract StakingNodeDelegation is StakingNodeTestBase { vm.expectRevert(); vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); - stakingNodeInstance.completeQueuedWithdrawals(new IDelegationManager.Withdrawal[](1), new uint256[](1)); + stakingNodeInstance.completeQueuedWithdrawals(new IDelegationManager.Withdrawal[](1)); vm.expectRevert(); vm.prank(actors.admin.STAKING_NODES_DELEGATOR); - stakingNodeInstance.completeQueuedWithdrawalsAsShares(new IDelegationManager.Withdrawal[](1), new uint256[](1)); + stakingNodeInstance.completeQueuedWithdrawalsAsShares(new IDelegationManager.Withdrawal[](1)); } function testDelegateUndelegateAndDelegateAgain() public { @@ -258,7 +253,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { // Delegate to operator1 vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator1, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator1, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); address delegatedOperator1 = delegationManager.delegatedTo(address(stakingNodeInstance)); @@ -274,7 +269,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { // Delegate to operator2 vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator2, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator2, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); address delegatedOperator2 = delegationManager.delegatedTo(address(stakingNodeInstance)); @@ -298,7 +293,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { // Delegate to operator1 vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator1, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator1, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); address delegatedOperator1 = delegationManager.delegatedTo(address(stakingNodeInstance)); @@ -357,7 +352,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { // Delegate to operator1 vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator1, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator1, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); address delegatedOperator1 = delegationManager.delegatedTo(address(stakingNodeInstance)); @@ -409,7 +404,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { // Synchronize vm.prank(actors.admin.STAKING_NODES_DELEGATOR); - stakingNodeInstance.synchronize(32 ether * validatorIndices.length, undelegateBlockNumber); + stakingNodeInstance.synchronize(); // Verify total assets stayed the same assertEq(yneth.totalAssets(), initialTotalAssets, "Total assets should not change after synchronization"); @@ -423,11 +418,8 @@ contract StakingNodeDelegation is StakingNodeTestBase { // complete queued withdrawals { - uint256[] memory middlewareTimesIndexes = new uint256[](calculatedWithdrawals.length); - // all is zeroed out by default - middlewareTimesIndexes[0] = 0; vm.prank(actors.admin.STAKING_NODES_DELEGATOR); - stakingNodeInstance.completeQueuedWithdrawalsAsShares(calculatedWithdrawals, middlewareTimesIndexes); + stakingNodeInstance.completeQueuedWithdrawalsAsShares(calculatedWithdrawals); } finalQueuedShares = stakingNodeInstance.getQueuedSharesAmount(); @@ -458,7 +450,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { // Delegate to operator1 vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator1, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator1, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); address delegatedOperator1 = delegationManager.delegatedTo(address(stakingNodeInstance)); @@ -510,7 +502,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { // Synchronize vm.prank(actors.admin.STAKING_NODES_DELEGATOR); - stakingNodeInstance.synchronize(32 ether * validatorIndices.length, undelegateBlockNumber); + stakingNodeInstance.synchronize(); // Verify total assets stayed the same assertEq(yneth.totalAssets(), initialTotalAssets, "Total assets should not change after synchronization"); @@ -524,11 +516,8 @@ contract StakingNodeDelegation is StakingNodeTestBase { // complete queued withdrawals { - uint256[] memory middlewareTimesIndexes = new uint256[](calculatedWithdrawals.length); - // all is zeroed out by default - middlewareTimesIndexes[0] = 0; vm.prank(actors.admin.STAKING_NODES_DELEGATOR); - stakingNodeInstance.completeQueuedWithdrawalsAsShares(calculatedWithdrawals, middlewareTimesIndexes); + stakingNodeInstance.completeQueuedWithdrawalsAsShares(calculatedWithdrawals); } finalQueuedShares = stakingNodeInstance.getQueuedSharesAmount(); @@ -543,7 +532,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator2, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator2, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); delegatedAddress = delegationManager.delegatedTo(address(stakingNodeInstance)); @@ -574,7 +563,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { // Delegate to operator1 vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator1, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator1, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); address delegatedOperator1 = delegationManager.delegatedTo(address(stakingNodeInstance)); @@ -626,7 +615,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { // Synchronize vm.prank(actors.admin.STAKING_NODES_DELEGATOR); - stakingNodeInstance.synchronize(32 ether * validatorIndices.length, undelegateBlockNumber); + stakingNodeInstance.synchronize(); // Verify total assets stayed the same assertEq(yneth.totalAssets(), initialTotalAssets, "Total assets should not change after synchronization"); @@ -637,7 +626,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator2, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator2, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); delegatedAddress = delegationManager.delegatedTo(address(stakingNodeInstance)); @@ -656,11 +645,8 @@ contract StakingNodeDelegation is StakingNodeTestBase { // complete queued withdrawals { - uint256[] memory middlewareTimesIndexes = new uint256[](calculatedWithdrawals.length); - // all is zeroed out by default - middlewareTimesIndexes[0] = 0; vm.prank(actors.admin.STAKING_NODES_DELEGATOR); - stakingNodeInstance.completeQueuedWithdrawalsAsShares(calculatedWithdrawals, middlewareTimesIndexes); + stakingNodeInstance.completeQueuedWithdrawalsAsShares(calculatedWithdrawals); } uint256 finalQueuedShares = stakingNodeInstance.getQueuedSharesAmount(); @@ -691,7 +677,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { // Delegate to operator2 vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator2, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator2, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); // Verify total assets stayed the same after delegation to operator2 @@ -725,7 +711,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { // Delegate to operator2 vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator2, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator2, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); address delegatedOperator2 = delegationManager.delegatedTo(address(stakingNodeInstance)); @@ -766,7 +752,7 @@ contract StakingNodeDelegation is StakingNodeTestBase { function testSetClaimer() public { vm.prank(actors.admin.STAKING_NODES_DELEGATOR); stakingNodeInstance.delegate( - operator1, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator1, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); // Create a claimer address @@ -867,9 +853,7 @@ contract StakingNodeVerifyWithdrawalCredentials is StakingNodeTestBase { CredentialProofs memory _proofs = beaconChain.getCredentialProofs(_validators); vm.startPrank(actors.ops.STAKING_NODES_OPERATOR); IEigenPodSimplified node = IEigenPodSimplified(address(stakingNodesManager.nodes(nodeId))); - vm.expectRevert( - "EigenPod._verifyWithdrawalCredentials: validator must be inactive to prove withdrawal credentials" - ); + vm.expectRevert(IEigenPodErrors.CredentialsAlreadyVerified.selector); node.verifyWithdrawalCredentials({ beaconTimestamp: _proofs.beaconTimestamp, stateRootProof: _proofs.stateRootProof, @@ -913,7 +897,7 @@ contract StakingNodeVerifyWithdrawalCredentials is StakingNodeTestBase { // make sure startCheckpoint cant be called again, which means that the checkpoint has started IStakingNode _node = stakingNodesManager.nodes(nodeId); - vm.expectRevert("EigenPod._startCheckpoint: must finish previous checkpoint before starting another"); + vm.expectRevert(IEigenPodErrors.CheckpointAlreadyActive.selector); vm.prank(actors.ops.STAKING_NODES_OPERATOR); _node.startCheckpoint(true); } @@ -954,14 +938,16 @@ contract StakingNodeVerifyWithdrawalCredentials is StakingNodeTestBase { IStakingNode _node = stakingNodesManager.nodes(nodeId); CheckpointProofs memory _cpProofs = beaconChain.getCheckpointProofs(_validators, _node.eigenPod().currentCheckpointTimestamp()); + uint256 _currentCheckpointTimestampBefore = stakingNodesManager.nodes(nodeId).eigenPod().currentCheckpointTimestamp(); IEigenPodSimplified(address(_node.eigenPod())).verifyCheckpointProofs({ balanceContainerProof: _cpProofs.balanceContainerProof, proofs: _cpProofs.balanceProofs }); - + uint256 _currentCheckpointTimestampAfter = stakingNodesManager.nodes(nodeId).eigenPod().currentCheckpointTimestamp(); + uint256 _lastCheckpointTimestamp = stakingNodesManager.nodes(nodeId).eigenPod().lastCheckpointTimestamp(); // check that proofsRemaining is 0 - IEigenPod.Checkpoint memory _checkpoint = stakingNodesManager.nodes(nodeId).eigenPod().currentCheckpoint(); - assertEq(_checkpoint.proofsRemaining, 0, "_testVerifyCheckpointsBeforeWithdrawalRequest: E0"); + assertEq(_currentCheckpointTimestampAfter, 0, "_testVerifyCheckpointsBeforeWithdrawalRequest: E0"); + assertEq(_lastCheckpointTimestamp, _currentCheckpointTimestampBefore, "_testVerifyCheckpointsBeforeWithdrawalRequest: E1"); stakingNodesManager.updateTotalETHStaked(); @@ -1046,7 +1032,7 @@ contract StakingNodeVerifyWithdrawalCredentials is StakingNodeTestBase { // make sure startCheckpoint cant be called again, which means that the checkpoint has started IStakingNode _node = stakingNodesManager.nodes(nodeId); - vm.expectRevert("EigenPod._startCheckpoint: must finish previous checkpoint before starting another"); + vm.expectRevert(IEigenPodErrors.CheckpointAlreadyActive.selector); vm.prank(actors.ops.STAKING_NODES_OPERATOR); _node.startCheckpoint(true); } @@ -1093,6 +1079,7 @@ contract StakingNodeVerifyWithdrawalCredentials is StakingNodeTestBase { IStakingNode _node = stakingNodesManager.nodes(nodeId); CheckpointProofs memory _cpProofs = beaconChain.getCheckpointProofs(_validators, _node.eigenPod().currentCheckpointTimestamp()); + uint256 _currentCheckpointTimestampBefore = stakingNodesManager.nodes(nodeId).eigenPod().currentCheckpointTimestamp(); IEigenPodSimplified(address(_node.eigenPod())).verifyCheckpointProofs({ balanceContainerProof: _cpProofs.balanceContainerProof, proofs: _cpProofs.balanceProofs @@ -1138,340 +1125,17 @@ contract StakingNodeVerifyWithdrawalCredentials is StakingNodeTestBase { "Pod owner shares should not decrease after verifying checkpoint" ); - IEigenPod.Checkpoint memory _checkpoint = stakingNodesManager.nodes(nodeId).eigenPod().currentCheckpoint(); - assertEq(_checkpoint.proofsRemaining, 0, "_testVerifyCheckpointsBeforeWithdrawalRequest: E0"); + uint256 _currentCheckpointTimestampAfter = stakingNodesManager.nodes(nodeId).eigenPod().currentCheckpointTimestamp(); + uint256 _lastCheckpointTimestamp = stakingNodesManager.nodes(nodeId).eigenPod().lastCheckpointTimestamp(); + assertEq(_currentCheckpointTimestampAfter, 0, "_testVerifyCheckpointsBeforeWithdrawalRequest: E0"); + assertEq(_lastCheckpointTimestamp, _currentCheckpointTimestampBefore, "_testVerifyCheckpointsBeforeWithdrawalRequest: E1"); assertApproxEqAbs( uint256(eigenPodManager.podOwnerDepositShares(address(stakingNodesManager.nodes(nodeId)))), AMOUNT * validatorCount, 1_000_000_000, - "_testVerifyCheckpointsBeforeWithdrawalRequest: E1" + "_testVerifyCheckpointsBeforeWithdrawalRequest: E2" ); } } } - -contract StakingNodeWithdrawals is StakingNodeTestBase { - - function testQueueWithdrawals() public { - // Setup - uint256 depositAmount = 32 ether; - address user = vm.addr(156_737); - vm.deal(user, 1000 ether); - yneth.depositETH{value: depositAmount}(user); - - uint256[] memory nodeIds = createStakingNodes(1); - uint256 nodeId = nodeIds[0]; - IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeIds[0]); - - // Create and register a validator - uint40[] memory validatorIndices = createValidators(repeat(nodeIds[0], 1), 1); - - registerValidators(repeat(nodeIds[0], 1)); - beaconChain.advanceEpoch_NoRewards(); - - // Verify withdrawal credentials - _verifyWithdrawalCredentials(nodeIds[0], validatorIndices[0]); - - // Simulate some rewards - beaconChain.advanceEpoch(); - - uint40[] memory _validators = new uint40[](1); - _validators[0] = validatorIndices[0]; - - startAndVerifyCheckpoint(nodeId, _validators); - - // Get initial state - StateSnapshot memory initialState = takeSnapshot(nodeIds[0]); - - // Queue withdrawals - uint256 withdrawalAmount = 1 ether; - vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); - stakingNodeInstance.queueWithdrawals(withdrawalAmount); - - // Get final state - StateSnapshot memory finalState = takeSnapshot(nodeIds[0]); - - // Assert - assertEq(finalState.totalAssets, initialState.totalAssets, "Total assets should remain unchanged"); - assertEq(finalState.totalSupply, initialState.totalSupply, "Total supply should remain unchanged"); - assertEq( - finalState.stakingNodeBalance, - initialState.stakingNodeBalance, - "Staking node balance should remain unchanged" - ); - assertEq( - finalState.queuedShares, - initialState.queuedShares + withdrawalAmount, - "Queued shares should increase by withdrawal amount" - ); - assertEq(finalState.withdrawnETH, initialState.withdrawnETH, "Withdrawn ETH should remain unchanged"); - assertEq( - finalState.unverifiedStakedETH, - initialState.unverifiedStakedETH, - "Unverified staked ETH should remain unchanged" - ); - assertEq( - finalState.podOwnerDepositShares, - initialState.podOwnerDepositShares - int256(withdrawalAmount), - "Pod owner shares should decrease by withdrawalAmount" - ); - } - - function testQueueWithdrawalsFailsWhenNotAdmin() public { - uint256[] memory nodeIds = createStakingNodes(1); - IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeIds[0]); - - uint256 withdrawalAmount = 1 ether; - vm.prank(address(0x1234567890123456789012345678901234567890)); - vm.expectRevert(StakingNode.NotStakingNodesWithdrawer.selector); - stakingNodeInstance.queueWithdrawals(withdrawalAmount); - } - - function testQueueWithdrawalsFailsWhenInsufficientBalance() public { - uint256[] memory nodeIds = createStakingNodes(1); - IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeIds[0]); - - uint256 withdrawalAmount = 100 ether; // Assuming this is more than the node's balance - vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); - vm.expectRevert("EigenPodManager.removeShares: cannot result in pod owner having negative shares"); - stakingNodeInstance.queueWithdrawals(withdrawalAmount); - } - - function testCompleteQueuedWithdrawalsWithMultipleValidators() public { - // Setup - uint256 validatorCount = 2; - uint256 depositAmount = 32 ether; - address user = vm.addr(156_737); - vm.deal(user, 1000 ether); - yneth.depositETH{value: depositAmount * validatorCount}(user); // Deposit for validators - - uint256[] memory nodeIds = createStakingNodes(1); - uint256 nodeId = nodeIds[0]; - IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeId); - - // Setup: Create multiple validators and verify withdrawal credentials - uint40[] memory validatorIndices = createValidators(repeat(nodeId, validatorCount), validatorCount); - beaconChain.advanceEpoch_NoRewards(); - registerValidators(repeat(nodeId, validatorCount)); - - beaconChain.advanceEpoch_NoRewards(); - - for (uint256 i = 0; i < validatorCount; i++) { - _verifyWithdrawalCredentials(nodeIds[0], validatorIndices[i]); - } - - beaconChain.advanceEpoch_NoRewards(); - - // Exit some validators - uint256 exitedValidatorCount = 1; - for (uint256 i = 0; i < exitedValidatorCount; i++) { - beaconChain.exitValidator(validatorIndices[i]); - } - - // Advance the beacon chain by one epoch without rewards - beaconChain.advanceEpoch_NoRewards(); - - // Start and verify checkpoint for all validators - startAndVerifyCheckpoint(nodeId, validatorIndices); - - // Queue withdrawals for exited validators - uint256 withdrawalAmount = 32 ether * exitedValidatorCount; - vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); - stakingNodeInstance.queueWithdrawals(withdrawalAmount); - - // Capture initial state - StateSnapshot memory before = takeSnapshot(nodeIds[0]); - - QueuedWithdrawalInfo[] memory queuedWithdrawals = new QueuedWithdrawalInfo[](1); - queuedWithdrawals[0] = QueuedWithdrawalInfo({withdrawnAmount: withdrawalAmount}); - _completeQueuedWithdrawals(queuedWithdrawals, nodeIds[0]); - - // Capture final state - StateSnapshot memory afterCompletion = takeSnapshot(nodeIds[0]); - - // Assertions - assertEq(afterCompletion.queuedShares, before.queuedShares - withdrawalAmount, "Queued shares should decrease"); - assertEq(afterCompletion.withdrawnETH, before.withdrawnETH + withdrawalAmount, "Withdrawn ETH should increase"); - assertEq(afterCompletion.podOwnerDepositShares, before.podOwnerDepositShares, "Pod owner shares should remain unchanged"); - assertEq( - afterCompletion.stakingNodeBalance, - before.stakingNodeBalance, - "Staking node balance should remain unchanged" - ); - } - - function testCompleteQueuedWithdrawalsWithSlashedValidators() public { - uint256 validatorCount = 2; - - { - // Setup - uint256 depositAmount = 32 ether; - address user = vm.addr(156_737); - vm.deal(user, 1000 ether); - yneth.depositETH{value: depositAmount * validatorCount}(user); // Deposit for validators - } - - uint256 nodeId = createStakingNodes(1)[0]; - IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeId); - - // Setup: Create multiple validators and verify withdrawal credentials - uint40[] memory validatorIndices = createValidators(repeat(nodeId, validatorCount), validatorCount); - beaconChain.advanceEpoch_NoRewards(); - registerValidators(repeat(nodeId, validatorCount)); - - beaconChain.advanceEpoch_NoRewards(); - - for (uint256 i = 0; i < validatorCount; i++) { - _verifyWithdrawalCredentials(nodeId, validatorIndices[i]); - } - - beaconChain.advanceEpoch_NoRewards(); - - uint256 slashedValidatorCount = 1; - // Slash some validators - uint40[] memory slashedValidators = new uint40[](slashedValidatorCount); - for (uint256 i = 0; i < slashedValidatorCount; i++) { - slashedValidators[i] = validatorIndices[i]; - } - beaconChain.slashValidators(slashedValidators); - - beaconChain.advanceEpoch_NoRewards(); - - // Exit remaining validators - uint256 exitedValidatorCount = validatorCount - slashedValidatorCount; - for (uint256 i = slashedValidatorCount; i < validatorCount; i++) { - beaconChain.exitValidator(validatorIndices[i]); - } - - // Advance the beacon chain by one epoch without rewards - beaconChain.advanceEpoch_NoRewards(); - - // Capture initial state - StateSnapshot memory before = takeSnapshot(nodeId); - - // Start and verify checkpoint for all validators - startAndVerifyCheckpoint(nodeId, validatorIndices); - - // Calculate expected withdrawal amount (slashed validators lose 1 ETH each) - uint256 withdrawalAmount = (32 ether * exitedValidatorCount); - - // Queue withdrawals for all validators - vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); - stakingNodeInstance.queueWithdrawals(withdrawalAmount); - - QueuedWithdrawalInfo[] memory queuedWithdrawals = new QueuedWithdrawalInfo[](1); - queuedWithdrawals[0] = QueuedWithdrawalInfo({withdrawnAmount: withdrawalAmount}); - _completeQueuedWithdrawals(queuedWithdrawals, nodeId); - - // Capture final state - StateSnapshot memory afterCompletion = takeSnapshot(nodeId); - - uint256 slashedAmount = slashedValidatorCount * (beaconChain.SLASH_AMOUNT_GWEI() * 1e9); - - // Assertions - assertEq( - afterCompletion.withdrawnETH, - before.withdrawnETH + withdrawalAmount, - "Withdrawn ETH should increase by the withdrawn amount" - ); - assertEq( - afterCompletion.podOwnerDepositShares, - before.podOwnerDepositShares - int256(slashedAmount) - int256(withdrawalAmount), - "Pod owner shares should decrease by SLASH_AMOUNT_GWEI per slashed validator and by withdrawalAmount" - ); - assertEq( - afterCompletion.stakingNodeBalance, - before.stakingNodeBalance - slashedAmount, - "Staking node balance should remain unchanged" - ); - - // Verify that the total withdrawn amount matches the expected amount - assertEq( - afterCompletion.withdrawnETH - before.withdrawnETH, - withdrawalAmount, - "Total withdrawn amount should match the expected amount" - ); - } - - function testQueueWithdrawalsBeforeExitingAndVerifyingValidator() public { - uint256 validatorCount = 1; - uint256 depositAmount = 32 ether; - address user = vm.addr(156_737); - vm.deal(user, 1000 ether); - yneth.depositETH{value: depositAmount * validatorCount}(user); - - uint256 nodeId = createStakingNodes(1)[0]; - IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeId); - - // Create and register a validator - uint40[] memory validatorIndices = createValidators(repeat(nodeId, validatorCount), validatorCount); - beaconChain.advanceEpoch_NoRewards(); - registerValidators(repeat(nodeId, validatorCount)); - - beaconChain.advanceEpoch_NoRewards(); - - // Verify withdrawal credentials - _verifyWithdrawalCredentials(nodeId, validatorIndices[0]); - - beaconChain.advanceEpoch_NoRewards(); - - // Capture initial state - StateSnapshot memory before = takeSnapshot(nodeId); - - // Queue withdrawals before exiting the validator - uint256 withdrawalAmount = 32 ether; - vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); - stakingNodeInstance.queueWithdrawals(withdrawalAmount); - - // Exit the validator - beaconChain.slashValidators(validatorIndices); - - beaconChain.advanceEpoch_NoRewards(); - - // Start and verify checkpoint - startAndVerifyCheckpoint(nodeId, validatorIndices); - - // Assert that podOwnerDepositShares are equal to negative slashingAmount - uint256 slashedAmount = beaconChain.SLASH_AMOUNT_GWEI() * 1e9; - assertEq( - eigenPodManager.podOwnerDepositShares(address(stakingNodeInstance)), - -int256(slashedAmount), - "Pod owner shares should be equal to negative slashing amount" - ); - - // Complete queued withdrawals - QueuedWithdrawalInfo[] memory queuedWithdrawals = new QueuedWithdrawalInfo[](1); - queuedWithdrawals[0] = QueuedWithdrawalInfo({withdrawnAmount: withdrawalAmount}); - _completeQueuedWithdrawals(queuedWithdrawals, nodeId); - - // Capture final state - StateSnapshot memory afterCompletion = takeSnapshot(nodeId); - - // Assertions - assertEq( - afterCompletion.withdrawnETH, - before.withdrawnETH + withdrawalAmount - slashedAmount, - "Withdrawn ETH should increase by the withdrawn amount" - ); - assertEq( - afterCompletion.podOwnerDepositShares, - before.podOwnerDepositShares - int256(withdrawalAmount), - "Pod owner shares should decrease by withdrawalAmount" - ); - assertEq( - afterCompletion.queuedShares, before.queuedShares, "Queued shares should decrease back to original value" - ); - assertEq( - afterCompletion.stakingNodeBalance, - before.stakingNodeBalance - slashedAmount, - "Staking node balance should remain unchanged" - ); - assertEq( - afterCompletion.withdrawnETH, - before.withdrawnETH + withdrawalAmount - slashedAmount, - "Total withdrawn amount should match the expected amount" - ); - } - -} diff --git a/test/integration/StakingNodeSlashing.t.sol b/test/integration/StakingNodeSlashing.t.sol new file mode 100644 index 000000000..c7a403fb5 --- /dev/null +++ b/test/integration/StakingNodeSlashing.t.sol @@ -0,0 +1,737 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {IStakingNode} from "src/interfaces/IStakingNode.sol"; +import {StakingNode} from "src/StakingNode.sol"; +import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {ISignatureUtilsMixinTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtilsMixin.sol"; +import {BytesLib} from "lib/eigenlayer-contracts/src/contracts/libraries/BytesLib.sol"; +import {EigenPod} from "lib/eigenlayer-contracts/src/contracts/pods/EigenPod.sol"; +import {EigenPodManager} from "lib/eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol"; +import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; +import {StakingNodeTestBase} from "./StakingNodeTestBase.sol"; +import {SlashingLib} from "lib/eigenlayer-contracts/src/contracts/libraries/SlashingLib.sol"; +import {IAllocationManager, IAllocationManagerTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {OperatorSet} from "lib/eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {AllocationManagerStorage} from "lib/eigenlayer-contracts/src/contracts/core/AllocationManagerStorage.sol"; +import {MockAVS} from "test/mocks/MockAVS.sol"; +import {IDelegationManagerTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {BeaconChainMock} from "lib/eigenlayer-contracts/src/test/integration/mocks/BeaconChainMock.t.sol"; + +contract StakingNodeOperatorSlashing is StakingNodeTestBase { + using stdStorage for StdStorage; + using BytesLib for bytes; + using SlashingLib for *; + + + address user = vm.addr(156_737); + uint40[] validatorIndices; + + address avs; + address operator1 = address(0x9999); + address operator2 = address(0x8888); + + uint256 nodeId; + IStakingNode stakingNodeInstance; + IAllocationManager allocationManager; + uint256 validatorCount = 2; + uint256 totalDepositedAmount; + + function setUp() public override { + super.setUp(); + + avs = address(new MockAVS()); + + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + for (uint256 i = 0; i < operators.length; i++) { + vm.prank(operators[i]); + delegationManager.registerAsOperator(address(0),1, "ipfs://some-ipfs-hash"); + } + + vm.roll(block.number + 2); + + nodeId = createStakingNodes(1)[0]; + stakingNodeInstance = stakingNodesManager.nodes(nodeId); + + vm.prank(actors.admin.STAKING_NODES_DELEGATOR); + stakingNodeInstance.delegate( + operator1, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + ); + + allocationManager = IAllocationManager(chainAddresses.eigenlayer.ALLOCATION_MANAGER_ADDRESS); + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = IStrategy(stakingNodeInstance.beaconChainETHStrategy()); + IAllocationManagerTypes.CreateSetParams[] memory createSetParams = new IAllocationManagerTypes.CreateSetParams[](1); + createSetParams[0] = IAllocationManagerTypes.CreateSetParams({ + operatorSetId: 1, + strategies: strategies + }); + + vm.startPrank(avs); + allocationManager.updateAVSMetadataURI(avs, "ipfs://some-metadata-uri"); + allocationManager.createOperatorSets(avs, createSetParams); + vm.stopPrank(); + + uint32 allocationConfigurationDelay = AllocationManagerStorage(address(allocationManager)).ALLOCATION_CONFIGURATION_DELAY(); + + uint32[] memory operatorSetIds = new uint32[](1); + operatorSetIds[0] = uint32(1); + IAllocationManagerTypes.RegisterParams memory registerParams = IAllocationManagerTypes.RegisterParams({ + avs: avs, + operatorSetIds: operatorSetIds, + data: "" + }); + OperatorSet memory operatorSet = OperatorSet({ + avs: avs, + id: 1 + }); + uint64[] memory newMagnitudes = new uint64[](1); + newMagnitudes[0] = uint64(1 ether); + IAllocationManagerTypes.AllocateParams[] memory allocateParams = new IAllocationManagerTypes.AllocateParams[](1); + allocateParams[0] = IAllocationManagerTypes.AllocateParams({ + operatorSet: operatorSet, + strategies: strategies, + newMagnitudes: newMagnitudes + }); + + vm.roll(block.number + allocationConfigurationDelay + 2); + vm.startPrank(operator1); + allocationManager.registerForOperatorSets(operator1, registerParams); + allocationManager.modifyAllocations(operator1, allocateParams); + vm.stopPrank(); + + vm.roll(block.number + allocationConfigurationDelay + 2); + + uint256 depositAmount = 32 ether; + totalDepositedAmount = depositAmount * validatorCount; + address user = vm.addr(156_737); + vm.deal(user, 1000 ether); + yneth.depositETH{value: totalDepositedAmount}(user); + + // Create and setup validators + validatorIndices = createValidators(repeat(nodeId, validatorCount), validatorCount); + beaconChain.advanceEpoch_NoRewards(); + registerValidators(repeat(nodeId, validatorCount)); + beaconChain.advanceEpoch_NoRewards(); + + for (uint256 i = 0; i < validatorCount; i++) { + _verifyWithdrawalCredentials(nodeId, validatorIndices[i]); + } + beaconChain.advanceEpoch_NoRewards(); + } + + function testSlashedOperatorBeforeQueuedWithdrawals() public { + + // Capture initial state + StateSnapshot memory initialState = takeSnapshot(nodeId); + IStrategy beaconChainETHStrategy = stakingNodeInstance.beaconChainETHStrategy(); + uint256 beaconChainSlashingFactorBefore = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + uint256 operatorMaxMagnitudeBefore = allocationManager.getMaxMagnitude(operator1, beaconChainETHStrategy); + + // Start and verify checkpoint for all validators + startAndVerifyCheckpoint(nodeId, validatorIndices); + + uint256 slashingPercent = 0.3 ether; + { + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = IStrategy(stakingNodeInstance.beaconChainETHStrategy()); + uint256[] memory wadsToSlash = new uint256[](1); + wadsToSlash[0] = slashingPercent; // slash 30% of the operator's stake + IAllocationManagerTypes.SlashingParams memory slashingParams = IAllocationManagerTypes.SlashingParams({ + operator: operator1, + operatorSetId: 1, + strategies: strategies, + wadsToSlash: wadsToSlash, + description: "Slashing operator1" + }); + + vm.prank(avs); + allocationManager.slashOperator(avs, slashingParams); + + stakingNodeInstance.stakingNodesManager().updateTotalETHStaked(); + } + + { + // Get final state + StateSnapshot memory finalState = takeSnapshot(nodeId); + uint256 beaconChainSlashingFactorAfter = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + uint256 operatorMaxMagnitudeAfter = allocationManager.getMaxMagnitude(operator1, beaconChainETHStrategy); + + uint256 slashedAmountInWei = totalDepositedAmount.mulWad(slashingPercent); + // Assert + assertEq(beaconChainSlashingFactorBefore, beaconChainSlashingFactorAfter, "Beacon chain slashing factor should not change"); + assertLt(operatorMaxMagnitudeAfter, operatorMaxMagnitudeBefore, "Operator max magnitude should decrease due to slashing"); + assertEq(finalState.totalAssets, initialState.totalAssets - slashedAmountInWei, "Total assets should decrease by slashed amount"); + assertEq(finalState.totalSupply, initialState.totalSupply, "Total supply should remain unchanged"); + assertEq( + finalState.stakingNodeBalance, + initialState.stakingNodeBalance - slashedAmountInWei, + "Staking node balance should decrease by slashed amount" + ); + assertEq( + finalState.queuedShares, + initialState.queuedShares, + "Queued shares should remain unchanged" + ); + assertEq(finalState.withdrawnETH, initialState.withdrawnETH, "Withdrawn ETH should remain unchanged"); + assertEq( + finalState.unverifiedStakedETH, + initialState.unverifiedStakedETH, + "Unverified staked ETH should remain unchanged" + ); + assertEq( + finalState.podOwnerDepositShares, + initialState.podOwnerDepositShares, + "Pod owner shares should remain unchanged" + ); + } + } + + function testSlashedOperatorBetweenQueuedAndCompletedWithdrawals() public { + + // Capture initial state + StateSnapshot memory initialState = takeSnapshot(nodeId); + IStrategy beaconChainETHStrategy = stakingNodeInstance.beaconChainETHStrategy(); + uint256 beaconChainSlashingFactorBefore = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + uint256 operatorMaxMagnitudeBefore = allocationManager.getMaxMagnitude(operator1, beaconChainETHStrategy); + + uint256 withdrawalAmount = 32 ether; + uint256 expectedWithdrawalAmount; + { + // Queue withdrawals for all validators + vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); + bytes32[] memory withdrawalRoots = stakingNodeInstance.queueWithdrawals(withdrawalAmount); + + uint256 queuedSharesAmountBeforeSlashing = stakingNodeInstance.queuedSharesAmount(); + assertEq(queuedSharesAmountBeforeSlashing, withdrawalAmount, "Queued shares should be equal to withdrawal amount"); + + // Exit validator + beaconChain.exitValidator(validatorIndices[0]); + beaconChain.advanceEpoch_NoRewards(); + + // Start and verify checkpoint for all validators + startAndVerifyCheckpoint(nodeId, validatorIndices); + + uint256 slashingPercent = 0.3 ether; + { + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = IStrategy(stakingNodeInstance.beaconChainETHStrategy()); + uint256[] memory wadsToSlash = new uint256[](1); + wadsToSlash[0] = slashingPercent; // slash 30% of the operator's stake + IAllocationManagerTypes.SlashingParams memory slashingParams = IAllocationManagerTypes.SlashingParams({ + operator: operator1, + operatorSetId: 1, + strategies: strategies, + wadsToSlash: wadsToSlash, + description: "Slashing operator1" + }); + + vm.prank(avs); + allocationManager.slashOperator(avs, slashingParams); + + // expect revert when completing withdrawals due to syncQueuedShares not done + _completeQueuedWithdrawals(withdrawalRoots, nodeId, true); + + vm.prank(actors.admin.STAKING_NODES_DELEGATOR); + stakingNodeInstance.syncQueuedShares(); + } + { + uint256 nodeBalanceReceived; + uint256 nodeBalanceBeforeWithdrawal = address(stakingNodeInstance).balance; + _completeQueuedWithdrawals(withdrawalRoots, nodeId, false); + uint256 nodeBalanceAfterWithdrawal = address(stakingNodeInstance).balance; + nodeBalanceReceived = nodeBalanceAfterWithdrawal - nodeBalanceBeforeWithdrawal; + expectedWithdrawalAmount = withdrawalAmount.mulWad(1 ether - slashingPercent); + assertEq(nodeBalanceReceived, expectedWithdrawalAmount, "Node's ETH balance should increase by expected withdrawal amount"); + } + } + + stakingNodeInstance.stakingNodesManager().updateTotalETHStaked(); + + { + // Get final state + StateSnapshot memory finalState = takeSnapshot(nodeId); + uint256 beaconChainSlashingFactorAfter = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + uint256 operatorMaxMagnitudeAfter = allocationManager.getMaxMagnitude(operator1, beaconChainETHStrategy); + uint256 slashedAmount = totalDepositedAmount - totalDepositedAmount.mulWad(operatorMaxMagnitudeAfter); + + // Assert + assertEq(beaconChainSlashingFactorBefore, beaconChainSlashingFactorAfter, "Beacon chain slashing factor should not change"); + assertLt(operatorMaxMagnitudeAfter, operatorMaxMagnitudeBefore, "Operator max magnitude should decrease due to slashing"); + assertEq(finalState.totalAssets, initialState.totalAssets - slashedAmount, "Total assets should decrease by slashed amount"); + assertEq(finalState.totalSupply, initialState.totalSupply, "Total supply should remain unchanged"); + assertEq( + finalState.stakingNodeBalance, + initialState.stakingNodeBalance - slashedAmount, + "Staking node balance should decrease by slashed amount" + ); + assertEq( + finalState.queuedShares, + initialState.queuedShares, + "Queued shares should remain unchanged" + ); + assertEq(finalState.withdrawnETH, initialState.withdrawnETH + expectedWithdrawalAmount, "Withdrawn ETH should remain unchanged"); + assertEq( + finalState.unverifiedStakedETH, + initialState.unverifiedStakedETH, + "Unverified staked ETH should remain unchanged" + ); + assertEq( + finalState.podOwnerDepositShares, + initialState.podOwnerDepositShares - int256(withdrawalAmount), + "Pod owner shares should remain unchanged" + ); + } + } + + function testSlashedOperatorAfterCompletedWithdrawals() public { + + // Capture initial state + StateSnapshot memory initialState = takeSnapshot(nodeId); + IStrategy beaconChainETHStrategy = stakingNodeInstance.beaconChainETHStrategy(); + uint256 beaconChainSlashingFactorBefore = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + uint256 operatorMaxMagnitudeBefore = allocationManager.getMaxMagnitude(operator1, beaconChainETHStrategy); + + // Queue and complete withdrawals before slashing + uint256 withdrawalAmount = 32 ether; + bytes32[] memory withdrawalRoots; + { + vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); + withdrawalRoots = stakingNodeInstance.queueWithdrawals(withdrawalAmount); + + beaconChain.exitValidator(validatorIndices[0]); + beaconChain.advanceEpoch_NoRewards(); + startAndVerifyCheckpoint(nodeId, validatorIndices); + + uint256 nodeBalanceBeforeWithdrawal = address(stakingNodeInstance).balance; + _completeQueuedWithdrawals(withdrawalRoots, nodeId, false); + uint256 nodeBalanceAfterWithdrawal = address(stakingNodeInstance).balance; + uint256 nodeBalanceReceived = nodeBalanceAfterWithdrawal - nodeBalanceBeforeWithdrawal; + assertEq(nodeBalanceReceived, withdrawalAmount, "Node should receive full withdrawal amount before slashing"); + } + + // Perform slashing after withdrawals + uint256 slashingPercent = 0.3 ether; + { + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = IStrategy(stakingNodeInstance.beaconChainETHStrategy()); + uint256[] memory wadsToSlash = new uint256[](1); + wadsToSlash[0] = slashingPercent; + + IAllocationManagerTypes.SlashingParams memory slashingParams = IAllocationManagerTypes.SlashingParams({ + operator: operator1, + operatorSetId: 1, + strategies: strategies, + wadsToSlash: wadsToSlash, + description: "Slashing operator1 after withdrawals" + }); + + vm.prank(avs); + allocationManager.slashOperator(avs, slashingParams); + } + + stakingNodeInstance.stakingNodesManager().updateTotalETHStaked(); + + // Verify final state + { + StateSnapshot memory finalState = takeSnapshot(nodeId); + uint256 beaconChainSlashingFactorAfter = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + uint256 operatorMaxMagnitudeAfter = allocationManager.getMaxMagnitude(operator1, beaconChainETHStrategy); + uint256 remainingDeposit = totalDepositedAmount - withdrawalAmount; + uint256 slashedAmount = remainingDeposit - remainingDeposit.mulWad(1 ether - slashingPercent); + uint256 expectedTotalAssets = withdrawalAmount + remainingDeposit - slashedAmount; + + // Assertions + assertEq(beaconChainSlashingFactorBefore, beaconChainSlashingFactorAfter, "Beacon chain slashing factor should not change"); + assertLt(operatorMaxMagnitudeAfter, operatorMaxMagnitudeBefore, "Operator max magnitude should decrease"); + assertEq(finalState.totalAssets, expectedTotalAssets, "Total assets should reflect withdrawal and slashing"); + assertEq(finalState.totalSupply, initialState.totalSupply, "Total supply should remain unchanged"); + assertEq(finalState.withdrawnETH, initialState.withdrawnETH + withdrawalAmount, "Withdrawn ETH should increase by withdrawal amount"); + assertEq(finalState.podOwnerDepositShares, initialState.podOwnerDepositShares - int256(withdrawalAmount), "Pod owner shares should decrease by withdrawal amount"); + } + } +} + +contract StakingNodeValidatorSlashing is StakingNodeTestBase { + + using SlashingLib for *; + + function testQueueWithdrawalsWithSlashedValidator() public { + uint256 validatorCount = 2; + uint256 totalDepositedAmount; + + { + // Setup + uint256 depositAmount = 32 ether; + totalDepositedAmount = depositAmount * validatorCount; + address user = vm.addr(156_737); + vm.deal(user, 1000 ether); + yneth.depositETH{value: totalDepositedAmount}(user); // Deposit for validators + } + + uint256 nodeId = createStakingNodes(1)[0]; + IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeId); + + // Setup: Create multiple validators and verify withdrawal credentials + uint40[] memory validatorIndices = createValidators(repeat(nodeId, validatorCount), validatorCount); + beaconChain.advanceEpoch_NoRewards(); + registerValidators(repeat(nodeId, validatorCount)); + + beaconChain.advanceEpoch_NoRewards(); + + for (uint256 i = 0; i < validatorCount; i++) { + _verifyWithdrawalCredentials(nodeId, validatorIndices[i]); + } + + beaconChain.advanceEpoch_NoRewards(); + + uint256 slashedValidatorCount = 1; + // Slash some validators + uint40[] memory slashedValidators = new uint40[](slashedValidatorCount); + for (uint256 i = 0; i < slashedValidatorCount; i++) { + slashedValidators[i] = validatorIndices[i]; + } + + // Capture initial state + StateSnapshot memory initialState = takeSnapshot(nodeId); + + beaconChain.slashValidators(slashedValidators, BeaconChainMock.SlashType.Minor); + + beaconChain.advanceEpoch_NoRewards(); + + uint256 beaconChainSlashingFactorBefore = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + + // Start and verify checkpoint for all validators + startAndVerifyCheckpoint(nodeId, validatorIndices); + + uint256 beaconChainSlashingFactorAfter = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + + // Queue withdrawals + uint256 withdrawalAmount = 1 ether; + vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); + bytes32[] memory withdrawalRoots = stakingNodeInstance.queueWithdrawals(withdrawalAmount); + (IDelegationManagerTypes.Withdrawal memory withdrawal, ) = delegationManager.getQueuedWithdrawal(withdrawalRoots[0]); + uint256 scaledShares = withdrawal.scaledShares[0]; + uint256 withdrawableShares = scaledShares.mulWad(beaconChainSlashingFactorAfter); + + // Get final state + StateSnapshot memory finalState = takeSnapshot(nodeId); + + uint256 slashedAmountInWei = beaconChain.MINOR_SLASH_AMOUNT_GWEI() * 1e9; + // Assert + assertLt(beaconChainSlashingFactorAfter, beaconChainSlashingFactorBefore, "Beacon chain slashing factor should decrease due to slashing"); + assertEq(finalState.totalAssets, initialState.totalAssets, "Total assets should remain unchanged"); + assertEq(finalState.totalSupply, initialState.totalSupply, "Total supply should remain unchanged"); + assertEq( + finalState.stakingNodeBalance, + initialState.stakingNodeBalance - slashedAmountInWei, + "Staking node balance should decrease by slashed amount" + ); + assertEq( + finalState.queuedShares, + initialState.queuedShares + withdrawableShares, + "Queued shares should increase by withdrawal amount" + ); + assertEq(finalState.withdrawnETH, initialState.withdrawnETH, "Withdrawn ETH should remain unchanged"); + assertEq( + finalState.unverifiedStakedETH, + initialState.unverifiedStakedETH, + "Unverified staked ETH should remain unchanged" + ); + assertEq( + finalState.podOwnerDepositShares, + initialState.podOwnerDepositShares - int256(withdrawalAmount), + "Pod owner shares should decrease by withdrawalAmount" + ); + } + + function testCompleteQueuedWithdrawalsWithSlashedValidatorsBeforeQueuing() public { + uint256 validatorCount = 2; + uint256 totalDepositedAmount; + + { + // Setup + uint256 depositAmount = 32 ether; + totalDepositedAmount = depositAmount * validatorCount; + address user = vm.addr(156_737); + vm.deal(user, 1000 ether); + yneth.depositETH{value: totalDepositedAmount}(user); // Deposit for validators + } + + uint256 nodeId = createStakingNodes(1)[0]; + IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeId); + + // Setup: Create multiple validators and verify withdrawal credentials + uint40[] memory validatorIndices = createValidators(repeat(nodeId, validatorCount), validatorCount); + beaconChain.advanceEpoch_NoRewards(); + registerValidators(repeat(nodeId, validatorCount)); + + beaconChain.advanceEpoch_NoRewards(); + + for (uint256 i = 0; i < validatorCount; i++) { + _verifyWithdrawalCredentials(nodeId, validatorIndices[i]); + } + + beaconChain.advanceEpoch_NoRewards(); + + uint256 slashedValidatorCount = 1; + // Slash some validators + uint40[] memory slashedValidators = new uint40[](slashedValidatorCount); + for (uint256 i = 0; i < slashedValidatorCount; i++) { + slashedValidators[i] = validatorIndices[i]; + } + + beaconChain.slashValidators(slashedValidators, BeaconChainMock.SlashType.Minor); + + beaconChain.advanceEpoch_NoRewards(); + + // Exit remaining validators + uint256 exitedValidatorCount = validatorCount - slashedValidatorCount; + for (uint256 i = slashedValidatorCount; i < validatorCount; i++) { + beaconChain.exitValidator(validatorIndices[i]); + } + + // Advance the beacon chain by one epoch without rewards + beaconChain.advanceEpoch_NoRewards(); + + // Capture initial state + StateSnapshot memory before = takeSnapshot(nodeId); + + // Start and verify checkpoint for all validators + startAndVerifyCheckpoint(nodeId, validatorIndices); + + // Calculate expected withdrawal amount (slashed validators lose 1 ETH each) + uint256 withdrawalAmount = (32 ether * exitedValidatorCount); + + // Queue withdrawals for all validators + vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); + bytes32[] memory withdrawalRoots = stakingNodeInstance.queueWithdrawals(withdrawalAmount); + + _completeQueuedWithdrawals(withdrawalRoots, nodeId, false); + + // Capture final state + StateSnapshot memory afterCompletion = takeSnapshot(nodeId); + uint256 _beaconChainSlashingFactor = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + uint256 expectedWithdrawalAmount = withdrawalAmount.mulWad(_beaconChainSlashingFactor); + uint256 slashedAmount = totalDepositedAmount - totalDepositedAmount.mulWad(_beaconChainSlashingFactor); + + // Assertions + assertEq( + afterCompletion.withdrawnETH, + before.withdrawnETH + expectedWithdrawalAmount, + "Withdrawn ETH should increase by the expected withdrawal amount" + ); + assertEq( + afterCompletion.podOwnerDepositShares, + before.podOwnerDepositShares - int256(withdrawalAmount), + "Pod owner shares should decrease by withdrawalAmount" + ); + assertEq( + afterCompletion.stakingNodeBalance + slashedAmount, + before.stakingNodeBalance, + "Staking node balance should decrease by slashedAmount" + ); + } + + function testCompleteQueuedWithdrawalsWithSlashedValidatorsBetweenQueuingAndCompletion() public { + uint256 validatorCount = 2; + uint256 totalDepositedAmount; + + { + // Setup + uint256 depositAmount = 32 ether; + totalDepositedAmount = depositAmount * validatorCount; + address user = vm.addr(156_737); + vm.deal(user, 1000 ether); + yneth.depositETH{value: totalDepositedAmount}(user); // Deposit for validators + } + + uint256 nodeId = createStakingNodes(1)[0]; + IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeId); + + // Setup: Create multiple validators and verify withdrawal credentials + uint40[] memory validatorIndices = createValidators(repeat(nodeId, validatorCount), validatorCount); + beaconChain.advanceEpoch_NoRewards(); + registerValidators(repeat(nodeId, validatorCount)); + + beaconChain.advanceEpoch_NoRewards(); + + for (uint256 i = 0; i < validatorCount; i++) { + _verifyWithdrawalCredentials(nodeId, validatorIndices[i]); + } + + beaconChain.advanceEpoch_NoRewards(); + + uint256 slashedValidatorCount = 1; + // Slash some validators + uint40[] memory slashedValidators = new uint40[](slashedValidatorCount); + for (uint256 i = 0; i < slashedValidatorCount; i++) { + slashedValidators[i] = validatorIndices[i]; + } + + + // Advance the beacon chain by one epoch without rewards + beaconChain.advanceEpoch_NoRewards(); + + // Capture initial state + StateSnapshot memory before = takeSnapshot(nodeId); + + uint256 exitedValidatorCount = validatorCount - slashedValidatorCount; + + // Calculate expected withdrawal amount (slashed validators lose 1 ETH each) + uint256 withdrawalAmount = (32 ether * exitedValidatorCount); + + // Queue withdrawals for all validators + vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); + bytes32[] memory withdrawalRoots = stakingNodeInstance.queueWithdrawals(withdrawalAmount); + + { + uint256 queuedSharesAmountBeforeSlashing = stakingNodeInstance.queuedSharesAmount(); + assertEq(queuedSharesAmountBeforeSlashing, withdrawalAmount, "Queued shares should be equal to withdrawal amount"); + + beaconChain.slashValidators(slashedValidators, BeaconChainMock.SlashType.Minor); + + // Exit remaining validators + for (uint256 i = slashedValidatorCount; i < validatorCount; i++) { + beaconChain.exitValidator(validatorIndices[i]); + } + + beaconChain.advanceEpoch_NoRewards(); + + // Start and verify checkpoint for all validators + startAndVerifyCheckpoint(nodeId, validatorIndices); + + _completeQueuedWithdrawals(withdrawalRoots, nodeId, true); + + vm.prank(actors.admin.STAKING_NODES_DELEGATOR); + stakingNodeInstance.syncQueuedShares(); + + uint256 queuedSharesAmountAfterSlashing = stakingNodeInstance.queuedSharesAmount(); + assertLt(queuedSharesAmountAfterSlashing, queuedSharesAmountBeforeSlashing, "Queued shares should decrease after slashing"); + } + + { + uint256 _beaconChainSlashingFactor = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + uint256 expectedWithdrawalAmount = withdrawalAmount.mulWad(_beaconChainSlashingFactor); + uint256 slashedAmount = totalDepositedAmount - totalDepositedAmount.mulWad(_beaconChainSlashingFactor); + + // Assertions + uint256 nodeBalanceBeforeWithdrawal = address(stakingNodeInstance).balance; + _completeQueuedWithdrawals(withdrawalRoots, nodeId, false); + uint256 nodeBalanceAfterWithdrawal = address(stakingNodeInstance).balance; + + // Capture final state + StateSnapshot memory afterCompletion = takeSnapshot(nodeId); + + assertEq(nodeBalanceAfterWithdrawal - nodeBalanceBeforeWithdrawal, expectedWithdrawalAmount, "Node's ETH balance should increase by expected withdrawal amount"); + + assertEq( + afterCompletion.withdrawnETH, + before.withdrawnETH + expectedWithdrawalAmount, + "Withdrawn ETH should increase by the expected withdrawal amount" + ); + assertEq( + afterCompletion.podOwnerDepositShares, + before.podOwnerDepositShares - int256(withdrawalAmount), + "Pod owner shares should decrease by withdrawalAmount" + ); + assertEq( + afterCompletion.stakingNodeBalance + slashedAmount, + before.stakingNodeBalance, + "Staking node balance should decrease by slashedAmount" + ); + } + } + + function testCompleteQueuedWithdrawalsWithSlashedValidatorsAfterCompletion() public { + uint256 validatorCount = 2; + uint256 totalDepositedAmount; + + { + // Setup + uint256 depositAmount = 32 ether; + totalDepositedAmount = depositAmount * validatorCount; + address user = vm.addr(156_737); + vm.deal(user, 1000 ether); + yneth.depositETH{value: totalDepositedAmount}(user); // Deposit for validators + } + + uint256 nodeId = createStakingNodes(1)[0]; + IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeId); + + // Setup: Create multiple validators and verify withdrawal credentials + uint40[] memory validatorIndices = createValidators(repeat(nodeId, validatorCount), validatorCount); + beaconChain.advanceEpoch_NoRewards(); + registerValidators(repeat(nodeId, validatorCount)); + + beaconChain.advanceEpoch_NoRewards(); + + for (uint256 i = 0; i < validatorCount; i++) { + _verifyWithdrawalCredentials(nodeId, validatorIndices[i]); + } + + beaconChain.advanceEpoch_NoRewards(); + + uint256 slashedValidatorCount = 1; + // Slash some validators + uint40[] memory slashedValidators = new uint40[](slashedValidatorCount); + for (uint256 i = 0; i < slashedValidatorCount; i++) { + slashedValidators[i] = validatorIndices[i]; + } + + // Exit remaining validators + uint256 exitedValidatorCount = validatorCount - slashedValidatorCount; + for (uint256 i = slashedValidatorCount; i < validatorCount; i++) { + beaconChain.exitValidator(validatorIndices[i]); + } + + // Advance the beacon chain by one epoch without rewards + beaconChain.advanceEpoch_NoRewards(); + + // Capture initial state + StateSnapshot memory before = takeSnapshot(nodeId); + + // Start and verify checkpoint for all validators + startAndVerifyCheckpoint(nodeId, validatorIndices); + + // Calculate expected withdrawal amount (slashed validators lose 1 ETH each) + uint256 withdrawalAmount = (32 ether * exitedValidatorCount); + + // Queue withdrawals for all validators + vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); + bytes32[] memory withdrawalRoots = stakingNodeInstance.queueWithdrawals(withdrawalAmount); + + _completeQueuedWithdrawals(withdrawalRoots, nodeId, false); + + beaconChain.slashValidators(slashedValidators, BeaconChainMock.SlashType.Minor); + + beaconChain.advanceEpoch_NoRewards(); + + // Capture final state + StateSnapshot memory afterCompletion = takeSnapshot(nodeId); + uint256 _beaconChainSlashingFactor = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + uint256 expectedWithdrawalAmount = withdrawalAmount.mulWad(_beaconChainSlashingFactor); + uint256 slashedAmount = totalDepositedAmount - totalDepositedAmount.mulWad(_beaconChainSlashingFactor); + + // Assertions + assertEq( + afterCompletion.withdrawnETH, + before.withdrawnETH + expectedWithdrawalAmount, + "Withdrawn ETH should increase by the expected withdrawal amount" + ); + assertEq( + afterCompletion.podOwnerDepositShares, + before.podOwnerDepositShares - int256(withdrawalAmount), + "Pod owner shares should decrease by withdrawalAmount" + ); + assertEq( + afterCompletion.stakingNodeBalance + slashedAmount, + before.stakingNodeBalance, + "Staking node balance should decrease by slashedAmount" + ); + } + +} \ No newline at end of file diff --git a/test/integration/StakingNodeTestBase.sol b/test/integration/StakingNodeTestBase.sol index 0cbacfdb7..bc968704c 100644 --- a/test/integration/StakingNodeTestBase.sol +++ b/test/integration/StakingNodeTestBase.sol @@ -71,25 +71,12 @@ contract StakingNodeTestBase is IntegrationBaseTest { }); } - function _completeQueuedWithdrawals(QueuedWithdrawalInfo[] memory queuedWithdrawals, uint256 nodeId) internal { + function _completeQueuedWithdrawals(bytes32[] memory withdrawalRoots, uint256 nodeId, bool shouldExpectRevert) internal { // create Withdrawal struct - IDelegationManagerTypes.Withdrawal[] memory _withdrawals = new IDelegationManagerTypes.Withdrawal[](queuedWithdrawals.length); + IDelegationManagerTypes.Withdrawal[] memory _withdrawals = new IDelegationManagerTypes.Withdrawal[](withdrawalRoots.length); { - for (uint256 i = 0; i < queuedWithdrawals.length; i++) { - uint256[] memory _shares = new uint256[](1); - _shares[0] = queuedWithdrawals[i].withdrawnAmount; - IStrategy[] memory _strategies = new IStrategy[](1); - _strategies[0] = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0); // beacon chain eth strat - address _stakingNode = address(stakingNodesManager.nodes(nodeId)); - _withdrawals[i] = IDelegationManagerTypes.Withdrawal({ - staker: _stakingNode, - delegatedTo: delegationManager.delegatedTo(_stakingNode), - withdrawer: _stakingNode, - nonce: delegationManager.cumulativeWithdrawalsQueued(_stakingNode) - 1, - startBlock: uint32(block.number), - strategies: _strategies, - scaledShares: _shares - }); + for (uint256 i = 0; i < withdrawalRoots.length; i++) { + (_withdrawals[i],) = delegationManager.getQueuedWithdrawal(withdrawalRoots[i]); } } @@ -103,11 +90,12 @@ contract StakingNodeTestBase is IntegrationBaseTest { // complete queued withdrawals { - uint256[] memory _middlewareTimesIndexes = new uint256[](_withdrawals.length); - // all is zeroed out by defailt - _middlewareTimesIndexes[0] = 0; + IStakingNode _node = stakingNodesManager.nodes(nodeId); vm.startPrank(actors.ops.STAKING_NODES_WITHDRAWER); - stakingNodesManager.nodes(nodeId).completeQueuedWithdrawals(_withdrawals, _middlewareTimesIndexes); + if (shouldExpectRevert) { + vm.expectRevert(); + } + _node.completeQueuedWithdrawals(_withdrawals); vm.stopPrank(); } } @@ -130,11 +118,8 @@ contract StakingNodeTestBase is IntegrationBaseTest { // complete queued withdrawals { - uint256[] memory _middlewareTimesIndexes = new uint256[](_withdrawals.length); - // all is zeroed out by defailt - _middlewareTimesIndexes[0] = 0; vm.startPrank(actors.admin.STAKING_NODES_DELEGATOR); - stakingNodesManager.nodes(nodeId).completeQueuedWithdrawalsAsShares(_withdrawals, _middlewareTimesIndexes); + stakingNodesManager.nodes(nodeId).completeQueuedWithdrawalsAsShares(_withdrawals); vm.stopPrank(); } } @@ -181,7 +166,7 @@ contract StakingNodeTestBase is IntegrationBaseTest { // start checkpoint { vm.startPrank(actors.ops.STAKING_NODES_OPERATOR); - stakingNodesManager.nodes(nodeId).startCheckpoint(true); + stakingNodesManager.nodes(nodeId).startCheckpoint(false); vm.stopPrank(); } // verify checkpoints diff --git a/test/integration/StakingNodeWithdrawals.t.sol b/test/integration/StakingNodeWithdrawals.t.sol new file mode 100644 index 000000000..4453a3a38 --- /dev/null +++ b/test/integration/StakingNodeWithdrawals.t.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {IDelegationManagerTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IStakingNode} from "src/interfaces/IStakingNode.sol"; +import {IStakingNodesManager} from "src/interfaces/IStakingNodesManager.sol"; +import {StakingNode} from "src/StakingNode.sol"; +import {EigenPod} from "lib/eigenlayer-contracts/src/contracts/pods/EigenPod.sol"; +import {EigenPodManager} from "lib/eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol"; +import {IEigenPodManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPodManager.sol"; +import {IEigenPod} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPod.sol"; +import {IEigenPodManagerErrors} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPodManager.sol"; +import {StakingNodeTestBase} from "./StakingNodeTestBase.sol"; +import {SlashingLib} from "lib/eigenlayer-contracts/src/contracts/libraries/SlashingLib.sol"; +import {BeaconChainMock} from "lib/eigenlayer-contracts/src/test/integration/mocks/BeaconChainMock.t.sol"; + +contract StakingNodeWithdrawals is StakingNodeTestBase { + + using SlashingLib for *; + + function testQueueWithdrawals() public { + // Setup + uint256 depositAmount = 32 ether; + address user = vm.addr(156_737); + vm.deal(user, 1000 ether); + yneth.depositETH{value: depositAmount}(user); + + uint256[] memory nodeIds = createStakingNodes(1); + uint256 nodeId = nodeIds[0]; + IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeIds[0]); + + // Create and register a validator + uint40[] memory validatorIndices = createValidators(repeat(nodeIds[0], 1), 1); + + registerValidators(repeat(nodeIds[0], 1)); + beaconChain.advanceEpoch_NoRewards(); + + // Verify withdrawal credentials + _verifyWithdrawalCredentials(nodeIds[0], validatorIndices[0]); + + // Simulate some rewards + beaconChain.advanceEpoch(); + + uint40[] memory _validators = new uint40[](1); + _validators[0] = validatorIndices[0]; + + startAndVerifyCheckpoint(nodeId, _validators); + + // Get initial state + StateSnapshot memory initialState = takeSnapshot(nodeIds[0]); + + // Queue withdrawals + uint256 withdrawalAmount = 1 ether; + vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); + stakingNodeInstance.queueWithdrawals(withdrawalAmount); + + // Get final state + StateSnapshot memory finalState = takeSnapshot(nodeIds[0]); + + // Assert + assertEq(finalState.totalAssets, initialState.totalAssets, "Total assets should remain unchanged"); + assertEq(finalState.totalSupply, initialState.totalSupply, "Total supply should remain unchanged"); + assertEq( + finalState.stakingNodeBalance, + initialState.stakingNodeBalance, + "Staking node balance should remain unchanged" + ); + assertEq( + finalState.queuedShares, + initialState.queuedShares + withdrawalAmount, + "Queued shares should increase by withdrawal amount" + ); + assertEq(finalState.withdrawnETH, initialState.withdrawnETH, "Withdrawn ETH should remain unchanged"); + assertEq( + finalState.unverifiedStakedETH, + initialState.unverifiedStakedETH, + "Unverified staked ETH should remain unchanged" + ); + assertEq( + finalState.podOwnerDepositShares, + initialState.podOwnerDepositShares - int256(withdrawalAmount), + "Pod owner shares should decrease by withdrawalAmount" + ); + } + + function testQueueWithdrawalsFailsWhenNotAdmin() public { + uint256[] memory nodeIds = createStakingNodes(1); + IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeIds[0]); + + uint256 withdrawalAmount = 1 ether; + vm.prank(address(0x1234567890123456789012345678901234567890)); + vm.expectRevert(StakingNode.NotStakingNodesWithdrawer.selector); + stakingNodeInstance.queueWithdrawals(withdrawalAmount); + } + + function testQueueWithdrawalsFailsWhenInsufficientBalance() public { + uint256[] memory nodeIds = createStakingNodes(1); + IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeIds[0]); + + uint256 withdrawalAmount = 100 ether; // Assuming this is more than the node's balance + vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); + vm.expectRevert(IEigenPodManagerErrors.SharesNegative.selector); + stakingNodeInstance.queueWithdrawals(withdrawalAmount); + } + + function testCompleteQueuedWithdrawalsWithMultipleValidators() public { + // Setup + uint256 validatorCount = 2; + uint256 depositAmount = 32 ether; + address user = vm.addr(156_737); + vm.deal(user, 1000 ether); + yneth.depositETH{value: depositAmount * validatorCount}(user); // Deposit for validators + + uint256[] memory nodeIds = createStakingNodes(1); + uint256 nodeId = nodeIds[0]; + IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeId); + + // Setup: Create multiple validators and verify withdrawal credentials + uint40[] memory validatorIndices = createValidators(repeat(nodeId, validatorCount), validatorCount); + beaconChain.advanceEpoch_NoRewards(); + registerValidators(repeat(nodeId, validatorCount)); + + beaconChain.advanceEpoch_NoRewards(); + + for (uint256 i = 0; i < validatorCount; i++) { + _verifyWithdrawalCredentials(nodeIds[0], validatorIndices[i]); + } + + beaconChain.advanceEpoch_NoRewards(); + + // Exit some validators + uint256 exitedValidatorCount = 1; + for (uint256 i = 0; i < exitedValidatorCount; i++) { + beaconChain.exitValidator(validatorIndices[i]); + } + + // Advance the beacon chain by one epoch without rewards + beaconChain.advanceEpoch_NoRewards(); + + // Start and verify checkpoint for all validators + startAndVerifyCheckpoint(nodeId, validatorIndices); + + // Queue withdrawals for exited validators + uint256 withdrawalAmount = 32 ether * exitedValidatorCount; + vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); + bytes32[] memory withdrawalRoots = stakingNodeInstance.queueWithdrawals(withdrawalAmount); + + // Capture initial state + StateSnapshot memory before = takeSnapshot(nodeIds[0]); + + _completeQueuedWithdrawals(withdrawalRoots, nodeIds[0], false); + + // Capture final state + StateSnapshot memory afterCompletion = takeSnapshot(nodeIds[0]); + + // Assertions + assertEq(afterCompletion.queuedShares, before.queuedShares - withdrawalAmount, "Queued shares should decrease"); + assertEq(afterCompletion.withdrawnETH, before.withdrawnETH + withdrawalAmount, "Withdrawn ETH should increase"); + assertEq(afterCompletion.podOwnerDepositShares, before.podOwnerDepositShares, "Pod owner shares should remain unchanged"); + assertEq( + afterCompletion.stakingNodeBalance, + before.stakingNodeBalance, + "Staking node balance should remain unchanged" + ); + } + + function testQueueWithdrawalsBeforeExitingAndVerifyingValidator() public { + uint256 validatorCount = 1; + uint256 depositAmount = 32 ether; + address user = vm.addr(156_737); + vm.deal(user, 1000 ether); + yneth.depositETH{value: depositAmount * validatorCount}(user); + + uint256 nodeId = createStakingNodes(1)[0]; + IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeId); + + // Create and register a validator + uint40[] memory validatorIndices = createValidators(repeat(nodeId, validatorCount), validatorCount); + beaconChain.advanceEpoch_NoRewards(); + registerValidators(repeat(nodeId, validatorCount)); + + beaconChain.advanceEpoch_NoRewards(); + + // Verify withdrawal credentials + _verifyWithdrawalCredentials(nodeId, validatorIndices[0]); + + beaconChain.advanceEpoch_NoRewards(); + + // Capture initial state + StateSnapshot memory before = takeSnapshot(nodeId); + + // Queue withdrawals before exiting the validator + uint256 withdrawalAmount = 32 ether; + vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); + bytes32[] memory _withdrawalRoots = stakingNodeInstance.queueWithdrawals(withdrawalAmount); + (IDelegationManagerTypes.Withdrawal memory _withdrawal, ) = delegationManager.getQueuedWithdrawal(_withdrawalRoots[0]); + uint256 _scaledShares = _withdrawal.scaledShares[0]; + + // Exit the validator + beaconChain.slashValidators(validatorIndices, BeaconChainMock.SlashType.Minor); + + beaconChain.advanceEpoch_NoRewards(); + + uint256 _beaconChainSlashingFactorBefore = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + assertEq(_beaconChainSlashingFactorBefore, 1e18, "_testQueueWithdrawalsBeforeExitingAndVerifyingValidator: E0"); + // Start and verify checkpoint + startAndVerifyCheckpoint(nodeId, validatorIndices); + + uint256 _beaconChainSlashingFactorAfter = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + assertLt(_beaconChainSlashingFactorAfter, 1e18, "_testQueueWithdrawalsBeforeExitingAndVerifyingValidator: E1"); + // Assert that podOwnerDepositShares are equal to negative slashingAmount + uint256 slashedAmount = beaconChain.MINOR_SLASH_AMOUNT_GWEI() * 1e9; + assertEq( + eigenPodManager.podOwnerDepositShares(address(stakingNodeInstance)), 0, + "Pod owner shares should not change" + ); + + vm.prank(actors.admin.STAKING_NODES_DELEGATOR); + stakingNodeInstance.syncQueuedShares(); + + _completeQueuedWithdrawals(_withdrawalRoots, nodeId, false); + + // Capture final state + StateSnapshot memory afterCompletion = takeSnapshot(nodeId); + + // Assertions + assertEq( + afterCompletion.withdrawnETH, + before.withdrawnETH + withdrawalAmount - slashedAmount, + "Withdrawn ETH should increase by the withdrawn amount" + ); + assertEq( + afterCompletion.podOwnerDepositShares, + before.podOwnerDepositShares - int256(withdrawalAmount), + "Pod owner shares should decrease by withdrawalAmount" + ); + assertEq( + afterCompletion.queuedShares, before.queuedShares, "Queued shares should decrease back to original value" + ); + assertEq( + afterCompletion.stakingNodeBalance, + before.stakingNodeBalance - slashedAmount, + "Staking node balance should remain unchanged" + ); + assertEq( + afterCompletion.withdrawnETH, + before.withdrawnETH + withdrawalAmount - slashedAmount, + "Total withdrawn amount should match the expected amount" + ); + } + +} \ No newline at end of file diff --git a/test/integration/ynEIGEN/TokenStakingNode.t.sol b/test/integration/ynEIGEN/TokenStakingNode.t.sol index 897d12ad7..385fa216e 100644 --- a/test/integration/ynEIGEN/TokenStakingNode.t.sol +++ b/test/integration/ynEIGEN/TokenStakingNode.t.sol @@ -7,7 +7,7 @@ import {IStrategyManager} from "lib/eigenlayer-contracts/src/contracts/interface import {IynEigen} from "src/interfaces/IynEigen.sol"; import {IPausable} from "lib/eigenlayer-contracts/src/contracts/interfaces/IPausable.sol"; import {IDelegationManager, IDelegationManagerTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; -import {ISignatureUtils} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {ISignatureUtilsMixinTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtilsMixin.sol"; import {TestAssetUtils} from "test/utils/TestAssetUtils.sol"; import {stdStorage, StdStorage} from "forge-std/Test.sol"; import {BytesLib} from "lib/eigenlayer-contracts/src/contracts/libraries/BytesLib.sol"; @@ -464,7 +464,7 @@ contract TokenStakingNodeDelegate is ynEigenIntegrationBaseTest { } function testTokenStakingNodeDelegate() public { - ISignatureUtils.SignatureWithExpiry memory signature; + ISignatureUtilsMixinTypes.SignatureWithExpiry memory signature; bytes32 approverSalt; vm.prank(actors.admin.STAKING_NODES_DELEGATOR); @@ -477,7 +477,7 @@ contract TokenStakingNodeDelegate is ynEigenIntegrationBaseTest { } function testTokenStakingNodeUndelegate() public { - ISignatureUtils.SignatureWithExpiry memory signature; + ISignatureUtilsMixinTypes.SignatureWithExpiry memory signature; bytes32 approverSalt; vm.prank(actors.admin.STAKING_NODES_DELEGATOR); @@ -513,7 +513,7 @@ contract TokenStakingNodeDelegate is ynEigenIntegrationBaseTest { // Delegate to operator1 vm.prank(actors.admin.STAKING_NODES_DELEGATOR); tokenStakingNodeInstance.delegate( - operator1, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator1, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); address delegatedOperator1 = delegationManager.delegatedTo(address(tokenStakingNodeInstance)); @@ -529,7 +529,7 @@ contract TokenStakingNodeDelegate is ynEigenIntegrationBaseTest { // Delegate to operator2 vm.prank(actors.admin.STAKING_NODES_DELEGATOR); tokenStakingNodeInstance.delegate( - operator2, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) + operator2, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0) ); address delegatedOperator2 = delegationManager.delegatedTo(address(tokenStakingNodeInstance)); @@ -562,7 +562,7 @@ contract TokenStakingNodeDelegate is ynEigenIntegrationBaseTest { assertEq(initialStrategyListLength, 2, "Initial strategy list length should be 2."); // Delegate to operator - ISignatureUtils.SignatureWithExpiry memory signature; + ISignatureUtilsMixinTypes.SignatureWithExpiry memory signature; bytes32 approverSalt; vm.prank(actors.admin.STAKING_NODES_DELEGATOR); tokenStakingNodeInstance.delegate(operator1, signature, approverSalt); @@ -643,7 +643,7 @@ contract TokenStakingNodeDelegate is ynEigenIntegrationBaseTest { assertEq(initialStrategyListLength, 2, "Initial strategy list length should be 2."); // Delegate to operator - ISignatureUtils.SignatureWithExpiry memory signature; + ISignatureUtilsMixinTypes.SignatureWithExpiry memory signature; bytes32 approverSalt; vm.prank(actors.admin.STAKING_NODES_DELEGATOR); tokenStakingNodeInstance.delegate(operator1, signature, approverSalt); @@ -710,7 +710,7 @@ contract TokenStakingNodeDelegate is ynEigenIntegrationBaseTest { assertEq(initialStrategyListLength, 2, "Initial strategy list length should be 2."); // Delegate to operator - ISignatureUtils.SignatureWithExpiry memory signature; + ISignatureUtilsMixinTypes.SignatureWithExpiry memory signature; bytes32 approverSalt; vm.prank(actors.admin.STAKING_NODES_DELEGATOR); tokenStakingNodeInstance.delegate(operator1, signature, approverSalt); @@ -833,7 +833,7 @@ contract TokenStakingNodeDelegate is ynEigenIntegrationBaseTest { } function testOperatorUndelegateTokenStakingNode() public { - ISignatureUtils.SignatureWithExpiry memory signature; + ISignatureUtilsMixinTypes.SignatureWithExpiry memory signature; bytes32 approverSalt; vm.prank(actors.admin.STAKING_NODES_DELEGATOR); @@ -925,7 +925,7 @@ contract TokenStakingNodeDelegate is ynEigenIntegrationBaseTest { assertEq(initialStrategyListLength, 2, "Initial strategy list length should be 2."); // Delegate to operator - ISignatureUtils.SignatureWithExpiry memory signature; + ISignatureUtilsMixinTypes.SignatureWithExpiry memory signature; bytes32 approverSalt; vm.prank(actors.admin.STAKING_NODES_DELEGATOR); tokenStakingNodeInstance.delegate(operator1, signature, approverSalt); @@ -1057,7 +1057,7 @@ contract TokenStakingNodeDelegate is ynEigenIntegrationBaseTest { assertEq(initialStrategyListLength, 2, "Initial strategy list length should be 2."); // Delegate to operator - ISignatureUtils.SignatureWithExpiry memory signature; + ISignatureUtilsMixinTypes.SignatureWithExpiry memory signature; bytes32 approverSalt; vm.prank(actors.admin.STAKING_NODES_DELEGATOR); tokenStakingNodeInstance.delegate(operator1, signature, approverSalt); @@ -1192,7 +1192,7 @@ contract TokenStakingNodeDelegate is ynEigenIntegrationBaseTest { assertEq(initialStrategyListLength, 2, "Initial strategy list length should be 2."); // Delegate to operator - ISignatureUtils.SignatureWithExpiry memory signature; + ISignatureUtilsMixinTypes.SignatureWithExpiry memory signature; bytes32 approverSalt; vm.prank(actors.admin.STAKING_NODES_DELEGATOR); tokenStakingNodeInstance.delegate(operator1, signature, approverSalt); diff --git a/test/mocks/MockAVS.sol b/test/mocks/MockAVS.sol new file mode 100644 index 000000000..1a93d6bdc --- /dev/null +++ b/test/mocks/MockAVS.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +contract MockAVS { + function registerOperator(address operator, address avs, uint32[] calldata operatorSetIds, bytes calldata data) external {} +} diff --git a/test/mocks/TestStakingNodeV2.sol b/test/mocks/TestStakingNodeV2.sol index 0a27fceed..a08a78d87 100644 --- a/test/mocks/TestStakingNodeV2.sol +++ b/test/mocks/TestStakingNodeV2.sol @@ -15,7 +15,7 @@ contract TestStakingNodeV2 is StakingNode { uint valueToBeInitialized; } - function initializeV4(ReInit memory reInit) public reinitializer(4) { + function initializeV5(ReInit memory reInit) public reinitializer(5) { valueToBeInitialized = reInit.valueToBeInitialized; } diff --git a/test/mocks/TestStakingNodesManagerV2.sol b/test/mocks/TestStakingNodesManagerV2.sol index ab746c396..43dda16d2 100644 --- a/test/mocks/TestStakingNodesManagerV2.sol +++ b/test/mocks/TestStakingNodesManagerV2.sol @@ -34,10 +34,13 @@ contract TestStakingNodesManagerV2 is StakingNodesManager { node.initializeV3(); } + if (initializedVersion == 3) { + node.initializeV4(); + } - if (initializedVersion == 3) { + if (initializedVersion == 4) { TestStakingNodeV2(payable(address(node))) - .initializeV4(TestStakingNodeV2.ReInit({valueToBeInitialized: 23})); + .initializeV5(TestStakingNodeV2.ReInit({valueToBeInitialized: 23})); } } diff --git a/test/scenarios/fork/ynETH/Base.t.sol b/test/scenarios/fork/ynETH/Base.t.sol index 0378a45d3..f5642c326 100644 --- a/test/scenarios/fork/ynETH/Base.t.sol +++ b/test/scenarios/fork/ynETH/Base.t.sol @@ -24,6 +24,7 @@ import {StakingNode} from "src/StakingNode.sol"; import {RewardsReceiver} from "src/RewardsReceiver.sol"; import {RewardsDistributor} from "src/RewardsDistributor.sol"; import {StakingNode} from "src/StakingNode.sol"; +import {HoleskyStakingNodesManager} from "src/HoleskyStakingNodesManager.sol"; import {WithdrawalQueueManager} from "src/WithdrawalQueueManager.sol"; import {ynETHRedemptionAssetsVault} from "src/ynETHRedemptionAssetsVault.sol"; import {IStakingNode} from "src/interfaces/IStakingNodesManager.sol"; @@ -42,6 +43,7 @@ contract Base is Test, Utils { ContractAddresses.ChainAddresses public chainAddresses; ActorAddresses public actorAddresses; ActorAddresses.Actors public actors; + ContractAddresses.ChainIds public chainIds; // Rewards RewardsReceiver public executionLayerReceiver; @@ -79,12 +81,21 @@ contract Base is Test, Utils { // On Mainnet only WithdrawalsProcessor has permission to run this, but the system is designed to run // them separately as well if needed. // Grant roles on StakingNodesManager for mainnet only - if (block.chainid == 1) { // Mainnet chain ID + if (block.chainid == chainIds.mainnet) { // Mainnet chain ID vm.startPrank(actors.admin.ADMIN); stakingNodesManager.grantRole(stakingNodesManager.WITHDRAWAL_MANAGER_ROLE(), actors.ops.WITHDRAWAL_MANAGER); stakingNodesManager.grantRole(stakingNodesManager.STAKING_NODES_WITHDRAWER_ROLE(), actors.ops.STAKING_NODES_WITHDRAWER); vm.stopPrank(); } + + // for(uint256 i = 0; i < stakingNodesManager.nodesLength(); i++) { + // vm.startPrank(actors.admin.STAKING_NODES_DELEGATOR); + // stakingNodesManager.nodes(i).syncQueuedShares(); + // vm.stopPrank(); + // } + upgradeStakingNodesManagerAndStakingNode(); + upgradeWithdrawalsProcessor(); + stakingNodesManager.updateTotalETHStaked(); } function assignContracts() internal { @@ -92,6 +103,7 @@ contract Base is Test, Utils { chainAddresses = contractAddresses.getChainAddresses(block.chainid); actorAddresses = new ActorAddresses(); actors = actorAddresses.getActors(block.chainid); + chainIds = contractAddresses.getChainIds(); // assign YieldNest addresses { @@ -123,18 +135,15 @@ contract Base is Test, Utils { } } - function upgradeStakingNodesManagerAndStakingNode() internal { + function upgradeStakingNodesManagerAndStakingNode() internal virtual { - // Upgrade StakingNode implementation - address newStakingNodeImpl = address(new StakingNode()); // Upgrade StakingNodesManager bytes memory initializeV3Data = abi.encodeWithSelector(stakingNodesManager.initializeV3.selector, chainAddresses.eigenlayer.REWARDS_COORDINATOR_ADDRESS); - address newStakingNodesManagerImpl = address(new StakingNodesManager()); - - uint256 totalAssetsBefore = yneth.totalAssets(); - + address newStakingNodesManagerImpl = address(new HoleskyStakingNodesManager()); + // commented here because totalAssets is broken in Holesky + // uint256 totalAssetsBefore = yneth.totalAssets(); vm.prank(actors.admin.PROXY_ADMIN_OWNER); ProxyAdmin(getTransparentUpgradeableProxyAdminAddress(address(stakingNodesManager))).upgradeAndCall( @@ -145,11 +154,28 @@ contract Base is Test, Utils { assertEq(address(stakingNodesManager.rewardsCoordinator()), chainAddresses.eigenlayer.REWARDS_COORDINATOR_ADDRESS, "rewardsCoordinator not set correctly after upgrade"); + // Upgrade StakingNode implementation + address newStakingNodeImpl = address(new StakingNode()); + + // Register new implementation vm.prank(actors.admin.STAKING_ADMIN); stakingNodesManager.upgradeStakingNodeImplementation(newStakingNodeImpl); - assertEq(yneth.totalAssets(), totalAssetsBefore, "totalAssets of ynETH changed after upgrade"); + // assertEq(yneth.totalAssets(), totalAssetsBefore, "totalAssets of ynETH changed after upgrade"); + } + + function upgradeWithdrawalsProcessor() internal { + + address newWithdrawalsProcessorImpl = address(new WithdrawalsProcessor()); + + vm.startPrank(actors.admin.PROXY_ADMIN_OWNER); + ProxyAdmin(getTransparentUpgradeableProxyAdminAddress(address(withdrawalsProcessor))).upgradeAndCall( + ITransparentUpgradeableProxy(address(withdrawalsProcessor)), + newWithdrawalsProcessorImpl, + "" + ); + vm.stopPrank(); } function createValidators(uint256[] memory nodeIds, uint256 count) public returns (uint40[] memory) { @@ -198,6 +224,18 @@ contract Base is Test, Utils { uint256[] memory previousStakingNodeBalances ) public { + for (uint i = 0; i < previousStakingNodeBalances.length; i++) { + IStakingNode stakingNodeInstance = stakingNodesManager.nodes(i); + vm.prank(actors.admin.STAKING_NODES_DELEGATOR); + stakingNodeInstance.synchronize(); + + uint256 currentStakingNodeBalance = stakingNodeInstance.getETHBalance(); + assertEq( + currentStakingNodeBalance, previousStakingNodeBalances[i], + string.concat("Staking node balance integrity check failed for node ID: ", vm.toString(i)) + ); + } + stakingNodesManager.updateTotalETHStaked(); assertEq(yneth.totalAssets(), previousTotalAssets, "Total assets integrity check failed"); assertEq(yneth.totalSupply(), previousTotalSupply, "Share mint integrity check failed"); @@ -207,14 +245,6 @@ contract Base is Test, Utils { stakingNodesManager.nodesLength(), "Number of staking nodes changed after upgrade" ); - for (uint i = 0; i < previousStakingNodeBalances.length; i++) { - IStakingNode stakingNodeInstance = stakingNodesManager.nodes(i); - uint256 currentStakingNodeBalance = stakingNodeInstance.getETHBalance(); - assertEq( - currentStakingNodeBalance, previousStakingNodeBalances[i], - string.concat("Staking node balance integrity check failed for node ID: ", vm.toString(i)) - ); - } } struct UpgradeState { diff --git a/test/scenarios/fork/ynETH/Delegation.t.sol b/test/scenarios/fork/ynETH/Delegation.t.sol index 26468c14b..e43ad4b12 100644 --- a/test/scenarios/fork/ynETH/Delegation.t.sol +++ b/test/scenarios/fork/ynETH/Delegation.t.sol @@ -14,12 +14,13 @@ contract YnETHDelegationScenarioTest is WithdrawalsScenarioTestBase { function test_undelegate_Scenario_undelegateByOperator() public { - // Log total assets before undelegation - uint256 totalAssetsBefore = yneth.totalAssets(); - IStakingNode stakingNode = stakingNodesManager.nodes(0); + vm.prank(actors.admin.STAKING_NODES_DELEGATOR); + stakingNode.synchronize(); + + uint256 totalAssetsBefore = yneth.totalAssets(); // Get initial ETH balance of staking node uint256 stakingNodeBalanceBefore = stakingNode.getETHBalance(); @@ -53,7 +54,7 @@ contract YnETHDelegationScenarioTest is WithdrawalsScenarioTestBase { // Call synchronize after verifying not synchronized vm.prank(actors.admin.STAKING_NODES_DELEGATOR); - stakingNode.synchronize(podSharesBefore, blockNumberBefore); + stakingNode.synchronize(); // Assert staking node balance remains unchanged after synchronization assertEq(stakingNodeBalanceBefore, stakingNode.getETHBalance(), "Staking node balance should not change after synchronization"); @@ -103,6 +104,9 @@ contract YnETHDelegationScenarioTest is WithdrawalsScenarioTestBase { uint256 podSharesBefore = signedPodSharesBefore < 0 ? 0 : uint256(signedPodSharesBefore); uint32 blockNumberBefore = uint32(block.number); + vm.prank(actors.admin.STAKING_NODES_DELEGATOR); + stakingNode.synchronize(); + // Call undelegate from delegator vm.startPrank(actors.admin.STAKING_NODES_DELEGATOR); stakingNode.undelegate(); @@ -121,10 +125,8 @@ contract YnETHDelegationScenarioTest is WithdrawalsScenarioTestBase { // Assert node is synchronized after undelegation assertTrue(stakingNode.isSynchronized(), "Node should be synchronized after undelegation"); - // Call synchronize after verifying synchronized - vm.expectRevert(StakingNode.AlreadySynchronized.selector); vm.prank(actors.admin.STAKING_NODES_DELEGATOR); - stakingNode.synchronize(podSharesBefore, blockNumberBefore); + stakingNode.synchronize(); // Assert staking node balance remains unchanged after synchronization assertEq(stakingNodeBalanceBefore, stakingNode.getETHBalance(), "Staking node balance should not change after synchronization"); diff --git a/test/scenarios/fork/ynETH/TestSlashingDeployment.t.sol b/test/scenarios/fork/ynETH/TestSlashingDeployment.t.sol new file mode 100644 index 000000000..1464be392 --- /dev/null +++ b/test/scenarios/fork/ynETH/TestSlashingDeployment.t.sol @@ -0,0 +1,445 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {StakingNodesManager} from "src/StakingNodesManager.sol"; +import {StakingNode} from "src/StakingNode.sol"; +import {WithdrawalsProcessor} from "src/WithdrawalsProcessor.sol"; +import {IStakingNode} from "src/interfaces/IStakingNode.sol"; +import {IStakingNodesManager} from "src/interfaces/IStakingNodesManager.sol"; +import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; +import {ProxyAdmin} from "lib/openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; +import { + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy +} from "lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IDelegationManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {DelegationManager} from "lib/eigenlayer-contracts/src/contracts/core/DelegationManager.sol"; +import {AllocationManager} from "lib/eigenlayer-contracts/src/contracts/core/AllocationManager.sol"; +import {EigenPodManager} from "lib/eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol"; +import {IPauserRegistry} from "lib/eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol"; +import {IBeacon} from "lib/eigenlayer-contracts/lib/openzeppelin-contracts-v4.9.0/contracts/proxy/beacon/IBeacon.sol"; +import {IPermissionController} from "lib/eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol"; +import {IETHPOSDeposit} from "lib/eigenlayer-contracts/src/contracts/interfaces/IETHPOSDeposit.sol"; +import {IStrategyManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; +import {IEigenPodManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPodManager.sol"; +import {IAllocationManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {Base} from "./Base.t.sol"; + +interface IOldEigenPodManager { + + function podOwnerShares( + address + ) external view returns (int256); + +} + +// Holesky slashing deployment test +contract SlashingDeploymentTest is Base { + + struct YnETHStateSnapshot { + uint256 totalAssets; + uint256 totalSupply; + uint256 totalStakingNodes; + uint256 totalDepositedInPool; + uint256 rate; + } + + struct StakingNodeStateSnapshot { + uint256 withdrawnETH; + uint256 unverifiedStakedETH; + uint256 queuedSharesAmount; + uint256 preELIP002QueuedSharesAmount; + int256 podOwnerDepositShares; + address delegatedTo; + uint256 ethBalance; + } + + modifier skipOnHolesky() { + vm.skip(_isHolesky(), "Impossible to test on Holesky"); + _; + } + + function _isHolesky() internal view returns (bool) { + return block.chainid == chainIds.holeksy; + } + + address public user = makeAddr("user"); + IPauserRegistry public pauserRegistry = IPauserRegistry(0x0c431C66F4dE941d089625E5B423D00707977060); + IStrategyManager public strategyManager = IStrategyManager(0x858646372CC42E1A627fcE94aa7A7033e7CF075A); + IETHPOSDeposit public ethposDeposit = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); + IBeacon public eigenPodBeacon = IBeacon(0x5a2a4F2F3C18f09179B6703e63D9eDD165909073); + string public version = "v1.3.0"; + + function setUp() public override { + super.assignContracts(); + deal(address(user), 100 ether); + } + + function test_depositBeforeEigenlayerSlashingDeployment() public skipOnHolesky { + vm.startPrank(user); + + stakingNodesManager.updateTotalETHStaked(); + YnETHStateSnapshot memory ynethStateSnapshotBefore = takeYnETHStateSnapshot(); + StakingNodeStateSnapshot[] memory stakingNodesStateSnapshotBefore = takeStakingNodesStateSnapshot(); + + uint256 depositAmount = 10 ether; + uint256 sharesBefore = yneth.balanceOf(address(this)); + yneth.depositETH{value: depositAmount}(address(this)); + uint256 sharesReceived = yneth.balanceOf(address(this)) - sharesBefore; + + // Take snapshots after deposit + stakingNodesManager.updateTotalETHStaked(); + YnETHStateSnapshot memory ynethStateSnapshotAfter = takeYnETHStateSnapshot(); + StakingNodeStateSnapshot[] memory stakingNodesStateSnapshotAfter = takeStakingNodesStateSnapshot(); + + assertEq( + ynethStateSnapshotAfter.totalAssets, + ynethStateSnapshotBefore.totalAssets + depositAmount, + "totalAssets not changed correctly" + ); + assertEq( + ynethStateSnapshotAfter.totalSupply, + ynethStateSnapshotBefore.totalSupply + sharesReceived, + "totalSupply not changed correctly" + ); + assertEq( + ynethStateSnapshotAfter.totalStakingNodes, + ynethStateSnapshotBefore.totalStakingNodes, + "totalStakingNodes changed" + ); + assertEq( + ynethStateSnapshotAfter.totalDepositedInPool, + ynethStateSnapshotBefore.totalDepositedInPool + depositAmount, + "totalDepositedInPool not changed correctly" + ); + assertEq(ynethStateSnapshotAfter.rate, ynethStateSnapshotBefore.rate, "rate changed"); + + for (uint256 i = 0; i < stakingNodesStateSnapshotBefore.length; i++) { + assertEq( + stakingNodesStateSnapshotBefore[i].withdrawnETH, + stakingNodesStateSnapshotAfter[i].withdrawnETH, + "withdrawnETH changed for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].unverifiedStakedETH, + stakingNodesStateSnapshotAfter[i].unverifiedStakedETH, + "unverifiedStakedETH changed for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].queuedSharesAmount, + stakingNodesStateSnapshotAfter[i].queuedSharesAmount, + "queuedSharesAmount wrong for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].preELIP002QueuedSharesAmount, + stakingNodesStateSnapshotAfter[i].preELIP002QueuedSharesAmount, + "queuedSharesAmount not changed to preELIP002QueuedSharesAmount for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].podOwnerDepositShares, + stakingNodesStateSnapshotAfter[i].podOwnerDepositShares, + "podOwnerDepositShares wrong for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].delegatedTo, + stakingNodesStateSnapshotAfter[i].delegatedTo, + "delegatedTo wrong for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].ethBalance, + stakingNodesStateSnapshotAfter[i].ethBalance, + "ethBalance wrong for staking node " + ); + } + } + + function test_depositAfterEigenlayerSlashingDeploymentAndBeforeUpgradeOfYnETH() public skipOnHolesky { + upgradeEigenlayerContracts(); + + vm.startPrank(user); + + // reverts because podOwnerShares function is not available due to eigenlayer contracts upgrade + vm.expectRevert(); + stakingNodesManager.updateTotalETHStaked(); + + YnETHStateSnapshot memory ynethStateSnapshotBefore = takeYnETHStateSnapshot(); + StakingNodeStateSnapshot[] memory stakingNodesStateSnapshotBefore = takeStakingNodesStateSnapshot(); + + uint256 depositAmount = 10 ether; + uint256 sharesBefore = yneth.balanceOf(address(this)); + yneth.depositETH{value: depositAmount}(address(this)); + uint256 sharesReceived = yneth.balanceOf(address(this)) - sharesBefore; + + YnETHStateSnapshot memory ynethStateSnapshotAfter = takeYnETHStateSnapshot(); + StakingNodeStateSnapshot[] memory stakingNodesStateSnapshotAfter = takeStakingNodesStateSnapshot(); + + assertEq( + ynethStateSnapshotAfter.totalAssets, + ynethStateSnapshotBefore.totalAssets + depositAmount, + "totalAssets not changed correctly" + ); + assertEq( + ynethStateSnapshotAfter.totalSupply, + ynethStateSnapshotBefore.totalSupply + sharesReceived, + "totalSupply not changed correctly" + ); + assertEq( + ynethStateSnapshotAfter.totalStakingNodes, + ynethStateSnapshotBefore.totalStakingNodes, + "totalStakingNodes changed" + ); + assertEq( + ynethStateSnapshotAfter.totalDepositedInPool, + ynethStateSnapshotBefore.totalDepositedInPool + depositAmount, + "totalDepositedInPool not changed correctly" + ); + assertEq(ynethStateSnapshotAfter.rate, ynethStateSnapshotBefore.rate, "rate changed"); + + for (uint256 i = 0; i < stakingNodesStateSnapshotBefore.length; i++) { + assertEq( + stakingNodesStateSnapshotBefore[i].withdrawnETH, + stakingNodesStateSnapshotAfter[i].withdrawnETH, + "withdrawnETH changed for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].unverifiedStakedETH, + stakingNodesStateSnapshotAfter[i].unverifiedStakedETH, + "unverifiedStakedETH changed for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].queuedSharesAmount, + stakingNodesStateSnapshotAfter[i].queuedSharesAmount, + "queuedSharesAmount wrong for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].preELIP002QueuedSharesAmount, + stakingNodesStateSnapshotAfter[i].preELIP002QueuedSharesAmount, + "queuedSharesAmount not changed to preELIP002QueuedSharesAmount for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].podOwnerDepositShares, + stakingNodesStateSnapshotAfter[i].podOwnerDepositShares, + "podOwnerDepositShares wrong for staking node after upgrade" + ); + assertEq( + stakingNodesStateSnapshotBefore[i].delegatedTo, + stakingNodesStateSnapshotAfter[i].delegatedTo, + "delegatedTo wrong for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].ethBalance, + stakingNodesStateSnapshotAfter[i].ethBalance, + "ethBalance wrong for staking node " + ); + assertEq(stakingNodesStateSnapshotBefore[i].ethBalance, 0, "ethBalance not reverted for staking node"); + } + } + + function test_depositAfterSlashingDeploymentByEigenlayerAfterUpgradeOfYnETH() public skipOnHolesky { + upgradeEigenlayerContracts(); + + + YnETHStateSnapshot memory ynethStateSnapshotBefore = takeYnETHStateSnapshot(); + StakingNodeStateSnapshot[] memory stakingNodesStateSnapshotBefore = takeStakingNodesStateSnapshot(); + + upgradeStakingNodesManagerAndStakingNode(); + + vm.startPrank(user); + + uint256 depositAmount = 10 ether; + uint256 sharesBefore = yneth.balanceOf(address(this)); + yneth.depositETH{value: depositAmount}(address(this)); + uint256 sharesReceived = yneth.balanceOf(address(this)) - sharesBefore; + + stakingNodesManager.updateTotalETHStaked(); + + YnETHStateSnapshot memory ynethStateSnapshotAfter = takeYnETHStateSnapshot(); + StakingNodeStateSnapshot[] memory stakingNodesStateSnapshotAfter = takeStakingNodesStateSnapshot(); + + assertEq( + ynethStateSnapshotAfter.totalAssets, + ynethStateSnapshotBefore.totalAssets + depositAmount, + "totalAssets not changed correctly" + ); + assertEq( + ynethStateSnapshotAfter.totalSupply, + ynethStateSnapshotBefore.totalSupply + sharesReceived, + "totalSupply not changed correctly" + ); + assertEq( + ynethStateSnapshotAfter.totalStakingNodes, + ynethStateSnapshotBefore.totalStakingNodes, + "totalStakingNodes changed" + ); + assertEq( + ynethStateSnapshotAfter.totalDepositedInPool, + ynethStateSnapshotBefore.totalDepositedInPool + depositAmount, + "totalDepositedInPool not changed correctly" + ); + assertEq(ynethStateSnapshotAfter.rate, ynethStateSnapshotBefore.rate, "rate changed"); + + for (uint256 i = 0; i < stakingNodesStateSnapshotBefore.length; i++) { + assertEq( + stakingNodesStateSnapshotBefore[i].withdrawnETH, + stakingNodesStateSnapshotAfter[i].withdrawnETH, + "withdrawnETH changed for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].unverifiedStakedETH, + stakingNodesStateSnapshotAfter[i].unverifiedStakedETH, + "unverifiedStakedETH changed for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].queuedSharesAmount, + stakingNodesStateSnapshotAfter[i].preELIP002QueuedSharesAmount, + "queuedSharesAmount not changed to preELIP002QueuedSharesAmount for staking node " + ); + assertEq( + stakingNodesStateSnapshotAfter[i].queuedSharesAmount, + 0, + "queuedSharesAmount not changed to 0 for staking node " + ); + assertEq( + stakingNodesStateSnapshotBefore[i].podOwnerDepositShares, + stakingNodesStateSnapshotAfter[i].podOwnerDepositShares, + "podOwnerDepositShares wrong for staking node after upgrade" + ); + assertEq( + stakingNodesStateSnapshotBefore[i].delegatedTo, + stakingNodesStateSnapshotAfter[i].delegatedTo, + "delegatedTo wrong for staking node " + ); + assertEq(stakingNodesStateSnapshotBefore[i].ethBalance, 0, "ethBalance didn't revert for staking node"); + assertGt(stakingNodesStateSnapshotAfter[i].ethBalance, 0, "ethBalance not reported correctly for staking node after upgrade"); + } + } + + function upgradeEigenlayerContracts() internal { + address oldEigenPodManagerImpl = getImplementationAddressOfTransparentUpgradeableProxy(address(eigenPodManager)); + address oldDelegationManagerImpl = + getImplementationAddressOfTransparentUpgradeableProxy(address(delegationManager)); + + address allocationManagerImpl = address( + new AllocationManager( + delegationManager, + pauserRegistry, + IPermissionController(address(0)), + 15 days, + 15 days, + version + ) + ); + + TransparentUpgradeableProxy allocationManagerProxy = new TransparentUpgradeableProxy( + allocationManagerImpl, + address(this), + abi.encodeWithSelector(AllocationManager.initialize.selector, address(this), false) + ); + + DelegationManager newDelegationManagerImpl = new DelegationManager( + strategyManager, + eigenPodManager, + IAllocationManager(address(allocationManagerProxy)), + pauserRegistry, + IPermissionController(address(0)), + 14 days, + version + ); + + EigenPodManager newEigenPodManagerImpl = new EigenPodManager( + ethposDeposit, + eigenPodBeacon, + delegationManager, + pauserRegistry, + version + ); + + vm.etch(oldDelegationManagerImpl, address(newDelegationManagerImpl).code); + vm.etch(oldEigenPodManagerImpl, address(newEigenPodManagerImpl).code); + } + + function getImplementationAddressOfTransparentUpgradeableProxy( + address proxy + ) internal view returns (address) { + bytes32 IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + return address(uint160(uint256(vm.load(proxy, IMPLEMENTATION_SLOT)))); + } + + function upgradeStakingNodesManagerAndStakingNode() internal override { + address newStakingNodesManagerImpl = address(new StakingNodesManager()); + + vm.prank(actors.admin.PROXY_ADMIN_OWNER); + ProxyAdmin(getTransparentUpgradeableProxyAdminAddress(address(stakingNodesManager))).upgradeAndCall( + ITransparentUpgradeableProxy(address(stakingNodesManager)), newStakingNodesManagerImpl, "" + ); + + // Upgrade StakingNode implementation + address newStakingNodeImpl = address(new StakingNode()); + + // Register new implementation + vm.prank(actors.admin.STAKING_ADMIN); + stakingNodesManager.upgradeStakingNodeImplementation(newStakingNodeImpl); + } + + function takeYnETHStateSnapshot() internal view returns (YnETHStateSnapshot memory) { + uint256 rate; + try yneth.convertToAssets(1 ether) returns (uint256 _rate) { + rate = _rate; + } catch { + rate = 0; + } + + return YnETHStateSnapshot({ + totalAssets: yneth.totalAssets(), + totalSupply: yneth.totalSupply(), + totalStakingNodes: stakingNodesManager.nodesLength(), + totalDepositedInPool: yneth.totalDepositedInPool(), + rate: rate + }); + } + + function takeStakingNodesStateSnapshot() internal view returns (StakingNodeStateSnapshot[] memory) { + uint256 nodeCount = stakingNodesManager.nodesLength(); + StakingNodeStateSnapshot[] memory stakingNodeStateSnapshot = new StakingNodeStateSnapshot[](nodeCount); + for (uint256 i = 0; i < nodeCount; i++) { + uint256 preELIP002QueuedSharesAmount; + int256 podOwnerDepositShares; + address delegatedTo; + uint256 ethBalance; + + // wrapping in try catch because preELIP002QueuedSharesAmount function won't be available before the upgrade + try stakingNodesManager.nodes(i).preELIP002QueuedSharesAmount() returns (uint256 _preELIP002QueuedSharesAmount) { + preELIP002QueuedSharesAmount = _preELIP002QueuedSharesAmount; + } catch { + preELIP002QueuedSharesAmount = 0; + } + + try eigenPodManager.podOwnerDepositShares(address(stakingNodesManager.nodes(i))) returns ( + int256 _podOwnerDepositShares + ) { + podOwnerDepositShares = _podOwnerDepositShares; + } catch { + podOwnerDepositShares = + IOldEigenPodManager(address(eigenPodManager)).podOwnerShares(address(stakingNodesManager.nodes(i))); + } + + try stakingNodesManager.nodes(i).getETHBalance() returns (uint256 _ethBalance) { + ethBalance = _ethBalance; + } catch { + ethBalance = 0; + } + + stakingNodeStateSnapshot[i] = StakingNodeStateSnapshot({ + withdrawnETH: stakingNodesManager.nodes(i).getWithdrawnETH(), + unverifiedStakedETH: stakingNodesManager.nodes(i).unverifiedStakedETH(), + queuedSharesAmount: stakingNodesManager.nodes(i).getQueuedSharesAmount(), + preELIP002QueuedSharesAmount: preELIP002QueuedSharesAmount, + podOwnerDepositShares: podOwnerDepositShares, + delegatedTo: delegatedTo, + ethBalance: ethBalance + }); + } + return stakingNodeStateSnapshot; + } + +} diff --git a/test/scenarios/fork/ynETH/Withdrawals.t.sol b/test/scenarios/fork/ynETH/Withdrawals.t.sol index 3a6fd7d07..33eed5490 100644 --- a/test/scenarios/fork/ynETH/Withdrawals.t.sol +++ b/test/scenarios/fork/ynETH/Withdrawals.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BSD 3-Clause License pragma solidity ^0.8.24; -import {IEigenPod} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPod.sol"; +import {IEigenPod, IEigenPodErrors} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPod.sol"; import {IStakingNode} from "src/interfaces/IStakingNode.sol"; import {IStakingNodesManager} from "src/interfaces/IStakingNodesManager.sol"; @@ -213,10 +213,8 @@ contract M3WithdrawalsTest is Base { // complete queued withdrawals { - uint256[] memory _middlewareTimesIndexes = new uint256[](1); - _middlewareTimesIndexes[0] = 0; vm.startPrank(actors.ops.STAKING_NODES_WITHDRAWER); - stakingNodesManager.nodes(nodeId).completeQueuedWithdrawals(_withdrawals, _middlewareTimesIndexes); + stakingNodesManager.nodes(nodeId).completeQueuedWithdrawals(_withdrawals); vm.stopPrank(); // check that queuedSharesAmount is 0, and withdrawnETH is 32 ETH (AMOUNT), and staking pod balance is 32 ETH (AMOUNT) @@ -321,14 +319,14 @@ contract M3WithdrawalsTest is Base { function _testStartCheckpoint() internal { IStakingNode _node = stakingNodesManager.nodes(nodeId); - vm.expectRevert("EigenPod._startCheckpoint: must finish previous checkpoint before starting another"); + vm.expectRevert(IEigenPodErrors.CheckpointAlreadyActive.selector); vm.prank(actors.ops.STAKING_NODES_OPERATOR); _node.startCheckpoint(true); } function _testVerifyCheckpointsBeforeWithdrawalRequest() internal { - IEigenPod.Checkpoint memory _checkpoint = stakingNodesManager.nodes(nodeId).eigenPod().currentCheckpoint(); - assertEq(_checkpoint.proofsRemaining, 0, "_testVerifyCheckpointsBeforeWithdrawalRequest: E0"); + uint256 eigenPodCurrentCheckPointTimestamp = stakingNodesManager.nodes(nodeId).eigenPod().currentCheckpointTimestamp(); + assertEq(eigenPodCurrentCheckPointTimestamp, 0, "_testVerifyCheckpointsBeforeWithdrawalRequest: E0"); assertApproxEqAbs(uint256(eigenPodManager.podOwnerDepositShares(address(stakingNodesManager.nodes(nodeId)))), AMOUNT, 1000000000, "_testVerifyCheckpointsBeforeWithdrawalRequest: E1"); } @@ -337,8 +335,8 @@ contract M3WithdrawalsTest is Base { } function _testVerifyCheckpointsAfterWithdrawalRequest() internal { - IEigenPod.Checkpoint memory _checkpoint = stakingNodesManager.nodes(nodeId).eigenPod().currentCheckpoint(); - assertEq(_checkpoint.proofsRemaining, 0, "_testVerifyCheckpointsAfterWithdrawalRequest: E0"); + uint256 eigenPodCurrentCheckPointTimestamp = stakingNodesManager.nodes(nodeId).eigenPod().currentCheckpointTimestamp(); + assertEq(eigenPodCurrentCheckPointTimestamp, 0, "_testVerifyCheckpointsAfterWithdrawalRequest: E0"); assertEq(uint256(eigenPodManager.podOwnerDepositShares(address(stakingNodesManager.nodes(nodeId)))), 1000000000, "_testVerifyCheckpointsAfterWithdrawalRequest: E1"); } diff --git a/test/scenarios/fork/ynETH/WithdrawalsScenarioTestBase.sol b/test/scenarios/fork/ynETH/WithdrawalsScenarioTestBase.sol index 5885c422c..9f305b51f 100644 --- a/test/scenarios/fork/ynETH/WithdrawalsScenarioTestBase.sol +++ b/test/scenarios/fork/ynETH/WithdrawalsScenarioTestBase.sol @@ -62,11 +62,8 @@ contract WithdrawalsScenarioTestBase is Base { // complete queued withdrawals { - uint256[] memory _middlewareTimesIndexes = new uint256[](_withdrawals.length); - // all is zeroed out by defailt - _middlewareTimesIndexes[0] = 0; vm.startPrank(actors.ops.STAKING_NODES_WITHDRAWER); - stakingNodesManager.nodes(nodeId).completeQueuedWithdrawals(_withdrawals, _middlewareTimesIndexes); + stakingNodesManager.nodes(nodeId).completeQueuedWithdrawals(_withdrawals); vm.stopPrank(); } } @@ -92,11 +89,8 @@ contract WithdrawalsScenarioTestBase is Base { // complete queued withdrawals { - uint256[] memory _middlewareTimesIndexes = new uint256[](_withdrawals.length); - // all is zeroed out by defailt - _middlewareTimesIndexes[0] = 0; vm.startPrank(actors.admin.STAKING_NODES_DELEGATOR); - stakingNodesManager.nodes(nodeId).completeQueuedWithdrawalsAsShares(_withdrawals, _middlewareTimesIndexes); + stakingNodesManager.nodes(nodeId).completeQueuedWithdrawalsAsShares(_withdrawals); vm.stopPrank(); } } @@ -127,12 +121,10 @@ contract WithdrawalsScenarioTestBase is Base { { - uint256[] memory _middlewareTimesIndexes = new uint256[](_withdrawals.length); vm.prank(actors.ops.WITHDRAWAL_MANAGER); withdrawalsProcessor.completeAndProcessWithdrawalsForNode( withdrawalAction, - _withdrawals, - _middlewareTimesIndexes + _withdrawals ); } } diff --git a/test/scenarios/fork/ynETH/WithdrawalsWithRewards-Scenario.t.sol b/test/scenarios/fork/ynETH/WithdrawalsWithRewards-Scenario.t.sol index fcfd929b0..202b8b031 100644 --- a/test/scenarios/fork/ynETH/WithdrawalsWithRewards-Scenario.t.sol +++ b/test/scenarios/fork/ynETH/WithdrawalsWithRewards-Scenario.t.sol @@ -11,6 +11,7 @@ import {BeaconChainMock, BeaconChainProofs, CheckpointProofs, CredentialProofs, import {Utils} from "script/Utils.sol"; import {ContractAddresses} from "script/ContractAddresses.sol"; import {ActorAddresses} from "script/Actors.sol"; +import {BeaconChainMock} from "lib/eigenlayer-contracts/src/test/integration/mocks/BeaconChainMock.t.sol"; import {WithdrawalsScenarioTestBase} from "./WithdrawalsScenarioTestBase.sol"; @@ -575,7 +576,7 @@ contract M3WithdrawalsWithRewardsTest is WithdrawalsScenarioTestBase { uint256 withdrawnAmount = amount; // NOTE: This triggers the a exit of all validators - beaconChain.slashValidators(validatorIndices); + beaconChain.slashValidators(validatorIndices, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch(); // queue withdrawals @@ -593,7 +594,7 @@ contract M3WithdrawalsWithRewardsTest is WithdrawalsScenarioTestBase { IEigenPod pod = stakingNodesManager.nodes(nodeId).eigenPod(); uint64 withdrawableGwei = pod.withdrawableRestakedExecutionLayerGwei(); - uint256 totalSlashAmount = beaconChain.SLASH_AMOUNT_GWEI() * state.validatorCount * 1e9; + uint256 totalSlashAmount = beaconChain.MINOR_SLASH_AMOUNT_GWEI() * state.validatorCount * 1e9; state.totalAssetsBefore = state.totalAssetsBefore + accumulatedRewards - totalSlashAmount; state.stakingNodeBalancesBefore[nodeId] = state.stakingNodeBalancesBefore[nodeId] + accumulatedRewards - totalSlashAmount; runSystemStateInvariants(state.totalAssetsBefore, state.totalSupplyBefore, state.stakingNodeBalancesBefore); @@ -616,7 +617,7 @@ contract M3WithdrawalsWithRewardsTest is WithdrawalsScenarioTestBase { uint256 withdrawnAmount = amount; // NOTE: This triggers the a exit of all validators - beaconChain.slashValidators(validatorIndices); + beaconChain.slashValidators(validatorIndices, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch(); // queue withdrawals @@ -630,7 +631,7 @@ contract M3WithdrawalsWithRewardsTest is WithdrawalsScenarioTestBase { startAndVerifyCheckpoint(nodeId, state); - uint256 totalSlashAmount = beaconChain.SLASH_AMOUNT_GWEI() * state.validatorCount * 1e9; + uint256 totalSlashAmount = beaconChain.MINOR_SLASH_AMOUNT_GWEI() * state.validatorCount * 1e9; state.totalAssetsBefore = state.totalAssetsBefore - totalSlashAmount; state.stakingNodeBalancesBefore[nodeId] = state.stakingNodeBalancesBefore[nodeId] - totalSlashAmount; runSystemStateInvariants(state.totalAssetsBefore, state.totalSupplyBefore, state.stakingNodeBalancesBefore); @@ -685,20 +686,20 @@ contract M3WithdrawalsWithRewardsTest is WithdrawalsScenarioTestBase { // Trigger slashing of validators after withdrawals were queued // NOTE: This triggers the a exit of all validators - beaconChain.slashValidators(validatorIndices); + beaconChain.slashValidators(validatorIndices, BeaconChainMock.SlashType.Minor); beaconChain.advanceEpoch(); startAndVerifyCheckpoint(nodeId, state); - uint256 totalSlashAmount = beaconChain.SLASH_AMOUNT_GWEI() * state.validatorCount * 1e9; + uint256 totalSlashAmount = beaconChain.MINOR_SLASH_AMOUNT_GWEI() * state.validatorCount * 1e9; state.totalAssetsBefore = state.totalAssetsBefore - totalSlashAmount; state.stakingNodeBalancesBefore[nodeId] = state.stakingNodeBalancesBefore[nodeId] - totalSlashAmount; runSystemStateInvariants(state.totalAssetsBefore, state.totalSupplyBefore, state.stakingNodeBalancesBefore); - // check podOwnerShares are now negative becaose validators were slashed after withdrawals were queued up + // Assert that the node's podOwnerShares is 0 and not negative assertEq( eigenPodManager.podOwnerDepositShares(address(stakingNodesManager.nodes(nodeId))), - 0 - int256(totalSlashAmount), + 0, "Node's podOwnerShares should be 0 after completing withdrawals" ); diff --git a/test/scenarios/ynEIGEN/Delegation.t.sol b/test/scenarios/ynEIGEN/Delegation.t.sol index 65dde4ada..7c09aa9b8 100644 --- a/test/scenarios/ynEIGEN/Delegation.t.sol +++ b/test/scenarios/ynEIGEN/Delegation.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; import {IDelegationManagerTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; -import {ISignatureUtils} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {ISignatureUtilsMixinTypes} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtilsMixin.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {TransparentUpgradeableProxy,ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; @@ -36,7 +36,7 @@ contract YnEigenDelegationScenarioTest is ynLSDeScenarioBaseTest { tokenStakingNode = tokenStakingNodesManager.nodes(0); vm.prank(actors.admin.TOKEN_STAKING_NODES_DELEGATOR); - tokenStakingNode.delegate(actors.ops.TOKEN_STAKING_NODE_OPERATOR, ISignatureUtils.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0)); + tokenStakingNode.delegate(actors.ops.TOKEN_STAKING_NODE_OPERATOR, ISignatureUtilsMixinTypes.SignatureWithExpiry({signature: "", expiry: 0}), bytes32(0)); } function test_undelegate_Scenario_undelegateByOperator1() public {