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/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/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..e240b023b 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("Legacy queued shares for node ", vm.toString(i), ": ", vm.toString(stakingNodes[i].legacyQueuedSharesAmount()), " wei (", vm.toString(stakingNodes[i].legacyQueuedSharesAmount() / 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/StakingNode.sol b/src/StakingNode.sol index ee9ea95be..bf9c35cc2 100644 --- a/src/StakingNode.sol +++ b/src/StakingNode.sol @@ -16,6 +16,7 @@ import {IEigenPodManager} from "lib/eigenlayer-contracts/src/contracts/interface 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 upgrade + */ + uint256 public legacyQueuedSharesAmount; + + /** + * @dev Maps a withdrawal root to the amount of shares that can be withdrawn. + * This is used to track the amount of withdrawable shares that are queued for withdrawal. + */ + mapping(bytes32 withdrawalRoot => uint256 withdrawableShares) public withdrawableSharesForWithdrawalRoot; + + /** + * @dev Allows only a whitelisted address to configure the contract */ modifier onlyOperator() { if (!stakingNodesManager.isStakingNodesOperator(msg.sender)) revert NotStakingNodesOperator(); @@ -174,8 +191,14 @@ 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)); + legacyQueuedSharesAmount = queuedSharesAmount; } + + // TODO: commented for holesky deployment. Uncomment for mainnet deployment. + // function initializeV4() external onlyStakingNodesManager reinitializer(4) { + // legacyQueuedSharesAmount = queuedSharesAmount; + // } //-------------------------------------------------------------------------------------- //---------------------------------- EIGENPOD CREATION ------------------------------ @@ -270,7 +293,7 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea ISignatureUtils.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 +307,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 +338,42 @@ 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(); + queuedSharesAmount = 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]; + withdrawableSharesForWithdrawalRoot[withdrawalRoot] = withdrawableShares; + queuedSharesAmount += withdrawableShares; + } + + emit QueuedSharesSynced(queuedSharesAmount + legacyQueuedSharesAmount); + } + //-------------------------------------------------------------------------------------- //---------------------------------- 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 +382,41 @@ 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 beaconChainSlashingFactor; + uint256 withdrawableShares; + bytes32[] memory fullWithdrawalRoots; + IEigenPodManager eigenPodManager = IEigenPodManager(IStakingNodesManager(stakingNodesManager).eigenPodManager()); - fullWithdrawalRoots = delegationManager.queueWithdrawals(params); + if (delegatedTo == address(0)) { + beaconChainSlashingFactor = eigenPodManager.beaconChainSlashingFactor(address(this)); + fullWithdrawalRoots = delegationManager.queueWithdrawals(params); + IDelegationManagerTypes.Withdrawal memory withdrawal = delegationManager.getQueuedWithdrawal(fullWithdrawalRoots[0]); + uint256 scaledShares = withdrawal.scaledShares[0]; + withdrawableShares = scaledShares.mulWad(beaconChainSlashingFactor); + } else { + uint256[] memory operatorSharesBefore = delegationManager.getOperatorShares(delegatedTo, strategies); - // After running queueWithdrawals, eigenPodManager.podOwnerShares(address(this)) decreases by `sharesAmount`. - // Therefore queuedSharesAmount increase by `sharesAmount`. + fullWithdrawalRoots = delegationManager.queueWithdrawals(params); - queuedSharesAmount += sharesAmount; - emit QueuedWithdrawals(sharesAmount, fullWithdrawalRoots); + uint256[] memory operatorSharesAfter = delegationManager.getOperatorShares(delegatedTo, strategies); + + withdrawableShares = operatorSharesBefore[0] - operatorSharesAfter[0]; + } + + // After running queueWithdrawals, eigenPodManager.getWithdrawableShares(address(this)) decreases by `withdrawableShares`. + // Therefore queuedSharesAmount increase by `withdrawableShares`. + queuedSharesAmount += withdrawableShares; + withdrawableSharesForWithdrawalRoot[fullWithdrawalRoots[0]] = withdrawableShares; + emit QueuedWithdrawals(depositSharesAmount, fullWithdrawalRoots); + + return fullWithdrawalRoots; } /** @@ -361,18 +426,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 +450,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 += _decreaseQueuedSharesOnCompleteQueuedWithdrawalAndGetTotalWithdrawableShare(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 +469,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 +505,49 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea } tokens[i] = new IERC20V4[](1); receiveAsTokens[i] = false; - totalWithdrawalAmount += withdrawals[i].scaledShares[0]; + + totalWithdrawableShares += _decreaseQueuedSharesOnCompleteQueuedWithdrawalAndGetTotalWithdrawableShare(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); + } - emit CompletedQueuedWithdrawals(withdrawals, totalWithdrawalAmount, 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 totalWithdrawableShareForWithdrawalRoot The total withdrawable shares for the withdrawal root + */ + function _decreaseQueuedSharesOnCompleteQueuedWithdrawalAndGetTotalWithdrawableShare( + IDelegationManager delegationManager, + IDelegationManager.Withdrawal calldata withdrawal + ) internal returns (uint256) { + uint256 totalWithdrawableShareForWithdrawalRoot = 0; + bytes32 withdrawalRoot = delegationManager.calculateWithdrawalRoot(withdrawal); + uint256 withdrawableSharesForWithdrawalRootValue = withdrawableSharesForWithdrawalRoot[withdrawalRoot]; + + if (withdrawableSharesForWithdrawalRootValue != 0) { + // If the withdrawableSharesForWithdrawalRootValue are not 0, that means withdrawal root exists in withdrawableSharesForWithdrawalRoot + // which means it was queued after the upgrade + // so we need to subtract the shares from queuedSharesAmount and set the withdrawableSharesForWithdrawalRoot to 0 + totalWithdrawableShareForWithdrawalRoot = withdrawableSharesForWithdrawalRootValue; + withdrawableSharesForWithdrawalRoot[withdrawalRoot] = 0; + queuedSharesAmount -= withdrawableSharesForWithdrawalRootValue; + } else { + // If the withdrawableSharesForWithdrawalRootValue are 0, that means withdrawal root doesn't exist in withdrawableSharesForWithdrawalRoot + // which means it was queued before the upgrade + // so we need to subtract the shares from legacyQueuedSharesAmount and queuedSharesAmount + totalWithdrawableShareForWithdrawalRoot = withdrawal.scaledShares[0]; + legacyQueuedSharesAmount -= withdrawal.scaledShares[0]; + } + return totalWithdrawableShareForWithdrawalRoot; } //-------------------------------------------------------------------------------------- @@ -468,50 +560,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(); } //-------------------------------------------------------------------------------------- @@ -552,18 +615,26 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea 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; - + // 3. queuedSharesAmount: Shares queued for withdrawal that can be withdrawn after accounting for slashing (1 share = 1 ETH) + // 4. legacyQueuedSharesAmount: Shares queued for withdrawal that were queued before the upgrade (1 share = 1 ETH) + // 5. beaconChainETHStrategyWithdrawableShares: Active shares in Eigenlayer, representing staked ETH that can be withdrawn after accounting for slashing + int256 totalETHBalance = + int256(withdrawnETH + unverifiedStakedETH + queuedSharesAmount + legacyQueuedSharesAmount + beaconChainETHStrategyWithdrawableShares); + + if (totalETHBalance < 0) { + return 0; + } return uint256(totalETHBalance); } diff --git a/src/StakingNodesManager.sol b/src/StakingNodesManager.sol index d8e508aae..d1d18fefc 100644 --- a/src/StakingNodesManager.sol +++ b/src/StakingNodesManager.sol @@ -250,13 +250,14 @@ contract StakingNodesManager is ) external 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++) { - updatedTotalETHStaked += _nodes[i].getETHBalance(); - } - emit TotalETHStakedUpdated(updatedTotalETHStaked); - totalETHStaked = updatedTotalETHStaked; + // TODO: commenting this for now because getETHBalance() is not available in current deployed version of stakingNode on holesky + // uint256 updatedTotalETHStaked = 0; + // IStakingNode[] memory _nodes = getAllNodes(); + // for (uint256 i = 0; i < _nodes.length; i++) { + // updatedTotalETHStaked += _nodes[i].getETHBalance(); + // } + // emit TotalETHStakedUpdated(updatedTotalETHStaked); + // totalETHStaked = updatedTotalETHStaked; } receive() external payable { @@ -470,6 +471,11 @@ contract StakingNodesManager is node.initializeV3(); initializedVersion = node.getInitializedVersion(); } + // TODO: commented for holesky deployment. Uncomment for mainnet deployment. + // 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. @@ -665,10 +671,6 @@ contract StakingNodesManager is uint256 updatedTotalETHStaked = 0; IStakingNode[] memory allNodes = getAllNodes(); for (uint256 i = 0; i < allNodes.length; i++) { - if (!allNodes[i].isSynchronized()) { - revert NodeNotSynchronized(); - } - 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..7dc2b7e49 100644 --- a/src/interfaces/IStakingNode.sol +++ b/src/interfaces/IStakingNode.sol @@ -8,14 +8,6 @@ import {IEigenPod} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEige import {ISignatureUtils} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.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. @@ -56,6 +48,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 legacyQueuedSharesAmount() 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 +73,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 +89,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/test/integration/RewardsReceiver.t.sol b/test/integration/RewardsReceiver.t.sol index 31fb541e0..48f3effaa 100644 --- a/test/integration/RewardsReceiver.t.sol +++ b/test/integration/RewardsReceiver.t.sol @@ -18,8 +18,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 testFailTransferETHNotWithrdrawerRole() public { diff --git a/test/integration/StakingNode.t.sol b/test/integration/StakingNode.t.sol index 118fa1b22..7ee98cb08 100644 --- a/test/integration/StakingNode.t.sol +++ b/test/integration/StakingNode.t.sol @@ -8,7 +8,6 @@ import {IPausable} from "lib/eigenlayer-contracts/src/contracts/interfaces/IPaus 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"; @@ -19,6 +18,8 @@ import {EigenPodManager} from "lib/eigenlayer-contracts/src/contracts/pods/Eigen 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 {IEigenPodErrors} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPod.sol"; +import {IEigenPodManagerErrors} from "lib/eigenlayer-contracts/src/contracts/interfaces/IEigenPodManager.sol"; import {TransparentUpgradeableProxy} from "lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import { @@ -32,6 +33,12 @@ import {Utils} from "script/Utils.sol"; import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; import {StakingNodeTestBase, IEigenPodSimplified} from "./StakingNodeTestBase.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"; +import {console} from "forge-std/console.sol"; contract StakingNodeEigenPod is StakingNodeTestBase { @@ -53,10 +60,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 @@ -245,11 +254,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 { @@ -409,7 +418,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 +432,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(); @@ -510,7 +516,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 +530,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(); @@ -626,7 +629,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"); @@ -656,11 +659,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(); @@ -867,9 +867,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 +911,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 +952,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 +1046,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 +1093,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,13 +1139,15 @@ 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" ); } } @@ -1153,6 +1156,8 @@ contract StakingNodeVerifyWithdrawalCredentials is StakingNodeTestBase { contract StakingNodeWithdrawals is StakingNodeTestBase { + using SlashingLib for *; + function testQueueWithdrawals() public { // Setup uint256 depositAmount = 32 ether; @@ -1218,6 +1223,95 @@ contract StakingNodeWithdrawals is StakingNodeTestBase { ); } + 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); + + 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.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 testQueueWithdrawalsFailsWhenNotAdmin() public { uint256[] memory nodeIds = createStakingNodes(1); IStakingNode stakingNodeInstance = stakingNodesManager.nodes(nodeIds[0]); @@ -1234,7 +1328,7 @@ contract StakingNodeWithdrawals is StakingNodeTestBase { 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"); + vm.expectRevert(IEigenPodManagerErrors.SharesNegative.selector); stakingNodeInstance.queueWithdrawals(withdrawalAmount); } @@ -1278,14 +1372,12 @@ contract StakingNodeWithdrawals is StakingNodeTestBase { // Queue withdrawals for exited validators uint256 withdrawalAmount = 32 ether * exitedValidatorCount; vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); - stakingNodeInstance.queueWithdrawals(withdrawalAmount); + bytes32[] memory withdrawalRoots = 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]); + _completeQueuedWithdrawals(withdrawalRoots, nodeIds[0], false); // Capture final state StateSnapshot memory afterCompletion = takeSnapshot(nodeIds[0]); @@ -1301,15 +1393,17 @@ contract StakingNodeWithdrawals is StakingNodeTestBase { ); } - function testCompleteQueuedWithdrawalsWithSlashedValidators() public { + 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: depositAmount * validatorCount}(user); // Deposit for validators + yneth.depositETH{value: totalDepositedAmount}(user); // Deposit for validators } uint256 nodeId = createStakingNodes(1)[0]; @@ -1334,6 +1428,7 @@ contract StakingNodeWithdrawals is StakingNodeTestBase { for (uint256 i = 0; i < slashedValidatorCount; i++) { slashedValidators[i] = validatorIndices[i]; } + beaconChain.slashValidators(slashedValidators); beaconChain.advanceEpoch_NoRewards(); @@ -1358,39 +1453,229 @@ contract StakingNodeWithdrawals is StakingNodeTestBase { // Queue withdrawals for all validators vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); - stakingNodeInstance.queueWithdrawals(withdrawalAmount); + bytes32[] memory withdrawalRoots = stakingNodeInstance.queueWithdrawals(withdrawalAmount); - QueuedWithdrawalInfo[] memory queuedWithdrawals = new QueuedWithdrawalInfo[](1); - queuedWithdrawals[0] = QueuedWithdrawalInfo({withdrawnAmount: withdrawalAmount}); - _completeQueuedWithdrawals(queuedWithdrawals, nodeId); + _completeQueuedWithdrawals(withdrawalRoots, nodeId, false); // Capture final state StateSnapshot memory afterCompletion = takeSnapshot(nodeId); - - uint256 slashedAmount = slashedValidatorCount * (beaconChain.SLASH_AMOUNT_GWEI() * 1e9); + uint256 _beaconChainSlashingFactor = eigenPodManager.beaconChainSlashingFactor(address(stakingNodeInstance)); + uint256 expectedWithdrawalAmount = withdrawalAmount.mulWad(_beaconChainSlashingFactor); + uint256 slashedAmount = totalDepositedAmount - totalDepositedAmount.mulWad(_beaconChainSlashingFactor); // Assertions assertEq( afterCompletion.withdrawnETH, - before.withdrawnETH + withdrawalAmount, - "Withdrawn ETH should increase by the withdrawn amount" + before.withdrawnETH + expectedWithdrawalAmount, + "Withdrawn ETH should increase by the expected withdrawal 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" + before.podOwnerDepositShares - int256(withdrawalAmount), + "Pod owner shares should decrease by withdrawalAmount" ); assertEq( - afterCompletion.stakingNodeBalance, - before.stakingNodeBalance - slashedAmount, - "Staking node balance should remain unchanged" + afterCompletion.stakingNodeBalance + slashedAmount, + before.stakingNodeBalance, + "Staking node balance should decrease by slashedAmount" ); + } - // Verify that the total withdrawn amount matches the expected amount + 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); + + // 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); + + 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, - withdrawalAmount, - "Total withdrawn amount should match the expected amount" + 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" ); } @@ -1422,28 +1707,33 @@ contract StakingNodeWithdrawals is StakingNodeTestBase { // Queue withdrawals before exiting the validator uint256 withdrawalAmount = 32 ether; vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); - stakingNodeInstance.queueWithdrawals(withdrawalAmount); + 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); 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.SLASH_AMOUNT_GWEI() * 1e9; assertEq( - eigenPodManager.podOwnerDepositShares(address(stakingNodeInstance)), - -int256(slashedAmount), - "Pod owner shares should be equal to negative slashing amount" + eigenPodManager.podOwnerDepositShares(address(stakingNodeInstance)), 0, + "Pod owner shares should not change" ); - // Complete queued withdrawals - QueuedWithdrawalInfo[] memory queuedWithdrawals = new QueuedWithdrawalInfo[](1); - queuedWithdrawals[0] = QueuedWithdrawalInfo({withdrawnAmount: withdrawalAmount}); - _completeQueuedWithdrawals(queuedWithdrawals, nodeId); + vm.prank(actors.admin.STAKING_NODES_DELEGATOR); + stakingNodeInstance.syncQueuedShares(); + + _completeQueuedWithdrawals(_withdrawalRoots, nodeId, false); // Capture final state StateSnapshot memory afterCompletion = takeSnapshot(nodeId); @@ -1475,3 +1765,335 @@ contract StakingNodeWithdrawals is StakingNodeTestBase { } } + +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),0, "ipfs://some-ipfs-hash"); + } + + nodeId = createStakingNodes(1)[0]; + stakingNodeInstance = stakingNodesManager.nodes(nodeId); + + vm.prank(actors.admin.STAKING_NODES_DELEGATOR); + stakingNodeInstance.delegate( + operator1, ISignatureUtils.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.prank(avs); + allocationManager.createOperatorSets(avs, createSetParams); + + 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); + + vm.startPrank(operator1); + allocationManager.registerForOperatorSets(operator1, registerParams); + allocationManager.modifyAllocations(operator1, allocateParams); + vm.stopPrank(); + + 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"); + } + } +} \ No newline at end of file diff --git a/test/integration/StakingNodeTestBase.sol b/test/integration/StakingNodeTestBase.sol index 0cbacfdb7..53cd6714f 100644 --- a/test/integration/StakingNodeTestBase.sol +++ b/test/integration/StakingNodeTestBase.sol @@ -71,25 +71,17 @@ 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++) { + // 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] = delegationManager.getQueuedWithdrawal(withdrawalRoots[i]); } } @@ -103,11 +95,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 +123,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 +171,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/mocks/MockAVS.sol b/test/mocks/MockAVS.sol new file mode 100644 index 000000000..8ded59139 --- /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, uint32[] calldata operatorSetIds, bytes calldata data) external {} +} diff --git a/test/mocks/TestStakingNodeV2.sol b/test/mocks/TestStakingNodeV2.sol index 0a27fceed..f2c644276 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 initializeV4(ReInit memory reInit) public reinitializer(5) { valueToBeInitialized = reInit.valueToBeInitialized; } diff --git a/test/mocks/TestStakingNodesManagerV2.sol b/test/mocks/TestStakingNodesManagerV2.sol index ab746c396..1860b932b 100644 --- a/test/mocks/TestStakingNodesManagerV2.sol +++ b/test/mocks/TestStakingNodesManagerV2.sol @@ -34,6 +34,10 @@ contract TestStakingNodesManagerV2 is StakingNodesManager { node.initializeV3(); } + // TODO: commented for holesky deployment. Uncomment for mainnet deployment. + // if (initializedVersion == 3) { + // node.initializeV4(); + // } if (initializedVersion == 3) { TestStakingNodeV2(payable(address(node))) diff --git a/test/scenarios/fork/ynETH/Base.t.sol b/test/scenarios/fork/ynETH/Base.t.sol index a97658040..55fbb2fe9 100644 --- a/test/scenarios/fork/ynETH/Base.t.sol +++ b/test/scenarios/fork/ynETH/Base.t.sol @@ -42,6 +42,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 +80,19 @@ 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(); + } + stakingNodesManager.updateTotalETHStaked(); } function assignContracts() internal { @@ -92,6 +100,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,19 +132,26 @@ contract Base is Test, Utils { } upgradeStakingNodesManagerAndStakingNode(); + upgradeWithdrawalsProcessor(); } function upgradeStakingNodesManagerAndStakingNode() internal { + // Upgrade StakingNode implementation address newStakingNodeImpl = address(new StakingNode()); + + // Register new implementation + vm.prank(actors.admin.STAKING_ADMIN); + stakingNodesManager.upgradeStakingNodeImplementation(newStakingNodeImpl); + // Upgrade StakingNodesManager bytes memory initializeV3Data = abi.encodeWithSelector(stakingNodesManager.initializeV3.selector, chainAddresses.eigenlayer.REWARDS_COORDINATOR_ADDRESS); address newStakingNodesManagerImpl = address(new StakingNodesManager()); - uint256 totalAssetsBefore = yneth.totalAssets(); + // uint256 totalAssetsBefore = yneth.totalAssets(); vm.prank(actors.admin.PROXY_ADMIN_OWNER); @@ -147,11 +163,23 @@ contract Base is Test, Utils { assertEq(address(stakingNodesManager.rewardsCoordinator()), chainAddresses.eigenlayer.REWARDS_COORDINATOR_ADDRESS, "rewardsCoordinator not set correctly after upgrade"); - // 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) { @@ -200,6 +228,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"); @@ -209,14 +249,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/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..1f67aeff2 100644 --- a/test/scenarios/fork/ynETH/WithdrawalsWithRewards-Scenario.t.sol +++ b/test/scenarios/fork/ynETH/WithdrawalsWithRewards-Scenario.t.sol @@ -695,10 +695,10 @@ contract M3WithdrawalsWithRewardsTest is WithdrawalsScenarioTestBase { 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" );