diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol index 22fcabec00..2723abfa52 100644 --- a/src/test/integration/IntegrationBase.t.sol +++ b/src/test/integration/IntegrationBase.t.sol @@ -545,6 +545,23 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter { uint64 curBCSF = _getBeaconChainSlashingFactor(staker); assertEq(curBCSF, 0, err); } + + function assert_BCSF_WAD( + User staker, + string memory err + ) internal view { + uint64 curBCSF = _getBeaconChainSlashingFactor(staker); + assertEq(curBCSF, WAD, err); + } + + function assert_ActiveValidatorCount( + User staker, + uint expectedCount, + string memory err + ) internal view { + uint curActiveValidatorCount = _getActiveValidatorCount(staker); + assertEq(curActiveValidatorCount, expectedCount, err); + } /******************************************************************************* SNAPSHOT ASSERTIONS @@ -2246,6 +2263,11 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter { /** * Helpful getters: */ + + function _randSlashType() internal returns (BeaconChainMock.SlashType) { + return BeaconChainMock.SlashType(_randUint({ min: 0, max: 2 })); + } + function _randBalanceUpdate( User staker, IStrategy[] memory strategies diff --git a/src/test/integration/tests/upgrade/EigenPod.t.sol b/src/test/integration/tests/upgrade/EigenPod.t.sol new file mode 100644 index 0000000000..9223dd9e0e --- /dev/null +++ b/src/test/integration/tests/upgrade/EigenPod.t.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "src/test/integration/UpgradeTest.t.sol"; + +contract Integration_Upgrade_EigenPod_Base is UpgradeTest { + User staker; + IStrategy[] strategies; + uint256[] tokenBalances; + uint256[] shares; + + function _init() internal virtual override { + _configAssetTypes(HOLDS_ETH); + _configUserTypes(DEFAULT); + + /// 0. Create a staker with underlying assets + (staker, strategies, tokenBalances) = _newRandomStaker(); + shares = _calculateExpectedShares(strategies, tokenBalances); + + /// 1. Deposit into strategies + staker.depositIntoEigenlayer(strategies, tokenBalances); + } +} + +contract Integration_Upgrade_EigenPod_SlashAfterUpgrade is Integration_Upgrade_EigenPod_Base { + uint40[] validators; + uint64 slashedGwei; + + function testFuzz_deposit_upgrade_slash_completeCheckpoint(uint24 _rand) public rand(_rand) { + uint64 initBeaconBalanceGwei = uint64(tokenBalances[0] / GWEI_TO_WEI); + + /// 2. Upgrade contracts + _upgradeEigenLayerContracts(); + + /// 3. Slash the staker partially + validators = staker.getActiveValidators(); + slashedGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor); + beaconChain.advanceEpoch_NoRewards(); // Withdraw slashed validators to pod + + // 4. Start and complete a checkpoint + staker.startCheckpoint(); + check_StartCheckpoint_WithPodBalance_State(staker, initBeaconBalanceGwei - slashedGwei); + staker.completeCheckpoint(); + check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedGwei); + } +} + +contract Integration_Upgrade_EigenPod_FullSlash is Integration_Upgrade_EigenPod_Base { + uint40[] validators; + uint64 slashedGwei; + + function _init() internal override { + super._init(); + + /// 2. Fully slash the staker + validators = staker.getActiveValidators(); + slashedGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Full); + beaconChain.advanceEpoch_NoRewards(); // Withdraw slashed validators to pod + + /// 3. Upgrade contracts + _upgradeEigenLayerContracts(); + } + + function testFuzz_deposit_fullSlash_upgrade_delegate(uint24 _rand) public rand(_rand) { + /// 4. Delegate to operator + (User operator,,) = _newRandomOperator(); + staker.delegateTo(operator); + check_Delegation_State(staker, operator, strategies, shares); + } + + function testFuzz_deposit_fullSlash_upgrade_deposit_delegate(uint24 _rand) public rand(_rand) { + // 5. Start a new validator & verify withdrawal credentials + cheats.deal(address(staker), 32 ether); + tokenBalances[0] = tokenBalances[0] + 32 ether; + (uint40[] memory newValidators, uint64 addedBeaconBalanceGwei) = staker.startValidators(); + beaconChain.advanceEpoch_NoRewards(); + staker.verifyWithdrawalCredentials(newValidators); + check_VerifyWC_State(staker, newValidators, addedBeaconBalanceGwei); + shares = _calculateExpectedShares(strategies, tokenBalances); + + // 6. Delegate to operator + (User operator,,) = _newRandomOperator(); + staker.delegateTo(operator); + check_Delegation_State(staker, operator, strategies, shares); + } +} + +contract Integration_Upgrade_EigenPod_NegativeShares is Integration_Upgrade_EigenPod_Base { + User operator; + Withdrawal withdrawal; + int[] tokenDeltas; + int[] balanceUpdateShareDelta; + + function _init() internal override { + super._init(); + + // 3. Delegate to operator + (operator,,) = _newRandomOperator(); + staker.delegateTo(operator); + + /// 4. Queue a withdrawal for all shares + Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares); + withdrawal = withdrawals[0]; + + /// 5. Update balance randomly (can be positive or negative) + (tokenDeltas, balanceUpdateShareDelta,) = _randBalanceUpdate(staker, strategies); + staker.updateBalances(strategies, tokenDeltas); + + /// 6. Upgrade contracts + _upgradeEigenLayerContracts(); + } + + function testFuzz_deposit_delegate_updateBalance_upgrade_completeAsShares(uint24 _rand) public rand(_rand) { + /// 7. Complete the withdrawal as shares + Withdrawal[] memory withdrawals = new Withdrawal[](1); + withdrawals[0] = withdrawal; + _rollBlocksForCompleteWithdrawals(withdrawals); + staker.completeWithdrawalAsShares(withdrawal); + + // Manually complete checks since we could still negative shares prior to the upgrade, causing a revert in the share check + (uint256[] memory expectedOperatorShareDelta, int256[] memory expectedStakerShareDelta) = + _getPostWithdrawalExpectedShareDeltas(balanceUpdateShareDelta[0], withdrawal); + assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending"); + assert_Snap_Unchanged_TokenBalances(staker, "staker should not have any change in underlying token balances"); + assert_Snap_Added_OperatorShares(operator, withdrawal.strategies, expectedOperatorShareDelta, "operator should have received shares"); + assert_Snap_Delta_StakerShares(staker, strategies, expectedStakerShareDelta, "staker should have received expected shares"); + } + + function testFuzz_deposit_delegate_updateBalance_upgrade_completeAsTokens( + uint24 _rand + ) public rand(_rand) { + /// 7. Complete the withdrawal as tokens + Withdrawal[] memory withdrawals = new Withdrawal[](1); + withdrawals[0] = withdrawal; + _rollBlocksForCompleteWithdrawals(withdrawals); + IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawal); + uint256[] memory expectedTokens = _getPostWithdrawalExpectedTokenDeltas(balanceUpdateShareDelta[0], withdrawal); + + // Manually complete checks since we could still negative shares prior to the upgrade, causing a revert in the share check + assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending"); + assert_Snap_Added_TokenBalances(staker, tokens, expectedTokens, "staker should have received expected tokens"); + assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed"); + + // If we had a positive balance update, then the staker shares should not have changed + if (balanceUpdateShareDelta[0] > 0) { + assert_Snap_Unchanged_Staker_DepositShares(staker, "staker shares should not have changed"); + } + // Else, the staker shares should have increased by the magnitude of the negative share delta + else { + int256[] memory expectedStakerShareDelta = new int256[](1); + expectedStakerShareDelta[0] = -balanceUpdateShareDelta[0]; + assert_Snap_Delta_StakerShares(staker, strategies, expectedStakerShareDelta, "staker should have received expected shares"); + } + } + + function _getPostWithdrawalExpectedShareDeltas( + int256 _balanceUpdateShareDelta, + Withdrawal memory _withdrawal + ) internal pure returns (uint256[] memory, int256[] memory) { + uint256[] memory operatorShareDelta = new uint256[](1); + int256[] memory stakerShareDelta = new int256[](1); + // The staker share delta is the withdrawal scaled shares since it can go from negative to positive + stakerShareDelta[0] = int256(_withdrawal.scaledShares[0]); + + if (_balanceUpdateShareDelta > 0) { + // If balanceUpdateShareDelta is positive, then the operator delta is the withdrawal scaled shares + operatorShareDelta[0] = _withdrawal.scaledShares[0]; + } else { + // Operator shares never went negative, so we can just add the withdrawal scaled shares and the negative share delta + operatorShareDelta[0] = uint256(int256(_withdrawal.scaledShares[0]) + _balanceUpdateShareDelta); + } + + return (operatorShareDelta, stakerShareDelta); + } + + function _getPostWithdrawalExpectedTokenDeltas( + int256 _balanceUpdateShareDelta, + Withdrawal memory _withdrawal + ) internal pure returns (uint256[] memory) { + uint256[] memory expectedTokenDeltas = new uint256[](1); + if (_balanceUpdateShareDelta > 0) { + // If we had a positive balance update, then the expected token delta is the withdrawal scaled shares + expectedTokenDeltas[0] = _withdrawal.scaledShares[0]; + } else { + // If we had a negative balance update, then the expected token delta is the withdrawal scaled shares plus the negative share delta + expectedTokenDeltas[0] = uint256(int256(_withdrawal.scaledShares[0]) + _balanceUpdateShareDelta); + } + return expectedTokenDeltas; + } +} \ No newline at end of file diff --git a/src/test/integration/tests/upgrade/EigenPod_Negative_Shares.t.sol b/src/test/integration/tests/upgrade/EigenPod_Negative_Shares.t.sol deleted file mode 100644 index 0897123c93..0000000000 --- a/src/test/integration/tests/upgrade/EigenPod_Negative_Shares.t.sol +++ /dev/null @@ -1,133 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.27; - -import "src/test/integration/UpgradeTest.t.sol"; - -contract Integration_Upgrade_EigenPod_Negative_Shares is UpgradeTest { - function _init() internal override { - _configAssetTypes(HOLDS_ETH); - _configUserTypes(DEFAULT); - } - - function testFuzz_deposit_delegate_updateBalance_upgrade_completeAsShares( - uint24 _rand - ) public rand(_rand) { - /// 0. Create an operator and staker with some underlying assets - (User staker, IStrategy[] memory strategies, uint256[] memory tokenBalances) = _newRandomStaker(); - (User operator,,) = _newRandomOperator(); - uint256[] memory shares = _calculateExpectedShares(strategies, tokenBalances); - - /// 1. Deposit into strategies - staker.depositIntoEigenlayer(strategies, tokenBalances); - - /// 2. Delegate to operator - staker.delegateTo(operator); - - /// 3. Queue a withdrawal for all shares - IDelegationManagerTypes.Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares); - IDelegationManagerTypes.Withdrawal memory withdrawal = withdrawals[0]; - - /// 4. Update balance randomly (can be positive or negative) - (int256[] memory tokenDeltas, int256[] memory balanceUpdateShareDelta,) = _randBalanceUpdate(staker, strategies); - staker.updateBalances(strategies, tokenDeltas); - - /// 5. Upgrade contracts - _upgradeEigenLayerContracts(); - - /// 6. Complete the withdrawal as shares - _rollBlocksForCompleteWithdrawals(withdrawals); - staker.completeWithdrawalAsShares(withdrawal); - - // Manually complete checks since we could still negative shares prior to the upgrade, causing a revert in the share check - (uint256[] memory expectedOperatorShareDelta, int256[] memory expectedStakerShareDelta) = - _getPostWithdrawalExpectedShareDeltas(balanceUpdateShareDelta[0], withdrawal); - assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending"); - assert_Snap_Unchanged_TokenBalances(staker, "staker should not have any change in underlying token balances"); - assert_Snap_Added_OperatorShares(operator, withdrawal.strategies, expectedOperatorShareDelta, "operator should have received shares"); - assert_Snap_Delta_StakerShares(staker, strategies, expectedStakerShareDelta, "staker should have received expected shares"); - } - - function testFuzz_deposit_delegate_updateBalance_upgrade_completeAsTokens( - uint24 _rand - ) public rand(_rand) { - /// 0. Create an operator and staker with some underlying assets - (User staker, IStrategy[] memory strategies, uint256[] memory tokenBalances) = _newRandomStaker(); - (User operator,,) = _newRandomOperator(); - uint256[] memory shares = _calculateExpectedShares(strategies, tokenBalances); - - /// 1. Deposit into strategies - staker.depositIntoEigenlayer(strategies, tokenBalances); - - /// 2. Delegate to operator - staker.delegateTo(operator); - - /// 3. Queue a withdrawal for all shares - IDelegationManagerTypes.Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares); - IDelegationManagerTypes.Withdrawal memory withdrawal = withdrawals[0]; - - /// 4. Update balance randomly (can be positive or negative) - (int256[] memory tokenDeltas, int256[] memory balanceUpdateShareDelta,) = _randBalanceUpdate(staker, strategies); - staker.updateBalances(strategies, tokenDeltas); - - /// 5. Upgrade contracts - _upgradeEigenLayerContracts(); - - /// 6. Complete the withdrawal as shares - _rollBlocksForCompleteWithdrawals(withdrawals); - IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawal); - uint256[] memory expectedTokens = _getPostWithdrawalExpectedTokenDeltas(balanceUpdateShareDelta[0], withdrawal); - - // Manually complete checks since we could still negative shares prior to the upgrade, causing a revert in the share check - assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending"); - assert_Snap_Added_TokenBalances(staker, tokens, expectedTokens, "staker should have received expected tokens"); - assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed"); - - // If we had a positive balance update, then the staker shares should not have changed - if (balanceUpdateShareDelta[0] > 0) { - assert_Snap_Unchanged_Staker_DepositShares(staker, "staker shares should not have changed"); - } - // Else, the staker shares should have increased by the magnitude of the negative share delta - else { - int256[] memory expectedStakerShareDelta = new int256[](1); - expectedStakerShareDelta[0] = -balanceUpdateShareDelta[0]; - assert_Snap_Delta_StakerShares(staker, strategies, expectedStakerShareDelta, "staker should have received expected shares"); - } - } - - - - function _getPostWithdrawalExpectedShareDeltas( - int256 balanceUpdateShareDelta, - IDelegationManagerTypes.Withdrawal memory withdrawal - ) internal pure returns (uint256[] memory, int256[] memory) { - uint256[] memory operatorShareDelta = new uint256[](1); - int256[] memory stakerShareDelta = new int256[](1); - // The staker share delta is the withdrawal scaled shares since it can go from negative to positive - stakerShareDelta[0] = int256(withdrawal.scaledShares[0]); - - if (balanceUpdateShareDelta > 0) { - // If balanceUpdateShareDelta is positive, then the operator delta is the withdrawal scaled shares - operatorShareDelta[0] = withdrawal.scaledShares[0]; - } else { - // Operator shares never went negative, so we can just add the withdrawal scaled shares and the negative share delta - operatorShareDelta[0] = uint256(int256(withdrawal.scaledShares[0]) + balanceUpdateShareDelta); - } - - return (operatorShareDelta, stakerShareDelta); - } - - function _getPostWithdrawalExpectedTokenDeltas( - int256 balanceUpdateShareDelta, - IDelegationManagerTypes.Withdrawal memory withdrawal - ) internal pure returns (uint256[] memory) { - uint256[] memory expectedTokenDeltas = new uint256[](1); - if (balanceUpdateShareDelta > 0) { - // If we had a positive balance update, then the expected token delta is the withdrawal scaled shares - expectedTokenDeltas[0] = withdrawal.scaledShares[0]; - } else { - // If we had a negative balance update, then the expected token delta is the withdrawal scaled shares plus the negative share delta - expectedTokenDeltas[0] = uint256(int256(withdrawal.scaledShares[0]) + balanceUpdateShareDelta); - } - return expectedTokenDeltas; - } -} diff --git a/src/test/integration/tests/upgrade/EigenPod_Slashing_Migration.t.sol b/src/test/integration/tests/upgrade/EigenPod_Slashing_Migration.t.sol index f7ed80228e..4455877579 100644 --- a/src/test/integration/tests/upgrade/EigenPod_Slashing_Migration.t.sol +++ b/src/test/integration/tests/upgrade/EigenPod_Slashing_Migration.t.sol @@ -5,53 +5,54 @@ import "src/test/integration/UpgradeTest.t.sol"; contract Integration_Upgrade_EigenPod_Slashing_Migration is UpgradeTest, EigenPodPausingConstants { + User staker; + uint40[] validators; + uint[] shares; + IStrategy[] strategies; + uint[] tokenBalances; + function _init() internal override { _configAssetTypes(HOLDS_ETH); _configUserTypes(DEFAULT); - } - /** - * 1. Verify validators' withdrawal credentials - * -- earn rewards on beacon chain (withdrawn to pod) - * 2. Start a checkpoint - * 3. Pause starting checkpoints - * 4. Complete in progress checkpoint - * 5. Upgrade EigenPod contracts - * 6. Exit subset of Validators - */ - function test_upgrade_eigenpod_migration(uint24 _rand) public rand(_rand) { - // Initialize state - (User staker, ,) = _newRandomStaker(); - - (uint40[] memory validators, ) = staker.startValidators(); - beaconChain.advanceEpoch_NoRewards(); - - // 1. Verify validators' withdrawal credentials - staker.verifyWithdrawalCredentials(validators); - - // Advance epoch, generating consensus rewards and withdrawing anything over 32 ETH - beaconChain.advanceEpoch(); + /// 0. Create a staker with underlying assets + (staker, strategies, tokenBalances) = _newRandomStaker(); + shares = _calculateExpectedShares(strategies, tokenBalances); + + /// 1. Deposit into strategies + staker.depositIntoEigenlayer(strategies, tokenBalances); + validators = staker.getActiveValidators(); + } - // 2. Start a checkpoint + function _completeEigenpodMigration() internal { + // Start a checkpoint staker.startCheckpoint(); - // 3. Pause checkpoint starting + // Pause checkpoint starting cheats.prank(pauserMultisig); eigenPodManager.pause(2 ** PAUSED_START_CHECKPOINT); cheats.expectRevert("EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager"); staker.startCheckpoint(); - // 4. Complete in progress checkpoint + // Complete in progress checkpoint staker.completeCheckpoint(); - // 5. Upgrade Contracts for slashing + // Upgrade Contracts for slashing _upgradeEigenLayerContracts(); // Unpause EigenPodManager cheats.prank(eigenLayerPauserReg.unpauser()); eigenPodManager.unpause(0); + } + + function testFuzz_earnRewards_migrate_exit(uint24 _rand) public rand(_rand) { + // 2. Advance epoch, generating consensus rewards and withdrawing anything over 32 ETH + beaconChain.advanceEpoch(); - // 6. Exit validators + // 3. Migrate & Upgrade + _completeEigenpodMigration(); + + // 4. Exit validators // Fully exit one or more validators and advance epoch without generating rewards uint40[] memory subset = _choose(validators); uint64 exitedBalanceGwei = staker.exitValidators(subset); @@ -59,8 +60,23 @@ contract Integration_Upgrade_EigenPod_Slashing_Migration is UpgradeTest, EigenPo staker.startCheckpoint(); check_StartCheckpoint_WithPodBalance_State(staker, exitedBalanceGwei); - staker.completeCheckpoint(); check_CompleteCheckpoint_WithExits_State(staker, subset, exitedBalanceGwei); } + + function testFuzz_slash_migrate(uint24 _rand) public rand(_rand) { + // 2. Slash validators + uint40[] memory subset = _choose(validators); + uint64 slashedGwei = beaconChain.slashValidators(subset, _randSlashType()); + beaconChain.advanceEpoch_NoRewards(); + shares[0] = shares[0] - slashedGwei * GWEI_TO_WEI; // Shares should decrease by slashed amount + + // 3. Migrate & Upgrade + _completeEigenpodMigration(); + + // Assertions + assert_BCSF_WAD(staker, "BCSF should be WAD"); + assert_ActiveValidatorCount(staker, validators.length - subset.length, "validator count should decrease"); + assert_HasExpectedShares(staker, strategies, shares, "shares should decrease by slashed amount"); + } } \ No newline at end of file