diff --git a/src/contracts/core/DelegationManager.sol b/src/contracts/core/DelegationManager.sol index 2265b1e4f6..e47df81350 100644 --- a/src/contracts/core/DelegationManager.sol +++ b/src/contracts/core/DelegationManager.sol @@ -59,6 +59,11 @@ contract DelegationManager is _; } + modifier onlyEigenPodManager() { + require(msg.sender == address(eigenPodManager), OnlyEigenPodManager()); + _; + } + /** * * INITIALIZING FUNCTIONS @@ -283,7 +288,7 @@ contract DelegationManager is // all shares and queued withdrawn and no delegated operator // reset staker's depositScalingFactor to default - depositScalingFactors[staker][strategies[i]] = WAD; + stakerScalingFactors[staker][strategies[i]].depositScalingFactor = WAD; } } @@ -400,37 +405,41 @@ contract DelegationManager is } } - // IGNORE THIS FUNCTION /** - * @notice Decreases a staker's delegated delegatedShares for a strategy. - * @param staker The address to increase the delegated scaled shares for their operator. - * @param strategy The strategy in which to decrease the delegated scaled shares. - * @param removedOwnedShares The number of shares to decremented for the strategy in the - * StrategyManager/EigenPodManager + * @notice Decreases a native restaker's delegated share balance in a strategy due to beacon chain slashing. This updates their beaconChainScalingFactor. + * Their operator's stakeShares are also updated (if they are delegated). + * @param staker The address to increase the delegated stakeShares for their operator. + * @param existingShares The number of shares the staker already has in the EPM. This does not change upon decreasing shares. + * @param proportionOfOldBalance The current pod owner shares proportion of the previous pod owner shares * - * @dev *If the staker is actively delegated*, then decreases the `staker`'s delegated delegatedShares in `strategy` by `scaledShares`. Otherwise does nothing. - * @dev Callable only by the StrategyManager or EigenPodManager. + * @dev *If the staker is actively delegated*, then decreases the `staker`'s delegated stakeShares in `strategy` by `proportionPodBalanceDecrease` proportion. Otherwise does nothing. + * @dev Callable only by the EigenPodManager. */ - function decreaseDelegatedShares( + function decreaseBeaconChainScalingFactor( address staker, - IStrategy strategy, - OwnedShares removedOwnedShares - ) external onlyStrategyManagerOrEigenPodManager { - // if the staker is delegated to an operator - // if (isDelegated(staker)) { - // address operator = delegatedTo[staker]; + Shares existingShares, + uint64 proportionOfOldBalance + ) external onlyEigenPodManager { + DelegatedShares delegatedSharesBefore = existingShares.toDelegatedShares(stakerScalingFactors[staker][beaconChainETHStrategy]); - // uint64 totalMagnitude = allocationManager.getTotalMagnitude(operator, strategy); + // decrease the staker's beaconChainScalingFactor proportionally + // forgefmt: disable-next-item + stakerScalingFactors[staker][beaconChainETHStrategy].decreaseBeaconChainScalingFactor(proportionOfOldBalance); - // // subtract strategy shares from delegated scaled shares - // _decreaseOperatorScaledShares({ - // operator: operator, - // staker: staker, - // strategy: strategy, - // shares: removedOwnedShares, - // totalMagnitude: totalMagnitude - // }); - // } + DelegatedShares delegatedSharesAfter = existingShares.toDelegatedShares(stakerScalingFactors[staker][beaconChainETHStrategy]); + + // if the staker is delegated to an operators + if (isDelegated(staker)) { + address operator = delegatedTo[staker]; + + // subtract strategy shares from delegated scaled shares + _decreaseDelegation({ + operator: operator, + staker: staker, + strategy: beaconChainETHStrategy, + delegatedShares: delegatedSharesBefore.sub(delegatedSharesAfter) + }); + } } /** @@ -553,9 +562,11 @@ contract DelegationManager is for (uint256 i = 0; i < withdrawal.strategies.length; i++) { IShareManager shareManager = _getShareManager(withdrawal.strategies[i]); - - // forgefmt: disable-next-line - OwnedShares ownedSharesToWithdraw = withdrawal.delegatedShares[i].toOwnedShares(totalMagnitudes[i]); + OwnedShares ownedSharesToWithdraw = + withdrawal.delegatedShares[i] + .scaleForCompleteWithdrawal(stakerScalingFactors[withdrawal.staker][withdrawal.strategies[i]]) + .toOwnedShares(totalMagnitudes[i]); + if (receiveAsTokens) { // Withdraws `shares` in `strategy` to `withdrawer`. If the shares are virtual beaconChainETH shares, // then a call is ultimately forwarded to the `staker`s EigenPod; otherwise a call is ultimately forwarded @@ -659,9 +670,9 @@ contract DelegationManager is IShareManager shareManager = _getShareManager(strategies[i]); // delegatedShares for staker to place into queueWithdrawal - delegatedSharesToWithdraw[i] = ownedSharesToWithdraw[i].toDelegatedShares(totalMagnitudes[i]); - - Shares sharesToWithdraw = delegatedSharesToWithdraw[i].toShares(depositScalingFactors[staker][strategies[i]]); + DelegatedShares delegatedSharesToRemove = ownedSharesToWithdraw[i].toDelegatedShares(totalMagnitudes[i]); + // TODO: should this include beaconChainScalingFactor? + Shares sharesToWithdraw = delegatedSharesToRemove.toShares(stakerScalingFactors[staker][strategies[i]]); // TODO: maybe have a getter to get shares for all strategies, like getDelegatableShares // check sharesToWithdraw is valid // but for inputted strategies @@ -675,9 +686,10 @@ contract DelegationManager is operator: operator, staker: staker, strategy: strategies[i], - delegatedShares: delegatedSharesToWithdraw[i] + delegatedShares: delegatedSharesToRemove }); } + delegatedSharesToWithdraw[i] = delegatedSharesToRemove.scaleForQueueWithdrawal(stakerScalingFactors[staker][strategies[i]]); // Remove active shares from EigenPodManager/StrategyManager // EigenPodManager: this call will revert if it would reduce the Staker's virtual beacon chain ETH shares below zero @@ -758,25 +770,26 @@ contract DelegationManager is // // newShares // = newPrincipalShares.toDelegatedShares(stakerScalingFactors[staker][strategy).toOwnedShares(totalMagnitude) - // = newPrincipalShares * newDepositScalingFactor / WAD * totalMagnitude / WAD - // = (existingPrincipalShares + addedShares) * newDepositScalingFactor / WAD * totalMagnitude / WAD + // = newPrincipalShares * newDepositScalingFactor / WAD * beaonChainScalingFactor / WAD * totalMagnitude / WAD + // = (existingPrincipalShares + addedShares) * newDepositScalingFactor / WAD * beaonChainScalingFactor / WAD * totalMagnitude / WAD // // we can solve for // OwnedShares existingOwnedShares = existingShares - .toDelegatedShares(depositScalingFactors[staker][strategy]) + .toDelegatedShares(stakerScalingFactors[staker][strategy]) .toOwnedShares(totalMagnitude); newDepositScalingFactor = existingOwnedShares .add(addedOwnedShares) .unwrap() .divWad(existingShares.unwrap() + addedOwnedShares.unwrap()) + .divWad(stakerScalingFactors[staker][strategy].getBeaconChainScalingFactor()) .divWad(totalMagnitude); } // update the staker's depositScalingFactor - depositScalingFactors[staker][strategy] = newDepositScalingFactor; + stakerScalingFactors[staker][strategy].depositScalingFactor = newDepositScalingFactor; } function _getShareManager( @@ -885,7 +898,7 @@ contract DelegationManager is // forgefmt: disable-next-item ownedShares[i] = shares - .toDelegatedShares(depositScalingFactors[staker][strategies[i]]) + .toDelegatedShares(stakerScalingFactors[staker][strategies[i]]) .toOwnedShares(totalMagnitude); } } diff --git a/src/contracts/core/DelegationManagerStorage.sol b/src/contracts/core/DelegationManagerStorage.sol index 89a6957e7d..9de11e1458 100644 --- a/src/contracts/core/DelegationManagerStorage.sol +++ b/src/contracts/core/DelegationManagerStorage.sol @@ -116,9 +116,13 @@ abstract contract DelegationManagerStorage is IDelegationManager { */ mapping(IStrategy => uint256) private __deprecated_strategyWithdrawalDelayBlocks; - /// @notice Mapping: staker => strategy => scaling factor used to calculate the staker's shares in the strategy. - /// This is updated upon each deposit based on the staker's currently delegated operator's totalMagnitude. - mapping(address => mapping(IStrategy => uint256)) public depositScalingFactors; + /// @notice Mapping: staker => strategy => + /// ( + /// scaling factor used to calculate the staker's shares in the strategy, + /// beacon chain scaling factor used to calculate the staker's withdrawable shares in the strategy. + /// ) + /// Note that we don't need the beaconChainScalingFactor for non beaconChainETHStrategy strategies, but it's nicer syntactically to keep it. + mapping(address => mapping(IStrategy => StakerScalingFactors)) public stakerScalingFactors; constructor( IStrategyManager _strategyManager, diff --git a/src/contracts/interfaces/IDelegationManager.sol b/src/contracts/interfaces/IDelegationManager.sol index 189acb1115..17c9c703ba 100644 --- a/src/contracts/interfaces/IDelegationManager.sol +++ b/src/contracts/interfaces/IDelegationManager.sol @@ -18,6 +18,8 @@ import "../libraries/SlashingLib.sol"; interface IDelegationManager is ISignatureUtils { /// @dev Thrown when msg.sender is not allowed to call a function error UnauthorizedCaller(); + /// @dev Thrown when msg.sender is not the EigenPodManager + error OnlyEigenPodManager(); /// Delegation Status @@ -362,19 +364,21 @@ interface IDelegationManager is ISignatureUtils { OwnedShares addedOwnedShares ) external; - /** - * @notice Decreases a staker's delegated share balance in a strategy. Note that before removing from operator shares, - * the delegated shares are scaled according to the operator's total magnitude as part of slashing accounting. Unlike - * `increaseDelegatedShares`, the staker's depositScalingFactor is not updated here. - * @param staker The address to increase the delegated scaled shares for their operator. - * @param strategy The strategy in which to decrease the delegated scaled shares. - * @param removedOwnedShares The number of shares to decremented for the strategy in the - * StrategyManager/EigenPodManager + /** + * @notice Decreases a native restaker's delegated share balance in a strategy due to beacon chain slashing. This updates their beaconChainScalingFactor. + * Their operator's stakeShares are also updated (if they are delegated). + * @param staker The address to increase the delegated stakeShares for their operator. + * @param existingShares The number of shares the staker already has in the EPM. This does not change upon decreasing shares. + * @param proportionOfOldBalance The current pod owner shares proportion of the previous pod owner shares * - * @dev *If the staker is actively delegated*, then decreases the `staker`'s delegated scaled shares in `strategy` by `scaledShares`. Otherwise does nothing. - * @dev Callable only by the StrategyManager or EigenPodManager. + * @dev *If the staker is actively delegated*, then decreases the `staker`'s delegated stakeShares in `strategy` by `proportionPodBalanceDecrease` proportion. Otherwise does nothing. + * @dev Callable only by the EigenPodManager. */ - function decreaseDelegatedShares(address staker, IStrategy strategy, OwnedShares removedOwnedShares) external; + function decreaseBeaconChainScalingFactor( + address staker, + Shares existingShares, + uint64 proportionOfOldBalance + ) external; /** * @notice returns the address of the operator that `staker` is delegated to. diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index 8701d26b78..638cda018c 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -13,8 +13,12 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts */ interface IEigenPod { - /// @dev Thrown when msg.sender is not allowed to call a function - error UnauthorizedCaller(); + /// @dev Thrown when msg.sender is not the EPM. + error OnlyEigenPodManager(); + /// @dev Thrown when msg.sender is not the pod owner. + error OnlyEigenPodOwner(); + /// @dev Thrown when msg.sender is not owner or the proof submitter. + error OnlyEigenPodOwnerOrProofSubmitter(); /// @dev Thrown when attempting an action that is currently paused. error CurrentlyPaused(); @@ -98,7 +102,14 @@ interface IEigenPod { bytes32 beaconBlockRoot; uint24 proofsRemaining; uint64 podBalanceGwei; - int128 balanceDeltasGwei; + // this used to be an int128 before the slashing release + // now it is an int64. (2^63 - 1) gwei * 1e-9 eth/gwei = 9_223_372_036.85 eth = 9 billion eth + int64 balanceDeltasGwei; + uint64 beaconChainBalanceBeforeGwei; + } + + struct ExtendedCheckpoint { + uint128 beaconChainBalanceBefore; } /** diff --git a/src/contracts/interfaces/IEigenPodManager.sol b/src/contracts/interfaces/IEigenPodManager.sol index 2ace138eb7..1d2e80f7d4 100644 --- a/src/contracts/interfaces/IEigenPodManager.sol +++ b/src/contracts/interfaces/IEigenPodManager.sol @@ -28,6 +28,9 @@ interface IEigenPodManager is IShareManager, IPausable { error SharesNegative(); /// @dev Thrown when the strategy is not the beaconChainETH strategy. error InvalidStrategy(); + /// @dev Thrown when the pods shares are negative and a beacon chain balance update is attempted. + /// The podOwner should complete legacy withdrawal first. + error LegacyWithdrawalsNotCompleted(); /// @notice Emitted to notify the deployment of an EigenPod event PodDeployed(address indexed eigenPod, address indexed podOwner); @@ -72,10 +75,15 @@ interface IEigenPodManager is IShareManager, IPausable { * to ensure that delegated shares are also tracked correctly * @param podOwner is the pod owner whose balance is being updated. * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares + * @param proportionPodBalanceDecrease is the proportion (of WAD) of the podOwner's balance that has changed * @dev Callable only by the podOwner's EigenPod contract. * @dev Reverts if `sharesDelta` is not a whole Gwei amount */ - function recordBeaconChainETHBalanceUpdate(address podOwner, int256 sharesDelta) external; + function recordBeaconChainETHBalanceUpdate( + address podOwner, + int256 sharesDelta, + uint64 proportionPodBalanceDecrease + ) external; /// @notice Returns the address of the `podOwner`'s EigenPod if it has been deployed. function ownerToPod( diff --git a/src/contracts/libraries/SlashingLib.sol b/src/contracts/libraries/SlashingLib.sol index c10aa3ca29..f2213132fd 100644 --- a/src/contracts/libraries/SlashingLib.sol +++ b/src/contracts/libraries/SlashingLib.sol @@ -35,72 +35,33 @@ uint64 constant WAD = 1e18; * - These live in the storage of the StrategyManager/EigenPodManager * - `stakerStrategyShares` in the SM is the staker's shares that have not been queued for withdrawal in a strategy * - `podOwnerShares` in the EPM is the staker's shares that have not been queued for withdrawal in the beaconChainETHStrategy + * + * Note that `withdrawal.delegatedShares` is scaled for the beaconChainETHStrategy to divide by the beaconChainScalingFactor upon queueing + * and multiply by the beaconChainScalingFactor upon withdrawal */ type OwnedShares is uint256; type DelegatedShares is uint256; type Shares is uint256; +struct StakerScalingFactors { + uint256 depositScalingFactor; + + // we need to know if the beaconChainScalingFactor is set because it can be set to 0 through 100% slashing + bool isBeaconChainScalingFactorSet; + uint64 beaconChainScalingFactor; +} + using SlashingLib for OwnedShares global; using SlashingLib for DelegatedShares global; using SlashingLib for Shares global; +using SlashingLib for StakerScalingFactors global; +// TODO: validate order of operations everywhere library SlashingLib { using Math for uint256; using SlashingLib for uint256; - function toShares( - DelegatedShares delegatedShares, - uint256 depositScalingFactor - ) internal pure returns (Shares) { - if (depositScalingFactor == 0) { - depositScalingFactor = WAD; - } - - // forgefmt: disable-next-item - return delegatedShares - .unwrap() - .divWad(depositScalingFactor) - .wrapShares(); - } - - function toDelegatedShares( - Shares shares, - uint256 depositScalingFactor - ) internal pure returns (DelegatedShares) { - if (depositScalingFactor == 0) { - depositScalingFactor = WAD; - } - - // forgefmt: disable-next-item - return shares - .unwrap() - .mulWad(depositScalingFactor) - .wrapDelegated(); - } - - function toOwnedShares( - DelegatedShares delegatedShares, - uint256 magnitude - ) internal pure returns (OwnedShares) { - // forgefmt: disable-next-item - return delegatedShares - .unwrap() - .mulWad(magnitude) - .wrapOwned(); - } - - function toDelegatedShares( - OwnedShares shares, - uint256 magnitude - ) internal pure returns (DelegatedShares) { - // forgefmt: disable-next-item - return shares - .unwrap() - .divWad(magnitude) - .wrapDelegated(); - } - // MATH function add(Shares x, uint256 y) internal pure returns (uint256) { @@ -139,6 +100,71 @@ library SlashingLib { return x.unwrap() - y; } + /// @dev beaconChainScalingFactor = 0 -> WAD for all non beaconChainETH strategies + function toShares( + DelegatedShares delegatedShares, + StakerScalingFactors storage ssf + ) internal view returns (Shares) { + return delegatedShares.unwrap() + .divWad(ssf.getDepositScalingFactor()) + .divWad(ssf.getBeaconChainScalingFactor()) + .wrapShares(); + } + + function toDelegatedShares( + OwnedShares shares, + uint256 magnitude + ) internal pure returns (DelegatedShares) { + // forgefmt: disable-next-item + return shares + .unwrap() + .divWad(magnitude) + .wrapDelegated(); + } + + function toOwnedShares(DelegatedShares delegatedShares, uint256 magnitude) internal view returns (OwnedShares) { + return delegatedShares + .unwrap() + .mulWad(magnitude) + .wrapOwned(); + } + + function scaleForQueueWithdrawal(DelegatedShares delegatedShares, StakerScalingFactors storage ssf) internal view returns (DelegatedShares) { + return delegatedShares + .unwrap() + .divWad(ssf.getBeaconChainScalingFactor()) + .wrapDelegated(); + } + + function scaleForCompleteWithdrawal(DelegatedShares delegatedShares, StakerScalingFactors storage ssf) internal view returns (DelegatedShares) { + return delegatedShares + .unwrap() + .mulWad(ssf.getBeaconChainScalingFactor()) + .wrapDelegated(); + } + + function decreaseBeaconChainScalingFactor(StakerScalingFactors storage ssf, uint64 proportionOfOldBalance) internal { + ssf.beaconChainScalingFactor = uint64(uint256(ssf.beaconChainScalingFactor).mulWad(proportionOfOldBalance)); + ssf.isBeaconChainScalingFactorSet = true; + } + + /// @dev beaconChainScalingFactor = 0 -> WAD for all non beaconChainETH strategies + function toDelegatedShares( + Shares shares, + StakerScalingFactors storage ssf + ) internal view returns (DelegatedShares) { + return shares.unwrap() + .mulWad(ssf.getDepositScalingFactor()) + .mulWad(ssf.getBeaconChainScalingFactor()) + .wrapDelegated(); + } + + function toDelegatedShares(Shares shares, uint256 magnitude) internal view returns (DelegatedShares) { + return shares.unwrap() + .mulWad(magnitude) + .wrapDelegated(); + } + // WAD MATH function mulWad(uint256 x, uint256 y) internal pure returns (uint256) { @@ -174,4 +200,12 @@ library SlashingLib { function wrapOwned(uint256 x) internal pure returns (OwnedShares) { return OwnedShares.wrap(x); } + + function getDepositScalingFactor(StakerScalingFactors storage ssf) internal view returns (uint256) { + return ssf.depositScalingFactor == 0 ? WAD : ssf.depositScalingFactor; + } + + function getBeaconChainScalingFactor(StakerScalingFactors storage ssf) internal view returns (uint64) { + return !ssf.isBeaconChainScalingFactorSet && ssf.beaconChainScalingFactor == 0 ? WAD : ssf.beaconChainScalingFactor; + } } diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index cfcc88eeca..ddeef0bd1d 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -61,19 +61,19 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC /// @notice Callable only by the EigenPodManager modifier onlyEigenPodManager() { - require(msg.sender == address(eigenPodManager), UnauthorizedCaller()); + require(msg.sender == address(eigenPodManager), OnlyEigenPodManager()); _; } /// @notice Callable only by the pod's owner modifier onlyEigenPodOwner() { - require(msg.sender == podOwner, UnauthorizedCaller()); + require(msg.sender == podOwner, OnlyEigenPodOwner()); _; } /// @notice Callable only by the pod's owner or proof submitter modifier onlyOwnerOrProofSubmitter() { - require(msg.sender == podOwner || msg.sender == proofSubmitter, UnauthorizedCaller()); + require(msg.sender == podOwner || msg.sender == proofSubmitter, OnlyEigenPodOwnerOrProofSubmitter()); _; } @@ -189,7 +189,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC // If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`. // The assumption is that if this is the case, any withdrawn ETH was already in // the pod when `startCheckpoint` was originally called. - (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ + (uint64 prevBalanceGwei, int64 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ validatorInfo: validatorInfo, checkpointTimestamp: checkpointTimestamp, balanceContainerRoot: balanceContainerProof.balanceContainerRoot, @@ -197,6 +197,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC }); checkpoint.proofsRemaining--; + checkpoint.beaconChainBalanceBeforeGwei += prevBalanceGwei; checkpoint.balanceDeltasGwei += balanceDeltaGwei; exitedBalancesGwei += exitedBalanceGwei; @@ -260,8 +261,12 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC ); } + if (currentCheckpointTimestamp != 0) { + _currentCheckpoint.beaconChainBalanceBeforeGwei += uint64(totalAmountToBeRestakedWei / GWEI_TO_WEI); + } + // Update the EigenPodManager on this pod's new balance - eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei), 0); // no decrease } /** @@ -490,8 +495,10 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. activeValidatorCount++; - uint64 lastCheckpointedAt = - currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; + uint64 lastCheckpointedAt = lastCheckpointTimestamp; + if (currentCheckpointTimestamp != 0) { + lastCheckpointedAt = currentCheckpointTimestamp; + } // Proofs complete - create the validator in state _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ @@ -511,11 +518,11 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC uint64 checkpointTimestamp, bytes32 balanceContainerRoot, BeaconChainProofs.BalanceProof calldata proof - ) internal returns (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) { + ) internal returns (uint64 prevBalanceGwei, int64 balanceDeltaGwei, uint64 exitedBalanceGwei) { uint40 validatorIndex = uint40(validatorInfo.validatorIndex); // Verify validator balance against `balanceContainerRoot` - uint64 prevBalanceGwei = validatorInfo.restakedBalanceGwei; + prevBalanceGwei = validatorInfo.restakedBalanceGwei; uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ balanceContainerRoot: balanceContainerRoot, validatorIndex: validatorIndex, @@ -542,12 +549,12 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; // If we reach this point, `balanceDeltaGwei` should always be negative, // so this should be a safe conversion - exitedBalanceGwei = uint64(uint128(-balanceDeltaGwei)); + exitedBalanceGwei = uint64(-balanceDeltaGwei); emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex); } - return (balanceDeltaGwei, exitedBalanceGwei); + return (prevBalanceGwei, balanceDeltaGwei, exitedBalanceGwei); } /** @@ -598,7 +605,8 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)), proofsRemaining: uint24(activeValidatorCount), podBalanceGwei: podBalanceGwei, - balanceDeltasGwei: 0 + balanceDeltasGwei: 0, + beaconChainBalanceBeforeGwei: 0 }); // Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint @@ -633,8 +641,19 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC delete currentCheckpointTimestamp; delete _currentCheckpoint; + // Calculate the slashing proportion + uint64 proportionOfOldBalance = 0; + if (totalShareDeltaWei < 0) { + uint256 totalRestakedBeforeWei = (withdrawableRestakedExecutionLayerGwei + checkpoint.beaconChainBalanceBeforeGwei) * GWEI_TO_WEI; + proportionOfOldBalance = uint64((totalRestakedBeforeWei + uint256(-totalShareDeltaWei)) * WAD / totalRestakedBeforeWei); + } + // Update pod owner's shares - eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei); + eigenPodManager.recordBeaconChainETHBalanceUpdate( + podOwner, + totalShareDeltaWei, + proportionOfOldBalance + ); emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); } else { _currentCheckpoint = checkpoint; @@ -654,8 +673,8 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC } /// @dev Calculates the delta between two Gwei amounts and returns as an int256 - function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) { - return int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei)); + function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int64) { + return int64(newAmountGwei) - int64(previousAmountGwei); } /** diff --git a/src/contracts/pods/EigenPodManager.sol b/src/contracts/pods/EigenPodManager.sol index a97152c8bf..4d7a12d866 100644 --- a/src/contracts/pods/EigenPodManager.sol +++ b/src/contracts/pods/EigenPodManager.sol @@ -100,46 +100,31 @@ contract EigenPodManager is * to ensure that delegated shares are also tracked correctly * @param podOwner is the pod owner whose balance is being updated. * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares + * @param proportionOfOldBalance is the proportion (of WAD) of the podOwner's previous balance before the delta * @dev Callable only by the podOwner's EigenPod contract. * @dev Reverts if `sharesDelta` is not a whole Gwei amount */ function recordBeaconChainETHBalanceUpdate( address podOwner, - int256 sharesDelta + int256 sharesDelta, + uint64 proportionOfOldBalance ) external onlyEigenPod(podOwner) nonReentrant { - // require(podOwner != address(0), InputAddressZero()); - // require(sharesDelta % int256(GWEI_TO_WEI) == 0, SharesNotMultipleOfGwei()); - // int256 currentPodOwnerShares = podOwnerShares[podOwner]; - // int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; - // podOwnerShares[podOwner] = updatedPodOwnerShares; - - // // inform the DelegationManager of the change in delegateable shares - // int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ - // sharesBefore: currentPodOwnerShares, - // sharesAfter: updatedPodOwnerShares - // }); - // // skip making a call to the DelegationManager if there is no change in delegateable shares - // // or if the currentPodShares < 0 and updatedPodShares is still < 0. Means no update required - // // in delegated shares - // if (changeInDelegatableShares != 0) { - // if (changeInDelegatableShares < 0) { - // delegationManager.decreaseDelegatedShares({ - // staker: podOwner, - // strategy: beaconChainETHStrategy, - // removedOwnedShares: uint256(-changeInDelegatableShares).wrapWithdrawable() - // }); - // } else { - // delegationManager.increaseDelegatedShares({ - // staker: podOwner, - // strategy: beaconChainETHStrategy, - // // existing shares from standpoint of the DelegationManager - // existingShares: (currentPodOwnerShares < 0 ? 0 : uint256(currentPodOwnerShares)).wrapShares(), - // addedOwnedShares: uint256(changeInDelegatableShares).wrapWithdrawable() - // }); - // } - // } - // emit PodSharesUpdated(podOwner, sharesDelta); - // emit NewTotalShares(podOwner, updatedPodOwnerShares); + require(podOwner != address(0), InputAddressZero()); + require(sharesDelta % int256(GWEI_TO_WEI) == 0, SharesNotMultipleOfGwei()); + // shares can only be negative if they were due to negative shareDeltas after queued withdrawals in before + // the slashing upgrade. Make people complete queued withdrawals before completing any further checkpoints. + // the only effects podOwner UX, not AVS UX, since the podOwner already has 0 shares in the DM if they + // have a negative shares in EPM. + require(podOwnerShares[podOwner] >= 0, LegacyWithdrawalsNotCompleted()); + if (sharesDelta > 0) { + _addOwnedShares(podOwner, uint256(sharesDelta).wrapOwned()); + } else if (sharesDelta < 0 && podOwnerShares[podOwner] > 0) { + delegationManager.decreaseBeaconChainScalingFactor( + podOwner, + uint256(podOwnerShares[podOwner]).wrapShares(), + proportionOfOldBalance + ); + } } /** @@ -177,20 +162,7 @@ contract EigenPodManager is OwnedShares shares ) external onlyDelegationManager { require(strategy == beaconChainETHStrategy, InvalidStrategy()); - require(staker != address(0), InputAddressZero()); - require(int256(shares.unwrap()) >= 0, SharesNegative()); - require(shares.unwrap() % GWEI_TO_WEI == 0, SharesNotMultipleOfGwei()); - int256 currentShares = podOwnerShares[staker]; - int256 updatedShares = currentShares + int256(shares.unwrap()); - podOwnerShares[staker] = updatedShares; - - emit PodSharesUpdated(staker, int256(shares.unwrap())); - emit NewTotalShares(staker, updatedShares); - - uint256 increaseInDelegateableShares = - uint256(_calculateChangeInDelegatableShares({sharesBefore: currentShares, sharesAfter: updatedShares})); - - // TODO: ADD SHARES BACK TO DM + _addOwnedShares(staker, shares); } /** @@ -263,31 +235,28 @@ contract EigenPodManager is return pod; } - /** - * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing - * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. - */ - function _calculateChangeInDelegatableShares( - int256 sharesBefore, - int256 sharesAfter - ) internal pure returns (int256) { - if (sharesBefore <= 0) { - if (sharesAfter <= 0) { - // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares - return 0; - } else { - // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount - return sharesAfter; - } - } else { - if (sharesAfter <= 0) { - // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount - return (-sharesBefore); - } else { - // if the shares started positive and stayed positive, then the change in delegateable shares - // is the difference between starting and ending amounts - return (sharesAfter - sharesBefore); - } + function _addOwnedShares( + address staker, + OwnedShares ownedShares + ) internal { + require(staker != address(0), InputAddressZero()); + + int256 addedOwnedShares = int256(ownedShares.unwrap()); + int256 currentShares = podOwnerShares[staker]; + int256 updatedShares = currentShares + addedOwnedShares; + podOwnerShares[staker] = updatedShares; + + emit PodSharesUpdated(staker, addedOwnedShares); + emit NewTotalShares(staker, updatedShares); + + if (updatedShares > 0) { + delegationManager.increaseDelegatedShares({ + staker: staker, + strategy: beaconChainETHStrategy, + // existing shares from standpoint of the DelegationManager + existingShares: currentShares < 0 ? Shares.wrap(0) : uint256(currentShares).wrapShares(), + addedOwnedShares: ownedShares + }); } } diff --git a/src/contracts/pods/EigenPodStorage.sol b/src/contracts/pods/EigenPodStorage.sol index 096bf539c6..aeecaf777e 100644 --- a/src/contracts/pods/EigenPodStorage.sol +++ b/src/contracts/pods/EigenPodStorage.sol @@ -68,7 +68,7 @@ abstract contract EigenPodStorage is IEigenPod { mapping(uint64 => uint64) public checkpointBalanceExitedGwei; /// @notice The current checkpoint, if there is one active - Checkpoint internal _currentCheckpoint; + Checkpoint internal _currentCheckpoint; /// @notice An address with permissions to call `startCheckpoint` and `verifyWithdrawalCredentials`, set /// by the podOwner. This role exists to allow a podOwner to designate a hot wallet that can call @@ -76,10 +76,13 @@ abstract contract EigenPodStorage is IEigenPod { /// @dev If this address is NOT set, only the podOwner can call `startCheckpoint` and `verifyWithdrawalCredentials` address public proofSubmitter; + /// @notice The total balance of the pod before the current checkpoint + uint128 public beaconChainBalanceBeforeCurrentCheckpoint; + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[36] private __gap; + uint256[35] private __gap; }