From 15dc3ac9f0a83bd76980435b339a9eef18526b3a Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:38:36 -0400 Subject: [PATCH 01/16] refactor: remove slash escrow factory --- .../deploy/devnet/deploy_from_scratch.s.sol | 11 +- .../local/deploy_from_scratch.slashing.s.sol | 11 +- script/releases/Env.sol | 27 +- script/utils/ExistingDeploymentParser.sol | 6 +- src/contracts/core/SlashEscrow.sol | 49 - src/contracts/core/SlashEscrowFactory.sol | 486 --------- .../core/SlashEscrowFactoryStorage.sol | 75 -- src/contracts/core/StrategyManager.sol | 9 +- src/contracts/core/StrategyManagerStorage.sol | 6 +- src/contracts/interfaces/ISlashEscrow.sol | 52 - .../interfaces/ISlashEscrowFactory.sol | 295 ------ src/test/integration/IntegrationBase.t.sol | 49 - src/test/integration/IntegrationChecks.t.sol | 15 - .../integration/IntegrationDeployer.t.sol | 29 +- ...posit_Delegate_Allocate_Slash_Escrow.t.sol | 379 ------- src/test/integration/users/AVS.t.sol | 5 - src/test/unit/SlashEscrowFactoryUnit.t.sol | 987 ------------------ src/test/unit/StrategyManagerUnit.t.sol | 1 - 18 files changed, 9 insertions(+), 2483 deletions(-) delete mode 100644 src/contracts/core/SlashEscrow.sol delete mode 100644 src/contracts/core/SlashEscrowFactory.sol delete mode 100644 src/contracts/core/SlashEscrowFactoryStorage.sol delete mode 100644 src/contracts/interfaces/ISlashEscrow.sol delete mode 100644 src/contracts/interfaces/ISlashEscrowFactory.sol delete mode 100644 src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Escrow.t.sol delete mode 100644 src/test/unit/SlashEscrowFactoryUnit.t.sol diff --git a/script/deploy/devnet/deploy_from_scratch.s.sol b/script/deploy/devnet/deploy_from_scratch.s.sol index 7df8146e49..320e84fce2 100644 --- a/script/deploy/devnet/deploy_from_scratch.s.sol +++ b/script/deploy/devnet/deploy_from_scratch.s.sol @@ -14,11 +14,9 @@ import "../../../src/contracts/core/AVSDirectory.sol"; import "../../../src/contracts/core/RewardsCoordinator.sol"; import "../../../src/contracts/core/AllocationManager.sol"; import "../../../src/contracts/permissions/PermissionController.sol"; -import "../../../src/contracts/core/SlashEscrowFactory.sol"; import "../../../src/contracts/strategies/StrategyBaseTVLLimits.sol"; import "../../../src/contracts/strategies/StrategyFactory.sol"; import "../../../src/contracts/strategies/StrategyBase.sol"; -import "../../../src/contracts/core/SlashEscrow.sol"; import "../../../src/contracts/pods/EigenPod.sol"; import "../../../src/contracts/pods/EigenPodManager.sol"; @@ -64,8 +62,6 @@ contract DeployFromScratch is Script, Test { AllocationManager public allocationManager; PermissionController public permissionController; PermissionController public permissionControllerImplementation; - SlashEscrowFactory public slashEscrowFactory; - SlashEscrowFactory public slashEscrowFactoryImplementation; EmptyContract public emptyContract; @@ -214,9 +210,6 @@ contract DeployFromScratch is Script, Test { permissionController = PermissionController( address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")) ); - slashEscrowFactory = SlashEscrowFactory( - address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")) - ); // if on mainnet, use the ETH2 deposit contract address if (chainId == 1) ethPOSDeposit = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); @@ -238,7 +231,7 @@ contract DeployFromScratch is Script, Test { SEMVER ); - strategyManagerImplementation = new StrategyManager(delegation, slashEscrowFactory, eigenLayerPauserReg, SEMVER); + strategyManagerImplementation = new StrategyManager(delegation, eigenLayerPauserReg, SEMVER); avsDirectoryImplementation = new AVSDirectory(delegation, eigenLayerPauserReg, SEMVER); eigenPodManagerImplementation = new EigenPodManager(ethPOSDeposit, eigenPodBeacon, delegation, eigenLayerPauserReg, SEMVER); @@ -267,8 +260,6 @@ contract DeployFromScratch is Script, Test { ); permissionControllerImplementation = new PermissionController(SEMVER); strategyFactoryImplementation = new StrategyFactory(strategyManager, eigenLayerPauserReg, SEMVER); - slashEscrowFactoryImplementation = - new SlashEscrowFactory(allocationManager, strategyManager, eigenLayerPauserReg, new SlashEscrow(), SEMVER); // Third, upgrade the proxy contracts to use the correct implementation contracts and initialize them. { diff --git a/script/deploy/local/deploy_from_scratch.slashing.s.sol b/script/deploy/local/deploy_from_scratch.slashing.s.sol index cadbe4aace..b70d597f86 100644 --- a/script/deploy/local/deploy_from_scratch.slashing.s.sol +++ b/script/deploy/local/deploy_from_scratch.slashing.s.sol @@ -14,8 +14,6 @@ import "../../../src/contracts/core/AVSDirectory.sol"; import "../../../src/contracts/core/RewardsCoordinator.sol"; import "../../../src/contracts/core/AllocationManager.sol"; import "../../../src/contracts/permissions/PermissionController.sol"; -import "../../../src/contracts/core/SlashEscrowFactory.sol"; -import "../../../src/contracts/core/SlashEscrow.sol"; import "../../../src/contracts/strategies/StrategyBaseTVLLimits.sol"; @@ -68,8 +66,6 @@ contract DeployFromScratch is Script, Test { AllocationManager public allocationManager; PermissionController public permissionControllerImplementation; PermissionController public permissionController; - SlashEscrowFactory public slashEscrowFactory; - SlashEscrowFactory public slashEscrowFactoryImplementation; EmptyContract public emptyContract; @@ -222,9 +218,6 @@ contract DeployFromScratch is Script, Test { permissionController = PermissionController( address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")) ); - slashEscrowFactory = SlashEscrowFactory( - address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")) - ); // if on mainnet, use the ETH2 deposit contract address if (chainId == 1) ethPOSDeposit = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); @@ -245,7 +238,7 @@ contract DeployFromScratch is Script, Test { MIN_WITHDRAWAL_DELAY, SEMVER ); - strategyManagerImplementation = new StrategyManager(delegation, slashEscrowFactory, eigenLayerPauserReg, SEMVER); + strategyManagerImplementation = new StrategyManager(delegation, eigenLayerPauserReg, SEMVER); avsDirectoryImplementation = new AVSDirectory(delegation, eigenLayerPauserReg, SEMVER); eigenPodManagerImplementation = new EigenPodManager(ethPOSDeposit, eigenPodBeacon, delegation, eigenLayerPauserReg, SEMVER); @@ -273,8 +266,6 @@ contract DeployFromScratch is Script, Test { SEMVER ); permissionControllerImplementation = new PermissionController(SEMVER); - slashEscrowFactoryImplementation = - new SlashEscrowFactory(allocationManager, strategyManager, eigenLayerPauserReg, new SlashEscrow(), SEMVER); // Third, upgrade the proxy contracts to use the correct implementation contracts and initialize them. { diff --git a/script/releases/Env.sol b/script/releases/Env.sol index 75dad851ff..a5911bab11 100644 --- a/script/releases/Env.sol +++ b/script/releases/Env.sol @@ -15,10 +15,6 @@ import "src/contracts/core/RewardsCoordinator.sol"; import "src/contracts/interfaces/IRewardsCoordinator.sol"; import "src/contracts/core/StrategyManager.sol"; -/// slashEscrow/ -import "src/contracts/core/SlashEscrow.sol"; -import "src/contracts/core/SlashEscrowFactory.sol"; - /// permissions/ import "src/contracts/permissions/PauserRegistry.sol"; import "src/contracts/permissions/PermissionController.sol"; @@ -96,9 +92,7 @@ library Env { return _envAddress("proxyAdmin"); } - function slashEscrowProxyAdmin() internal view returns (address) { - return _envAddress("slashEscrowProxyAdmin"); - } + function ethPOS() internal view returns (IETHPOSDeposit) { return IETHPOSDeposit(_envAddress("ethPOS")); @@ -330,26 +324,7 @@ library Env { return StrategyFactory(_deployedImpl(type(StrategyFactory).name)); } - /** - * slashEscrow/ - */ - function slashEscrow( - DeployedImpl - ) internal view returns (SlashEscrow) { - return SlashEscrow(_deployedImpl(type(SlashEscrow).name)); - } - - function slashEscrowFactory( - DeployedProxy - ) internal view returns (SlashEscrowFactory) { - return SlashEscrowFactory(_deployedProxy(type(SlashEscrowFactory).name)); - } - function slashEscrowFactory( - DeployedImpl - ) internal view returns (SlashEscrowFactory) { - return SlashEscrowFactory(_deployedImpl(type(SlashEscrowFactory).name)); - } /** * token/ diff --git a/script/utils/ExistingDeploymentParser.sol b/script/utils/ExistingDeploymentParser.sol index 929f10dba9..1889e862d8 100644 --- a/script/utils/ExistingDeploymentParser.sol +++ b/script/utils/ExistingDeploymentParser.sol @@ -11,8 +11,6 @@ import "../../src/contracts/core/AVSDirectory.sol"; import "../../src/contracts/core/RewardsCoordinator.sol"; import "../../src/contracts/core/AllocationManager.sol"; import "../../src/contracts/permissions/PermissionController.sol"; -import "../../src/contracts/core/SlashEscrowFactory.sol"; -import "../../src/contracts/core/SlashEscrow.sol"; import "../../src/contracts/strategies/StrategyFactory.sol"; import "../../src/contracts/strategies/StrategyBase.sol"; @@ -137,9 +135,7 @@ contract ExistingDeploymentParser is Script, Logger { StrategyBase public baseStrategyImplementation; StrategyBase public strategyFactoryBeaconImplementation; - /// @dev SlashEscrowFactory - SlashEscrowFactory public slashEscrowFactory; - SlashEscrowFactory public slashEscrowFactoryImplementation; + // Token ProxyAdmin public tokenProxyAdmin; diff --git a/src/contracts/core/SlashEscrow.sol b/src/contracts/core/SlashEscrow.sol deleted file mode 100644 index e85877228e..0000000000 --- a/src/contracts/core/SlashEscrow.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.27; - -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin-upgrades/contracts/proxy/ClonesUpgradeable.sol"; -import "../interfaces/ISlashEscrow.sol"; -import "../interfaces/ISlashEscrowFactory.sol"; - -contract SlashEscrow is ISlashEscrow { - using OperatorSetLib for *; - using SafeERC20 for IERC20; - - /// @inheritdoc ISlashEscrow - function releaseTokens( - ISlashEscrowFactory slashEscrowFactory, - ISlashEscrow slashEscrowImplementation, - OperatorSet calldata operatorSet, - uint256 slashId, - address recipient, - IStrategy strategy - ) external { - // Assert that the deployment parameters are valid by validating against the address of this proxy. - require( - verifyDeploymentParameters(slashEscrowFactory, slashEscrowImplementation, operatorSet, slashId), - InvalidDeploymentParameters() - ); - - // Assert that the caller is the slash escrow factory. - require(msg.sender == address(slashEscrowFactory), OnlySlashEscrowFactory()); - - // Burn or redistribute the underlying tokens. - IERC20 underlyingToken = strategy.underlyingToken(); - underlyingToken.safeTransfer(recipient, underlyingToken.balanceOf(address(this))); - } - - /// @inheritdoc ISlashEscrow - function verifyDeploymentParameters( - ISlashEscrowFactory slashEscrowFactory, - ISlashEscrow slashEscrowImplementation, - OperatorSet calldata operatorSet, - uint256 slashId - ) public view returns (bool) { - return ClonesUpgradeable.predictDeterministicAddress( - address(slashEscrowImplementation), - keccak256(abi.encodePacked(operatorSet.key(), slashId)), - address(slashEscrowFactory) - ) == address(this); - } -} diff --git a/src/contracts/core/SlashEscrowFactory.sol b/src/contracts/core/SlashEscrowFactory.sol deleted file mode 100644 index 325c6464b1..0000000000 --- a/src/contracts/core/SlashEscrowFactory.sol +++ /dev/null @@ -1,486 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.27; - -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin-upgrades/contracts/proxy/ClonesUpgradeable.sol"; -import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; -import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; -import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; -import "../permissions/Pausable.sol"; -import "../mixins/SemVerMixin.sol"; -import "./SlashEscrowFactoryStorage.sol"; - -contract SlashEscrowFactory is - Initializable, - SlashEscrowFactoryStorage, - OwnableUpgradeable, - Pausable, - SemVerMixin, - ReentrancyGuardUpgradeable -{ - using SafeERC20 for IERC20; - using OperatorSetLib for *; - using EnumerableSet for *; - using ClonesUpgradeable for address; - - modifier onlyStrategyManager() { - require(msg.sender == address(strategyManager), OnlyStrategyManager()); - _; - } - - /** - * - * INITIALIZATION - * - */ - constructor( - IAllocationManager _allocationManager, - IStrategyManager _strategyManager, - IPauserRegistry _pauserRegistry, - ISlashEscrow _slashEscrowImplementation, - string memory _version - ) - SlashEscrowFactoryStorage(_allocationManager, _strategyManager, _slashEscrowImplementation) - Pausable(_pauserRegistry) - SemVerMixin(_version) - { - _disableInitializers(); - } - - /// @inheritdoc ISlashEscrowFactory - function initialize( - address initialOwner, - uint256 initialPausedStatus, - uint32 initialGlobalDelayBlocks - ) external initializer { - _transferOwnership(initialOwner); - _setPausedStatus(initialPausedStatus); - _setGlobalEscrowDelay(initialGlobalDelayBlocks); - } - - /** - * - * ACTIONS - * - */ - - /// @inheritdoc ISlashEscrowFactory - function initiateSlashEscrow( - OperatorSet calldata operatorSet, - uint256 slashId, - IStrategy strategy - ) external onlyStrategyManager { - // Create storage pointers for readability. - EnumerableSet.UintSet storage pendingSlashIds = _pendingSlashIds[operatorSet.key()]; - EnumerableSet.AddressSet storage pendingStrategiesForSlashId = - _pendingStrategiesForSlashId[operatorSet.key()][slashId]; - - // Note: Since this function can be called multiple times for the same operatorSet/slashId, we check - // if the slash escrow is already deployed. If it is not, we deploy it and update the pending mappings. - if (!isDeployedSlashEscrow(operatorSet, slashId)) { - // Deploy the `SlashEscrow`. - _deploySlashEscrow(operatorSet, slashId); - - // Update the pending mappings. - _pendingOperatorSets.add(operatorSet.key()); - pendingSlashIds.add(slashId); - } - - // Add the strategy to the pending strategies for the slash ID. - pendingStrategiesForSlashId.add(address(strategy)); - - // Calculate the complete block for the strategy, and fetch the current complete block. - // The escrow delay for a strategy is determined by taking the maximum of: - // 1. The global escrow delay (applies to all strategies). - // 2. The strategy-specific delay (if set). - uint32 completeBlock = uint32(block.number + getStrategyEscrowDelay(strategy) + 1); - uint32 currentCompleteBlock = _slashIdToCompleteBlock[operatorSet.key()][slashId]; - - // Only update the maturity block if the new calculated maturity block (with the strategy delay) - // is further in the future than the currently stored value. This ensures the escrow cannot be released - // before the longest required delay among all strategies for this slashId. - if (completeBlock > currentCompleteBlock) { - _slashIdToCompleteBlock[operatorSet.key()][slashId] = completeBlock; - } - - // Emit the start escrow event. We can use the block.number here because all strategies - // in a given operatorSet/slashId will have their escrow initiated in the same transaction. - emit StartEscrow(operatorSet, slashId, strategy, uint32(block.number)); - } - - /// @inheritdoc ISlashEscrowFactory - function releaseSlashEscrow( - OperatorSet calldata operatorSet, - uint256 slashId - ) external onlyWhenNotPaused(PAUSED_RELEASE_ESCROW) nonReentrant { - address redistributionRecipient = allocationManager.getRedistributionRecipient(operatorSet); - - _checkReleaseSlashEscrow(operatorSet, slashId, redistributionRecipient); - - // Calling `clearBurnOrRedistributableShares` will transfer the underlying tokens to the `SlashEscrow`. - // NOTE: While `clearBurnOrRedistributableShares` may have already been called, we call it again to ensure that the - // underlying tokens are actually in escrow before processing and removing storage (which would otherwise prevent - // the tokens from being released). - strategyManager.clearBurnOrRedistributableShares(operatorSet, slashId); - - // Process the slash escrow for each strategy. - address[] memory strategies = _pendingStrategiesForSlashId[operatorSet.key()][slashId].values(); - for (uint256 i = 0; i < strategies.length; ++i) { - _processSlashEscrowByStrategy({ - operatorSet: operatorSet, - slashId: slashId, - slashEscrow: getSlashEscrow(operatorSet, slashId), - redistributionRecipient: redistributionRecipient, - strategy: IStrategy(strategies[i]) - }); - } - - // Update the slash escrow storage. - _updateSlashEscrowStorage(operatorSet, slashId); - } - - /// @inheritdoc ISlashEscrowFactory - function releaseSlashEscrowByStrategy( - OperatorSet calldata operatorSet, - uint256 slashId, - IStrategy strategy - ) external virtual onlyWhenNotPaused(PAUSED_RELEASE_ESCROW) nonReentrant { - address redistributionRecipient = allocationManager.getRedistributionRecipient(operatorSet); - - _checkReleaseSlashEscrow(operatorSet, slashId, redistributionRecipient); - - // Calling `clearBurnOrRedistributableSharesByStrategy` will transfer the underlying tokens to the `SlashEscrow`. - // NOTE: While the strategy may have already been cleared, we call it again to ensure that the - // underlying tokens are actually in escrow before processing and removing storage (which would otherwise prevent - // the tokens from being released). - strategyManager.clearBurnOrRedistributableSharesByStrategy(operatorSet, slashId, strategy); - - // Release the slashEscrow. - _processSlashEscrowByStrategy({ - operatorSet: operatorSet, - slashId: slashId, - slashEscrow: getSlashEscrow(operatorSet, slashId), - redistributionRecipient: redistributionRecipient, - strategy: strategy - }); - - // Update the slash escrow storage. - _updateSlashEscrowStorage(operatorSet, slashId); - } - - /** - * - * PAUSER/UNPAUSER ACTIONS - * - */ - - /// @inheritdoc ISlashEscrowFactory - function pauseEscrow(OperatorSet calldata operatorSet, uint256 slashId) external virtual onlyPauser { - require(!_paused[operatorSet.key()][slashId], IPausable.InvalidNewPausedStatus()); - _paused[operatorSet.key()][slashId] = true; - emit EscrowPaused(operatorSet, slashId); - } - - /// @inheritdoc ISlashEscrowFactory - function unpauseEscrow(OperatorSet calldata operatorSet, uint256 slashId) external virtual onlyUnpauser { - require(_paused[operatorSet.key()][slashId], IPausable.InvalidNewPausedStatus()); - _paused[operatorSet.key()][slashId] = false; - emit EscrowUnpaused(operatorSet, slashId); - } - - /** - * - * OWNER ACTIONS - * - */ - - /// @inheritdoc ISlashEscrowFactory - function setGlobalEscrowDelay( - uint32 delay - ) external onlyOwner { - _setGlobalEscrowDelay(delay); - } - - /// @inheritdoc ISlashEscrowFactory - function setStrategyEscrowDelay(IStrategy strategy, uint32 delay) external onlyOwner { - _strategyEscrowDelayBlocks[address(strategy)] = delay; - emit StrategyEscrowDelaySet(strategy, delay); - } - - /** - * - * HELPERS - * - */ - - /// @notice Checks that the slash escrow can be released. - function _checkReleaseSlashEscrow( - OperatorSet calldata operatorSet, - uint256 slashId, - address redistributionRecipient - ) internal view { - // If the redistribution recipient is not the default burn address... - if (redistributionRecipient != DEFAULT_BURN_ADDRESS) { - require(msg.sender == redistributionRecipient, OnlyRedistributionRecipient()); - } - - // Assert that the slash ID is not paused - require(!isEscrowPaused(operatorSet, slashId), IPausable.CurrentlyPaused()); - - // Assert that the escrow delay has elapsed - // `getEscrowCompleteBlock` returns the block number at which the escrow can be released, so - // we require that the current block number is greater than OR equal to the complete block. - require(block.number >= getEscrowCompleteBlock(operatorSet, slashId), EscrowDelayNotElapsed()); - } - - /// @notice Processes the slash escrow for a single strategy. - function _processSlashEscrowByStrategy( - OperatorSet calldata operatorSet, - uint256 slashId, - ISlashEscrow slashEscrow, - address redistributionRecipient, - IStrategy strategy - ) internal { - // Create storage pointer for readability. - EnumerableSet.AddressSet storage pendingStrategiesForSlashId = - _pendingStrategiesForSlashId[operatorSet.key()][slashId]; - - // Burn or redistribute the underlying tokens for the strategy. - slashEscrow.releaseTokens({ - slashEscrowFactory: ISlashEscrowFactory(address(this)), - slashEscrowImplementation: slashEscrowImplementation, - operatorSet: operatorSet, - slashId: slashId, - recipient: redistributionRecipient, - strategy: strategy - }); - - // Remove the strategy and underlying amount from the pending strategies escrow map. - pendingStrategiesForSlashId.remove(address(strategy)); - emit EscrowComplete(operatorSet, slashId, strategy, redistributionRecipient); - } - - function _updateSlashEscrowStorage(OperatorSet calldata operatorSet, uint256 slashId) internal { - // Create storage pointers for readability. - EnumerableSet.Bytes32Set storage pendingOperatorSets = _pendingOperatorSets; - EnumerableSet.UintSet storage pendingSlashIds = _pendingSlashIds[operatorSet.key()]; - uint256 totalPendingForSlashId = _pendingStrategiesForSlashId[operatorSet.key()][slashId].length(); - - // If there are no more strategies to process, remove the slash ID from the pending slash IDs set. - if (totalPendingForSlashId == 0) { - pendingSlashIds.remove(slashId); - - // Delete the start block for the slash ID. - delete _slashIdToCompleteBlock[operatorSet.key()][slashId]; - - // If there are no more slash IDs for the operator set, remove the operator set from the pending operator sets set. - if (pendingSlashIds.length() == 0) { - pendingOperatorSets.remove(operatorSet.key()); - } - } - } - - /// @notice Sets the global escrow delay. - function _setGlobalEscrowDelay( - uint32 delay - ) internal { - _globalEscrowDelayBlocks = delay; - emit GlobalEscrowDelaySet(delay); - } - - /** - * @notice Deploys a `SlashEscrow` - * @param operatorSet The operator set whose slash escrow is being deployed. - * @param slashId The slash ID of the slash escrow that is being deployed. - * @dev The slash escrow is deployed in `initiateSlashEscrow` - */ - function _deploySlashEscrow(OperatorSet calldata operatorSet, uint256 slashId) internal { - address(slashEscrowImplementation).cloneDeterministic(computeSlashEscrowSalt(operatorSet, slashId)); - } - - /** - * - * GETTERS - * - */ - - /// @inheritdoc ISlashEscrowFactory - function getPendingOperatorSets() public view returns (OperatorSet[] memory operatorSets) { - bytes32[] memory operatorSetKeys = _pendingOperatorSets.values(); - - operatorSets = new OperatorSet[](operatorSetKeys.length); - - for (uint256 i = 0; i < operatorSetKeys.length; ++i) { - operatorSets[i] = operatorSetKeys[i].decode(); - } - - return operatorSets; - } - - /// @inheritdoc ISlashEscrowFactory - function getTotalPendingOperatorSets() external view returns (uint256) { - return _pendingOperatorSets.length(); - } - - /// @inheritdoc ISlashEscrowFactory - function isPendingOperatorSet( - OperatorSet calldata operatorSet - ) external view returns (bool) { - return _pendingOperatorSets.contains(operatorSet.key()); - } - - /// @inheritdoc ISlashEscrowFactory - function getPendingSlashIds( - OperatorSet memory operatorSet - ) public view returns (uint256[] memory) { - return _pendingSlashIds[operatorSet.key()].values(); - } - - /// @inheritdoc ISlashEscrowFactory - function getTotalPendingSlashIds( - OperatorSet calldata operatorSet - ) external view returns (uint256) { - return _pendingSlashIds[operatorSet.key()].length(); - } - - /// @inheritdoc ISlashEscrowFactory - function getPendingEscrows() - external - view - returns ( - OperatorSet[] memory operatorSets, - bool[] memory isRedistributing, - uint256[][] memory slashIds, - uint32[][] memory completeBlocks - ) - { - operatorSets = getPendingOperatorSets(); - isRedistributing = new bool[](operatorSets.length); - slashIds = new uint256[][](operatorSets.length); - completeBlocks = new uint32[][](operatorSets.length); - - // Populate all arrays. - for (uint256 i = 0; i < operatorSets.length; i++) { - // Get whether the operator set is redistributing. - isRedistributing[i] = allocationManager.isRedistributingOperatorSet(operatorSets[i]); - - // Get the pending slash IDs for the operator set. - slashIds[i] = getPendingSlashIds(operatorSets[i]); - - // For each slashId, get the complete block. - completeBlocks[i] = new uint32[](slashIds[i].length); - for (uint256 j = 0; j < slashIds[i].length; j++) { - completeBlocks[i][j] = getEscrowCompleteBlock(operatorSets[i], slashIds[i][j]); - } - } - } - - /// @inheritdoc ISlashEscrowFactory - function isPendingSlashId(OperatorSet calldata operatorSet, uint256 slashId) external view returns (bool) { - return _pendingSlashIds[operatorSet.key()].contains(slashId); - } - - /// @inheritdoc ISlashEscrowFactory - function getPendingStrategiesForSlashId( - OperatorSet memory operatorSet, - uint256 slashId - ) public view returns (IStrategy[] memory strategies) { - EnumerableSet.AddressSet storage pendingStrategiesForSlashId = - _pendingStrategiesForSlashId[operatorSet.key()][slashId]; - - uint256 length = pendingStrategiesForSlashId.length(); - - strategies = new IStrategy[](length); - - for (uint256 i = 0; i < length; ++i) { - address strategy = pendingStrategiesForSlashId.at(i); - - strategies[i] = IStrategy(strategy); - } - } - - /// @inheritdoc ISlashEscrowFactory - function getPendingStrategiesForSlashIds( - OperatorSet memory operatorSet - ) public view returns (IStrategy[][] memory strategies) { - EnumerableSet.UintSet storage pendingSlashIds = _pendingSlashIds[operatorSet.key()]; - - uint256 length = pendingSlashIds.length(); - - strategies = new IStrategy[][](length); - - for (uint256 i = 0; i < length; ++i) { - strategies[i] = getPendingStrategiesForSlashId(operatorSet, pendingSlashIds.at(i)); - } - } - - /// @inheritdoc ISlashEscrowFactory - function getTotalPendingStrategiesForSlashId( - OperatorSet calldata operatorSet, - uint256 slashId - ) external view returns (uint256) { - return _pendingStrategiesForSlashId[operatorSet.key()][slashId].length(); - } - - /// @inheritdoc ISlashEscrowFactory - function getPendingUnderlyingAmountForStrategy( - OperatorSet calldata operatorSet, - uint256 slashId, - IStrategy strategy - ) external view returns (uint256) { - return strategy.underlyingToken().balanceOf(address(getSlashEscrow(operatorSet, slashId))); - } - - /// @inheritdoc ISlashEscrowFactory - function isEscrowPaused(OperatorSet calldata operatorSet, uint256 slashId) public view returns (bool) { - return _paused[operatorSet.key()][slashId] || paused(PAUSED_RELEASE_ESCROW); - } - - /// @inheritdoc ISlashEscrowFactory - function getEscrowCompleteBlock(OperatorSet memory operatorSet, uint256 slashId) public view returns (uint32) { - return _slashIdToCompleteBlock[operatorSet.key()][slashId]; - } - - /// @inheritdoc ISlashEscrowFactory - function getStrategyEscrowDelay( - IStrategy strategy - ) public view returns (uint32) { - uint32 globalDelay = _globalEscrowDelayBlocks; - uint32 strategyDelay = _strategyEscrowDelayBlocks[address(strategy)]; - - // Return whichever delay is greater. - return strategyDelay > globalDelay ? strategyDelay : globalDelay; - } - - /// @inheritdoc ISlashEscrowFactory - function getGlobalEscrowDelay() external view returns (uint32) { - return _globalEscrowDelayBlocks; - } - - /// @inheritdoc ISlashEscrowFactory - function computeSlashEscrowSalt(OperatorSet calldata operatorSet, uint256 slashId) public pure returns (bytes32) { - return keccak256(abi.encodePacked(operatorSet.key(), slashId)); - } - - /// @inheritdoc ISlashEscrowFactory - function isDeployedSlashEscrow(OperatorSet calldata operatorSet, uint256 slashId) public view returns (bool) { - return isDeployedSlashEscrow(getSlashEscrow(operatorSet, slashId)); - } - - /// @inheritdoc ISlashEscrowFactory - function isDeployedSlashEscrow( - ISlashEscrow slashEscrow - ) public view returns (bool) { - return address(slashEscrow).code.length != 0; - } - - /// @inheritdoc ISlashEscrowFactory - function getSlashEscrow(OperatorSet calldata operatorSet, uint256 slashId) public view returns (ISlashEscrow) { - return ISlashEscrow( - address(slashEscrowImplementation).predictDeterministicAddress({ - salt: computeSlashEscrowSalt(operatorSet, slashId), - deployer: address(this) - }) - ); - } -} diff --git a/src/contracts/core/SlashEscrowFactoryStorage.sol b/src/contracts/core/SlashEscrowFactoryStorage.sol deleted file mode 100644 index a6e820e5b9..0000000000 --- a/src/contracts/core/SlashEscrowFactoryStorage.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.27; - -import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import "../interfaces/ISlashEscrowFactory.sol"; -import "../interfaces/IAllocationManager.sol"; -import "../interfaces/IStrategyManager.sol"; -import "../interfaces/IStrategy.sol"; -import "../interfaces/ISlashEscrow.sol"; - -abstract contract SlashEscrowFactoryStorage is ISlashEscrowFactory { - // Constants - - /// @dev The default burn address for slashed funds. - address internal constant DEFAULT_BURN_ADDRESS = 0x00000000000000000000000000000000000E16E4; - - /// @notice The pause status for the `releaseSlashEscrow` function. - /// @dev Allows all escrow outflows to be temporarily halted. - uint8 internal constant PAUSED_RELEASE_ESCROW = 0; - - // Immutable Storage - - /// @notice Returns the EigenLayer `AllocationManager` address. - IAllocationManager public immutable allocationManager; - - /// @notice Returns the EigenLayer `StrategyManager` address. - IStrategyManager public immutable strategyManager; - - /// @notice Returns the implementation contract for the slash escrow. - /// @dev This value should not be changed on future upgrades. - ISlashEscrow public immutable slashEscrowImplementation; - - // Mutable Storage - - /// @dev Returns a list of operator sets that have pending slash IDs. - EnumerableSet.Bytes32Set internal _pendingOperatorSets; - - /// @dev Returns a list of pending slash IDs for a given operator set. - mapping(bytes32 operatorSetKey => EnumerableSet.UintSet) internal _pendingSlashIds; - - /// @dev Returns an enumerable mapping of strategies to their underlying amounts for a given slash ID. - mapping(bytes32 operatorSetKey => mapping(uint256 slashId => EnumerableSet.AddressSet)) internal - _pendingStrategiesForSlashId; - - /// @dev Returns the start block for a given slash ID. - mapping(bytes32 operatorSetKey => mapping(uint256 slashId => uint32 completeBlock)) internal _slashIdToCompleteBlock; - - /// @notice Returns the paused status for a given operator set and slash ID. - mapping(bytes32 operatorSetKey => mapping(uint256 slashId => bool paused)) internal _paused; - - /// @dev Returns the global escrow delay for all strategies. - uint32 internal _globalEscrowDelayBlocks; - - /// @dev Returns the escrow delay for a given strategy. - mapping(address strategy => uint32 delay) internal _strategyEscrowDelayBlocks; - - // Constructor - - constructor( - IAllocationManager _allocationManager, - IStrategyManager _strategyManager, - ISlashEscrow _slashEscrowImplementation - ) { - allocationManager = _allocationManager; - strategyManager = _strategyManager; - slashEscrowImplementation = _slashEscrowImplementation; - } - - /** - * @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[42] private __gap; -} diff --git a/src/contracts/core/StrategyManager.sol b/src/contracts/core/StrategyManager.sol index 3ecc65bd64..15e6cbebf6 100644 --- a/src/contracts/core/StrategyManager.sol +++ b/src/contracts/core/StrategyManager.sol @@ -54,11 +54,10 @@ contract StrategyManager is */ constructor( IDelegationManager _delegation, - ISlashEscrowFactory _slashEscrowFactory, IPauserRegistry _pauserRegistry, string memory _version ) - StrategyManagerStorage(_delegation, _slashEscrowFactory) + StrategyManagerStorage(_delegation) Pausable(_pauserRegistry) SignatureUtilsMixin(_version) { @@ -162,8 +161,6 @@ contract StrategyManager is // Add the shares to the operator set's burn or redistributable shares. require(burnOrRedistributableShares.set(address(strategy), sharesToBurn), StrategyAlreadyInSlash()); - // Notify the `SlashEscrowFactory` contract that it received underlying tokens to burn or redistribute. - slashEscrowFactory.initiateSlashEscrow(operatorSet, slashId, strategy); emit BurnOrRedistributableSharesIncreased(operatorSet, slashId, strategy, sharesToBurn); } @@ -199,9 +196,9 @@ contract StrategyManager is uint256 amountOut; if (sharesToRemove != 0) { - // Withdraw the shares to the slash escrow. + // Withdraw the shares to the burn address. amountOut = IStrategy(strategy).withdraw({ - recipient: address(slashEscrowFactory.getSlashEscrow(operatorSet, slashId)), + recipient: DEFAULT_BURN_ADDRESS, token: IStrategy(strategy).underlyingToken(), amountShares: sharesToRemove }); diff --git a/src/contracts/core/StrategyManagerStorage.sol b/src/contracts/core/StrategyManagerStorage.sol index 04ba533ebf..b0960450ab 100644 --- a/src/contracts/core/StrategyManagerStorage.sol +++ b/src/contracts/core/StrategyManagerStorage.sol @@ -6,7 +6,6 @@ import "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; import "../interfaces/IAVSDirectory.sol"; import "../interfaces/IDelegationManager.sol"; import "../interfaces/IEigenPodManager.sol"; -import "../interfaces/ISlashEscrowFactory.sol"; import "../interfaces/IStrategy.sol"; import "../interfaces/IStrategyManager.sol"; @@ -36,8 +35,6 @@ abstract contract StrategyManagerStorage is IStrategyManager { IDelegationManager public immutable delegation; - ISlashEscrowFactory public immutable slashEscrowFactory; - // Mutatables /// @dev Do not remove, deprecated storage. @@ -87,9 +84,8 @@ abstract contract StrategyManagerStorage is IStrategyManager { /** * @param _delegation The delegation contract of EigenLayer. */ - constructor(IDelegationManager _delegation, ISlashEscrowFactory _slashEscrowFactory) { + constructor(IDelegationManager _delegation) { delegation = _delegation; - slashEscrowFactory = _slashEscrowFactory; } /** diff --git a/src/contracts/interfaces/ISlashEscrow.sol b/src/contracts/interfaces/ISlashEscrow.sol deleted file mode 100644 index f3088b7ebd..0000000000 --- a/src/contracts/interfaces/ISlashEscrow.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.27; - -import "../interfaces/ISlashEscrowFactory.sol"; -import "../libraries/OperatorSetLib.sol"; -import "../interfaces/IStrategy.sol"; - -interface ISlashEscrow { - /// @notice Thrown when the provided deployment parameters do not create this contract's address. - error InvalidDeploymentParameters(); - - /// @notice Thrown when the caller is not the slash escrow factory. - error OnlySlashEscrowFactory(); - - /** - * @notice Burns or redistributes the underlying tokens of the strategies. - * @param slashEscrowFactory The factory contract that created the slash escrow. - * @param slashEscrowImplementation The implementation contract that was used to create the slash escrow. - * @param operatorSet The operator set that was used to create the slash escrow. - * @param slashId The slash ID that was used to create the slash escrow. - * @param recipient The recipient of the underlying tokens. - * @param strategy The strategy that was used to create the slash escrow. - */ - function releaseTokens( - ISlashEscrowFactory slashEscrowFactory, - ISlashEscrow slashEscrowImplementation, - OperatorSet calldata operatorSet, - uint256 slashId, - address recipient, - IStrategy strategy - ) external; - - /** - * @notice Verifies the deployment parameters of the slash escrow. - * @param slashEscrowFactory The factory contract that created the slash escrow. - * @param slashEscrowImplementation The implementation contract that was used to create the slash escrow. - * @param operatorSet The operator set that was used to create the slash escrow. - * @param slashId The slash ID that was used to create the slash escrow. - * @return True if the provided parameters create this contract's address, false otherwise. - * @dev Uses ClonesUpgradeable.predictDeterministicAddress() to compute the expected address from the parameters. - * - Compares the computed address against this contract's address to validate parameter integrity. - * - Provides a stateless validation mechanism for releaseTokens() inputs. - * - Security relies on the cryptographic properties of CREATE2 address derivation. - * - Attack vector would require finding a hash collision in the CREATE2 address computation. - */ - function verifyDeploymentParameters( - ISlashEscrowFactory slashEscrowFactory, - ISlashEscrow slashEscrowImplementation, - OperatorSet calldata operatorSet, - uint256 slashId - ) external view returns (bool); -} diff --git a/src/contracts/interfaces/ISlashEscrowFactory.sol b/src/contracts/interfaces/ISlashEscrowFactory.sol deleted file mode 100644 index 3121048e6a..0000000000 --- a/src/contracts/interfaces/ISlashEscrowFactory.sol +++ /dev/null @@ -1,295 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity >=0.5.0; - -import "../interfaces/IStrategy.sol"; -import "../libraries/OperatorSetLib.sol"; -import "../interfaces/ISlashEscrow.sol"; - -interface ISlashEscrowFactoryErrors { - /// @notice Thrown when a caller is not the strategy manager. - error OnlyStrategyManager(); - - /// @notice Thrown when a caller is not the redistribution recipient. - error OnlyRedistributionRecipient(); - - /// @notice Thrown when a escrow is not mature. - error EscrowNotMature(); - - /// @notice Thrown when the escrow delay has not elapsed. - error EscrowDelayNotElapsed(); -} - -interface ISlashEscrowFactoryEvents { - /// @notice Emitted when a escrow is initiated. - event StartEscrow(OperatorSet operatorSet, uint256 slashId, IStrategy strategy, uint32 startBlock); - - /// @notice Emitted when a escrow is released. - event EscrowComplete(OperatorSet operatorSet, uint256 slashId, IStrategy strategy, address recipient); - - /// @notice Emitted when a escrow is paused. - event EscrowPaused(OperatorSet operatorSet, uint256 slashId); - - /// @notice Emitted when a escrow is unpaused. - event EscrowUnpaused(OperatorSet operatorSet, uint256 slashId); - - /// @notice Emitted when a global escrow delay is set. - event GlobalEscrowDelaySet(uint32 delay); - - /// @notice Emitted when a escrow delay is set. - event StrategyEscrowDelaySet(IStrategy strategy, uint32 delay); -} - -interface ISlashEscrowFactory is ISlashEscrowFactoryErrors, ISlashEscrowFactoryEvents { - /** - * @notice Initializes the initial owner and paused status. - * @param initialOwner The initial owner of the router. - * @param initialPausedStatus The initial paused status of the router. - * @param initialGlobalDelayBlocks The initial global escrow delay. - */ - function initialize(address initialOwner, uint256 initialPausedStatus, uint32 initialGlobalDelayBlocks) external; - - /** - * @notice Initiates a slash escrow. - * @param operatorSet The operator set whose escrow is being locked up. - * @param slashId The slash ID of the escrow that is being locked up. - * @param strategy The strategy that whose underlying tokens are being redistributed. - * @dev This function can be called multiple times for a given `operatorSet` and `slashId`. - */ - function initiateSlashEscrow(OperatorSet calldata operatorSet, uint256 slashId, IStrategy strategy) external; - - /** - * @notice Releases an escrow by transferring tokens from the `SlashEscrow` to the operator set's redistribution recipient. - * @param operatorSet The operator set whose escrow is being released. - * @param slashId The slash ID of the escrow that is being released. - * @dev The caller must be the escrow recipient, unless the escrow recipient - * is the default burn address in which case anyone can call. - * @dev The slash escrow is released once the delay for ALL strategies has elapsed. - */ - function releaseSlashEscrow(OperatorSet calldata operatorSet, uint256 slashId) external; - - /** - * @notice Releases an escrow for a single strategy in a slash. - * @param operatorSet The operator set whose escrow is being released. - * @param slashId The slash ID of the escrow that is being released. - * @param strategy The strategy whose escrow is being released. - * @dev The caller must be the redistribution recipient, unless the redistribution recipient - * is the default burn address in which case anyone can call. - * @dev The slash escrow is released once the delay for ALL strategies has elapsed. - */ - function releaseSlashEscrowByStrategy( - OperatorSet calldata operatorSet, - uint256 slashId, - IStrategy strategy - ) external; - - /** - * @notice Pauses an individual slash escrow. - * @param operatorSet The operator set whose escrow is being paused. - * @param slashId The slash ID of the escrow that is being paused. - * @dev Allows governance to pause a specific slash escrow in response to security incidents or other emergencies. - * @dev When paused, the slashed funds become locked and can only be rescued through a protocol upgrade. - * @dev To pause all escrows simultaneously, use the `pauseAll()` function instead. - */ - function pauseEscrow(OperatorSet calldata operatorSet, uint256 slashId) external; - - /** - * @notice Unpauses an individual slash escrow. - * @param operatorSet The operator set whose escrow is being unpaused. - * @param slashId The slash ID of the escrow that is being unpaused. - */ - function unpauseEscrow(OperatorSet calldata operatorSet, uint256 slashId) external; - - /** - * @notice Sets the delay for the escrow of a strategies underlying token. - * @dev The largest of all strategy delays or global delay will be used. - * @dev This delay setting only applies to new slashes and does not affect existing ones. - * @param strategy The strategy whose escrow delay is being set. - * @param delay The delay for the escrow. - */ - function setStrategyEscrowDelay(IStrategy strategy, uint32 delay) external; - - /** - * @notice Sets a global delay applicable to all strategies. - * @dev This delay setting only applies to new slashes and does not affect existing ones. - * @param delay The delay for the escrow. - */ - function setGlobalEscrowDelay( - uint32 delay - ) external; - - /** - * @notice Returns the operator sets that have pending escrows. - * @return operatorSets The operator sets that have pending escrows. - */ - function getPendingOperatorSets() external view returns (OperatorSet[] memory operatorSets); - - /** - * @notice Returns the total number of operator sets with pending escrows. - * @return The total number of operator sets with pending escrows. - */ - function getTotalPendingOperatorSets() external view returns (uint256); - - /** - * @notice Returns whether an operator set has pending escrows. - * @param operatorSet The operator set whose pending escrows are being queried. - * @return Whether the operator set has pending escrows. - */ - function isPendingOperatorSet( - OperatorSet calldata operatorSet - ) external view returns (bool); - - /** - * @notice Returns the pending slash IDs for an operator set. - * @param operatorSet The operator set whose pending slash IDs are being queried. - */ - function getPendingSlashIds( - OperatorSet calldata operatorSet - ) external view returns (uint256[] memory); - - /** - * @notice Returns the pending escrows and their release blocks. - * @return operatorSets The pending operator sets. - * @return isRedistributing Whether the operator set is redistributing. - * @return slashIds The pending slash IDs for each operator set. Indexed by operator set. - * @return completeBlocks The block at which a slashID can be released. Indexed by [operatorSet][slashId] - */ - function getPendingEscrows() - external - view - returns ( - OperatorSet[] memory operatorSets, - bool[] memory isRedistributing, - uint256[][] memory slashIds, - uint32[][] memory completeBlocks - ); - - /** - * @notice Returns the total number of slash IDs for an operator set. - * @param operatorSet The operator set whose total slash IDs are being queried. - * @return The total number of slash IDs for the operator set. - */ - function getTotalPendingSlashIds( - OperatorSet calldata operatorSet - ) external view returns (uint256); - - /** - * @notice Returns whether a slash ID is pending for an operator set. - * @param operatorSet The operator set whose pending slash IDs are being queried. - * @param slashId The slash ID of the slash that is being queried. - * @return Whether the slash ID is pending for the operator set. - */ - function isPendingSlashId(OperatorSet calldata operatorSet, uint256 slashId) external view returns (bool); - - /** - * @notice Returns the pending strategies for a slash ID for an operator set. - * @dev This is a variant that returns the pending strategies for a slash ID for an operator set. - * @param operatorSet The operator set whose pending strategies are being queried. - * @param slashId The slash ID of the strategies that are being queried. - * @return strategies The strategies that are pending strategies. - */ - function getPendingStrategiesForSlashId( - OperatorSet calldata operatorSet, - uint256 slashId - ) external view returns (IStrategy[] memory strategies); - - /** - * @notice Returns all pending strategies for all slash IDs for an operator set. - * @dev This is a variant that returns all pending strategies for all slash IDs for an operator set. - * @param operatorSet The operator set whose pending strategies are being queried. - * @return strategies The strategies that are pending strategies. - */ - function getPendingStrategiesForSlashIds( - OperatorSet calldata operatorSet - ) external view returns (IStrategy[][] memory strategies); - - /** - * @notice Returns the number of pending strategies for a slash ID for an operator set. - * @param operatorSet The operator set whose pending strategies are being queried. - * @param slashId The slash ID of the strategies that are being queried. - * @return The number of pending strategies. - */ - function getTotalPendingStrategiesForSlashId( - OperatorSet calldata operatorSet, - uint256 slashId - ) external view returns (uint256); - - /** - * @notice Returns the pending underlying amount for a strategy for an operator set and slash ID. - * @param operatorSet The operator set whose pending underlying amount is being queried. - * @param slashId The slash ID of the escrow that is being queried. - * @param strategy The strategy whose pending underlying amount is being queried. - * @return The pending underlying amount. - */ - function getPendingUnderlyingAmountForStrategy( - OperatorSet calldata operatorSet, - uint256 slashId, - IStrategy strategy - ) external view returns (uint256); - - /** - * @notice Returns the paused status of a escrow. - * @param operatorSet The operator set whose escrow is being queried. - * @param slashId The slash ID of the escrow that is being queried. - * @return The paused status of the escrow. - */ - function isEscrowPaused(OperatorSet calldata operatorSet, uint256 slashId) external view returns (bool); - - /** - * @notice Returns the block at which the escrow can be released. - * @param operatorSet The operator set whose start block is being queried. - * @param slashId The slash ID of the start block that is being queried. - * @return The block at which the escrow can be released. - */ - function getEscrowCompleteBlock(OperatorSet calldata operatorSet, uint256 slashId) external view returns (uint32); - - /** - * @notice Returns the escrow delay for a strategy. - * @param strategy The strategy whose escrow delay is being queried. - * @return The escrow delay. - */ - function getStrategyEscrowDelay( - IStrategy strategy - ) external view returns (uint32); - - /** - * @notice Returns the global escrow delay. - * @return The global escrow delay. - */ - function getGlobalEscrowDelay() external view returns (uint32); - - /** - * @notice Returns the salt for a slash escrow. - * @param operatorSet The operator set whose slash escrow is being queried. - * @param slashId The slash ID of the slash escrow that is being queried. - * @return The salt for the slash escrow. - */ - function computeSlashEscrowSalt( - OperatorSet calldata operatorSet, - uint256 slashId - ) external pure returns (bytes32); - - /** - * @notice Returns whether a slash escrow is deployed or not. - * @param operatorSet The operator set whose slash escrow is being queried. - * @param slashId The slash ID of the slash escrow that is being queried. - * @return Whether the slash escrow is deployed. - */ - function isDeployedSlashEscrow(OperatorSet calldata operatorSet, uint256 slashId) external view returns (bool); - - /** - * @notice Returns whether a slash escrow is deployed. - * @param slashEscrow The slash escrow that is being queried. - * @return Whether the slash escrow is deployed. - */ - function isDeployedSlashEscrow( - ISlashEscrow slashEscrow - ) external view returns (bool); - - /** - * @notice Returns the slash escrow for an operator set and slash ID. - * @param operatorSet The operator set whose slash escrow is being queried. - * @param slashId The slash ID of the slash escrow that is being queried. - * @return The slash escrow. - */ - function getSlashEscrow(OperatorSet calldata operatorSet, uint256 slashId) external view returns (ISlashEscrow); -} diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol index bc8bf3f9c1..eb7d853d98 100644 --- a/src/test/integration/IntegrationBase.t.sol +++ b/src/test/integration/IntegrationBase.t.sol @@ -1417,23 +1417,6 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter { uint slashedAtLeast = prevShares[i] - curShares[i]; // Not factoring in slashable shares in queue here, because that gets more complex (TODO) assertTrue(curBurnable >= (prevBurnable + slashedAtLeast), err); - - // TODO: Improve this check in the future, it's not very optimized. - // In the future, we can simply use a flag to communicate whether the operator set is redistributable. - if (curShares[i] == prevShares[i]) continue; - bool flag = false; - for (uint j = 0; j < params.strategies.length; j++) { - if (params.strategies[j] == BEACONCHAIN_ETH_STRAT) flag = true; - } - if (flag) continue; - - assertTrue(_getIsPendingOperatorSet(operatorSet), "operator set should be pending"); - - assertTrue(_getIsPendingSlashId(operatorSet, slashId), "slash id should be pending"); - assertFalse(_getPrevIsPendingSlashId(operatorSet, slashId), "slash id should not be pending"); - - assertTrue(_getIsDeployedSlashEscrow(operatorSet, slashId), "escrow should be deployed"); - assertFalse(_getPrevIsDeployedSlashEscrow(operatorSet, slashId), "escrow should not be deployed"); } } @@ -2779,38 +2762,6 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter { : strategyManager.getBurnOrRedistributableShares(operatorSet, slashId, strategy); } - function _getPrevIsPendingOperatorSet(OperatorSet memory operatorSet) internal timewarp returns (bool) { - return _getIsPendingOperatorSet(operatorSet); - } - - function _getIsPendingOperatorSet(OperatorSet memory operatorSet) internal view returns (bool) { - return slashEscrowFactory.isPendingOperatorSet(operatorSet); - } - - function _getPrevIsPendingSlashId(OperatorSet memory operatorSet, uint slashId) internal timewarp returns (bool) { - return _getIsPendingSlashId(operatorSet, slashId); - } - - function _getIsPendingSlashId(OperatorSet memory operatorSet, uint slashId) internal view returns (bool) { - return slashEscrowFactory.isPendingSlashId(operatorSet, slashId); - } - - function _getPrevEscrowCompleteBlock(OperatorSet memory operatorSet, uint slashId) internal timewarp returns (uint) { - return _getEscrowCompleteBlock(operatorSet, slashId); - } - - function _getEscrowCompleteBlock(OperatorSet memory operatorSet, uint slashId) internal view returns (uint) { - return slashEscrowFactory.getEscrowCompleteBlock(operatorSet, slashId); - } - - function _getPrevIsDeployedSlashEscrow(OperatorSet memory operatorSet, uint slashId) internal timewarp returns (bool) { - return _getIsDeployedSlashEscrow(operatorSet, slashId); - } - - function _getIsDeployedSlashEscrow(OperatorSet memory operatorSet, uint slashId) internal view returns (bool) { - return slashEscrowFactory.isDeployedSlashEscrow(operatorSet, slashId); - } - function _getPrevSlashableSharesInQueue(User operator, IStrategy[] memory strategies) internal timewarp returns (uint[] memory) { return _getSlashableSharesInQueue(operator, strategies); } diff --git a/src/test/integration/IntegrationChecks.t.sol b/src/test/integration/IntegrationChecks.t.sol index f0639ea7d6..1f1398a2fa 100644 --- a/src/test/integration/IntegrationChecks.t.sol +++ b/src/test/integration/IntegrationChecks.t.sol @@ -1121,21 +1121,6 @@ contract IntegrationCheckUtils is IntegrationBase { initTokenBalances, "redistribution recipient should have underlying token balances" ); - - assertFalse(_getIsPendingSlashId(operatorSet, slashId), "slash id should not be pending"); - assertEq(_getEscrowCompleteBlock(operatorSet, slashId), 0, "escrow complete block should be deleted after"); - assertTrue(_getIsDeployedSlashEscrow(operatorSet, slashId), "escrow should be deployed after"); - } - - function check_releaseSlashEscrow_State_NoneRemaining( - OperatorSet memory operatorSet, - uint slashId, - IStrategy[] memory strategies, - uint[] memory initTokenBalances, - address redistributionRecipient - ) internal { - check_releaseSlashEscrow_State(operatorSet, slashId, strategies, initTokenBalances, redistributionRecipient); - assertFalse(_getIsPendingOperatorSet(operatorSet), "operator set should not be pending"); } /** diff --git a/src/test/integration/IntegrationDeployer.t.sol b/src/test/integration/IntegrationDeployer.t.sol index f46a3b6a06..296510445b 100644 --- a/src/test/integration/IntegrationDeployer.t.sol +++ b/src/test/integration/IntegrationDeployer.t.sol @@ -280,21 +280,9 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { // First, deploy the new contracts as empty contracts emptyContract = new EmptyContract(); - - slashEscrowFactory = - SlashEscrowFactory(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))); - // Deploy new implementation contracts and upgrade all proxies to point to them _deployImplementations(); _upgradeProxies(); - - // Initialize the newly-deployed proxy - slashEscrowFactory.initialize({ - initialOwner: communityMultisig, - initialPausedStatus: 0, - initialGlobalDelayBlocks: INITIAL_GLOBAL_DELAY_BLOCKS - }); - cheats.stopPrank(); } @@ -314,8 +302,6 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { AllocationManager(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))); permissionController = PermissionController(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))); - slashEscrowFactory = - SlashEscrowFactory(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))); eigenPodBeacon = new UpgradeableBeacon(address(emptyContract)); strategyBeacon = new UpgradeableBeacon(address(emptyContract)); } @@ -335,7 +321,7 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { DELEGATION_MANAGER_MIN_WITHDRAWAL_DELAY_BLOCKS, version ); - strategyManagerImplementation = new StrategyManager(delegationManager, slashEscrowFactory, eigenLayerPauserReg, version); + strategyManagerImplementation = new StrategyManager(delegationManager, eigenLayerPauserReg, version); rewardsCoordinatorImplementation = new RewardsCoordinator( IRewardsCoordinatorTypes.RewardsCoordinatorConstructorParams({ delegationManager: delegationManager, @@ -355,8 +341,6 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { eigenPodManagerImplementation = new EigenPodManager(DEPOSIT_CONTRACT, eigenPodBeacon, delegationManager, eigenLayerPauserReg, "9.9.9"); strategyFactoryImplementation = new StrategyFactory(strategyManager, eigenLayerPauserReg, "9.9.9"); - slashEscrowFactoryImplementation = - new SlashEscrowFactory(allocationManager, strategyManager, eigenLayerPauserReg, new SlashEscrow(), "9.9.9"); // Beacon implementations eigenPodImplementation = new EigenPod(DEPOSIT_CONTRACT, eigenPodManager, "v9.9.9"); @@ -405,11 +389,6 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { ITransparentUpgradeableProxy(payable(address(strategyFactory))), address(strategyFactoryImplementation) ); - // SlashEscrowFactory - eigenLayerProxyAdmin.upgrade( - ITransparentUpgradeableProxy(payable(address(slashEscrowFactory))), address(slashEscrowFactoryImplementation) - ); - // EigenPod beacon eigenPodBeacon.upgradeTo(address(eigenPodImplementation)); @@ -441,12 +420,6 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { allocationManager.initialize({initialPausedStatus: 0}); strategyFactory.initialize({_initialOwner: executorMultisig, _initialPausedStatus: 0, _strategyBeacon: strategyBeacon}); - - slashEscrowFactory.initialize({ - initialOwner: communityMultisig, - initialPausedStatus: 0, - initialGlobalDelayBlocks: INITIAL_GLOBAL_DELAY_BLOCKS - }); } /// @dev Deploy a strategy and its underlying token, push to global lists of tokens/strategies, and whitelist diff --git a/src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Escrow.t.sol b/src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Escrow.t.sol deleted file mode 100644 index c4ec84a933..0000000000 --- a/src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Escrow.t.sol +++ /dev/null @@ -1,379 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.27; - -import "src/test/integration/IntegrationChecks.t.sol"; -import "src/test/integration/users/User.t.sol"; -import {console} from "forge-std/console.sol"; - -contract Integration_Deposit_Delegate_Allocate_Slash_Escrow is IntegrationCheckUtils { - using ArrayLib for *; - - AVS avs; - User staker; - User operator; - OperatorSet operatorSet; - AllocateParams allocateParams; - SlashingParams slashParams; - uint slashId; - IStrategy[] strategies; - IERC20[] tokens; - uint[] initTokenBalances; - uint[] initDepositShares; - address payable redistributionRecipient; - bool isRedistributing; - - function _init() internal virtual override { - _configAssetTypes(HOLDS_LST); - - (staker, strategies, initTokenBalances) = _newRandomStaker(); - operator = _newRandomOperator_NoAssets(); - (avs,) = _newRandomAVS(); - - isRedistributing = cheats.randomBool(); - - if (isRedistributing) { - redistributionRecipient = payable(cheats.randomAddress()); - cheats.label(redistributionRecipient, "redistributionRecipient"); - operatorSet = avs.createRedistributingOperatorSet(strategies, redistributionRecipient); - } else { - operatorSet = avs.createOperatorSet(strategies); - redistributionRecipient = payable(allocationManager.getRedistributionRecipient(operatorSet)); // burn address - } - - tokens = _getUnderlyingTokens(strategies); - - // 1) Register operator for operator set. - operator.registerForOperatorSet(operatorSet); - check_Registration_State_NoAllocation(operator, operatorSet, strategies); - - // 2) Deposit Into Strategies - initDepositShares = _calculateExpectedShares(strategies, initTokenBalances); - staker.depositIntoEigenlayer(strategies, initTokenBalances); - - // 3) Delegate to operator - staker.delegateTo(operator); - check_Delegation_State(staker, operator, strategies, initDepositShares); - - // 4) Operator allocates to operator set. - allocateParams = _genAllocation_AllAvailable(operator, operatorSet); - operator.modifyAllocations(allocateParams); - check_Base_IncrAlloc_State(operator, allocateParams); - - // 5) Roll forward to complete allocation. - _rollBlocksForCompleteAllocation(operator, operatorSet, strategies); - - // 6) Operator is full slashed. - slashParams = _genSlashing_Full(operator, operatorSet); - (slashId,) = avs.slashOperator(slashParams); - check_Base_Slashing_State(operator, allocateParams, slashParams, slashId); - - // Roll forward to the escrow delay. - _rollBlocksForCompleteSlashEscrow(); - } - - function _shuffleStrategiesAndBalances() internal { - // Reorder strategies and initTokenBalances - for (uint i = 0; i < strategies.length; i++) { - uint randomIndex = cheats.randomUint(0, strategies.length - 1); - IStrategy tempStrategy = strategies[i]; - strategies[i] = strategies[randomIndex]; - strategies[randomIndex] = tempStrategy; - - uint tempBalance = initTokenBalances[i]; - initTokenBalances[i] = initTokenBalances[randomIndex]; - initTokenBalances[randomIndex] = tempBalance; - } - } - - function testFuzz_fullSlash_releaseAll(uint24 _random) public rand(_random) { - // 7) Release escrow, expect success. - cheats.prank(redistributionRecipient); - slashEscrowFactory.releaseSlashEscrow({operatorSet: operatorSet, slashId: 1}); - check_releaseSlashEscrow_State_NoneRemaining(operatorSet, slashId, strategies, initTokenBalances, redistributionRecipient); - } - - function testFuzz_fullSlash_releaseAllByStrategy(uint24 _random) public rand(_random) { - // Randomize the order of strategies and initTokenBalances. - _shuffleStrategiesAndBalances(); - - // 7) Release escrow, expect success. - for (uint i = 0; i < strategies.length; i++) { - cheats.prank(redistributionRecipient); - slashEscrowFactory.releaseSlashEscrowByStrategy({operatorSet: operatorSet, slashId: 1, strategy: strategies[i]}); - } - check_releaseSlashEscrow_State_NoneRemaining(operatorSet, slashId, strategies, initTokenBalances, redistributionRecipient); - } - - function testFuzz_fullSlash_clearAll_releaseAll(uint24 _random) public rand(_random) { - // 7) Clear burnable shares (transfers tokens to escrow). - avs.clearBurnOrRedistributableShares(operatorSet, slashId); - assert_HasUnderlyingTokenBalances( - User(payable(address(slashEscrowFactory.getSlashEscrow(operatorSet, slashId)))), - strategies, - initTokenBalances, - "slash escrow should have underlying token balances" - ); - for (uint i = 0; i < strategies.length; i++) { - assertEq( - strategyManager.getBurnOrRedistributableShares(operatorSet, slashId, strategies[i]), 0, "no burnable shares should remain" - ); - } - - // 8) Release escrow, expect success. - cheats.prank(redistributionRecipient); - slashEscrowFactory.releaseSlashEscrow({operatorSet: operatorSet, slashId: 1}); - check_releaseSlashEscrow_State_NoneRemaining(operatorSet, slashId, strategies, initTokenBalances, redistributionRecipient); - } - - function testFuzz_fullSlash_clearAll_releaseByStrategy(uint24 _random) public rand(_random) { - avs.clearBurnOrRedistributableShares(operatorSet, slashId); - assert_HasUnderlyingTokenBalances( - User(payable(address(slashEscrowFactory.getSlashEscrow(operatorSet, slashId)))), - strategies, - initTokenBalances, - "slash escrow should have underlying token balances" - ); - for (uint i = 0; i < strategies.length; i++) { - assertEq( - strategyManager.getBurnOrRedistributableShares(operatorSet, slashId, strategies[i]), 0, "no burnable shares should remain" - ); - } - - // Randomize the order of strategies and initTokenBalances. - _shuffleStrategiesAndBalances(); - - // 8) Release escrow, expect success. - for (uint i = 0; i < strategies.length; i++) { - cheats.prank(redistributionRecipient); - slashEscrowFactory.releaseSlashEscrowByStrategy({operatorSet: operatorSet, slashId: 1, strategy: strategies[i]}); - } - - check_releaseSlashEscrow_State_NoneRemaining(operatorSet, slashId, strategies, initTokenBalances, redistributionRecipient); - } - - function testFuzz_fullSlash_clearByStrategy_releaseAll(uint24 _random) public rand(_random) { - // Randomize the order of strategies and initTokenBalances. - _shuffleStrategiesAndBalances(); - - // 7) Clear burnable shares (transfers tokens to escrow). - for (uint i = 0; i < strategies.length; i++) { - avs.clearBurnOrRedistributableSharesByStrategy(operatorSet, slashId, strategies[i]); - assert_HasUnderlyingTokenBalance( - User(payable(address(slashEscrowFactory.getSlashEscrow(operatorSet, slashId)))), - strategies[i], - initTokenBalances[i], - "slash escrow should have underlying token balance" - ); - assertEq( - strategyManager.getBurnOrRedistributableShares(operatorSet, slashId, strategies[i]), 0, "no burnable shares should remain" - ); - } - - // 8) Release escrow, expect success. - cheats.prank(redistributionRecipient); - slashEscrowFactory.releaseSlashEscrow({operatorSet: operatorSet, slashId: 1}); - check_releaseSlashEscrow_State_NoneRemaining(operatorSet, slashId, strategies, initTokenBalances, redistributionRecipient); - } - - function testFuzz_fullSlash_clearByStrategy_releaseByStrategy(uint24 _random) public rand(_random) { - // Randomize the order of strategies and initTokenBalances. - _shuffleStrategiesAndBalances(); - - // 7) Clear burnable shares (transfers tokens to escrow). - for (uint i = 0; i < strategies.length; i++) { - avs.clearBurnOrRedistributableSharesByStrategy(operatorSet, slashId, strategies[i]); - assert_HasUnderlyingTokenBalance( - User(payable(address(slashEscrowFactory.getSlashEscrow(operatorSet, slashId)))), - strategies[i], - initTokenBalances[i], - "slash escrow should have underlying token balance" - ); - assertEq( - strategyManager.getBurnOrRedistributableShares(operatorSet, slashId, strategies[i]), 0, "no burnable shares should remain" - ); - } - - // 8) Release escrow, expect success. - for (uint i = 0; i < strategies.length; i++) { - cheats.prank(redistributionRecipient); - slashEscrowFactory.releaseSlashEscrowByStrategy({operatorSet: operatorSet, slashId: 1, strategy: strategies[i]}); - } - - check_releaseSlashEscrow_State_NoneRemaining(operatorSet, slashId, strategies, initTokenBalances, redistributionRecipient); - } -} - -contract Integration_Deposit_Delegate_Allocate_SlashOnlySomeStrategies_Escrow is IntegrationCheckUtils { - using ArrayLib for *; - - AVS avs; - User staker; - User operator; - OperatorSet operatorSet; - AllocateParams allocateParams; - SlashingParams slashParams; - uint slashId; - IStrategy[] strategies; - IERC20[] tokens; - uint[] initTokenBalances; - uint[] initDepositShares; - address payable redistributionRecipient; - bool isRedistributing; - - function testFuzz_fullSlash_clearByStrategy_releaseByStrategy(uint24 _random) public rand(_random) { - _configAssetTypes(HOLDS_LST); - - (staker, strategies, initTokenBalances) = _newRandomStaker(); - operator = _newRandomOperator_NoAssets(); - (avs,) = _newRandomAVS(); - - cheats.assume(strategies.length >= 2); - - // Modify the length of the array in memory (thus ignoring remaining elements). - assembly { - sstore(strategies.slot, 2) - } - - if (isRedistributing) { - redistributionRecipient = payable(cheats.randomAddress()); - cheats.label(redistributionRecipient, "redistributionRecipient"); - operatorSet = avs.createRedistributingOperatorSet(strategies, redistributionRecipient); - } else { - operatorSet = avs.createOperatorSet(strategies); - redistributionRecipient = payable(allocationManager.getRedistributionRecipient(operatorSet)); // burn address - } - tokens = _getUnderlyingTokens(strategies); - - // 1) Register operator for operator set. - operator.registerForOperatorSet(operatorSet); - check_Registration_State_NoAllocation(operator, operatorSet, strategies); - - // 2) Deposit Into Strategies - initDepositShares = _calculateExpectedShares(strategies, initTokenBalances); - staker.depositIntoEigenlayer(strategies, initTokenBalances); - - // 3) Delegate to operator - staker.delegateTo(operator); - check_Delegation_State(staker, operator, strategies, initDepositShares); - - // 4) Operator allocates to operator set. - allocateParams = _genAllocation_AllAvailable(operator, operatorSet); - operator.modifyAllocations(allocateParams); - check_Base_IncrAlloc_State(operator, allocateParams); - - // 5) Roll forward to complete allocation. - _rollBlocksForCompleteAllocation(operator, operatorSet, strategies); - - // 6) Operator is full slashed. - slashParams = _genSlashing_SingleStrategy(operator, operatorSet, strategies[0]); - (slashId,) = avs.slashOperator(slashParams); - check_Base_Slashing_State(operator, allocateParams, slashParams, slashId); - - // Roll forward to the escrow delay. - _rollBlocksForCompleteSlashEscrow(); - - // 7) Clear burnable shares (transfers tokens to escrow). - avs.clearBurnOrRedistributableSharesByStrategy(operatorSet, slashId, strategies[0]); - assert_HasUnderlyingTokenBalances( - User(payable(address(slashEscrowFactory.getSlashEscrow(operatorSet, slashId)))), - strategies[0].toArray(), - initTokenBalances, - "slash escrow should have underlying token balances" - ); - assertEq(strategyManager.getBurnOrRedistributableShares(operatorSet, slashId, strategies[0]), 0, "no burnable shares should remain"); - - // 8) Release escrow, expect success. - cheats.prank(redistributionRecipient); - slashEscrowFactory.releaseSlashEscrowByStrategy({operatorSet: operatorSet, slashId: 1, strategy: strategies[0]}); - check_releaseSlashEscrow_State_NoneRemaining( - operatorSet, slashId, strategies[0].toArray(), initTokenBalances[0].toArrayU256(), redistributionRecipient - ); - assertEq(slashEscrowFactory.getTotalPendingSlashIds(operatorSet), 0, "no pending slash ids should remain"); - } -} - -contract Integration_Deposit_Delegate_Allocate_Slash_Escrow_Timing is IntegrationCheckUtils { - using ArrayLib for *; - - AVS avs; - User staker; - User operator; - OperatorSet operatorSet; - AllocateParams allocateParams; - SlashingParams slashParams; - uint slashId; - IStrategy[] strategies; - IERC20[] tokens; - uint[] initTokenBalances; - uint[] initDepositShares; - address payable redistributionRecipient; - bool isRedistributing; - - function _init() internal virtual override { - _configAssetTypes(HOLDS_LST); - - (staker, strategies, initTokenBalances) = _newRandomStaker(); - operator = _newRandomOperator_NoAssets(); - (avs,) = _newRandomAVS(); - - if (isRedistributing) { - redistributionRecipient = payable(cheats.randomAddress()); - cheats.label(redistributionRecipient, "redistributionRecipient"); - operatorSet = avs.createRedistributingOperatorSet(strategies, redistributionRecipient); - } else { - operatorSet = avs.createOperatorSet(strategies); - redistributionRecipient = payable(allocationManager.getRedistributionRecipient(operatorSet)); // burn address - } - tokens = _getUnderlyingTokens(strategies); - - // 1) Register operator for operator set. - operator.registerForOperatorSet(operatorSet); - check_Registration_State_NoAllocation(operator, operatorSet, strategies); - - // 2) Deposit Into Strategies - initDepositShares = _calculateExpectedShares(strategies, initTokenBalances); - staker.depositIntoEigenlayer(strategies, initTokenBalances); - - // 3) Delegate to operator - staker.delegateTo(operator); - check_Delegation_State(staker, operator, strategies, initDepositShares); - - // 4) Operator allocates to operator set. - allocateParams = _genAllocation_AllAvailable(operator, operatorSet); - operator.modifyAllocations(allocateParams); - check_Base_IncrAlloc_State(operator, allocateParams); - - // 5) Roll forward to complete allocation. - _rollBlocksForCompleteAllocation(operator, operatorSet, strategies); - } - - function testFuzz_fullSlash_EscrowTiming_RightBeforeFails(uint24 _random) public rand(_random) { - // 6) Operator is full slashed. - slashParams = _genSlashing_Full(operator, operatorSet); - (slashId,) = avs.slashOperator(slashParams); - check_Base_Slashing_State(operator, allocateParams, slashParams, slashId); - - // Roll forward to just before the escrow delay. - cheats.roll(block.number + INITIAL_GLOBAL_DELAY_BLOCKS); - - // Attempt to release escrow before delay has elapsed, expect revert. - cheats.prank(redistributionRecipient); - cheats.expectRevert(ISlashEscrowFactoryErrors.EscrowDelayNotElapsed.selector); - slashEscrowFactory.releaseSlashEscrow({operatorSet: operatorSet, slashId: 1}); - } - - function testFuzz_fullSlash_EscrowTiming_RightAfterPasses(uint24 _random) public rand(_random) { - // 6) Operator is full slashed. - slashParams = _genSlashing_Full(operator, operatorSet); - (slashId,) = avs.slashOperator(slashParams); - check_Base_Slashing_State(operator, allocateParams, slashParams, slashId); - - // Roll forward to the escrow delay. - _rollBlocksForCompleteSlashEscrow(); - - // 7) Release escrow, expect success. - cheats.prank(redistributionRecipient); - slashEscrowFactory.releaseSlashEscrow({operatorSet: operatorSet, slashId: 1}); - check_releaseSlashEscrow_State_NoneRemaining(operatorSet, slashId, strategies, initTokenBalances, redistributionRecipient); - } -} diff --git a/src/test/integration/users/AVS.t.sol b/src/test/integration/users/AVS.t.sol index 2e987b57f6..d5bb1a6b11 100644 --- a/src/test/integration/users/AVS.t.sol +++ b/src/test/integration/users/AVS.t.sol @@ -6,7 +6,6 @@ import "forge-std/Test.sol"; import "src/contracts/interfaces/IAllocationManager.sol"; import "src/contracts/interfaces/IPermissionController.sol"; import "src/contracts/interfaces/IStrategyFactory.sol"; -import "src/contracts/interfaces/ISlashEscrowFactory.sol"; import "src/test/mocks/ERC20Mock.sol"; import "src/test/integration/users/User.t.sol"; @@ -23,7 +22,6 @@ interface IAVSDeployer { function strategyManager() external view returns (IStrategyManager); function strategyFactory() external view returns (IStrategyFactory); function permissionController() external view returns (IPermissionController); - function slashEscrowFactory() external view returns (ISlashEscrowFactory); function timeMachine() external view returns (TimeMachine); } @@ -39,7 +37,6 @@ contract AVS is Logger, IAllocationManagerTypes, IAVSRegistrar { IDelegationManager immutable delegationManager; IStrategyManager immutable strategyManager; IStrategyFactory immutable strategyFactory; - ISlashEscrowFactory immutable slashEscrowFactory; TimeMachine immutable timeMachine; string _NAME; @@ -52,7 +49,6 @@ contract AVS is Logger, IAllocationManagerTypes, IAVSRegistrar { delegationManager = deployer.delegationManager(); strategyManager = deployer.strategyManager(); strategyFactory = deployer.strategyFactory(); - slashEscrowFactory = deployer.slashEscrowFactory(); timeMachine = deployer.timeMachine(); _NAME = name; cheats.label(address(this), NAME_COLORED()); @@ -180,7 +176,6 @@ contract AVS is Logger, IAllocationManagerTypes, IAVSRegistrar { ) ); } - cheats.label(address(slashEscrowFactory.getSlashEscrow(OperatorSet(address(this), params.operatorSetId), slashId)), "slashEscrow"); _tryPrankAppointee_AllocationManager(IAllocationManager.slashOperator.selector); (slashId, shares) = allocationManager.slashOperator(address(this), params); print.gasUsed(); diff --git a/src/test/unit/SlashEscrowFactoryUnit.t.sol b/src/test/unit/SlashEscrowFactoryUnit.t.sol deleted file mode 100644 index 1d7bfb431f..0000000000 --- a/src/test/unit/SlashEscrowFactoryUnit.t.sol +++ /dev/null @@ -1,987 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.27; - -import {MockERC20} from "src/test/mocks/MockERC20.sol"; -import "src/test/utils/EigenLayerUnitTestSetup.sol"; -import "src/contracts/core/SlashEscrowFactory.sol"; -import "src/contracts/core/SlashEscrow.sol"; - -contract SlashEscrowFactoryUnitTests is EigenLayerUnitTestSetup, ISlashEscrowFactoryEvents { - /// @notice default address for burning slashed shares and transferring underlying tokens - address public constant DEFAULT_BURN_ADDRESS = 0x00000000000000000000000000000000000E16E4; - - /// @dev Index for flag that pauses calling `releaseSlashEscrow` - uint8 constant PAUSED_RELEASE_ESCROW = 0; - - SlashEscrowFactory factory; - - OperatorSet defaultOperatorSet; - IStrategy defaultStrategy; - MockERC20 defaultToken; - uint defaultSlashId; - address defaultRedistributionRecipient; - address defaultOwner; - SlashEscrow slashEscrowImplementation; - uint32 defaultGlobalDelayBlocks = uint32(4 days / 12 seconds); - - function setUp() public virtual override { - EigenLayerUnitTestSetup.setUp(); - - defaultOperatorSet = OperatorSet(cheats.randomAddress(), 0); - defaultStrategy = IStrategy(cheats.randomAddress()); - defaultToken = new MockERC20(); - defaultSlashId = 1; - defaultRedistributionRecipient = address(cheats.randomAddress()); - defaultOwner = address(cheats.randomAddress()); - slashEscrowImplementation = new SlashEscrow(); - allocationManagerMock.setRedistributionRecipient(defaultOperatorSet, defaultRedistributionRecipient); - - factory = SlashEscrowFactory( - address( - new TransparentUpgradeableProxy( - address( - new SlashEscrowFactory( - IAllocationManager(address(allocationManagerMock)), - IStrategyManager(address(strategyManagerMock)), - IPauserRegistry(address(pauserRegistry)), - ISlashEscrow(address(slashEscrowImplementation)), - "1.0.0" - ) - ), - address(eigenLayerProxyAdmin), - abi.encodeWithSelector(SlashEscrowFactory.initialize.selector, defaultOwner, 0, uint32(defaultGlobalDelayBlocks)) - ) - ) - ); - } - - function _rollForwardDefaultEscrowDelay() internal { - cheats.roll(block.number + defaultGlobalDelayBlocks + 1); - } - - /// @dev Sets the return value for the next call to `strategy.underlyingToken()`. - function _mockStrategyUnderlyingTokenCall(IStrategy strategy, address underlyingToken) internal { - cheats.mockCall(address(strategy), abi.encodeWithSelector(IStrategy.underlyingToken.selector), abi.encode(underlyingToken)); - } - - /// @dev Returns the pending underlying amount for a given strategy and token. - function _getPendingUnderlyingAmountForStrategy(OperatorSet memory operatorSet, uint slashId, IStrategy strategy, MockERC20 token) - internal - returns (uint) - { - _mockStrategyUnderlyingTokenCall(strategy, address(token)); - return factory.getPendingUnderlyingAmountForStrategy(operatorSet, slashId, strategy); - } - - /// @dev Starts a escrow for a given strategy and token. - /// - Calls as the `StrategyManager`. - /// - Asserts that the `StartEscrow` event is emitted. - /// - Mocks the strategy sending the underlying token to the `SlashEscrow`. - function _initiateSlashEscrow(OperatorSet memory operatorSet, uint slashId, IStrategy strategy, MockERC20 token, uint underlyingAmount) - internal - { - cheats.prank(address(strategyManagerMock)); - cheats.expectEmit(true, true, true, true); - emit StartEscrow(operatorSet, slashId, strategy, uint32(block.number)); - factory.initiateSlashEscrow(operatorSet, slashId, strategy); - deal(address(token), address(factory.getSlashEscrow(operatorSet, slashId)), underlyingAmount); - } - - /// @dev Calls the `releaseSlashEscrow` function as the redistribution recipient. - /// - Asserts that the `Escrow` event is emitted - function _releaseSlashEscrow(OperatorSet memory operatorSet, uint slashId) internal { - (IStrategy[] memory strategies) = factory.getPendingStrategiesForSlashId(operatorSet, slashId); - - address redistributionRecipient = allocationManagerMock.getRedistributionRecipient(operatorSet); - - for (uint i = 0; i < strategies.length; i++) { - cheats.expectEmit(true, true, true, true); - emit EscrowComplete(operatorSet, slashId, strategies[i], redistributionRecipient); - } - - // If the redistribution recipient is any address - if (redistributionRecipient != DEFAULT_BURN_ADDRESS) cheats.prank(defaultRedistributionRecipient); - else cheats.prank(cheats.randomAddress()); - factory.releaseSlashEscrow(operatorSet, slashId); - } - - /// @dev Calls the `releaseSlashEscrow` function as the redistribution recipient. - /// - Asserts that the `Escrow` event is emitted - function _releaseSlashEscrowByStrategy(OperatorSet memory operatorSet, uint slashId, IStrategy strategy) internal { - address redistributionRecipient = allocationManagerMock.getRedistributionRecipient(operatorSet); - // If the redistribution recipient is any address - if (redistributionRecipient != DEFAULT_BURN_ADDRESS) cheats.prank(redistributionRecipient); - else cheats.prank(cheats.randomAddress()); - - cheats.expectEmit(true, true, true, true); - emit EscrowComplete(operatorSet, slashId, strategy, redistributionRecipient); - - factory.releaseSlashEscrowByStrategy(operatorSet, slashId, strategy); - - assertEq(factory.computeSlashEscrowSalt(operatorSet, slashId), keccak256(abi.encodePacked(operatorSet.key(), slashId))); - assertTrue(factory.isDeployedSlashEscrow(operatorSet, slashId)); - ISlashEscrow slashEscrow = factory.getSlashEscrow(operatorSet, slashId); - assertTrue(factory.isDeployedSlashEscrow(slashEscrow)); - assertTrue(slashEscrow.verifyDeploymentParameters(factory, slashEscrowImplementation, operatorSet, slashId)); - } - - /// @dev Asserts that the operator set and slash ID are pending, and that the strategy and underlying amount are in the pending escrows. - function _checkStartEscrows( - OperatorSet memory operatorSet, - uint slashId, - IStrategy strategy, - MockERC20 token, - uint expectedUnderlyingAmount, - uint expectedCount - ) internal { - // Assert that the operator set and slash ID are pending. - assertTrue(factory.isPendingOperatorSet(operatorSet)); - assertTrue(factory.isPendingSlashId(operatorSet, slashId)); - - OperatorSet[] memory pendingOperatorSets = factory.getPendingOperatorSets(); - uint[] memory pendingSlashIds = factory.getPendingSlashIds(operatorSet); - assertEq(pendingOperatorSets[pendingOperatorSets.length - 1].key(), operatorSet.key()); - assertEq(pendingSlashIds[pendingSlashIds.length - 1], slashId); - - // Assert that the underlying amount in escrow for the (operator set, slash ID, strategy) is correct. - assertEq(_getPendingUnderlyingAmountForStrategy(operatorSet, slashId, strategy, token), expectedUnderlyingAmount); - - // Assert that the number of pending escrows is correct. - (IStrategy[] memory strategies) = factory.getPendingStrategiesForSlashId(operatorSet, slashId); - - assertEq(strategies.length, expectedCount); - assertEq(factory.getTotalPendingStrategiesForSlashId(operatorSet, slashId), expectedCount); - - // Assert that the escrow is deployed - assertEq(factory.computeSlashEscrowSalt(operatorSet, slashId), keccak256(abi.encodePacked(operatorSet.key(), slashId))); - assertTrue(factory.isDeployedSlashEscrow(operatorSet, slashId)); - ISlashEscrow slashEscrow = factory.getSlashEscrow(operatorSet, slashId); - assertTrue(factory.isDeployedSlashEscrow(slashEscrow)); - assertTrue(slashEscrow.verifyDeploymentParameters(factory, slashEscrowImplementation, operatorSet, slashId)); - } - - /// @dev Sets the redistribution recipient to the default burn address if `shouldBurn` is true. - function _setRedistributionRecipient(bool shouldBurn) internal { - if (shouldBurn) allocationManagerMock.setRedistributionRecipient(defaultOperatorSet, DEFAULT_BURN_ADDRESS); - } -} - -contract SlashEscrowFactoryUnitTests_initialize is SlashEscrowFactoryUnitTests { - function test_initialize() public { - assertEq(address(factory.allocationManager()), address(allocationManagerMock)); - assertEq(address(factory.strategyManager()), address(strategyManagerMock)); - assertEq(address(factory.pauserRegistry()), address(pauserRegistry)); - assertEq(address(factory.slashEscrowImplementation()), address(slashEscrowImplementation)); - assertEq(factory.getGlobalEscrowDelay(), defaultGlobalDelayBlocks); - assertEq(factory.paused(), 0); - } -} - -contract SlashEscrowFactoryUnitTests_initiateSlashEscrow is SlashEscrowFactoryUnitTests { - /// @dev Asserts only the `StrategyManager` can call `initiateSlashEscrow`. - function test_initiateSlashEscrow_onlyStrategyManager() public { - cheats.expectRevert(ISlashEscrowFactoryErrors.OnlyStrategyManager.selector); - factory.initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy); - } - - function testFuzz_initiateSlashEscrow_multipleStrategies(uint r) public { - // Initialize arrays to store test data for multiple strategies - uint numStrategies = bound(r, 2, 10); - // Set up each strategy with random data and start escrow - for (uint i = 0; i < numStrategies; i++) { - // Generate random strategy address and token - IStrategy strategy = IStrategy(cheats.randomAddress()); - MockERC20 token = new MockERC20(); - uint underlyingAmount = cheats.randomUint(); - - // Start escrow for this strategy - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, strategy, token, underlyingAmount); - // Verify the escrow was started correctly - _checkStartEscrows(defaultOperatorSet, defaultSlashId, strategy, token, underlyingAmount, i + 1); - } - } - - function test_initiateSlashEscrow_correctness(uint underlyingAmount) public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, underlyingAmount); - _checkStartEscrows(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, underlyingAmount, 1); - } -} - -contract SlashEscrowFactoryUnitTests_releaseSlashEscrow is SlashEscrowFactoryUnitTests { - /// @dev Asserts that the function reverts if the caller is not the redistribution recipient. - function testFuzz_releaseSlashEscrow_OnlyRedistributionRecipient(uint underlyingAmount) public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, underlyingAmount); - cheats.expectRevert(ISlashEscrowFactoryErrors.OnlyRedistributionRecipient.selector); - factory.releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - } - - function testFuzz_releaseSlashEscrow_globalPause(uint underlyingAmount) public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, underlyingAmount); - cheats.prank(pauser); - factory.pause(2 ** PAUSED_RELEASE_ESCROW); - cheats.prank(defaultRedistributionRecipient); - cheats.expectRevert(IPausable.CurrentlyPaused.selector); - factory.releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - } - - /// @dev Asserts that the function reverts if the redistribution is paused. - function testFuzz_releaseSlashEscrow_paused(uint underlyingAmount) public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, underlyingAmount); - cheats.prank(pauser); - factory.pauseEscrow(defaultOperatorSet, defaultSlashId); - cheats.prank(defaultRedistributionRecipient); - cheats.expectRevert(IPausable.CurrentlyPaused.selector); - factory.releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - } - - /// @dev Asserts that the function reverts if the operator set and slash ID do not exist. - /// NOTE: `releaseSlashEscrow` does not revert when a slash ID does not exist for an operator set. - function testFuzz_releaseSlashEscrow_nonexistentSlashIdForOperatorSet(uint underlyingAmount) public { - cheats.prank(defaultRedistributionRecipient); - factory.releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - assertFalse(factory.isPendingOperatorSet(defaultOperatorSet)); - assertFalse(factory.isPendingSlashId(defaultOperatorSet, defaultSlashId)); - } - - function testFuzz_releaseSlashEscrow_delayNotElapsed(uint underlyingAmount) public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, underlyingAmount); - cheats.roll(block.number + defaultGlobalDelayBlocks); - cheats.prank(defaultRedistributionRecipient); - cheats.expectRevert(ISlashEscrowFactoryErrors.EscrowDelayNotElapsed.selector); - factory.releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - } - - /// @dev Tests that multiple strategies can be burned or redistributed in a single call - function testFuzz_releaseSlashEscrow_multipleStrategies_sameDelay(uint r) public { - // Initialize arrays to store test data for multiple strategies - uint numStrategies = bound(r, 2, 10); - IStrategy[] memory strategies = new IStrategy[](numStrategies); - MockERC20[] memory tokens = new MockERC20[](numStrategies); - uint[] memory underlyingAmounts = new uint[](numStrategies); - - // // Randomly update the redistribution to be the default burn address - // _setRedistributionRecipient(r % 2 == 0); - - // Set up each strategy with random data and start escrow - for (uint i = 0; i < numStrategies; i++) { - // Generate random strategy address and token - strategies[i] = IStrategy(cheats.randomAddress()); - tokens[i] = new MockERC20(); - underlyingAmounts[i] = cheats.randomUint(); - - // Start escrow for this strategy - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i], underlyingAmounts[i]); - // Verify the escrow was started correctly - _checkStartEscrows(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i], underlyingAmounts[i], i + 1); - } - - // Advance time to allow escrow to occur - _rollForwardDefaultEscrowDelay(); - - // Set up mock calls for each strategy's underlying token - for (uint i = 0; i < numStrategies; i++) { - _mockStrategyUnderlyingTokenCall(strategies[i], address(tokens[i])); - } - - // Execute the escrow - _releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - - // Checks - - // Assert that the operator set and slash ID are no longer pending. - assertFalse(factory.isPendingOperatorSet(defaultOperatorSet)); - assertFalse(factory.isPendingSlashId(defaultOperatorSet, defaultSlashId)); - assertEq(factory.getTotalPendingOperatorSets(), 0); - assertEq(factory.getTotalPendingSlashIds(defaultOperatorSet), 0); - - // Assert that the strategies and underlying amounts are no longer in the pending escrows. - assertEq(factory.getTotalPendingStrategiesForSlashId(defaultOperatorSet, defaultSlashId), 0); - - // Assert that the underlying amounts are no longer set. - for (uint i = 0; i < numStrategies; i++) { - assertEq(_getPendingUnderlyingAmountForStrategy(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i]), 0); - } - - // Assert that the start block for the (operator set, slash ID) is no longer set. - assertEq(factory.getEscrowCompleteBlock(defaultOperatorSet, defaultSlashId), 0); - } - - /// @dev Tests that multiple strategies with different delays are processed correctly - function testFuzz_releaseSlashEscrow_multipleStrategies_differentDelays(uint r) public { - // Initialize arrays to store test data for multiple strategies - uint numStrategies = bound(r, 2, 10); - IStrategy[] memory strategies = new IStrategy[](numStrategies); - MockERC20[] memory tokens = new MockERC20[](numStrategies); - uint[] memory underlyingAmounts = new uint[](numStrategies); - uint32[] memory delays = new uint32[](numStrategies); - - // Randomly update the redistribution to be the default burn address - _setRedistributionRecipient(r % 2 == 0); - - // Set global delay less than all the strategy delays. - cheats.prank(defaultOwner); - factory.setGlobalEscrowDelay(0.5 days / 12 seconds); - - // Set up each strategy with random data and different delays - for (uint i = 0; i < numStrategies; i++) { - // Generate random strategy address and token - strategies[i] = IStrategy(cheats.randomAddress()); - tokens[i] = new MockERC20(); - underlyingAmounts[i] = cheats.randomUint(); - - // Set different delays for each strategy (increasing delays) - delays[i] = uint32((i + 1) * 1 days / 12 seconds); // 1 day, 2 days, 3 days, etc. - - // Set the strategy-specific delay - cheats.prank(defaultOwner); - factory.setStrategyEscrowDelay(strategies[i], delays[i]); - - // Start escrow for this strategy - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i], underlyingAmounts[i]); - // Verify the escrow was started correctly - _checkStartEscrows(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i], underlyingAmounts[i], i + 1); - } - - // Assert that we cannot release the slash escrow until the delay has elapsed for ALL strategies - cheats.roll(block.number + 1 days / 12 seconds * numStrategies); - cheats.prank(defaultRedistributionRecipient); - cheats.expectRevert(ISlashEscrowFactoryErrors.EscrowDelayNotElapsed.selector); - factory.releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - - // Roll forward to the next block - cheats.roll(block.number + 1); - - // Set up mock calls for each strategy's underlying token - for (uint i = 0; i < numStrategies; i++) { - _mockStrategyUnderlyingTokenCall(strategies[i], address(tokens[i])); - } - - // Execute the escrow - _releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - - // Verify that all strategies have been processed - assertFalse(factory.isPendingOperatorSet(defaultOperatorSet)); - assertFalse(factory.isPendingSlashId(defaultOperatorSet, defaultSlashId)); - assertEq(factory.getTotalPendingOperatorSets(), 0); - assertEq(factory.getTotalPendingSlashIds(defaultOperatorSet), 0); - assertEq(factory.getTotalPendingStrategiesForSlashId(defaultOperatorSet, defaultSlashId), 0); - - // Verify that all underlying amounts are cleared - for (uint i = 0; i < numStrategies; i++) { - assertEq(_getPendingUnderlyingAmountForStrategy(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i]), 0); - } - - // Verify that the start block is cleared - assertEq(factory.getEscrowCompleteBlock(defaultOperatorSet, defaultSlashId), 0); - } - - /// @dev Tests that operatorSets are only cleared once all slash IDs are released - function testFuzz_releaseSlashEscrow_multipleReleases(uint r) public { - uint numEscrows = bound(r, 2, 5); - - IStrategy[] memory strategies = new IStrategy[](1); - MockERC20[] memory tokens = new MockERC20[](1); - uint[] memory underlyingAmounts = new uint[](1); - - underlyingAmounts[0] = cheats.randomUint() / numEscrows; - - strategies[0] = IStrategy(cheats.randomAddress()); - tokens[0] = new MockERC20(); - - // Set up numEscrows slash escrows for the same operator set - for (uint i = 0; i < numEscrows; i++) { - // Start escrow for this slash - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId + i, strategies[0], tokens[0], underlyingAmounts[0]); - // Verify the escrow was started correctly - _checkStartEscrows(defaultOperatorSet, defaultSlashId + i, strategies[0], tokens[0], underlyingAmounts[0], 1); - } - - _rollForwardDefaultEscrowDelay(); - - // Release the first n-1 slash escrows - for (uint i = 0; i < numEscrows - 1; i++) { - _releaseSlashEscrow(defaultOperatorSet, defaultSlashId + i); - } - - // Assert that the operator set is still pending - assertTrue(factory.isPendingOperatorSet(defaultOperatorSet)); - assertTrue(factory.isPendingSlashId(defaultOperatorSet, defaultSlashId + numEscrows - 1)); - assertEq(factory.getTotalPendingOperatorSets(), 1); - assertEq(factory.getTotalPendingSlashIds(defaultOperatorSet), 1); - - // Release the last escrow - _releaseSlashEscrow(defaultOperatorSet, defaultSlashId + numEscrows - 1); - - // Assert that the operator set is no longer pending - assertFalse(factory.isPendingOperatorSet(defaultOperatorSet)); - assertFalse(factory.isPendingSlashId(defaultOperatorSet, defaultSlashId + numEscrows - 1)); - assertEq(factory.getTotalPendingOperatorSets(), 0); - } -} - -contract SlashEscrowFactoryUnitTests_releaseSlashEscrowByStrategy is SlashEscrowFactoryUnitTests { - /// @dev Asserts that the function reverts if the caller is not the redistribution recipient. - function testFuzz_releaseSlashEscrowByStrategy_OnlyRedistributionRecipient(uint underlyingAmount) public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, underlyingAmount); - cheats.expectRevert(ISlashEscrowFactoryErrors.OnlyRedistributionRecipient.selector); - factory.releaseSlashEscrowByStrategy(defaultOperatorSet, defaultSlashId, defaultStrategy); - } - - function testFuzz_releaseSlashEscrowByStrategy_globalPause(uint underlyingAmount) public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, underlyingAmount); - cheats.prank(pauser); - factory.pause(2 ** PAUSED_RELEASE_ESCROW); - cheats.prank(defaultRedistributionRecipient); - cheats.expectRevert(IPausable.CurrentlyPaused.selector); - factory.releaseSlashEscrowByStrategy(defaultOperatorSet, defaultSlashId, defaultStrategy); - } - - /// @dev Asserts that the function reverts if the redistribution is paused. - function testFuzz_releaseSlashEscrowByStrategy_paused(uint underlyingAmount) public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, underlyingAmount); - cheats.prank(pauser); - factory.pauseEscrow(defaultOperatorSet, defaultSlashId); - cheats.prank(defaultRedistributionRecipient); - cheats.expectRevert(IPausable.CurrentlyPaused.selector); - factory.releaseSlashEscrowByStrategy(defaultOperatorSet, defaultSlashId, defaultStrategy); - } - - /// @dev Asserts that the function reverts if the operator set and slash ID do not exist. - /// NOTE: `releaseSlashEscrowByStrategy` DOES revert when a slash ID does not exist for an operator set. - function testFuzz_releaseSlashEscrowByStrategy_nonexistentSlashIdForOperatorSet(uint underlyingAmount) public { - cheats.prank(defaultRedistributionRecipient); - cheats.expectRevert(); - factory.releaseSlashEscrowByStrategy(defaultOperatorSet, defaultSlashId, defaultStrategy); - assertFalse(factory.isPendingOperatorSet(defaultOperatorSet)); - assertFalse(factory.isPendingSlashId(defaultOperatorSet, defaultSlashId)); - } - - function testFuzz_releaseSlashEscrowByStrategy_delayNotElapsed(uint underlyingAmount) public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, underlyingAmount); - cheats.roll(block.number + defaultGlobalDelayBlocks); - cheats.prank(defaultRedistributionRecipient); - cheats.expectRevert(ISlashEscrowFactoryErrors.EscrowDelayNotElapsed.selector); - factory.releaseSlashEscrowByStrategy(defaultOperatorSet, defaultSlashId, defaultStrategy); - } - - function testFuzz_releaseSlashEscrowByStrategy_multipleStrategies(uint underlyingAmount) public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, underlyingAmount); - cheats.roll(block.number + defaultGlobalDelayBlocks); - cheats.prank(defaultRedistributionRecipient); - cheats.expectRevert(ISlashEscrowFactoryErrors.EscrowDelayNotElapsed.selector); - factory.releaseSlashEscrowByStrategy(defaultOperatorSet, defaultSlashId, defaultStrategy); - } - - /// @dev Tests that multiple strategies can be burned or redistributed across multiple calls - function testFuzz_releaseSlashEscrowByStrategy__multipleStrategies_sameDelay(uint r) public { - // Initialize arrays to store test data for multiple strategies - uint numStrategies = bound(r, 2, 10); - IStrategy[] memory strategies = new IStrategy[](numStrategies); - MockERC20[] memory tokens = new MockERC20[](numStrategies); - uint[] memory underlyingAmounts = new uint[](numStrategies); - - // Randomly update the redistribution to be the default burn address - _setRedistributionRecipient(r % 2 == 0); - - // Set up each strategy with random data and start escrow - for (uint i = 0; i < numStrategies; i++) { - // Generate random strategy address and token - strategies[i] = IStrategy(cheats.randomAddress()); - tokens[i] = new MockERC20(); - underlyingAmounts[i] = cheats.randomUint(); - - // Start escrow for this strategy - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i], underlyingAmounts[i]); - // Verify the escrow was started correctly - _checkStartEscrows(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i], underlyingAmounts[i], i + 1); - } - - // Advance time to allow escrow to occur - _rollForwardDefaultEscrowDelay(); - - // Set up mock calls for each strategy's underlying token - for (uint i = 0; i < numStrategies; i++) { - _mockStrategyUnderlyingTokenCall(strategies[i], address(tokens[i])); - } - - // Release the first n-1 slash escrows - for (uint i = 0; i < numStrategies - 1; i++) { - _releaseSlashEscrowByStrategy(defaultOperatorSet, defaultSlashId, strategies[i]); - } - - // Assert that the slashId and operatorSet are still pending - assertTrue(factory.isPendingOperatorSet(defaultOperatorSet)); - assertTrue(factory.isPendingSlashId(defaultOperatorSet, defaultSlashId)); - assertEq(factory.getTotalPendingOperatorSets(), 1); - assertEq(factory.getTotalPendingSlashIds(defaultOperatorSet), 1); - assertEq(factory.getTotalPendingStrategiesForSlashId(defaultOperatorSet, defaultSlashId), 1); - - // Release the last slash escrow - _releaseSlashEscrowByStrategy(defaultOperatorSet, defaultSlashId, strategies[numStrategies - 1]); - - // Checks - - // Assert that the operator set and slash ID are no longer pending. - assertFalse(factory.isPendingOperatorSet(defaultOperatorSet)); - assertFalse(factory.isPendingSlashId(defaultOperatorSet, defaultSlashId)); - assertEq(factory.getTotalPendingOperatorSets(), 0); - assertEq(factory.getTotalPendingSlashIds(defaultOperatorSet), 0); - - // Assert that the strategies and underlying amounts are no longer in the pending escrows. - assertEq(factory.getTotalPendingStrategiesForSlashId(defaultOperatorSet, defaultSlashId), 0); - - // Assert that the underlying amounts are no longer set. - for (uint i = 0; i < numStrategies; i++) { - assertEq(_getPendingUnderlyingAmountForStrategy(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i]), 0); - } - - // Assert that the start block for the (operator set, slash ID) is no longer set. - assertEq(factory.getEscrowCompleteBlock(defaultOperatorSet, defaultSlashId), 0); - } - - /// @dev Tests that multiple strategies can be burned or redistributed across multiple calls - function testFuzz_releaseSlashEscrowByStrategy__multipleStrategies_byIndexThenAll(uint r) public { - // Initialize arrays to store test data for multiple strategies - uint numStrategies = bound(r, 2, 10); - IStrategy[] memory strategies = new IStrategy[](numStrategies); - MockERC20[] memory tokens = new MockERC20[](numStrategies); - uint[] memory underlyingAmounts = new uint[](numStrategies); - - // Randomly update the redistribution to be the default burn address - _setRedistributionRecipient(r % 2 == 0); - - // Set up each strategy with random data and start escrow - for (uint i = 0; i < numStrategies; i++) { - // Generate random strategy address and token - strategies[i] = IStrategy(cheats.randomAddress()); - tokens[i] = new MockERC20(); - underlyingAmounts[i] = cheats.randomUint(); - - // Start escrow for this strategy - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i], underlyingAmounts[i]); - // Verify the escrow was started correctly - _checkStartEscrows(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i], underlyingAmounts[i], i + 1); - } - - // Advance time to allow escrow to occur - _rollForwardDefaultEscrowDelay(); - - // Release the first index - _releaseSlashEscrowByStrategy(defaultOperatorSet, defaultSlashId, strategies[0]); - - // Assert that the slashId and operatorSet are still pending - assertTrue(factory.isPendingOperatorSet(defaultOperatorSet)); - assertTrue(factory.isPendingSlashId(defaultOperatorSet, defaultSlashId)); - assertEq(factory.getTotalPendingOperatorSets(), 1); - assertEq(factory.getTotalPendingSlashIds(defaultOperatorSet), 1); - assertEq(factory.getTotalPendingStrategiesForSlashId(defaultOperatorSet, defaultSlashId), numStrategies - 1); - - // Release remaining - _releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - - // Checks - - // Assert that the operator set and slash ID are no longer pending. - assertFalse(factory.isPendingOperatorSet(defaultOperatorSet)); - assertFalse(factory.isPendingSlashId(defaultOperatorSet, defaultSlashId)); - assertEq(factory.getTotalPendingOperatorSets(), 0); - assertEq(factory.getTotalPendingSlashIds(defaultOperatorSet), 0); - - // Assert that the strategies and underlying amounts are no longer in the pending escrows. - assertEq(factory.getTotalPendingStrategiesForSlashId(defaultOperatorSet, defaultSlashId), 0); - - // Assert that the underlying amounts are no longer set. - for (uint i = 0; i < numStrategies; i++) { - assertEq(_getPendingUnderlyingAmountForStrategy(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i]), 0); - } - - // Assert that the start block for the (operator set, slash ID) is no longer set. - assertEq(factory.getEscrowCompleteBlock(defaultOperatorSet, defaultSlashId), 0); - } -} - -contract SlashEscrowFactoryUnitTests_pauseEscrow is SlashEscrowFactoryUnitTests { - function test_pauseEscrow_onlyPauser() public { - cheats.prank(cheats.randomAddress()); - cheats.expectRevert(IPausable.OnlyPauser.selector); - factory.pauseEscrow(defaultOperatorSet, defaultSlashId); - } - - function test_pauseEscrow_correctness() public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, 100); - - cheats.prank(pauser); - factory.pauseEscrow(defaultOperatorSet, defaultSlashId); - assertTrue(factory.isEscrowPaused(defaultOperatorSet, defaultSlashId)); - - _rollForwardDefaultEscrowDelay(); - - cheats.prank(defaultRedistributionRecipient); - cheats.expectRevert(IPausable.CurrentlyPaused.selector); - factory.releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - } -} - -contract SlashEscrowFactoryUnitTests_unpauseEscrow is SlashEscrowFactoryUnitTests { - function test_unpauseEscrow_onlyUnpauser() public { - cheats.prank(cheats.randomAddress()); - cheats.expectRevert(IPausable.OnlyUnpauser.selector); - factory.unpauseEscrow(defaultOperatorSet, defaultSlashId); - } - - function test_unpauseEscrow_correctness() public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, 100); - - cheats.prank(pauser); - factory.pauseEscrow(defaultOperatorSet, defaultSlashId); - assertTrue(factory.isEscrowPaused(defaultOperatorSet, defaultSlashId)); - - _rollForwardDefaultEscrowDelay(); - - cheats.prank(defaultRedistributionRecipient); - cheats.expectRevert(IPausable.CurrentlyPaused.selector); - factory.releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - - cheats.prank(unpauser); - factory.unpauseEscrow(defaultOperatorSet, defaultSlashId); - assertFalse(factory.isEscrowPaused(defaultOperatorSet, defaultSlashId)); - - // Should not revert... - _mockStrategyUnderlyingTokenCall(defaultStrategy, address(defaultToken)); - _releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - } -} - -contract SlashEscrowFactoryUnitTests_setStrategyEscrowDelay is SlashEscrowFactoryUnitTests { - function test_setStrategyEscrowDelay_onlyOwner() public { - cheats.prank(cheats.randomAddress()); - cheats.expectRevert("Ownable: caller is not the owner"); - factory.setStrategyEscrowDelay(defaultStrategy, uint32(25)); - } - - function testFuzz_setStrategyEscrowDelay_correctness(uint32 delay) public { - delay = uint32(bound(delay, defaultGlobalDelayBlocks + 1, type(uint).max)); - cheats.prank(defaultOwner); - cheats.expectEmit(true, true, true, true); - emit StrategyEscrowDelaySet(defaultStrategy, delay); - factory.setStrategyEscrowDelay(defaultStrategy, delay); - // Returns strategy delay since strategy delay is larger than global delay. - assertEq(factory.getStrategyEscrowDelay(defaultStrategy), delay); - } -} - -contract SlashEscrowFactoryUnitTests_getEscrowDelay is SlashEscrowFactoryUnitTests { - function testFuzz_getEscrowDelay_correctness(uint r) public { - // Initialize arrays to store test data for multiple strategies - uint numStrategies = bound(r, 2, 10); - IStrategy[] memory strategies = new IStrategy[](numStrategies); - MockERC20[] memory tokens = new MockERC20[](numStrategies); - uint[] memory underlyingAmounts = new uint[](numStrategies); - uint32[] memory delays = new uint32[](numStrategies); - - // Set global delay less than all the strategy delays. - cheats.prank(defaultOwner); - factory.setGlobalEscrowDelay(0.5 days / 12 seconds); - - // Set up each strategy with random data and different delays - for (uint i = 0; i < numStrategies; i++) { - // Generate random strategy address and token - strategies[i] = IStrategy(cheats.randomAddress()); - tokens[i] = new MockERC20(); - underlyingAmounts[i] = cheats.randomUint(); - - // Set different delays for each strategy (increasing delays) - delays[i] = uint32((i + 1) * 1 days / 12 seconds); // 1 day, 2 days, 3 days, etc. - - // Set the strategy-specific delay - cheats.prank(defaultOwner); - factory.setStrategyEscrowDelay(strategies[i], delays[i]); - - // Start escrow for this strategy - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i], underlyingAmounts[i]); - // Verify the escrow was started correctly - _checkStartEscrows(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i], underlyingAmounts[i], i + 1); - } - - // The complete block should be the maximum delay across all strategies - assertEq(factory.getEscrowCompleteBlock(defaultOperatorSet, defaultSlashId), block.number + delays[numStrategies - 1] + 1); - } - - function testFuzz_getEscrowCompleteBlock_multipleSlashes(uint r) public { - uint startBlock = block.number; - uint32 firstDelay = uint32(1 days / 12 seconds); - uint32 secondDelay = uint32(2 days / 12 seconds); - - IStrategy[] memory strategies = new IStrategy[](1); - MockERC20[] memory tokens = new MockERC20[](1); - uint[] memory underlyingAmounts = new uint[](1); - strategies[0] = IStrategy(cheats.randomAddress()); - tokens[0] = new MockERC20(); - underlyingAmounts[0] = uint96(cheats.randomUint()); - - cheats.prank(defaultOwner); - factory.setGlobalEscrowDelay(firstDelay); - - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, strategies[0], tokens[0], underlyingAmounts[0]); - - cheats.prank(defaultOwner); - factory.setGlobalEscrowDelay(secondDelay); - - uint secondSlashId = defaultSlashId + 1; - _initiateSlashEscrow(defaultOperatorSet, secondSlashId, strategies[0], tokens[0], underlyingAmounts[0]); - - assertEq(factory.getEscrowCompleteBlock(defaultOperatorSet, defaultSlashId), startBlock + firstDelay + 1); - assertEq(factory.getEscrowCompleteBlock(defaultOperatorSet, secondSlashId), startBlock + secondDelay + 1); - - cheats.roll(startBlock + firstDelay + 1); - _mockStrategyUnderlyingTokenCall(strategies[0], address(tokens[0])); - _releaseSlashEscrow(defaultOperatorSet, defaultSlashId); - - cheats.roll(startBlock + secondDelay + 1); - _mockStrategyUnderlyingTokenCall(strategies[0], address(tokens[0])); - _releaseSlashEscrow(defaultOperatorSet, secondSlashId); - } -} - -contract SlashEscrowFactoryUnitTests_setGlobalEscrowDelay is SlashEscrowFactoryUnitTests { - function test_setGlobalEscrowDelay_onlyOwner() public { - cheats.prank(cheats.randomAddress()); - cheats.expectRevert("Ownable: caller is not the owner"); - factory.setGlobalEscrowDelay(100); - } - - function testFuzz_setGlobalEscrowDelay_correctness(uint32 delay) public { - cheats.prank(defaultOwner); - cheats.expectEmit(true, true, true, true); - emit GlobalEscrowDelaySet(delay); - factory.setGlobalEscrowDelay(delay); - assertEq(factory.getGlobalEscrowDelay(), delay); - } -} - -contract SlashEscrowFactoryUnitTests_getPendingEscrows is SlashEscrowFactoryUnitTests { - function test_getPendingEscrows_singleSlashId() public { - // Start escrow for a single strategy - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, 100); - - // Get pending escrows for the specific slash ID - (IStrategy[] memory strategies) = factory.getPendingStrategiesForSlashId(defaultOperatorSet, defaultSlashId); - - // Verify results - assertEq(strategies.length, 1); - assertEq(address(strategies[0]), address(defaultStrategy)); - } - - function test_getPendingEscrows_multipleStrategies() public { - // Create multiple strategies and start escrows - IStrategy strategy1 = IStrategy(cheats.randomAddress()); - IStrategy strategy2 = IStrategy(cheats.randomAddress()); - MockERC20 token1 = new MockERC20(); - MockERC20 token2 = new MockERC20(); - - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, strategy1, token1, 100); - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, strategy2, token2, 200); - - // Get pending escrows for the specific slash ID - (IStrategy[] memory strategies) = factory.getPendingStrategiesForSlashId(defaultOperatorSet, defaultSlashId); - - // Verify results - assertEq(strategies.length, 2); - assertEq(address(strategies[0]), address(strategy1)); - assertEq(address(strategies[1]), address(strategy2)); - assertEq(_getPendingUnderlyingAmountForStrategy(defaultOperatorSet, defaultSlashId, strategy1, token1), 100); - assertEq(_getPendingUnderlyingAmountForStrategy(defaultOperatorSet, defaultSlashId, strategy2, token2), 200); - } - - function test_getPendingEscrows_multipleSlashIds() public { - // Create multiple slash IDs and strategies - uint slashId1 = 1; - uint slashId2 = 2; - IStrategy strategy1 = IStrategy(cheats.randomAddress()); - IStrategy strategy2 = IStrategy(cheats.randomAddress()); - MockERC20 token1 = new MockERC20(); - MockERC20 token2 = new MockERC20(); - - // Start escrows for different slash IDs - _initiateSlashEscrow(defaultOperatorSet, slashId1, strategy1, token1, 100); - _initiateSlashEscrow(defaultOperatorSet, slashId2, strategy2, token2, 200); - - // Get pending escrows for all slash IDs of the operator set - (IStrategy[][] memory strategies) = factory.getPendingStrategiesForSlashIds(defaultOperatorSet); - - // Verify results - assertEq(strategies.length, 2); - - // Check first slash ID - assertEq(strategies[0].length, 1); - assertEq(address(strategies[0][0]), address(strategy1)); - assertEq(_getPendingUnderlyingAmountForStrategy(defaultOperatorSet, slashId1, strategy1, token1), 100); - - // Check second slash ID - assertEq(strategies[1].length, 1); - assertEq(address(strategies[1][0]), address(strategy2)); - assertEq(_getPendingUnderlyingAmountForStrategy(defaultOperatorSet, slashId2, strategy2, token2), 200); - } - - function test_getPendingEscrows_empty() public { - // Test with no pending escrows - (IStrategy[] memory strategies) = factory.getPendingStrategiesForSlashId(defaultOperatorSet, defaultSlashId); - assertEq(strategies.length, 0); - - (IStrategy[][] memory strategies2) = factory.getPendingStrategiesForSlashIds(defaultOperatorSet); - assertEq(strategies2.length, 0); - assertEq(factory.getTotalPendingStrategiesForSlashId(defaultOperatorSet, defaultSlashId), 0); - } -} - -contract SlashEscrowFactoryUnitTests_getEscrowCompleteBlock is SlashEscrowFactoryUnitTests { - function test_getEscrowCompleteBlock() public { - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, 100); - assertEq(factory.getEscrowCompleteBlock(defaultOperatorSet, defaultSlashId), block.number + defaultGlobalDelayBlocks + 1); - } - - function testFuzz_getEscrowCompleteBlock_multipleStrategies(uint r) public { - // Initialize arrays to store test data for multiple strategies - uint numStrategies = bound(r, 2, 10); - IStrategy[] memory strategies = new IStrategy[](numStrategies); - MockERC20[] memory tokens = new MockERC20[](numStrategies); - uint[] memory underlyingAmounts = new uint[](numStrategies); - uint32[] memory delays = new uint32[](numStrategies); - - // Set global delay less than all the strategy delays. - cheats.prank(defaultOwner); - factory.setGlobalEscrowDelay(0.5 days / 12 seconds); - - // Set up each strategy with random data and different delays - for (uint i = 0; i < numStrategies; i++) { - // Generate random strategy address and token - strategies[i] = IStrategy(cheats.randomAddress()); - tokens[i] = new MockERC20(); - underlyingAmounts[i] = cheats.randomUint(); - - // Set different delays for each strategy (increasing delays) - delays[i] = uint32((i + 1) * 1 days / 12 seconds); // 1 day, 2 days, 3 days, etc. - - // Set the strategy-specific delay - cheats.prank(defaultOwner); - factory.setStrategyEscrowDelay(strategies[i], delays[i]); - - // Start escrow for this strategy - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i], underlyingAmounts[i]); - // Verify the escrow was started correctly - _checkStartEscrows(defaultOperatorSet, defaultSlashId, strategies[i], tokens[i], underlyingAmounts[i], i + 1); - } - - // The complete block should be the maximum delay across all strategies - assertEq(factory.getEscrowCompleteBlock(defaultOperatorSet, defaultSlashId), block.number + delays[numStrategies - 1] + 1); - } -} - -contract SlashEscrowFactoryUnitTests_getPendingEscrowsFull is SlashEscrowFactoryUnitTests { - function test_getPendingEscrows() public { - uint32 dayInBlocks = 1 days / 12 seconds; - uint32 initialBlock = uint32(block.number); - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, 100); - - cheats.roll(block.number + dayInBlocks); - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId + 1, defaultStrategy, defaultToken, 100); - - cheats.roll(block.number + dayInBlocks); - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId + 2, defaultStrategy, defaultToken, 100); - - (OperatorSet[] memory operatorSets, bool[] memory isRedistributing, uint[][] memory slashIds, uint32[][] memory completeBlocks) = - factory.getPendingEscrows(); - assertEq(operatorSets.length, 1); - assertEq(operatorSets[0].key(), defaultOperatorSet.key()); - assertEq(isRedistributing.length, 1); - assertEq(isRedistributing[0], true); - assertEq(slashIds.length, 1); - assertEq(slashIds[0].length, 3); - assertEq(completeBlocks.length, 1); - assertEq(completeBlocks[0].length, 3); - - // Assert that the complete blocks are correct - assertEq(completeBlocks[0][0], initialBlock + defaultGlobalDelayBlocks + 1); - assertEq(completeBlocks[0][1], initialBlock + defaultGlobalDelayBlocks + dayInBlocks + 1); - assertEq(completeBlocks[0][2], initialBlock + defaultGlobalDelayBlocks + dayInBlocks * 2 + 1); - } -} - -contract SlashEscrowFactoryUnitTests_SlashEscrowProxy is SlashEscrowFactoryUnitTests { - address maliciousCaller; - - function setUp() public override { - super.setUp(); - _initiateSlashEscrow(defaultOperatorSet, defaultSlashId, defaultStrategy, defaultToken, 100); - maliciousCaller = cheats.randomAddress(); - } - - function test_SlashEscrowProxy_InvalidDeploymentParameters_BadFactory() public { - ISlashEscrow slashEscrow = factory.getSlashEscrow(defaultOperatorSet, defaultSlashId); - - cheats.prank(maliciousCaller); - cheats.expectRevert(ISlashEscrow.InvalidDeploymentParameters.selector); - slashEscrow.releaseTokens( - ISlashEscrowFactory(maliciousCaller), - slashEscrowImplementation, - defaultOperatorSet, - defaultSlashId, - defaultRedistributionRecipient, - defaultStrategy - ); - } - - function test_SlashEscrowProxy_InvalidDeploymentParameters_BadSlashEscrowImplementation() public { - ISlashEscrow slashEscrow = factory.getSlashEscrow(defaultOperatorSet, defaultSlashId); - - cheats.prank(maliciousCaller); - cheats.expectRevert(ISlashEscrow.InvalidDeploymentParameters.selector); - slashEscrow.releaseTokens( - factory, ISlashEscrow(maliciousCaller), defaultOperatorSet, defaultSlashId, defaultRedistributionRecipient, defaultStrategy - ); - } - - function test_SlashEscrowProxy_InvalidDeploymentParameters_BadOperatorSet() public { - ISlashEscrow slashEscrow = factory.getSlashEscrow(defaultOperatorSet, defaultSlashId); - - cheats.prank(maliciousCaller); - cheats.expectRevert(ISlashEscrow.InvalidDeploymentParameters.selector); - slashEscrow.releaseTokens( - factory, - slashEscrowImplementation, - OperatorSet(maliciousCaller, 1), - defaultSlashId, - defaultRedistributionRecipient, - defaultStrategy - ); - } - - function test_SlashEscrowProxy_InvalidDeploymentParameters_BadSlashId(uint slashId) public { - ISlashEscrow slashEscrow = factory.getSlashEscrow(defaultOperatorSet, defaultSlashId); - - cheats.assume(slashId != defaultSlashId); - - cheats.prank(maliciousCaller); - cheats.expectRevert(ISlashEscrow.InvalidDeploymentParameters.selector); - slashEscrow.releaseTokens( - factory, slashEscrowImplementation, defaultOperatorSet, slashId, defaultRedistributionRecipient, defaultStrategy - ); - } - - function test_SlashEscrowProxy_OnlySlashEscrowFactory() public { - ISlashEscrow slashEscrow = factory.getSlashEscrow(defaultOperatorSet, defaultSlashId); - - cheats.expectRevert(ISlashEscrow.OnlySlashEscrowFactory.selector); - slashEscrow.releaseTokens( - factory, slashEscrowImplementation, defaultOperatorSet, defaultSlashId, defaultRedistributionRecipient, defaultStrategy - ); - } - - function test_SlashEscrowProxy_OnlySlashEscrowFactory_BadRecipient() public { - ISlashEscrow slashEscrow = factory.getSlashEscrow(defaultOperatorSet, defaultSlashId); - - cheats.prank(maliciousCaller); - cheats.expectRevert(ISlashEscrow.OnlySlashEscrowFactory.selector); - slashEscrow.releaseTokens(factory, slashEscrowImplementation, defaultOperatorSet, defaultSlashId, maliciousCaller, defaultStrategy); - } -} diff --git a/src/test/unit/StrategyManagerUnit.t.sol b/src/test/unit/StrategyManagerUnit.t.sol index dff67cc3f0..0b147d55b5 100644 --- a/src/test/unit/StrategyManagerUnit.t.sol +++ b/src/test/unit/StrategyManagerUnit.t.sol @@ -44,7 +44,6 @@ contract StrategyManagerUnitTests is EigenLayerUnitTestSetup, IStrategyManagerEv EigenLayerUnitTestSetup.setUp(); strategyManagerImplementation = new StrategyManager( IDelegationManager(address(delegationManagerMock)), - ISlashEscrowFactory(address(slashEscrowFactoryMock)), pauserRegistry, "9.9.9" ); From 97b23fbafa207bbd41e0723ce7f84a8e8261ec09 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:48:27 -0400 Subject: [PATCH 02/16] refactor: remove remaining escrow stuffs --- docs/core/SlashEscrowFactory.md | 220 ------------------ .../deploy/devnet/deploy_from_scratch.s.sol | 2 +- .../local/deploy_from_scratch.slashing.s.sol | 2 +- script/releases/Env.sol | 4 - script/utils/ExistingDeploymentParser.sol | 2 - src/contracts/core/StrategyManager.sol | 79 ++++--- src/contracts/core/StrategyManagerStorage.sol | 6 +- src/contracts/interfaces/IStrategyManager.sol | 16 +- src/test/integration/IntegrationBase.t.sol | 4 - src/test/integration/IntegrationChecks.t.sol | 15 -- .../integration/IntegrationDeployer.t.sol | 2 +- src/test/mocks/SlashEscrowFactoryMock.sol | 16 -- src/test/unit/StrategyManagerUnit.t.sol | 38 ++- src/test/utils/EigenLayerUnitTestSetup.sol | 4 - 14 files changed, 85 insertions(+), 325 deletions(-) delete mode 100644 docs/core/SlashEscrowFactory.md delete mode 100644 src/test/mocks/SlashEscrowFactoryMock.sol diff --git a/docs/core/SlashEscrowFactory.md b/docs/core/SlashEscrowFactory.md deleted file mode 100644 index abd0f4b0e2..0000000000 --- a/docs/core/SlashEscrowFactory.md +++ /dev/null @@ -1,220 +0,0 @@ -## SlashEscrowFactory - -| File | Notes | -| -------- | -------- | -| [`SlashEscrowFactory.sol`](../../src/contracts/core/SlashEscrowFactory.sol) | single slash escrow factory | -| [`SlashEscrowFactoryStorage.sol`](../../src/contracts/core/SlashEscrowFactory.sol) | state variables | -| [`ISlashEscrowFactory.sol`](../../src/contracts/interfaces/ISlashEscrowFactory.sol) | interface | - -SlashEscrow: - -| File | Notes | -| -------- | -------- | -| [`SlashEscrow.sol`](../../src/contracts/core/SlashEscrow.sol) | Instance, deployed per `OperatorSet`, `slashId` that stores slashed funds | -| [`ISlashEscrow.sol`](../../src/contracts/interface/ISlashEscrow.sol) | interface | - -## Overview -The `SlashEscrowFactory` handles the burning or redistribution of slashed funds out of the EigenLayer protocol. The `SlashEscrowFactory` is responsible for (i) enforcing an escrow delay upon an AVS calling [`slashOperator`](./AllocationManager.md#slashoperator), (ii) deploying the `SlashEscrow` for each slash, and (iii) releasing funds from the escrow contract upon completion of a the escrow delay. - -> **Note:** If the protocol is paused due to a security incident, slashed funds will remain locked in the `SlashEscrow` contracts. A protocol upgrade would be required to rescue these funds. - -## Parameterization -* `DEFAULT_BURN_ADDRESS = 0x00000000000000000000000000000000000E16E4` - * The address to which burnt funds are sent -* `_globalEscrowDelayBlocks = 28800` (4 days in blocks) - * On testnet this value is `5` (1 minute in blocks) - -## Initiating and Releasing Escrow -An escrow is initiated for each slash triggered by an AVS. A slash can contain multiple strategies and is unique per `operatorSet` and `slashId`. - -*[`SlashEscrowFactory.initiateSlashEscrow`](#initiateslashescrow) -*[`SlashEscrowFactory.releaseSlashEscrow`](#releaseslashescrow) -*[`SlashEscrowFactory.releaseSlashEscrowByStrategy`](#releaseslashescrow) - -#### `initiateSlashEscrow` -```solidity -/** - * @notice Locks up a escrow. - * @param operatorSet The operator set whose escrow is being locked up. - * @param slashId The slash ID of the escrow that is being locked up. - * @param strategy The strategy that whose underlying tokens are being redistributed. - * @dev This function can be called multiple times for a given `operatorSet` and `slashId`. - */ -function initiateSlashEscrow( - OperatorSet calldata operatorSet, - uint256 slashId, - IStrategy strategy) -external onlyStrategyManager; -``` -Initiates an escrow for a given `operatorSet`, `slashId`, and `strategy`. This function can be called multiple times in the same transaction by the `StrategyManager`, as a single slash can contain multiple strategies. The `operatorSet`, `slashId`, and `strategy` are each stored in an [EnumerableSet](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/utils/structs/EnumerableSet.sol). - -*Effects*: -* If the operatorSet and slashID have not already been set to pending: - * The `SlashEscrow` contract is [deployed](#deployslashescrow) - * Adds the `operatorSet `to `pendingOperatorSets` - * Adds the `slashId` to the operatorSet's `pendingSlashIds` -* Adds the strategy to `pendingStrategiesForSlashId` -* Emits a `StartEscrow` event - -*Requirements*: -* Can only be called by the `StrategyManager` - -#### `deploySlashEscrow` - -```solidity -/** - * @notice Deploys a counterfactual `SlashEscrow` if code hasn't already been deployed. - * @param operatorSet The operator set whose slash escrow is being deployed. - * @param slashId The slash ID of the slash escrow that is being deployed. - */ -function _deploySlashEscrow( - OperatorSet calldata operatorSet, - uint256 slashId) -internal; -``` - -The internal function is called on `initiateEscrow`. A unique slash escrow contract is deployed per `operatorSet` and `slashId` - -SlashEscrows are deployed deterministically using [Open Zeppelin's Clones Upgradeable Library](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/f6febd79e2a3a17e26969dd0d450c6ebd64bf459/contracts/proxy/ClonesUpgradeable.sol), which is a minimal, non-upgradeable proxy. The deployment salt is a concatenation of the `operatorSet` and `slashId`, which together are guaranteed to be unique. - -#### `releaseSlashEscrow` - -```solidity -/** - * @notice Releases an escrow by transferring tokens from the `SlashEscrow` to the operator set's redistribution recipient. - * @param operatorSet The operator set whose escrow is being released. - * @param slashId The slash ID of the escrow that is being released. - * @dev The caller must be the escrow recipient, unless the escrow recipient - * is the default burn address in which case anyone can call. - * @dev The slash escrow is released once the delay for ALL strategies has elapsed. - */ -function releaseSlashEscrow( - OperatorSet calldata operatorSet, - uint256 slashId) -external onlyWhenNotPaused(PAUSED_RELEASE_ESCROW); - -/** - * @notice Releases an escrow for a single strategy in a slash. - * @param operatorSet The operator set whose escrow is being released. - * @param slashId The slash ID of the escrow that is being released. - * @param strategy The strategy whose escrow is being released. - * @dev The caller must be the redistribution recipient, unless the redistribution recipient - * is the default burn address in which case anyone can call. - * @dev The slash escrow is released once the delay for ALL strategies has elapsed. - */ -function releaseSlashEscrowByStrategy( - OperatorSet calldata operatorSet, - uint256 slashId, - IStrategy strategy -) external; -``` -At or after the `getEscrowCompleteBlock`, tokens are transferred from the `SlashEscrow` contract to the [`redistributionRecipient`](./AllocationManager.md#createredistributingoperatorsets) of an `operatorSet`. - -For each `strategy` in the `slash`, tokens are transferred from the slash's unique `SlashEscrow` contract to the `redistributionRecipient`. - -To accommodate the unlimited number of strategies that can be added to an operatorSet, and a token transfer revert blocking the release of other tokens, a user can release a single strategy from escrow via `releaseSlashEscrowByStrategy`. - -*Effects*: -* Call [`StrategyManager.clearBurnOrRedistributableShares`](./StrategyManager.md#clearburnorredistributableshares). This function may have already been called prior and will no-op if so. We call it again for sanity to ensure that all tokens are transferred to the `SlashEscrow` contract. For `releaseEscrowByStrategy` we call the by strategy variant: `StrategyManager.clearBurnOrRedistributableSharesByStrategy` -* For each strategy: - * Call [`SlashEscrow.releaseTokens`](#releasetokens) - * Emits an `EscrowComplete` - * Remove the `strategy` from the `_pendingStrategiesForSlashId` -* If all strategies from an operatorSet/slashId have been released: - * Remove the `slashId` from `_pendingSlashIds` - * Delete the complete block for the `slashId` -* If the `operatorSet` has no more pending slashes, remove it from `pendingOperatorSets` - -*Requirements*: -* The global paused status MUST NOT be set: `PAUSED_RELEASE_ESCROW` -* The escrow paused status MUST NOT be set: `pauseEscrow` -* If the operatorSet is redistributable, caller MUST be the `redistributionRecipient` -* The escrow delay MUST have elapsed - -## SlashEscrow - -A [minimal proxy](https://eips.ethereum.org/EIPS/eip-1167) contract deployed from the `SlashEscrowFactory`. This contract releases funds once an escrow has elapsed. - -### `releaseTokens` - -```solidity -/** - * @notice Burns or redistributes the underlying tokens of the strategies. - * @param slashEscrowFactory The factory contract that created the slash escrow. - * @param slashEscrowImplementation The implementation contract that was used to create the slash escrow. - * @param operatorSet The operator set that was used to create the slash escrow. - * @param slashId The slash ID that was used to create the slash escrow. - * @param recipient The recipient of the underlying tokens. - * @param strategy The strategy that was used to create the slash escrow. - */ -function releaseTokens( - ISlashEscrowFactory slashEscrowFactory, - ISlashEscrow slashEscrowImplementation, - OperatorSet calldata operatorSet, - uint256 slashId, - address recipient, - IStrategy strategy -) external; -``` -Sends this contract's balance for a given `strategy`'s underlyingToken to the `recipient`. The `slashEscrowFactory`, `slashEscrowImplementation`, `operatorSet`, and `slashId` are passed in to validate deployment parameters of the contract, enabling this contract to have no storage. - -*Effects*: -* Transfers the balance of the strategy's `underlyingToken` to `redistributionRecipient` - -*Requirements*: -* Calldata passed in MUST match output of `ClonesUpgradeable.predictDeterministicAddress` -* Caller MUST be the `SlashEscrowFactory` - -## System Configuration - -The `owner` of the `SlashEscrowFactory` can update a `_globalEscrowDelayBlocks` and a per-strategy `_strategyEscrowDelayBlocks`. For a given strategy, its delay is given by `max(_globalEscrowDelayBlocks, _strategyEscrowDelayBlocks)`. For an escrow with multiple strategies, the total time for it to be released is the maximum of each of its strategy's delays. - -The following methods concern the `owner` and its abilities in the `SlashEscrowFactory` -* [`SlashEscrowFactory.setGlobalEscrowDelay`](#setglobalescrowdelay) -* [`SlashEscrowFactory.setStrategyEscrowDelay`](#setstrategyescrowdelay) - -#### `setGlobalEscrowDelay` - -```solidity -/** - * @notice Sets the delay for the escrow of all strategies underlying tokens globally. - * @dev This delay setting only applies to new slashes and does not affect existing ones. - * @param delay The delay for the escrow. - */ -function setGlobalEscrowDelay( - uint32 delay -) external onlyOwner; -``` -Sets the global escrow delay observed by all strategies. *Note: If this value is updated, previously existing escrows will not be affected.* - -*Effects*: -* Sets the `_globalEscrowDelayBlocks` -* Emits a `GlobalEscrowDelaySet` event - -*Requirements*: -* Caller MUST be the `owner` - -#### `setStrategyEscrowDelay` - -```solidity -/** - * @notice Sets the delay for the escrow of a strategies underlying token. - * @dev The maximum of the strategy and global delay is used - * @dev This delay setting only applies to new slashes and does not affect existing ones. - * @param strategy The strategy whose escrow delay is being set. - * @param delay The delay for the escrow. - */ -function setStrategyEscrowDelay( - IStrategy strategy, - uint32 delay -) external onlyOwner; -``` - -Sets the delay for a given strategy. *Note: If this value is updated, previously existing escrows will not be affected.* - -*Effects*: -* Updates the `_strategyEscrowDelayBlocks` for the given `strategy` -* Emits a `StrategyEscrowDelaySet` event - -*Requirements*: -* Caller MUST be the `owner` diff --git a/script/deploy/devnet/deploy_from_scratch.s.sol b/script/deploy/devnet/deploy_from_scratch.s.sol index 320e84fce2..7d6711c893 100644 --- a/script/deploy/devnet/deploy_from_scratch.s.sol +++ b/script/deploy/devnet/deploy_from_scratch.s.sol @@ -231,7 +231,7 @@ contract DeployFromScratch is Script, Test { SEMVER ); - strategyManagerImplementation = new StrategyManager(delegation, eigenLayerPauserReg, SEMVER); + strategyManagerImplementation = new StrategyManager(allocationManager, delegation, eigenLayerPauserReg, SEMVER); avsDirectoryImplementation = new AVSDirectory(delegation, eigenLayerPauserReg, SEMVER); eigenPodManagerImplementation = new EigenPodManager(ethPOSDeposit, eigenPodBeacon, delegation, eigenLayerPauserReg, SEMVER); diff --git a/script/deploy/local/deploy_from_scratch.slashing.s.sol b/script/deploy/local/deploy_from_scratch.slashing.s.sol index b70d597f86..428bea963b 100644 --- a/script/deploy/local/deploy_from_scratch.slashing.s.sol +++ b/script/deploy/local/deploy_from_scratch.slashing.s.sol @@ -238,7 +238,7 @@ contract DeployFromScratch is Script, Test { MIN_WITHDRAWAL_DELAY, SEMVER ); - strategyManagerImplementation = new StrategyManager(delegation, eigenLayerPauserReg, SEMVER); + strategyManagerImplementation = new StrategyManager(allocationManager, delegation, eigenLayerPauserReg, SEMVER); avsDirectoryImplementation = new AVSDirectory(delegation, eigenLayerPauserReg, SEMVER); eigenPodManagerImplementation = new EigenPodManager(ethPOSDeposit, eigenPodBeacon, delegation, eigenLayerPauserReg, SEMVER); diff --git a/script/releases/Env.sol b/script/releases/Env.sol index a5911bab11..568ca26f0e 100644 --- a/script/releases/Env.sol +++ b/script/releases/Env.sol @@ -92,8 +92,6 @@ library Env { return _envAddress("proxyAdmin"); } - - function ethPOS() internal view returns (IETHPOSDeposit) { return IETHPOSDeposit(_envAddress("ethPOS")); } @@ -324,8 +322,6 @@ library Env { return StrategyFactory(_deployedImpl(type(StrategyFactory).name)); } - - /** * token/ */ diff --git a/script/utils/ExistingDeploymentParser.sol b/script/utils/ExistingDeploymentParser.sol index 1889e862d8..5d46763f7e 100644 --- a/script/utils/ExistingDeploymentParser.sol +++ b/script/utils/ExistingDeploymentParser.sol @@ -135,8 +135,6 @@ contract ExistingDeploymentParser is Script, Logger { StrategyBase public baseStrategyImplementation; StrategyBase public strategyFactoryBeaconImplementation; - - // Token ProxyAdmin public tokenProxyAdmin; IEigen public EIGEN; diff --git a/src/contracts/core/StrategyManager.sol b/src/contracts/core/StrategyManager.sol index 15e6cbebf6..db77d57ae7 100644 --- a/src/contracts/core/StrategyManager.sol +++ b/src/contracts/core/StrategyManager.sol @@ -53,14 +53,11 @@ contract StrategyManager is * @param _delegation The delegation contract of EigenLayer. */ constructor( + IAllocationManager _allocationManager, IDelegationManager _delegation, IPauserRegistry _pauserRegistry, string memory _version - ) - StrategyManagerStorage(_delegation) - Pausable(_pauserRegistry) - SignatureUtilsMixin(_version) - { + ) StrategyManagerStorage(_allocationManager, _delegation) Pausable(_pauserRegistry) SignatureUtilsMixin(_version) { _disableInitializers(); } @@ -168,7 +165,7 @@ contract StrategyManager is function clearBurnOrRedistributableShares( OperatorSet calldata operatorSet, uint256 slashId - ) external returns (uint256[] memory) { + ) external nonReentrant returns (uint256[] memory) { // Get the strategies to clear. address[] memory strategies = _burnOrRedistributableShares[operatorSet.key()][slashId].keys(); uint256 length = strategies.length; @@ -176,7 +173,12 @@ contract StrategyManager is // Note: We don't need to iterate backwards since we're indexing into the `EnumerableMap` directly. for (uint256 i = 0; i < length; ++i) { - amounts[i] = clearBurnOrRedistributableSharesByStrategy(operatorSet, slashId, IStrategy(strategies[i])); + amounts[i] = _clearBurnOrRedistributableShares({ + operatorSet: operatorSet, + slashId: slashId, + strategy: IStrategy(strategies[i]), + recipient: allocationManager.getRedistributionRecipient(operatorSet) + }); } return amounts; @@ -187,27 +189,13 @@ contract StrategyManager is OperatorSet calldata operatorSet, uint256 slashId, IStrategy strategy - ) public nonReentrant returns (uint256) { - EnumerableMap.AddressToUintMap storage burnOrRedistributableShares = - _burnOrRedistributableShares[operatorSet.key()][slashId]; - - (, uint256 sharesToRemove) = burnOrRedistributableShares.tryGet(address(strategy)); - burnOrRedistributableShares.remove(address(strategy)); - - uint256 amountOut; - if (sharesToRemove != 0) { - // Withdraw the shares to the burn address. - amountOut = IStrategy(strategy).withdraw({ - recipient: DEFAULT_BURN_ADDRESS, - token: IStrategy(strategy).underlyingToken(), - amountShares: sharesToRemove - }); - - // Emit an event to notify the that burnable shares have been decreased. - emit BurnOrRedistributableSharesDecreased(operatorSet, slashId, strategy, sharesToRemove); - } - - return amountOut; + ) external nonReentrant returns (uint256) { + return _clearBurnOrRedistributableShares({ + operatorSet: operatorSet, + slashId: slashId, + strategy: strategy, + recipient: allocationManager.getRedistributionRecipient(operatorSet) + }); } /// @inheritdoc IStrategyManager @@ -384,6 +372,41 @@ contract StrategyManager is stakerStrategyList[staker].pop(); } + /** + * @notice Clears burn/redistributable shares and sends underlying tokens to recipient. + * @param operatorSet The operator set to clear the shares for. + * @param slashId The slash id to clear the shares for. + * @param strategy The strategy to clear the shares for. + * @param recipient The recipient to withdraw the shares to. + */ + function _clearBurnOrRedistributableShares( + OperatorSet calldata operatorSet, + uint256 slashId, + IStrategy strategy, + address recipient + ) internal returns (uint256) { + EnumerableMap.AddressToUintMap storage burnOrRedistributableShares = + _burnOrRedistributableShares[operatorSet.key()][slashId]; + + (, uint256 sharesToRemove) = burnOrRedistributableShares.tryGet(address(strategy)); + burnOrRedistributableShares.remove(address(strategy)); + + uint256 amountOut; + if (sharesToRemove != 0) { + // Withdraw the shares to the burn address. + amountOut = IStrategy(strategy).withdraw({ + recipient: recipient, + token: IStrategy(strategy).underlyingToken(), + amountShares: sharesToRemove + }); + + // Emit an event to notify the that burnable shares have been decreased. + emit BurnOrRedistributableSharesDecreased(operatorSet, slashId, strategy, sharesToRemove); + } + + return amountOut; + } + /** * @notice Internal function for modifying the `strategyWhitelister`. Used inside of the `setStrategyWhitelister` and `initialize` functions. * @param newStrategyWhitelister The new address for the `strategyWhitelister` to take. diff --git a/src/contracts/core/StrategyManagerStorage.sol b/src/contracts/core/StrategyManagerStorage.sol index b0960450ab..00dd5d2d31 100644 --- a/src/contracts/core/StrategyManagerStorage.sol +++ b/src/contracts/core/StrategyManagerStorage.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.27; import "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import "../interfaces/IAllocationManager.sol"; import "../interfaces/IAVSDirectory.sol"; import "../interfaces/IDelegationManager.sol"; import "../interfaces/IEigenPodManager.sol"; @@ -33,6 +34,8 @@ abstract contract StrategyManagerStorage is IStrategyManager { // Immutables + IAllocationManager public immutable allocationManager; + IDelegationManager public immutable delegation; // Mutatables @@ -84,7 +87,8 @@ abstract contract StrategyManagerStorage is IStrategyManager { /** * @param _delegation The delegation contract of EigenLayer. */ - constructor(IDelegationManager _delegation) { + constructor(IAllocationManager _allocationManager, IDelegationManager _delegation) { + allocationManager = _allocationManager; delegation = _delegation; } diff --git a/src/contracts/interfaces/IStrategyManager.sol b/src/contracts/interfaces/IStrategyManager.sol index 22628c9cb5..619ba192c6 100644 --- a/src/contracts/interfaces/IStrategyManager.sol +++ b/src/contracts/interfaces/IStrategyManager.sol @@ -51,7 +51,7 @@ interface IStrategyManagerEvents { OperatorSet operatorSet, uint256 slashId, IStrategy strategy, uint256 shares ); - /// @notice Emitted when shares marked for burning or redistribution are decreased and transferred to the `SlashEscrow` + /// @notice Emitted when shares marked for burning or redistribution are decreased and transferred to the operator set's redistribution recipient event BurnOrRedistributableSharesDecreased( OperatorSet operatorSet, uint256 slashId, IStrategy strategy, uint256 shares ); @@ -136,11 +136,11 @@ interface IStrategyManager is IStrategyManagerErrors, IStrategyManagerEvents, IS ) external; /** - * @notice Removes burned shares from storage and transfers the underlying tokens for the slashId to the slash escrow. + * @notice Removes burned shares from storage and transfers the underlying tokens for the slashId to the redistribution recipient. * @dev Reentrancy is checked in the `clearBurnOrRedistributableSharesByStrategy` function. * @param operatorSet The operator set to burn shares in. * @param slashId The slash ID to burn shares in. - * @return The amounts of tokens transferred to the slash escrow for each strategy + * @return The amounts of tokens transferred to the redistribution recipient for each strategy */ function clearBurnOrRedistributableShares( OperatorSet calldata operatorSet, @@ -148,11 +148,11 @@ interface IStrategyManager is IStrategyManagerErrors, IStrategyManagerEvents, IS ) external returns (uint256[] memory); /** - * @notice Removes a single strategy's shares from storage and transfers the underlying tokens for the slashId to the slash escrow. + * @notice Removes a single strategy's shares from storage and transfers the underlying tokens for the slashId to the redistribution recipient. * @param operatorSet The operator set to burn shares in. * @param slashId The slash ID to burn shares in. * @param strategy The strategy to burn shares in. - * @return The amount of tokens transferred to the slash escrow for the strategy. + * @return The amount of tokens transferred to the redistribution recipient for the strategy. */ function clearBurnOrRedistributableSharesByStrategy( OperatorSet calldata operatorSet, @@ -161,7 +161,7 @@ interface IStrategyManager is IStrategyManagerErrors, IStrategyManagerEvents, IS ) external returns (uint256); /** - * @notice Returns the strategies and shares that have NOT been sent to escrow for a given slashId. + * @notice Returns the strategies and shares that have NOT been sent to the redistribution recipient for a given slashId. * @param operatorSet The operator set to burn or redistribute shares in. * @param slashId The slash ID to burn or redistribute shares in. * @return The strategies and shares for the given slashId. @@ -177,7 +177,7 @@ interface IStrategyManager is IStrategyManagerErrors, IStrategyManagerEvents, IS * @param slashId The slash ID to burn or redistribute shares in. * @param strategy The strategy to get the shares for. * @return The shares for the given strategy for the given slashId. - * @dev This function will return revert if the shares have already been sent to escrow. + * @dev This function will return revert if the shares have already been sent to the redistribution recipient. */ function getBurnOrRedistributableShares( OperatorSet calldata operatorSet, @@ -186,7 +186,7 @@ interface IStrategyManager is IStrategyManagerErrors, IStrategyManagerEvents, IS ) external view returns (uint256); /** - * @notice Returns the number of strategies that have NOT been sent to escrow for a given slashId. + * @notice Returns the number of strategies that have NOT been sent to the redistribution recipient for a given slashId. * @param operatorSet The operator set to burn or redistribute shares in. * @param slashId The slash ID to burn or redistribute shares in. * @return The number of strategies for the given slashId. diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol index eb7d853d98..60253e6649 100644 --- a/src/test/integration/IntegrationBase.t.sol +++ b/src/test/integration/IntegrationBase.t.sol @@ -2564,10 +2564,6 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter { cheats.roll(latest + 1); } - function _rollBlocksForCompleteSlashEscrow() internal { - cheats.roll(block.number + INITIAL_GLOBAL_DELAY_BLOCKS + 1); - } - /// @dev Uses timewarp modifier to get the operator set strategy allocations at the last snapshot. function _getPrevAllocations(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies) internal diff --git a/src/test/integration/IntegrationChecks.t.sol b/src/test/integration/IntegrationChecks.t.sol index 1f1398a2fa..c9ed0b08b1 100644 --- a/src/test/integration/IntegrationChecks.t.sol +++ b/src/test/integration/IntegrationChecks.t.sol @@ -1108,21 +1108,6 @@ contract IntegrationCheckUtils is IntegrationBase { ); } - function check_releaseSlashEscrow_State( - OperatorSet memory operatorSet, - uint slashId, - IStrategy[] memory strategies, - uint[] memory initTokenBalances, - address redistributionRecipient - ) internal { - assert_HasUnderlyingTokenBalances( - User(payable(redistributionRecipient)), - strategies, - initTokenBalances, - "redistribution recipient should have underlying token balances" - ); - } - /** * * DUAL SLASHING CHECKS diff --git a/src/test/integration/IntegrationDeployer.t.sol b/src/test/integration/IntegrationDeployer.t.sol index 296510445b..7f2aadc718 100644 --- a/src/test/integration/IntegrationDeployer.t.sol +++ b/src/test/integration/IntegrationDeployer.t.sol @@ -321,7 +321,7 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { DELEGATION_MANAGER_MIN_WITHDRAWAL_DELAY_BLOCKS, version ); - strategyManagerImplementation = new StrategyManager(delegationManager, eigenLayerPauserReg, version); + strategyManagerImplementation = new StrategyManager(allocationManager, delegationManager, eigenLayerPauserReg, version); rewardsCoordinatorImplementation = new RewardsCoordinator( IRewardsCoordinatorTypes.RewardsCoordinatorConstructorParams({ delegationManager: delegationManager, diff --git a/src/test/mocks/SlashEscrowFactoryMock.sol b/src/test/mocks/SlashEscrowFactoryMock.sol deleted file mode 100644 index 3823740e75..0000000000 --- a/src/test/mocks/SlashEscrowFactoryMock.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.9; - -import "forge-std/Test.sol"; -import "src/contracts/interfaces/IStrategy.sol"; -import "src/contracts/libraries/OperatorSetLib.sol"; - -contract SlashEscrowFactoryMock is Test { - receive() external payable {} - fallback() external payable {} - - function getSlashEscrow(OperatorSet calldata operatorSet, uint slashId) public view returns (address) { - // Hash the operatorSet and slashId to get a random address - return address(uint160(uint(keccak256(abi.encode(operatorSet, slashId))))); - } -} diff --git a/src/test/unit/StrategyManagerUnit.t.sol b/src/test/unit/StrategyManagerUnit.t.sol index 0b147d55b5..8588f50609 100644 --- a/src/test/unit/StrategyManagerUnit.t.sol +++ b/src/test/unit/StrategyManagerUnit.t.sol @@ -43,9 +43,7 @@ contract StrategyManagerUnitTests is EigenLayerUnitTestSetup, IStrategyManagerEv function setUp() public override { EigenLayerUnitTestSetup.setUp(); strategyManagerImplementation = new StrategyManager( - IDelegationManager(address(delegationManagerMock)), - pauserRegistry, - "9.9.9" + IAllocationManager(address(allocationManagerMock)), IDelegationManager(address(delegationManagerMock)), pauserRegistry, "9.9.9" ); strategyManager = StrategyManager( address( @@ -1210,14 +1208,14 @@ contract StrategyManagerUnitTests_clearBurnOrRedistributableShares is StrategyMa emit BurnOrRedistributableSharesDecreased(defaultOperatorSet, defaultSlashId, strategy, shares); strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId); - (IStrategy[] memory escrowStrats, uint[] memory escrowShares) = + (IStrategy[] memory slashStrats, uint[] memory slashShares) = strategyManager.getBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId); - assertEq(escrowStrats.length, 0, "strats length should be 0"); - assertEq(escrowShares.length, 0, "shares length should be 0"); + assertEq(slashStrats.length, 0, "strats length should be 0"); + assertEq(slashShares.length, 0, "shares length should be 0"); assertEq(strategyManager.getBurnOrRedistributableCount(defaultOperatorSet, defaultSlashId), 0, "count should be 0"); - address slashEscrow = slashEscrowFactoryMock.getSlashEscrow(defaultOperatorSet, defaultSlashId); - assertEq(dummyToken.balanceOf(slashEscrow), shares, "strategy balance of slash escrow invalid"); + address redistributionRecipient = allocationManagerMock.getRedistributionRecipient(defaultOperatorSet); + assertEq(dummyToken.balanceOf(redistributionRecipient), shares, "strategy balance of redistribution recipient invalid"); assertEq(dummyToken.balanceOf(address(strategy)), 0, "strategy balance should be 0"); } @@ -1232,14 +1230,14 @@ contract StrategyManagerUnitTests_clearBurnOrRedistributableShares is StrategyMa emit BurnOrRedistributableSharesDecreased(defaultOperatorSet, defaultSlashId, strategy, shares); strategyManager.clearBurnOrRedistributableSharesByStrategy(defaultOperatorSet, defaultSlashId, strategy); - (IStrategy[] memory escrowStrats, uint[] memory escrowShares) = + (IStrategy[] memory slashStrats, uint[] memory slashShares) = strategyManager.getBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId); - assertEq(escrowStrats.length, 0, "strats length should be 0"); - assertEq(escrowShares.length, 0, "shares length should be 0"); + assertEq(slashStrats.length, 0, "strats length should be 0"); + assertEq(slashShares.length, 0, "shares length should be 0"); assertEq(strategyManager.getBurnOrRedistributableCount(defaultOperatorSet, defaultSlashId), 0, "count should be 0"); - address slashEscrow = slashEscrowFactoryMock.getSlashEscrow(defaultOperatorSet, defaultSlashId); - assertEq(dummyToken.balanceOf(slashEscrow), shares, "strategy balance of slash escrow invalid"); + address redistributionRecipient = allocationManagerMock.getRedistributionRecipient(defaultOperatorSet); + assertEq(dummyToken.balanceOf(redistributionRecipient), shares, "strategy balance of redistribution recipient invalid"); assertEq(dummyToken.balanceOf(address(strategy)), 0, "strategy balance should be 0"); } @@ -1261,15 +1259,15 @@ contract StrategyManagerUnitTests_clearBurnOrRedistributableShares is StrategyMa strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId); - (IStrategy[] memory escrowStrats, uint[] memory escrowShares) = + (IStrategy[] memory slashStrats, uint[] memory slashShares) = strategyManager.getBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId); - assertEq(escrowStrats.length, 0, "strats length should be 0"); - assertEq(escrowShares.length, 0, "shares length should be 0"); + assertEq(slashStrats.length, 0, "strats length should be 0"); + assertEq(slashShares.length, 0, "shares length should be 0"); assertEq(strategyManager.getBurnOrRedistributableCount(defaultOperatorSet, defaultSlashId), 0, "count should be 0"); // The dummyStrats all have the same token, assert total balance - address slashEscrow = slashEscrowFactoryMock.getSlashEscrow(defaultOperatorSet, defaultSlashId); - assertEq(dummyToken.balanceOf(slashEscrow), totalSharesToAdd, "total balance should be total shares to add"); + address defaultRedistributionRecipient = allocationManagerMock.getRedistributionRecipient(defaultOperatorSet); + assertEq(dummyToken.balanceOf(defaultRedistributionRecipient), totalSharesToAdd, "total balance should be total shares to add"); assertEq(dummyToken.balanceOf(address(strategies[0])), 0, "strategy balance should be 0"); assertEq(dummyToken.balanceOf(address(strategies[1])), 0, "strategy balance should be 0"); assertEq(dummyToken.balanceOf(address(strategies[2])), 0, "strategy balance should be 0"); @@ -1311,8 +1309,8 @@ contract StrategyManagerUnitTests_clearBurnOrRedistributableShares is StrategyMa } // The dummyStrats all have the same token, assert total balance - address slashEscrow = slashEscrowFactoryMock.getSlashEscrow(defaultOperatorSet, defaultSlashId); - assertEq(dummyToken.balanceOf(slashEscrow), totalShares, "total balance should be total shares to add"); + address redistributionRecipient = allocationManagerMock.getRedistributionRecipient(defaultOperatorSet); + assertEq(dummyToken.balanceOf(redistributionRecipient), totalShares, "total balance should be total shares to add"); assertEq(dummyToken.balanceOf(address(strategies[0])), 0, "strategy balance should be 0"); assertEq(dummyToken.balanceOf(address(strategies[1])), 0, "strategy balance should be 0"); assertEq(dummyToken.balanceOf(address(strategies[2])), 0, "strategy balance should be 0"); diff --git a/src/test/utils/EigenLayerUnitTestSetup.sol b/src/test/utils/EigenLayerUnitTestSetup.sol index d3873f0178..6563d67925 100644 --- a/src/test/utils/EigenLayerUnitTestSetup.sol +++ b/src/test/utils/EigenLayerUnitTestSetup.sol @@ -16,7 +16,6 @@ import "src/test/mocks/AllocationManagerMock.sol"; import "src/test/mocks/StrategyManagerMock.sol"; import "src/test/mocks/DelegationManagerMock.sol"; import "src/test/mocks/EigenPodManagerMock.sol"; -import "src/test/mocks/SlashEscrowFactoryMock.sol"; import "src/test/mocks/EmptyContract.sol"; import "src/test/utils/ArrayLib.sol"; @@ -44,7 +43,6 @@ abstract contract EigenLayerUnitTestSetup is Test { StrategyManagerMock strategyManagerMock; DelegationManagerMock delegationManagerMock; EigenPodManagerMock eigenPodManagerMock; - SlashEscrowFactoryMock slashEscrowFactoryMock; EmptyContract emptyContract; mapping(address => bool) public isExcludedFuzzAddress; @@ -83,7 +81,6 @@ abstract contract EigenLayerUnitTestSetup is Test { StrategyManagerMock(payable(address(new StrategyManagerMock(IDelegationManager(address(delegationManagerMock)))))); delegationManagerMock = DelegationManagerMock(payable(address(new DelegationManagerMock()))); eigenPodManagerMock = EigenPodManagerMock(payable(address(new EigenPodManagerMock(pauserRegistry)))); - slashEscrowFactoryMock = SlashEscrowFactoryMock(payable(address(new SlashEscrowFactoryMock()))); emptyContract = new EmptyContract(); isExcludedFuzzAddress[address(0)] = true; @@ -95,6 +92,5 @@ abstract contract EigenLayerUnitTestSetup is Test { isExcludedFuzzAddress[address(strategyManagerMock)] = true; isExcludedFuzzAddress[address(delegationManagerMock)] = true; isExcludedFuzzAddress[address(eigenPodManagerMock)] = true; - isExcludedFuzzAddress[address(slashEscrowFactoryMock)] = true; } } From 34ad06f2060c18f19aa0a4ac9c3e9fe222d33be5 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:54:31 -0400 Subject: [PATCH 03/16] test: track pending sets and slashIds --- src/contracts/core/StrategyManager.sol | 14 ++ src/contracts/core/StrategyManagerStorage.sol | 9 +- src/test/unit/StrategyManagerUnit.t.sol | 209 ++++++++++++++++++ 3 files changed, 231 insertions(+), 1 deletion(-) diff --git a/src/contracts/core/StrategyManager.sol b/src/contracts/core/StrategyManager.sol index db77d57ae7..71a8d8ffbb 100644 --- a/src/contracts/core/StrategyManager.sol +++ b/src/contracts/core/StrategyManager.sol @@ -31,6 +31,8 @@ contract StrategyManager is using SlashingLib for *; using SafeERC20 for IERC20; using EnumerableMap for EnumerableMap.AddressToUintMap; + using EnumerableSet for EnumerableSet.Bytes32Set; + using EnumerableSet for EnumerableSet.UintSet; modifier onlyStrategyWhitelister() { require(msg.sender == strategyWhitelister, OnlyStrategyWhitelister()); @@ -158,6 +160,10 @@ contract StrategyManager is // Add the shares to the operator set's burn or redistributable shares. require(burnOrRedistributableShares.set(address(strategy), sharesToBurn), StrategyAlreadyInSlash()); + // NOTE: Duplicate operator sets and slash ids will not revert, but will not be added. + _pendingOperatorSets.add(operatorSet.key()); + _pendingSlashIds[operatorSet.key()].add(slashId); + emit BurnOrRedistributableSharesIncreased(operatorSet, slashId, strategy, sharesToBurn); } @@ -400,6 +406,14 @@ contract StrategyManager is amountShares: sharesToRemove }); + // Remove the slash id from the pending slash ids. + _pendingSlashIds[operatorSet.key()].remove(slashId); + + // If there are no more pending slash ids for this operator set, remove the operator set from the pending operator sets. + if (_pendingSlashIds[operatorSet.key()].length() == 0) { + _pendingOperatorSets.remove(operatorSet.key()); + } + // Emit an event to notify the that burnable shares have been decreased. emit BurnOrRedistributableSharesDecreased(operatorSet, slashId, strategy, sharesToRemove); } diff --git a/src/contracts/core/StrategyManagerStorage.sol b/src/contracts/core/StrategyManagerStorage.sol index 00dd5d2d31..06691e7b04 100644 --- a/src/contracts/core/StrategyManagerStorage.sol +++ b/src/contracts/core/StrategyManagerStorage.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.27; import "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "../interfaces/IAllocationManager.sol"; import "../interfaces/IAVSDirectory.sol"; @@ -82,6 +83,12 @@ abstract contract StrategyManagerStorage is IStrategyManager { mapping(bytes32 operatorSetKey => mapping(uint256 slashId => EnumerableMap.AddressToUintMap)) internal _burnOrRedistributableShares; + /// @notice Returns a list of operator sets who have pending burn or redistributable shares. + EnumerableSet.Bytes32Set internal _pendingOperatorSets; + + /// @notice Returns a list of slash ids for each operator set who have pending burn or redistributable shares. + mapping(bytes32 operatorSetKey => EnumerableSet.UintSet) internal _pendingSlashIds; + // Construction /** @@ -97,5 +104,5 @@ abstract contract StrategyManagerStorage is IStrategyManager { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[35] private __gap; + uint256[31] private __gap; } diff --git a/src/test/unit/StrategyManagerUnit.t.sol b/src/test/unit/StrategyManagerUnit.t.sol index 8588f50609..c2fd54bbb8 100644 --- a/src/test/unit/StrategyManagerUnit.t.sol +++ b/src/test/unit/StrategyManagerUnit.t.sol @@ -1507,3 +1507,212 @@ contract StrategyManagerUnitTests_removeStrategiesFromDepositWhitelist is Strate } } } + +contract StrategyManagerUnitTests_pendingOperatorSetsAndSlashIds is StrategyManagerUnitTests { + /// @notice Test that pending operator sets and slash IDs are properly tracked + function test_pendingTracking_singleOperatorSet_singleSlashId() external { + IStrategy strategy = dummyStrat; + uint shares = 100; + + // Add shares for a single operator set and slash ID + cheats.prank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, shares); + _depositIntoStrategySuccessfully(strategy, address(this), shares); + + // Clear the shares + strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId); + + // Verify that we can add the same operator set and slash ID again + // This proves they were properly removed from the pending sets + cheats.prank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, shares); + } + + /// @notice Test tracking with multiple slash IDs for the same operator set + function test_pendingTracking_singleOperatorSet_multipleSlashIds() external { + IStrategy strategy = dummyStrat; + uint shares = 100; + uint numSlashIds = 3; + + // Add shares for multiple slash IDs + for (uint i = 0; i < numSlashIds; i++) { + cheats.prank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, i, strategy, shares); + _depositIntoStrategySuccessfully(strategy, address(this), shares); + } + + // Clear shares for first slash ID only + strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, 0); + + // Verify we can re-add slash ID 0 (proves it was removed from pending) + cheats.prank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, 0, strategy, shares); + _depositIntoStrategySuccessfully(strategy, address(this), shares); + + // Clear all remaining slash IDs + for (uint i = 0; i <= numSlashIds; i++) { + if (strategyManager.getBurnOrRedistributableCount(defaultOperatorSet, i) > 0) { + strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, i); + } + } + + // Verify we can re-add all slash IDs (proves operator set was removed when last slash ID was cleared) + for (uint i = 0; i < numSlashIds; i++) { + cheats.prank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, i, strategy, shares); + } + } + + /// @notice Test tracking with multiple operator sets + function test_pendingTracking_multipleOperatorSets() external { + IStrategy strategy = dummyStrat; + uint shares = 100; + + // Create multiple operator sets + OperatorSet memory operatorSet1 = OperatorSet(address(0x1), 1); + OperatorSet memory operatorSet2 = OperatorSet(address(0x2), 2); + OperatorSet memory operatorSet3 = OperatorSet(address(0x3), 3); + + // Set up redistribution recipients for each operator set + allocationManagerMock.setRedistributionRecipient(operatorSet1, address(0x100)); + allocationManagerMock.setRedistributionRecipient(operatorSet2, address(0x200)); + allocationManagerMock.setRedistributionRecipient(operatorSet3, address(0x300)); + + // Add shares for each operator set + cheats.startPrank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(operatorSet1, 1, strategy, shares); + strategyManager.increaseBurnOrRedistributableShares(operatorSet2, 2, strategy, shares); + strategyManager.increaseBurnOrRedistributableShares(operatorSet3, 3, strategy, shares); + cheats.stopPrank(); + + // Fund the strategy + _depositIntoStrategySuccessfully(strategy, address(this), shares * 3); + + // Clear shares for operatorSet1 + strategyManager.clearBurnOrRedistributableShares(operatorSet1, 1); + + // Verify we can re-add operatorSet1 (proves it was removed from pending) + cheats.prank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(operatorSet1, 1, strategy, shares); + + // Clear remaining operator sets + strategyManager.clearBurnOrRedistributableShares(operatorSet2, 2); + strategyManager.clearBurnOrRedistributableShares(operatorSet3, 3); + } + + /// @notice Test that partially clearing shares doesn't remove the tracking + function test_pendingTracking_partialClear() external { + IStrategy strategy1 = dummyStrat; + IStrategy strategy2 = dummyStrat2; + uint shares = 100; + + // Add shares for two strategies + cheats.startPrank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy1, shares); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy2, shares); + cheats.stopPrank(); + + // Fund the strategies + _depositIntoStrategySuccessfully(strategy1, address(this), shares); + _depositIntoStrategySuccessfully(strategy2, address(this), shares); + + // Clear only strategy1 + strategyManager.clearBurnOrRedistributableSharesByStrategy(defaultOperatorSet, defaultSlashId, strategy1); + + // Verify strategy2 still has shares + assertEq( + strategyManager.getBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy2), + shares, + "strategy2 should still have shares" + ); + + // Try to add strategy1 again - this should succeed, but strategy2 would fail + cheats.prank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy1, shares); + + // Try to add strategy2 again - this should revert + cheats.prank(address(delegationManagerMock)); + cheats.expectRevert(IStrategyManagerErrors.StrategyAlreadyInSlash.selector); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy2, shares); + } + + /// @notice Test edge case with zero shares + function test_pendingTracking_zeroShares() external { + IStrategy strategy = dummyStrat; + + // Add shares with zero amount + cheats.prank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, 0); + + // Clear should handle zero shares gracefully + strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId); + + // Should be able to add again + cheats.prank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, 100); + } + + /// @notice Test complex scenario with multiple operator sets and slash IDs + function test_pendingTracking_complexScenario() external { + IStrategy[] memory strategies = new IStrategy[](3); + strategies[0] = dummyStrat; + strategies[1] = dummyStrat2; + strategies[2] = dummyStrat3; + + uint shares = 100; + + // Create multiple operator sets + OperatorSet[] memory operatorSets = new OperatorSet[](2); + operatorSets[0] = OperatorSet(address(0x1), 1); + operatorSets[1] = OperatorSet(address(0x2), 2); + + // Set up redistribution recipients + allocationManagerMock.setRedistributionRecipient(operatorSets[0], address(0x100)); + allocationManagerMock.setRedistributionRecipient(operatorSets[1], address(0x200)); + + // Add shares for multiple combinations + cheats.startPrank(address(delegationManagerMock)); + // OperatorSet 0: slash IDs 0 and 1 with different strategies + strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 0, strategies[0], shares); + strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 0, strategies[1], shares); + strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 1, strategies[2], shares); + + // OperatorSet 1: slash ID 0 with all strategies + strategyManager.increaseBurnOrRedistributableShares(operatorSets[1], 0, strategies[0], shares); + strategyManager.increaseBurnOrRedistributableShares(operatorSets[1], 0, strategies[1], shares); + strategyManager.increaseBurnOrRedistributableShares(operatorSets[1], 0, strategies[2], shares); + cheats.stopPrank(); + + // Fund all strategies with enough balance for all potential withdrawals + // Each strategy needs enough for all operator sets that will withdraw from it + _depositIntoStrategySuccessfully(strategies[0], address(this), shares * 2); // Used by both operator sets + _depositIntoStrategySuccessfully(strategies[1], address(this), shares * 2); // Used by both operator sets + _depositIntoStrategySuccessfully(strategies[2], address(this), shares * 2); // Used by both operator sets + + // Clear operatorSet[0] slash ID 0 (should not remove operatorSet[0] from pending) + strategyManager.clearBurnOrRedistributableShares(operatorSets[0], 0); + + // Verify we can re-add strategies to operatorSet[0] slash ID 0 + cheats.startPrank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 0, strategies[0], shares); + strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 0, strategies[1], shares); + cheats.stopPrank(); + + // Fund strategies again for the re-added shares + _depositIntoStrategySuccessfully(strategies[0], address(this), shares); + _depositIntoStrategySuccessfully(strategies[1], address(this), shares); + + // Clear operatorSet[0] completely + strategyManager.clearBurnOrRedistributableShares(operatorSets[0], 0); + strategyManager.clearBurnOrRedistributableShares(operatorSets[0], 1); + + // Clear operatorSet[1] + strategyManager.clearBurnOrRedistributableShares(operatorSets[1], 0); + + // Verify we can re-add everything + cheats.startPrank(address(delegationManagerMock)); + strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 0, strategies[0], shares); + strategyManager.increaseBurnOrRedistributableShares(operatorSets[1], 0, strategies[0], shares); + cheats.stopPrank(); + } +} From 64e26fa318f2ce7237659b9545d20f338d2cceec Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:41:20 -0400 Subject: [PATCH 04/16] chore: make fmt --- src/test/unit/StrategyManagerUnit.t.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/unit/StrategyManagerUnit.t.sol b/src/test/unit/StrategyManagerUnit.t.sol index c2fd54bbb8..131dac43be 100644 --- a/src/test/unit/StrategyManagerUnit.t.sol +++ b/src/test/unit/StrategyManagerUnit.t.sol @@ -1639,7 +1639,7 @@ contract StrategyManagerUnitTests_pendingOperatorSetsAndSlashIds is StrategyMana /// @notice Test edge case with zero shares function test_pendingTracking_zeroShares() external { IStrategy strategy = dummyStrat; - + // Add shares with zero amount cheats.prank(address(delegationManagerMock)); strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, 0); @@ -1658,7 +1658,7 @@ contract StrategyManagerUnitTests_pendingOperatorSetsAndSlashIds is StrategyMana strategies[0] = dummyStrat; strategies[1] = dummyStrat2; strategies[2] = dummyStrat3; - + uint shares = 100; // Create multiple operator sets @@ -1676,7 +1676,7 @@ contract StrategyManagerUnitTests_pendingOperatorSetsAndSlashIds is StrategyMana strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 0, strategies[0], shares); strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 0, strategies[1], shares); strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 1, strategies[2], shares); - + // OperatorSet 1: slash ID 0 with all strategies strategyManager.increaseBurnOrRedistributableShares(operatorSets[1], 0, strategies[0], shares); strategyManager.increaseBurnOrRedistributableShares(operatorSets[1], 0, strategies[1], shares); @@ -1686,7 +1686,7 @@ contract StrategyManagerUnitTests_pendingOperatorSetsAndSlashIds is StrategyMana // Fund all strategies with enough balance for all potential withdrawals // Each strategy needs enough for all operator sets that will withdraw from it _depositIntoStrategySuccessfully(strategies[0], address(this), shares * 2); // Used by both operator sets - _depositIntoStrategySuccessfully(strategies[1], address(this), shares * 2); // Used by both operator sets + _depositIntoStrategySuccessfully(strategies[1], address(this), shares * 2); // Used by both operator sets _depositIntoStrategySuccessfully(strategies[2], address(this), shares * 2); // Used by both operator sets // Clear operatorSet[0] slash ID 0 (should not remove operatorSet[0] from pending) From 41c7a5f0d7bdc4a0a648584996ef1a49b3d6c68c Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:00:56 -0400 Subject: [PATCH 05/16] feat: assert new dsf non zero --- src/contracts/libraries/SlashingLib.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/contracts/libraries/SlashingLib.sol b/src/contracts/libraries/SlashingLib.sol index f608480f08..255e8eceb0 100644 --- a/src/contracts/libraries/SlashingLib.sol +++ b/src/contracts/libraries/SlashingLib.sol @@ -36,6 +36,9 @@ library SlashingLib { using SlashingLib for uint256; using SafeCastUpgradeable for uint256; + /// @dev Thrown if an updated deposit scaling factor is 0 to avoid underflow. + error InvalidDepositScalingFactor(); + // WAD MATH function mulWad(uint256 x, uint256 y) internal pure returns (uint256) { @@ -135,6 +138,9 @@ library SlashingLib { .divWad(slashingFactor); dsf._scalingFactor = newDepositScalingFactor; + + // Avoid potential underflow. + require(newDepositScalingFactor != 0, InvalidDepositScalingFactor()); } /// @dev Reset the staker's DSF for a strategy by setting it to 0. This is the same From d8e5bb1a1a8d7744b50778d155e56d9891a7dd67 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:01:59 -0400 Subject: [PATCH 06/16] chore: make fmt --- src/contracts/libraries/SlashingLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contracts/libraries/SlashingLib.sol b/src/contracts/libraries/SlashingLib.sol index 255e8eceb0..24b512ed87 100644 --- a/src/contracts/libraries/SlashingLib.sol +++ b/src/contracts/libraries/SlashingLib.sol @@ -138,7 +138,7 @@ library SlashingLib { .divWad(slashingFactor); dsf._scalingFactor = newDepositScalingFactor; - + // Avoid potential underflow. require(newDepositScalingFactor != 0, InvalidDepositScalingFactor()); } From 1cdc29c07ff0ac7007522b9e803f585b56a6b24d Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:15:01 -0400 Subject: [PATCH 07/16] test: invalid dsf reverts --- src/test/unit/libraries/SlashingLibUnit.t.sol | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/test/unit/libraries/SlashingLibUnit.t.sol diff --git a/src/test/unit/libraries/SlashingLibUnit.t.sol b/src/test/unit/libraries/SlashingLibUnit.t.sol new file mode 100644 index 0000000000..142ef32fa5 --- /dev/null +++ b/src/test/unit/libraries/SlashingLibUnit.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; +import "src/contracts/libraries/SlashingLib.sol"; + +contract SlashingLibHarness { + DepositScalingFactor dsf; + + function unsafeUpdate(uint scalingFactor) public { + dsf._scalingFactor = scalingFactor; + } + + function update(uint prevDepositShares, uint addedShares, uint slashingFactor) public { + dsf.update(prevDepositShares, addedShares, slashingFactor); + } +} + +contract SlashingLibUnitTests is Test { + SlashingLibHarness harness; + + function setUp() public { + harness = new SlashingLibHarness(); + } + + function test_Revert_InvalidDepositScalingFactor() public { + harness.unsafeUpdate(1); + + vm.expectRevert(SlashingLib.InvalidDepositScalingFactor.selector); + harness.update({prevDepositShares: 1e18, addedShares: 1, slashingFactor: WAD - 1}); + } +} From f1c4ac7ae1e269306a3b72a05097cbca3d98f950 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:37:03 -0400 Subject: [PATCH 08/16] feat: add getters --- src/contracts/core/StrategyManager.sol | 18 ++++++++++++++++++ src/contracts/interfaces/IStrategyManager.sol | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/contracts/core/StrategyManager.sol b/src/contracts/core/StrategyManager.sol index 71a8d8ffbb..361c8876c6 100644 --- a/src/contracts/core/StrategyManager.sol +++ b/src/contracts/core/StrategyManager.sol @@ -28,6 +28,7 @@ contract StrategyManager is StrategyManagerStorage, SignatureUtilsMixin { + using OperatorSetLib for *; using SlashingLib for *; using SafeERC20 for IERC20; using EnumerableMap for EnumerableMap.AddressToUintMap; @@ -547,4 +548,21 @@ contract StrategyManager is return (strategies, shares); } + + /// @inheritdoc IStrategyManager + function getPendingOperatorSets() external view returns (OperatorSet[] memory) { + uint256 totalEntries = _pendingOperatorSets.length(); + OperatorSet[] memory operatorSets = new OperatorSet[](totalEntries); + for (uint256 i = 0; i < totalEntries; i++) { + operatorSets[i] = _pendingOperatorSets.at(i).decode(); + } + return operatorSets; + } + + /// @inheritdoc IStrategyManager + function getPendingSlashIds( + OperatorSet calldata operatorSet + ) external view returns (uint256[] memory) { + return _pendingSlashIds[operatorSet.key()].values(); + } } diff --git a/src/contracts/interfaces/IStrategyManager.sol b/src/contracts/interfaces/IStrategyManager.sol index 619ba192c6..15f0c098f0 100644 --- a/src/contracts/interfaces/IStrategyManager.sol +++ b/src/contracts/interfaces/IStrategyManager.sol @@ -285,4 +285,20 @@ interface IStrategyManager is IStrategyManagerErrors, IStrategyManagerEvents, IS uint256 nonce, uint256 expiry ) external view returns (bytes32); + + /** + * @notice Returns the operator sets that have pending burn or redistributable shares. + * @return The operator sets that have pending burn or redistributable shares. + */ + function getPendingOperatorSets() external view returns (OperatorSet[] memory); + + /** + * @notice Returns the slash IDs that are pending to be burned or redistributed. + * @dev This function will return revert if the operator set has no pending burn or redistributable shares. + * @param operatorSet The operator set to get the pending slash IDs for. + * @return The slash IDs that are pending to be burned or redistributed. + */ + function getPendingSlashIds( + OperatorSet calldata operatorSet + ) external view returns (uint256[] memory); } From 963a232bdbaa900930700af1a4fc5fbf696b2996 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:42:05 -0400 Subject: [PATCH 09/16] fix: clearing pending --- src/contracts/core/StrategyManager.sol | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/contracts/core/StrategyManager.sol b/src/contracts/core/StrategyManager.sol index 361c8876c6..f7536d23ec 100644 --- a/src/contracts/core/StrategyManager.sol +++ b/src/contracts/core/StrategyManager.sol @@ -407,6 +407,14 @@ contract StrategyManager is amountShares: sharesToRemove }); + // Emit an event to notify the that burnable shares have been decreased. + emit BurnOrRedistributableSharesDecreased(operatorSet, slashId, strategy, sharesToRemove); + } + + uint256 remainingStrategies = _burnOrRedistributableShares[operatorSet.key()][slashId].keys().length; + + // If there are no more strategies to burn or redistribute... + if (remainingStrategies == 0) { // Remove the slash id from the pending slash ids. _pendingSlashIds[operatorSet.key()].remove(slashId); @@ -414,9 +422,6 @@ contract StrategyManager is if (_pendingSlashIds[operatorSet.key()].length() == 0) { _pendingOperatorSets.remove(operatorSet.key()); } - - // Emit an event to notify the that burnable shares have been decreased. - emit BurnOrRedistributableSharesDecreased(operatorSet, slashId, strategy, sharesToRemove); } return amountOut; From 46273010133759155012bbe98d81941f98ad3277 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:29:08 -0400 Subject: [PATCH 10/16] test: getter coverage --- src/contracts/core/StrategyManager.sol | 2 +- src/test/unit/StrategyManagerUnit.t.sol | 378 +++++++++++++----------- 2 files changed, 214 insertions(+), 166 deletions(-) diff --git a/src/contracts/core/StrategyManager.sol b/src/contracts/core/StrategyManager.sol index f7536d23ec..02f6717ea7 100644 --- a/src/contracts/core/StrategyManager.sol +++ b/src/contracts/core/StrategyManager.sol @@ -411,7 +411,7 @@ contract StrategyManager is emit BurnOrRedistributableSharesDecreased(operatorSet, slashId, strategy, sharesToRemove); } - uint256 remainingStrategies = _burnOrRedistributableShares[operatorSet.key()][slashId].keys().length; + uint256 remainingStrategies = burnOrRedistributableShares.keys().length; // If there are no more strategies to burn or redistribute... if (remainingStrategies == 0) { diff --git a/src/test/unit/StrategyManagerUnit.t.sol b/src/test/unit/StrategyManagerUnit.t.sol index 131dac43be..d86ea93a85 100644 --- a/src/test/unit/StrategyManagerUnit.t.sol +++ b/src/test/unit/StrategyManagerUnit.t.sol @@ -1114,6 +1114,16 @@ contract StrategyManagerUnitTests_increaseBurnOrRedistributableShares is Strateg "get burn or redistributable shares count is wrong" ); + // Check pending operator sets and slash IDs + OperatorSet[] memory pendingOperatorSets = strategyManager.getPendingOperatorSets(); + assertEq(pendingOperatorSets.length, 1, "should have 1 pending operator set"); + assertEq(pendingOperatorSets[0].avs, defaultOperatorSet.avs, "pending operator set AVS mismatch"); + assertEq(pendingOperatorSets[0].id, defaultOperatorSet.id, "pending operator set ID mismatch"); + + uint[] memory pendingSlashIds = strategyManager.getPendingSlashIds(defaultOperatorSet); + assertEq(pendingSlashIds.length, 1, "should have 1 pending slash ID"); + assertEq(pendingSlashIds[0], defaultSlashId, "pending slash ID mismatch"); + // Sanity check that the strategy is not in the OLD burnable shares mapping assertEq(strategyManager.getBurnableShares(strategy), 0, "get burnable shares is wrong"); } @@ -1154,6 +1164,16 @@ contract StrategyManagerUnitTests_increaseBurnOrRedistributableShares is Strateg strategies.length, "get burn or redistributable shares count is wrong" ); + + // Check pending operator sets and slash IDs - still only 1 of each since same operator set and slash ID + OperatorSet[] memory pendingOperatorSets = strategyManager.getPendingOperatorSets(); + assertEq(pendingOperatorSets.length, 1, "should have 1 pending operator set"); + assertEq(pendingOperatorSets[0].avs, defaultOperatorSet.avs, "pending operator set AVS mismatch"); + assertEq(pendingOperatorSets[0].id, defaultOperatorSet.id, "pending operator set ID mismatch"); + + uint[] memory pendingSlashIds = strategyManager.getPendingSlashIds(defaultOperatorSet); + assertEq(pendingSlashIds.length, 1, "should have 1 pending slash ID"); + assertEq(pendingSlashIds[0], defaultSlashId, "pending slash ID mismatch"); } function testFuzz_existingShares(uint existingBurnableShares, uint addedSharesToBurn) external { @@ -1184,6 +1204,83 @@ contract StrategyManagerUnitTests_increaseBurnOrRedistributableShares is Strateg addedSharesToBurn, "added shares to burn wrong" ); + + // Check pending operator sets and slash IDs - should have 1 operator set but 2 slash IDs + OperatorSet[] memory pendingOperatorSets = strategyManager.getPendingOperatorSets(); + assertEq(pendingOperatorSets.length, 1, "should have 1 pending operator set"); + assertEq(pendingOperatorSets[0].avs, defaultOperatorSet.avs, "pending operator set AVS mismatch"); + assertEq(pendingOperatorSets[0].id, defaultOperatorSet.id, "pending operator set ID mismatch"); + + uint[] memory pendingSlashIds = strategyManager.getPendingSlashIds(defaultOperatorSet); + assertEq(pendingSlashIds.length, 2, "should have 2 pending slash IDs"); + // Note: We can't guarantee order, so just check both are present + bool hasDefaultSlashId = false; + bool hasNextSlashId = false; + for (uint i = 0; i < pendingSlashIds.length; i++) { + if (pendingSlashIds[i] == defaultSlashId) hasDefaultSlashId = true; + if (pendingSlashIds[i] == nextSlashId) hasNextSlashId = true; + } + assertTrue(hasDefaultSlashId, "should have default slash ID"); + assertTrue(hasNextSlashId, "should have next slash ID"); + } + + function test_multipleOperatorSetsAndSlashIds_AllCleared() external { + IStrategy strategy = dummyStrat; + uint shares = 100; + + // Create a second operator set + OperatorSet memory operatorSet2 = OperatorSet(address(0x2), 1); + + // Add shares for multiple operator sets and slash IDs + cheats.startPrank(address(delegationManagerMock)); + + // Add to defaultOperatorSet with defaultSlashId + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, shares); + + // Add to defaultOperatorSet with a different slashId + uint slashId2 = defaultSlashId + 1; + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, slashId2, strategy, shares); + + // Add to operatorSet2 with defaultSlashId + strategyManager.increaseBurnOrRedistributableShares(operatorSet2, defaultSlashId, strategy, shares); + + cheats.stopPrank(); + + // Check pending sets - should have 2 operator sets + OperatorSet[] memory pendingOperatorSets = strategyManager.getPendingOperatorSets(); + assertEq(pendingOperatorSets.length, 2, "should have 2 pending operator sets"); + + // Check pending slash IDs for each operator set + assertEq(strategyManager.getPendingSlashIds(defaultOperatorSet).length, 2, "defaultOperatorSet should have 2 pending slash IDs"); + assertEq(strategyManager.getPendingSlashIds(operatorSet2).length, 1, "operatorSet2 should have 1 pending slash ID"); + + // Deposit shares to strategies to enable clearing + _depositIntoStrategySuccessfully(strategy, address(this), shares * 3); + + // Clear one slash ID from defaultOperatorSet + strategyManager.clearBurnOrRedistributableSharesByStrategy(defaultOperatorSet, defaultSlashId, strategy); + + // Check pending sets - should still have 2 operator sets since defaultOperatorSet still has slashId2 + pendingOperatorSets = strategyManager.getPendingOperatorSets(); + assertEq(pendingOperatorSets.length, 2, "should still have 2 pending operator sets"); + assertEq(strategyManager.getPendingSlashIds(defaultOperatorSet).length, 1, "defaultOperatorSet should have 1 pending slash ID"); + + // Clear the second slash ID from defaultOperatorSet + strategyManager.clearBurnOrRedistributableSharesByStrategy(defaultOperatorSet, slashId2, strategy); + + // Now defaultOperatorSet should be removed from pending, but operatorSet2 should remain + pendingOperatorSets = strategyManager.getPendingOperatorSets(); + assertEq(pendingOperatorSets.length, 1, "should have 1 pending operator set"); + assertEq(pendingOperatorSets[0].avs, operatorSet2.avs, "remaining operator set should be operatorSet2"); + assertEq(strategyManager.getPendingSlashIds(defaultOperatorSet).length, 0, "defaultOperatorSet should have no pending slash IDs"); + + // Clear operatorSet2 + strategyManager.clearBurnOrRedistributableSharesByStrategy(operatorSet2, defaultSlashId, strategy); + + // All pending sets should be cleared now + pendingOperatorSets = strategyManager.getPendingOperatorSets(); + assertEq(pendingOperatorSets.length, 0, "should have no pending operator sets"); + assertEq(strategyManager.getPendingSlashIds(operatorSet2).length, 0, "operatorSet2 should have no pending slash IDs"); } } @@ -1204,6 +1301,10 @@ contract StrategyManagerUnitTests_clearBurnOrRedistributableShares is StrategyMa IStrategy strategy = dummyStrat; _increaseBurnOrRedistributableShares(strategy, shares); + // Check that pending operator set and slash ID are added + assertEq(strategyManager.getPendingOperatorSets().length, 1, "should have 1 pending operator set after adding shares"); + assertEq(strategyManager.getPendingSlashIds(defaultOperatorSet).length, 1, "should have 1 pending slash ID after adding shares"); + cheats.expectEmit(true, true, true, true, address(strategyManager)); emit BurnOrRedistributableSharesDecreased(defaultOperatorSet, defaultSlashId, strategy, shares); strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId); @@ -1214,6 +1315,10 @@ contract StrategyManagerUnitTests_clearBurnOrRedistributableShares is StrategyMa assertEq(slashShares.length, 0, "shares length should be 0"); assertEq(strategyManager.getBurnOrRedistributableCount(defaultOperatorSet, defaultSlashId), 0, "count should be 0"); + // Check that pending operator set and slash ID are removed since all strategies are cleared + assertEq(strategyManager.getPendingOperatorSets().length, 0, "pending operator sets should be empty after clearing"); + assertEq(strategyManager.getPendingSlashIds(defaultOperatorSet).length, 0, "pending slash ids should be empty after clearing"); + address redistributionRecipient = allocationManagerMock.getRedistributionRecipient(defaultOperatorSet); assertEq(dummyToken.balanceOf(redistributionRecipient), shares, "strategy balance of redistribution recipient invalid"); assertEq(dummyToken.balanceOf(address(strategy)), 0, "strategy balance should be 0"); @@ -1226,6 +1331,10 @@ contract StrategyManagerUnitTests_clearBurnOrRedistributableShares is StrategyMa console.log("strategy balance", dummyToken.balanceOf(address(strategy))); + // Check that pending operator set and slash ID are added + assertEq(strategyManager.getPendingOperatorSets().length, 1, "should have 1 pending operator set after adding shares"); + assertEq(strategyManager.getPendingSlashIds(defaultOperatorSet).length, 1, "should have 1 pending slash ID after adding shares"); + cheats.expectEmit(true, true, true, true, address(strategyManager)); emit BurnOrRedistributableSharesDecreased(defaultOperatorSet, defaultSlashId, strategy, shares); strategyManager.clearBurnOrRedistributableSharesByStrategy(defaultOperatorSet, defaultSlashId, strategy); @@ -1236,6 +1345,10 @@ contract StrategyManagerUnitTests_clearBurnOrRedistributableShares is StrategyMa assertEq(slashShares.length, 0, "shares length should be 0"); assertEq(strategyManager.getBurnOrRedistributableCount(defaultOperatorSet, defaultSlashId), 0, "count should be 0"); + // Check that pending operator set and slash ID are removed since all strategies are cleared + assertEq(strategyManager.getPendingOperatorSets().length, 0, "pending operator sets should be empty after clearing"); + assertEq(strategyManager.getPendingSlashIds(defaultOperatorSet).length, 0, "pending slash ids should be empty after clearing"); + address redistributionRecipient = allocationManagerMock.getRedistributionRecipient(defaultOperatorSet); assertEq(dummyToken.balanceOf(redistributionRecipient), shares, "strategy balance of redistribution recipient invalid"); assertEq(dummyToken.balanceOf(address(strategy)), 0, "strategy balance should be 0"); @@ -1288,6 +1401,10 @@ contract StrategyManagerUnitTests_clearBurnOrRedistributableShares is StrategyMa _increaseBurnOrRedistributableShares(strategies, sharesToAdd); + // Check that pending operator set and slash ID are added + assertEq(strategyManager.getPendingOperatorSets().length, 1, "should have 1 pending operator set after adding shares"); + assertEq(strategyManager.getPendingSlashIds(defaultOperatorSet).length, 1, "should have 1 pending slash ID after adding shares"); + // Remove shares in random order uint[] memory indices = new uint[](3); indices[0] = 1; // dummyStrat2 @@ -1306,6 +1423,18 @@ contract StrategyManagerUnitTests_clearBurnOrRedistributableShares is StrategyMa strategies.length - i - 1, "count not correct" ); + + // Check pending sets - should remain until last strategy is cleared + if (i < strategies.length - 1) { + assertEq(strategyManager.getPendingOperatorSets().length, 1, "should still have 1 pending operator set"); + assertEq(strategyManager.getPendingSlashIds(defaultOperatorSet).length, 1, "should still have 1 pending slash ID"); + } else { + // After last strategy is cleared, pending sets should be empty + assertEq(strategyManager.getPendingOperatorSets().length, 0, "pending operator sets should be empty after clearing all"); + assertEq( + strategyManager.getPendingSlashIds(defaultOperatorSet).length, 0, "pending slash ids should be empty after clearing all" + ); + } } // The dummyStrats all have the same token, assert total balance @@ -1508,211 +1637,130 @@ contract StrategyManagerUnitTests_removeStrategiesFromDepositWhitelist is Strate } } -contract StrategyManagerUnitTests_pendingOperatorSetsAndSlashIds is StrategyManagerUnitTests { - /// @notice Test that pending operator sets and slash IDs are properly tracked - function test_pendingTracking_singleOperatorSet_singleSlashId() external { - IStrategy strategy = dummyStrat; - uint shares = 100; - - // Add shares for a single operator set and slash ID - cheats.prank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, shares); - _depositIntoStrategySuccessfully(strategy, address(this), shares); - - // Clear the shares - strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId); - - // Verify that we can add the same operator set and slash ID again - // This proves they were properly removed from the pending sets - cheats.prank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, shares); +contract StrategyManagerUnitTests_getPendingOperatorSets is StrategyManagerUnitTests { + function test_getPendingOperatorSets_InitiallyZero() external { + OperatorSet[] memory pendingOperatorSets = strategyManager.getPendingOperatorSets(); + assertEq(pendingOperatorSets.length, 0, "should have 0 pending operator sets"); } - /// @notice Test tracking with multiple slash IDs for the same operator set - function test_pendingTracking_singleOperatorSet_multipleSlashIds() external { + function test_getPendingOperatorSets_SingleOperatorSet() external { IStrategy strategy = dummyStrat; uint shares = 100; - uint numSlashIds = 3; - // Add shares for multiple slash IDs - for (uint i = 0; i < numSlashIds; i++) { - cheats.prank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, i, strategy, shares); - _depositIntoStrategySuccessfully(strategy, address(this), shares); - } - - // Clear shares for first slash ID only - strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, 0); + // Staker must deposit first + address staker = address(this); + uint depositAmount = 1e18; + _depositIntoStrategySuccessfully(strategy, staker, depositAmount); - // Verify we can re-add slash ID 0 (proves it was removed from pending) cheats.prank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, 0, strategy, shares); - _depositIntoStrategySuccessfully(strategy, address(this), shares); - - // Clear all remaining slash IDs - for (uint i = 0; i <= numSlashIds; i++) { - if (strategyManager.getBurnOrRedistributableCount(defaultOperatorSet, i) > 0) { - strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, i); - } - } - - // Verify we can re-add all slash IDs (proves operator set was removed when last slash ID was cleared) - for (uint i = 0; i < numSlashIds; i++) { - cheats.prank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, i, strategy, shares); - } + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, shares); + OperatorSet[] memory pendingOperatorSets = strategyManager.getPendingOperatorSets(); + assertEq(pendingOperatorSets.length, 1, "should have 1 pending operator set"); + assertEq(pendingOperatorSets[0].avs, defaultOperatorSet.avs, "pending operator set AVS mismatch"); + assertEq(pendingOperatorSets[0].id, defaultOperatorSet.id, "pending operator set ID mismatch"); + // Clear shares + strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId); + pendingOperatorSets = strategyManager.getPendingOperatorSets(); + assertEq(pendingOperatorSets.length, 0, "should have 0 pending operator sets"); } - /// @notice Test tracking with multiple operator sets - function test_pendingTracking_multipleOperatorSets() external { + function test_getPendingOperatorSets_MultipleOperatorSets() external { IStrategy strategy = dummyStrat; uint shares = 100; + // Staker must deposit first + address staker = address(this); + uint depositAmount = 1e18; + _depositIntoStrategySuccessfully(strategy, staker, depositAmount); + // Create multiple operator sets OperatorSet memory operatorSet1 = OperatorSet(address(0x1), 1); OperatorSet memory operatorSet2 = OperatorSet(address(0x2), 2); OperatorSet memory operatorSet3 = OperatorSet(address(0x3), 3); - // Set up redistribution recipients for each operator set - allocationManagerMock.setRedistributionRecipient(operatorSet1, address(0x100)); - allocationManagerMock.setRedistributionRecipient(operatorSet2, address(0x200)); - allocationManagerMock.setRedistributionRecipient(operatorSet3, address(0x300)); - // Add shares for each operator set cheats.startPrank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(operatorSet1, 1, strategy, shares); - strategyManager.increaseBurnOrRedistributableShares(operatorSet2, 2, strategy, shares); - strategyManager.increaseBurnOrRedistributableShares(operatorSet3, 3, strategy, shares); + strategyManager.increaseBurnOrRedistributableShares(operatorSet1, defaultSlashId, strategy, shares); + strategyManager.increaseBurnOrRedistributableShares(operatorSet2, defaultSlashId, strategy, shares); + strategyManager.increaseBurnOrRedistributableShares(operatorSet3, defaultSlashId, strategy, shares); cheats.stopPrank(); - // Fund the strategy - _depositIntoStrategySuccessfully(strategy, address(this), shares * 3); - - // Clear shares for operatorSet1 - strategyManager.clearBurnOrRedistributableShares(operatorSet1, 1); - - // Verify we can re-add operatorSet1 (proves it was removed from pending) - cheats.prank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(operatorSet1, 1, strategy, shares); - - // Clear remaining operator sets - strategyManager.clearBurnOrRedistributableShares(operatorSet2, 2); - strategyManager.clearBurnOrRedistributableShares(operatorSet3, 3); - } - - /// @notice Test that partially clearing shares doesn't remove the tracking - function test_pendingTracking_partialClear() external { - IStrategy strategy1 = dummyStrat; - IStrategy strategy2 = dummyStrat2; - uint shares = 100; - - // Add shares for two strategies - cheats.startPrank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy1, shares); - strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy2, shares); - cheats.stopPrank(); + OperatorSet[] memory pendingOperatorSets = strategyManager.getPendingOperatorSets(); + assertEq(pendingOperatorSets.length, 3, "should have 3 pending operator sets"); - // Fund the strategies - _depositIntoStrategySuccessfully(strategy1, address(this), shares); - _depositIntoStrategySuccessfully(strategy2, address(this), shares); + // Verify all operator sets are present + assertEq(pendingOperatorSets[0].avs, operatorSet1.avs, "pending operator set 1 AVS mismatch"); + assertEq(pendingOperatorSets[0].id, operatorSet1.id, "pending operator set 1 ID mismatch"); + assertEq(pendingOperatorSets[1].avs, operatorSet2.avs, "pending operator set 2 AVS mismatch"); + assertEq(pendingOperatorSets[1].id, operatorSet2.id, "pending operator set 2 ID mismatch"); + assertEq(pendingOperatorSets[2].avs, operatorSet3.avs, "pending operator set 3 AVS mismatch"); + assertEq(pendingOperatorSets[2].id, operatorSet3.id, "pending operator set 3 ID mismatch"); - // Clear only strategy1 - strategyManager.clearBurnOrRedistributableSharesByStrategy(defaultOperatorSet, defaultSlashId, strategy1); + strategyManager.clearBurnOrRedistributableShares(operatorSet1, defaultSlashId); + assertEq(strategyManager.getPendingOperatorSets().length, 2, "should have 2 pending operator sets"); - // Verify strategy2 still has shares - assertEq( - strategyManager.getBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy2), - shares, - "strategy2 should still have shares" - ); + strategyManager.clearBurnOrRedistributableShares(operatorSet2, defaultSlashId); + assertEq(strategyManager.getPendingOperatorSets().length, 1, "should have 1 pending operator sets"); - // Try to add strategy1 again - this should succeed, but strategy2 would fail - cheats.prank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy1, shares); + strategyManager.clearBurnOrRedistributableShares(operatorSet3, defaultSlashId); + assertEq(strategyManager.getPendingOperatorSets().length, 0, "should have 0 pending operator sets"); + } +} - // Try to add strategy2 again - this should revert - cheats.prank(address(delegationManagerMock)); - cheats.expectRevert(IStrategyManagerErrors.StrategyAlreadyInSlash.selector); - strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy2, shares); +contract StrategyManagerUnitTests_getPendingSlashIds is StrategyManagerUnitTests { + function test_getPendingSlashIds_InitiallyZero() external { + uint[] memory pendingSlashIds = strategyManager.getPendingSlashIds(defaultOperatorSet); + assertEq(pendingSlashIds.length, 0, "should have 0 pending slash IDs"); } - /// @notice Test edge case with zero shares - function test_pendingTracking_zeroShares() external { + function test_getPendingSlashIds_SingleOperatorSet_SingleSlashId() external { IStrategy strategy = dummyStrat; + uint shares = 100; - // Add shares with zero amount - cheats.prank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, 0); - - // Clear should handle zero shares gracefully - strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId); + address staker = address(this); + uint depositAmount = 1e18; + _depositIntoStrategySuccessfully(strategy, staker, depositAmount); - // Should be able to add again cheats.prank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, 100); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, shares); + uint[] memory pendingSlashIds = strategyManager.getPendingSlashIds(defaultOperatorSet); + assertEq(pendingSlashIds.length, 1, "should have 1 pending slash ID"); + assertEq(pendingSlashIds[0], defaultSlashId, "pending slash ID mismatch"); } - /// @notice Test complex scenario with multiple operator sets and slash IDs - function test_pendingTracking_complexScenario() external { - IStrategy[] memory strategies = new IStrategy[](3); - strategies[0] = dummyStrat; - strategies[1] = dummyStrat2; - strategies[2] = dummyStrat3; - + function test_getPendingSlashIds_SingleOperatorSet_MultipleSlashIds() external { + IStrategy strategy = dummyStrat; uint shares = 100; - // Create multiple operator sets - OperatorSet[] memory operatorSets = new OperatorSet[](2); - operatorSets[0] = OperatorSet(address(0x1), 1); - operatorSets[1] = OperatorSet(address(0x2), 2); - - // Set up redistribution recipients - allocationManagerMock.setRedistributionRecipient(operatorSets[0], address(0x100)); - allocationManagerMock.setRedistributionRecipient(operatorSets[1], address(0x200)); - - // Add shares for multiple combinations - cheats.startPrank(address(delegationManagerMock)); - // OperatorSet 0: slash IDs 0 and 1 with different strategies - strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 0, strategies[0], shares); - strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 0, strategies[1], shares); - strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 1, strategies[2], shares); - - // OperatorSet 1: slash ID 0 with all strategies - strategyManager.increaseBurnOrRedistributableShares(operatorSets[1], 0, strategies[0], shares); - strategyManager.increaseBurnOrRedistributableShares(operatorSets[1], 0, strategies[1], shares); - strategyManager.increaseBurnOrRedistributableShares(operatorSets[1], 0, strategies[2], shares); - cheats.stopPrank(); - - // Fund all strategies with enough balance for all potential withdrawals - // Each strategy needs enough for all operator sets that will withdraw from it - _depositIntoStrategySuccessfully(strategies[0], address(this), shares * 2); // Used by both operator sets - _depositIntoStrategySuccessfully(strategies[1], address(this), shares * 2); // Used by both operator sets - _depositIntoStrategySuccessfully(strategies[2], address(this), shares * 2); // Used by both operator sets - - // Clear operatorSet[0] slash ID 0 (should not remove operatorSet[0] from pending) - strategyManager.clearBurnOrRedistributableShares(operatorSets[0], 0); + address staker = address(this); + uint depositAmount = 1e18; + _depositIntoStrategySuccessfully(strategy, staker, depositAmount); - // Verify we can re-add strategies to operatorSet[0] slash ID 0 cheats.startPrank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 0, strategies[0], shares); - strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 0, strategies[1], shares); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId, strategy, shares); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId + 1, strategy, shares); + strategyManager.increaseBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId + 2, strategy, shares); cheats.stopPrank(); - // Fund strategies again for the re-added shares - _depositIntoStrategySuccessfully(strategies[0], address(this), shares); - _depositIntoStrategySuccessfully(strategies[1], address(this), shares); - - // Clear operatorSet[0] completely - strategyManager.clearBurnOrRedistributableShares(operatorSets[0], 0); - strategyManager.clearBurnOrRedistributableShares(operatorSets[0], 1); - - // Clear operatorSet[1] - strategyManager.clearBurnOrRedistributableShares(operatorSets[1], 0); + uint[] memory pendingSlashIds = strategyManager.getPendingSlashIds(defaultOperatorSet); + assertEq(pendingSlashIds.length, 3, "should have 3 pending slash IDs"); + assertEq(pendingSlashIds[0], defaultSlashId, "pending slash ID mismatch"); + assertEq(pendingSlashIds[1], defaultSlashId + 1, "pending slash ID mismatch"); + assertEq(pendingSlashIds[2], defaultSlashId + 2, "pending slash ID mismatch"); - // Verify we can re-add everything - cheats.startPrank(address(delegationManagerMock)); - strategyManager.increaseBurnOrRedistributableShares(operatorSets[0], 0, strategies[0], shares); - strategyManager.increaseBurnOrRedistributableShares(operatorSets[1], 0, strategies[0], shares); - cheats.stopPrank(); + strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId); + pendingSlashIds = strategyManager.getPendingSlashIds(defaultOperatorSet); + assertEq(pendingSlashIds.length, 2, "should have 2 pending slash IDs"); + assertEq(pendingSlashIds[1], defaultSlashId + 1, "pending slash ID mismatch"); + assertEq(pendingSlashIds[0], defaultSlashId + 2, "pending slash ID mismatch"); + + strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId + 1); + pendingSlashIds = strategyManager.getPendingSlashIds(defaultOperatorSet); + assertEq(pendingSlashIds.length, 1, "should have 1 pending slash IDs"); + assertEq(pendingSlashIds[0], defaultSlashId + 2, "pending slash ID mismatch"); + + strategyManager.clearBurnOrRedistributableShares(defaultOperatorSet, defaultSlashId + 2); + pendingSlashIds = strategyManager.getPendingSlashIds(defaultOperatorSet); + assertEq(pendingSlashIds.length, 0, "should have 0 pending slash IDs"); } } From 21bb37f56fc0371803d98f8730725a972b55075b Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:43:29 -0400 Subject: [PATCH 11/16] feat: cannot add `eigenStrategy` to operator sets --- .../deploy/devnet/deploy_from_scratch.s.sol | 5 +++++ .../local/deploy_from_scratch.slashing.s.sol | 6 +++++ src/contracts/core/AllocationManager.sol | 9 +++++++- .../core/AllocationManagerStorage.sol | 12 +++++++++- .../harnesses/AllocationManagerHarness.sol | 2 ++ .../integration/IntegrationDeployer.t.sol | 8 ++++++- src/test/unit/AllocationManagerUnit.t.sol | 22 +++++++++++++++++++ 7 files changed, 61 insertions(+), 3 deletions(-) diff --git a/script/deploy/devnet/deploy_from_scratch.s.sol b/script/deploy/devnet/deploy_from_scratch.s.sol index 7d6711c893..f196d06e9c 100644 --- a/script/deploy/devnet/deploy_from_scratch.s.sol +++ b/script/deploy/devnet/deploy_from_scratch.s.sol @@ -69,6 +69,8 @@ contract DeployFromScratch is Script, Test { address operationsMultisig; address pauserMultisig; + IStrategy eigenStrategy; + // the ETH2 deposit contract -- if not on mainnet, we deploy a mock as stand-in IETHPOSDeposit public ethPOSDeposit; @@ -163,6 +165,8 @@ contract DeployFromScratch is Script, Test { operationsMultisig = stdJson.readAddress(config_data, ".multisig_addresses.operationsMultisig"); pauserMultisig = stdJson.readAddress(config_data, ".multisig_addresses.pauserMultisig"); + eigenStrategy = IStrategy(stdJson.readAddress(config_data, ".addresses.token.eigenStrategy")); + require(executorMultisig != address(0), "executorMultisig address not configured correctly!"); require(operationsMultisig != address(0), "operationsMultisig address not configured correctly!"); @@ -252,6 +256,7 @@ contract DeployFromScratch is Script, Test { ); allocationManagerImplementation = new AllocationManager( delegation, + eigenStrategy, eigenLayerPauserReg, permissionController, DEALLOCATION_DELAY, diff --git a/script/deploy/local/deploy_from_scratch.slashing.s.sol b/script/deploy/local/deploy_from_scratch.slashing.s.sol index 428bea963b..627083f202 100644 --- a/script/deploy/local/deploy_from_scratch.slashing.s.sol +++ b/script/deploy/local/deploy_from_scratch.slashing.s.sol @@ -73,6 +73,8 @@ contract DeployFromScratch is Script, Test { address operationsMultisig; address pauserMultisig; + IStrategy eigenStrategy; + string SEMVER; // the ETH2 deposit contract -- if not on mainnet, we deploy a mock as stand-in @@ -170,6 +172,9 @@ contract DeployFromScratch is Script, Test { executorMultisig = stdJson.readAddress(config_data, ".multisig_addresses.executorMultisig"); operationsMultisig = stdJson.readAddress(config_data, ".multisig_addresses.operationsMultisig"); pauserMultisig = stdJson.readAddress(config_data, ".multisig_addresses.pauserMultisig"); + + eigenStrategy = IStrategy(stdJson.readAddress(config_data, ".addresses.token.eigenStrategy")); + // load token list bytes memory strategyConfigsRaw = stdJson.parseRaw(config_data, ".strategies"); strategyConfigs = abi.decode(strategyConfigsRaw, (StrategyConfig[])); @@ -259,6 +264,7 @@ contract DeployFromScratch is Script, Test { ); allocationManagerImplementation = new AllocationManager( delegation, + eigenStrategy, eigenLayerPauserReg, permissionController, DEALLOCATION_DELAY, diff --git a/src/contracts/core/AllocationManager.sol b/src/contracts/core/AllocationManager.sol index 9bb290a144..bf4cd5253c 100644 --- a/src/contracts/core/AllocationManager.sol +++ b/src/contracts/core/AllocationManager.sol @@ -38,13 +38,14 @@ contract AllocationManager is */ constructor( IDelegationManager _delegation, + IStrategy _eigenStrategy, IPauserRegistry _pauserRegistry, IPermissionController _permissionController, uint32 _DEALLOCATION_DELAY, uint32 _ALLOCATION_CONFIGURATION_DELAY, string memory _version ) - AllocationManagerStorage(_delegation, _DEALLOCATION_DELAY, _ALLOCATION_CONFIGURATION_DELAY) + AllocationManagerStorage(_delegation, _eigenStrategy, _DEALLOCATION_DELAY, _ALLOCATION_CONFIGURATION_DELAY) Pausable(_pauserRegistry) PermissionControllerMixin(_permissionController) SemVerMixin(_version) @@ -290,6 +291,7 @@ contract AllocationManager is ) external checkCanCall(avs) { OperatorSet memory operatorSet = OperatorSet(avs, operatorSetId); require(_operatorSets[avs].contains(operatorSet.id), InvalidOperatorSet()); + for (uint256 i = 0; i < strategies.length; i++) { _addStrategyToOperatorSet( operatorSet, strategies[i], isRedistributingOperatorSet(OperatorSet(avs, operatorSetId)) @@ -426,6 +428,7 @@ contract AllocationManager is // We do not currently support redistributing beaconchain ETH. if (isRedistributing) { require(strategy != BEACONCHAIN_ETH_STRAT, InvalidStrategy()); + require(strategy != eigenStrategy, InvalidStrategy()); } require(_operatorSetStrategies[operatorSet.key()].add(address(strategy)), StrategyAlreadyInOperatorSet()); @@ -460,6 +463,10 @@ contract AllocationManager is } for (uint256 j = 0; j < params.strategies.length; j++) { + if (isRedistributing) { + require(params.strategies[j] != eigenStrategy, InvalidStrategy()); + } + _addStrategyToOperatorSet(operatorSet, params.strategies[j], isRedistributing); } } diff --git a/src/contracts/core/AllocationManagerStorage.sol b/src/contracts/core/AllocationManagerStorage.sol index ee23ef0adc..c87c6f1bb5 100644 --- a/src/contracts/core/AllocationManagerStorage.sol +++ b/src/contracts/core/AllocationManagerStorage.sol @@ -33,6 +33,10 @@ abstract contract AllocationManagerStorage is IAllocationManager { /// @notice The DelegationManager contract for EigenLayer IDelegationManager public immutable delegation; + /// @notice The Eigen strategy contract + /// @dev Cannot be added to redistributing operator sets + IStrategy public immutable eigenStrategy; + /// @notice Delay before deallocations are clearable and can be added back into freeMagnitude /// In this window, deallocations still remain slashable by the operatorSet they were allocated to. uint32 public immutable DEALLOCATION_DELAY; @@ -111,8 +115,14 @@ abstract contract AllocationManagerStorage is IAllocationManager { // Construction - constructor(IDelegationManager _delegation, uint32 _DEALLOCATION_DELAY, uint32 _ALLOCATION_CONFIGURATION_DELAY) { + constructor( + IDelegationManager _delegation, + IStrategy _eigenStrategy, + uint32 _DEALLOCATION_DELAY, + uint32 _ALLOCATION_CONFIGURATION_DELAY + ) { delegation = _delegation; + eigenStrategy = _eigenStrategy; DEALLOCATION_DELAY = _DEALLOCATION_DELAY; ALLOCATION_CONFIGURATION_DELAY = _ALLOCATION_CONFIGURATION_DELAY; } diff --git a/src/test/harnesses/AllocationManagerHarness.sol b/src/test/harnesses/AllocationManagerHarness.sol index aec2fbd85b..dd8ec83525 100644 --- a/src/test/harnesses/AllocationManagerHarness.sol +++ b/src/test/harnesses/AllocationManagerHarness.sol @@ -10,6 +10,7 @@ contract AllocationManagerHarness is AllocationManager { constructor( IDelegationManager _delegation, + IStrategy _eigenStrategy, IPauserRegistry _pauserRegistry, IPermissionController _permissionController, uint32 _DEALLOCATION_DELAY, @@ -17,6 +18,7 @@ contract AllocationManagerHarness is AllocationManager { ) AllocationManager( _delegation, + _eigenStrategy, _pauserRegistry, _permissionController, _DEALLOCATION_DELAY, diff --git a/src/test/integration/IntegrationDeployer.t.sol b/src/test/integration/IntegrationDeployer.t.sol index 7f2aadc718..1a2b00a2c5 100644 --- a/src/test/integration/IntegrationDeployer.t.sol +++ b/src/test/integration/IntegrationDeployer.t.sol @@ -309,7 +309,13 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { /// Deploy an implementation contract for each contract in the system function _deployImplementations() public { allocationManagerImplementation = new AllocationManager( - delegationManager, eigenLayerPauserReg, permissionController, DEALLOCATION_DELAY, ALLOCATION_CONFIGURATION_DELAY, version + delegationManager, + eigenStrategy, + eigenLayerPauserReg, + permissionController, + DEALLOCATION_DELAY, + ALLOCATION_CONFIGURATION_DELAY, + version ); permissionControllerImplementation = new PermissionController(version); delegationManagerImplementation = new DelegationManager( diff --git a/src/test/unit/AllocationManagerUnit.t.sol b/src/test/unit/AllocationManagerUnit.t.sol index 032be21fb8..88ae92b95c 100644 --- a/src/test/unit/AllocationManagerUnit.t.sol +++ b/src/test/unit/AllocationManagerUnit.t.sol @@ -47,6 +47,7 @@ contract AllocationManagerUnitTests is EigenLayerUnitTestSetup, IAllocationManag IStrategy[] defaultStrategies; address defaultOperator = address(this); address defaultAVS = address(new MockAVSRegistrar()); + IStrategy eigenStrategy; /// ----------------------------------------------------------------------- /// Internal Storage Helpers @@ -59,6 +60,7 @@ contract AllocationManagerUnitTests is EigenLayerUnitTestSetup, IAllocationManag function setUp() public virtual override { EigenLayerUnitTestSetup.setUp(); + eigenStrategy = IStrategy(cheats.randomAddress()); _initializeAllocationManager(pauserRegistry, 0); tokenMock = new ERC20PresetFixedSupply("Mock Token", "MOCK", type(uint).max, address(this)); strategyMock = StrategyBase( @@ -97,6 +99,7 @@ contract AllocationManagerUnitTests is EigenLayerUnitTestSetup, IAllocationManag address( new AllocationManagerHarness( IDelegationManager(address(delegationManagerMock)), + eigenStrategy, _pauserRegistry, IPermissionController(address(permissionController)), DEALLOCATION_DELAY, @@ -3666,6 +3669,15 @@ contract AllocationManagerUnitTests_addStrategiesToOperatorSet is AllocationMana allocationManager.addStrategiesToOperatorSet(defaultAVS, defaultOperatorSet.id, defaultStrategies); } + function test_addStrategiesToOperatorSet_EigenStrategyInRedistributingSet() public { + OperatorSet memory operatorSet = OperatorSet(defaultAVS, 2); + _createRedistributingOperatorSet(operatorSet, defaultStrategies, cheats.randomAddress()); + + cheats.prank(defaultAVS); + cheats.expectRevert(InvalidStrategy.selector); + allocationManager.addStrategiesToOperatorSet(defaultAVS, operatorSet.id, IStrategy(address(eigenStrategy)).toArray()); + } + function test_addStrategiesToOperatorSet_BeaconChainStratInRedistributingSet() public { // Create a redistributing operator set CreateSetParams[] memory createSetParams = new CreateSetParams[](1); @@ -3813,6 +3825,16 @@ contract AllocationManagerUnitTests_createRedistributingOperatorSets is Allocati ); } + function testRevert_createRedistributingOperatorSets_EigenStrategyInRedistributingSet() public { + cheats.prank(defaultAVS); + cheats.expectRevert(InvalidStrategy.selector); + allocationManager.createRedistributingOperatorSets( + defaultAVS, + CreateSetParams(defaultOperatorSet.id + 1, IStrategy(address(eigenStrategy)).toArray()).toArray(), + address(this).toArray() + ); + } + function testRevert_createRedistributingOperatorSets_NonexistentAVSMetadata(Randomness r) public rand(r) { address avs = r.Address(); address redistributionRecipient = r.Address(); From 82e552bd138425c9f2c5951d22ba0629306903f7 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:55:14 -0400 Subject: [PATCH 12/16] test: passing --- .../local/deploy_from_scratch.slashing.anvil.config.json | 5 +++++ script/deploy/local/deploy_from_scratch.slashing.s.sol | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/script/configs/local/deploy_from_scratch.slashing.anvil.config.json b/script/configs/local/deploy_from_scratch.slashing.anvil.config.json index 4ee617819a..b557f8ab74 100644 --- a/script/configs/local/deploy_from_scratch.slashing.anvil.config.json +++ b/script/configs/local/deploy_from_scratch.slashing.anvil.config.json @@ -57,6 +57,11 @@ "OPERATOR_SET_GENESIS_REWARDS_TIMESTAMP": 1720656000, "OPERATOR_SET_MAX_RETROACTIVE_LENGTH": 2592000 }, + "addresses": { + "token": { + "eigenStrategy": "0x0000000000000000000000000000000000000000" + } + }, "ethPOSDepositAddress": "0x00000000219ab540356cBB839Cbe05303d7705Fa", "semver": "v0.0.0" } \ No newline at end of file diff --git a/script/deploy/local/deploy_from_scratch.slashing.s.sol b/script/deploy/local/deploy_from_scratch.slashing.s.sol index 627083f202..49022cdef1 100644 --- a/script/deploy/local/deploy_from_scratch.slashing.s.sol +++ b/script/deploy/local/deploy_from_scratch.slashing.s.sol @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.so import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "../../../src/contracts/interfaces/IETHPOSDeposit.sol"; +import "../../../src/contracts/strategies/EigenStrategy.sol"; import "../../../src/contracts/core/StrategyManager.sol"; import "../../../src/contracts/core/DelegationManager.sol"; @@ -173,8 +174,6 @@ contract DeployFromScratch is Script, Test { operationsMultisig = stdJson.readAddress(config_data, ".multisig_addresses.operationsMultisig"); pauserMultisig = stdJson.readAddress(config_data, ".multisig_addresses.pauserMultisig"); - eigenStrategy = IStrategy(stdJson.readAddress(config_data, ".addresses.token.eigenStrategy")); - // load token list bytes memory strategyConfigsRaw = stdJson.parseRaw(config_data, ".strategies"); strategyConfigs = abi.decode(strategyConfigsRaw, (StrategyConfig[])); @@ -224,6 +223,8 @@ contract DeployFromScratch is Script, Test { address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")) ); + eigenStrategy = IStrategy(new EigenStrategy(strategyManager, eigenLayerPauserReg, SEMVER)); + // if on mainnet, use the ETH2 deposit contract address if (chainId == 1) ethPOSDeposit = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); // if not on mainnet, deploy a mock From 09b2b638d5d3bcc91eb0e1ace95aff764e21ad34 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:13:45 -0400 Subject: [PATCH 13/16] docs: remove slash escrow docs --- README.md | 29 ----------------------------- docs/core/StrategyManager.md | 8 ++++---- 2 files changed, 4 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index ac00972917..3fc53e026b 100644 --- a/README.md +++ b/README.md @@ -119,16 +119,6 @@ You can view the deployed contract addresses below, or check out the code itself | [`AllocationManager`](https://github.com/Layr-Labs/eigenlayer-contracts/blob//v1.5.0-rc.0/src/contracts/core/AllocationManager.sol) | [`0x78469728304326CBc65f8f95FA756B0B73164462`](https://holesky.etherscan.io/address/0x78469728304326CBc65f8f95FA756B0B73164462) | [`0x5912...A04Ea`](https://holesky.etherscan.io/address/0x5912005643201649542F5AE6CCE96C12b4DA04Ea) | Proxy: [`TUP@4.9.0`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | | [`PermissionController`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v1.4.2/src/contracts/permissions/PermissionController.sol) | [`0x598cb226B591155F767dA17AfE7A2241a68C5C10`](https://holesky.etherscan.io/address/0x598cb226B591155F767dA17AfE7A2241a68C5C10) | [`0x7ab0...a2b9`](https://holesky.etherscan.io/address/0x7ab0ebd25d5ffe7527600ca5b2858c1a3faba2b9#code) | Proxy: [`TUP@4.9.0`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | -###### Slashing - -These contracts handle the burning/redistribution of slashed funds. The `SlashEscrowFactory` is upgradeable by the `SlashEscrowProxyAdmin`: - -| Name | Proxy | Implementation | Notes | -| -------- | -------- | -------- | -------- | -| [`OZ Proxy Admin (SlashEscrowProxyAdmin)`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/transparent/ProxyAdmin.sol) | - | [`0x18dc...966b`](https://holesky.etherscan.io/address/0x0AA4F4791872211374d5912B67F5673E757CE430) | | -| [`SlashEscrowFactory`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v1.5.0-rc.0/src/contracts/core/SlashEscrowFactory.sol) | [`0xcc444eccD13E29033A46D3cbd4d30a2f70c10cbe`](https://holesky..etherscan.io/address/0xA5022befe84Ad0f5aAdc12e9c59230bc076083A5) | [`0xB643...348B`](https://holesky.etherscan.io/address/0xB64333C42F3c187744ad9F5d317C243A7788348B) | Proxy: [`TUP@4.9.0`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | -| [`SlashEscrow (Clone Implementation)`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v1.5.0-rc.0/src/contracts/core/SlashEscrow.sol) | - | [`0xa84b...ab2d`](https://holesky.etherscan.io/address/0x9c4cAc1e205cB33B4596E4f612eFdFDAe278A9CC) | [`EIP-1167 Clone`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/ClonesUpgradeable.sol) | - ###### Strategies Anyone can deploy and whitelist strategies for standard ERC20s by using the `StrategyFactory` deployed to the address below (see [docs](./docs/core/StrategyManager.md#strategyfactorydeploynewstrategy)). Strategies deployed from the `StrategyFactory` are deployed using the beacon proxy pattern: @@ -211,16 +201,6 @@ You can view the deployed contract addresses below, or check out the code itself | [`AllocationManager`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v1.5.0-rc.0/src/contracts/core/AllocationManager.sol) | [`0x42583067658071247ec8CE0A516A58f682002d07`](https://sepolia.etherscan.io/address/0x42583067658071247ec8CE0A516A58f682002d07) | [`0xb368...DAd6`](https://sepolia.etherscan.io/address/0xb36883818b5a4D25C409A81946DE9067cdC8DAd6) | Proxy: [`TUP@4.9.0`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | | [`PermissionController`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v1.3.0/src/contracts/permissions/PermissionController.sol) | [`0x44632dfBdCb6D3E21EF613B0ca8A6A0c618F5a37`](https://sepolia.etherscan.io/address/0x44632dfBdCb6D3E21EF613B0ca8A6A0c618F5a37) | [`0x59B1...f525`](https://sepolia.etherscan.io/address/0x59B11b191B572888703E150E45F5015e0fFcf525) | Proxy: [`TUP@4.9.0`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | -###### Slashing - -These contracts handle the burning/redistribution of slashed funds. The `SlashEscrowFactory` is upgradeable by the `SlashEscrowProxyAdmin`: - -| Name | Proxy | Implementation | Notes | -| -------- | -------- | -------- | -------- | -| [`OZ Proxy Admin (SlashEscrowProxyAdmin)`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/transparent/ProxyAdmin.sol) | - | [`0x18dc...966b`](https://sepolia.etherscan.io/address/0x18dc7D96d26b4F43ac464349D5D4af0310Ca966b) | `SlashEscrowFactory` proxy admin | -| [`SlashEscrowFactory`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v1.5.0-rc.0/src/contracts/core/SlashEscrowFactory.sol) | [`0xA5022befe84Ad0f5aAdc12e9c59230bc076083A5`](https://sepolia.etherscan.io/address/0xA5022befe84Ad0f5aAdc12e9c59230bc076083A5) | [`0x7A0D...883E`](https://sepolia.etherscan.io/address/0x7A0D6553941BFc3864E5EEdEa7B2d9EA6Eb5883E) | Proxy: [`TUP@4.9.0`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | - | -| [`SlashEscrow`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v1.5.0-rc.0/src/contracts/core/SlashEscrow.sol) | - | [`0xa84b596F9456f473AD3241431fde8C135a63ab2d`](https://sepolia.etherscan.io/address/0xa84b596F9456f473AD3241431fde8C135a63ab2d) | [`EIP-1167 Clone`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/ClonesUpgradeable.sol) | - ###### Strategies Anyone can deploy and whitelist strategies for standard ERC20s by using the `StrategyFactory` deployed to the address below (see [docs](./docs/core/StrategyManager.md#strategyfactorydeploynewstrategy)). Strategies deployed from the `StrategyFactory` are deployed using the beacon proxy pattern: @@ -288,15 +268,6 @@ The following strategies differ significantly from the other strategies deployed | [`AllocationManager`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v1.5.0-rc.0/src/contracts/core/AllocationManager.sol) | [`0x95a7431400F362F3647a69535C5666cA0133CAA0`](https://hoodi.etherscan.io/address/0x95a7431400F362F3647a69535C5666cA0133CAA0) | [`0x5ae8...9349`](https://hoodi.etherscan.io/address/0x5ae8152fb88c26ff9ca5C014c94fca3c68029349) | Proxy: [`TUP@4.9.0`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | | [`PermissionController`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v1.4.1/src/contracts/permissions/PermissionController.sol) | [`0xdcCF401fD121d8C542E96BC1d0078884422aFAD2`](https://hoodi.etherscan.io/address/0xdcCF401fD121d8C542E96BC1d0078884422aFAD2) | [`0x2D73...eA27`](https://hoodi.etherscan.io/address/0x2D731E7993a100afd19454B98eEEC7b90366eA27) | Proxy: [`TUP@4.9.0`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | -###### Slashing - -These contracts handle the burning/redistribution of slashed funds. The `SlashEscrowFactory` is upgradeable by the `SlashEscrowProxyAdmin`: - -| Name | Proxy | Implementation | Notes | -| -------- | -------- | -------- | -------- | -| [`OZ Proxy Admin (SlashEscrowProxyAdmin)`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/transparent/ProxyAdmin.sol) | - | [`0xa789...F545`](https://hoodi.etherscan.io/address/0xa789c91ECDdae96865913130B786140Ee17aF545) | | -| [`SlashEscrowFactory`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v1.5.0-rc.0/src/contracts/core/SlashEscrowFactory.sol) | [`0x885C0CC8118E428a2C04de58A93eB15Ed4F0e064`](https://hoodi..etherscan.io/address/0x885C0CC8118E428a2C04de58A93eB15Ed4F0e064) | [`0x4258...2d07`](https://hoodi.etherscan.io/address/0x42583067658071247ec8CE0A516A58f682002d07) | Proxy: [`TUP@4.9.0`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | -| [`SlashEscrow (Clone Implementation)`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v1.5.0-rc.0/src/contracts/core/SlashEscrow.sol) | - | [`0x889B...420d`](https://hoodi.etherscan.io/address/0x889B040116f453D89e9d6d692Ad70Edd7357420d) | [`EIP-1167 Clone`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/proxy/ClonesUpgradeable.sol) | ###### Strategies diff --git a/docs/core/StrategyManager.md b/docs/core/StrategyManager.md index b875b8790d..0db2552955 100644 --- a/docs/core/StrategyManager.md +++ b/docs/core/StrategyManager.md @@ -251,7 +251,7 @@ This method directs the `strategy` to convert the input deposit shares to tokens ## Increasing/Clearing Slashed Shares Slashes shares are marked as burnable or redistributable. Anybody can call -`clearBurnOrRedistributableShares` to send tokens to the slash's `SlashEscrow` contract. Shares to clear are stored in `_burnOrRedistributableShares`, a nested [EnumerableMap](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/utils/structs/EnumerableMap.sol). The operatorSet and slashId are used to index into the enumerableMap of strategies to shares. The following methods handle clearing burn or redistributable shares: +`clearBurnOrRedistributableShares` to send tokens to the slash's `redistributionRecipient`. Shares to clear are stored in `_burnOrRedistributableShares`, a nested [EnumerableMap](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/utils/structs/EnumerableMap.sol). The operatorSet and slashId are used to index into the enumerableMap of strategies to shares. The following methods handle clearing burn or redistributable shares: * [`StrategyManager.increaseBurnOrRedistributableShares`](#increaseburnorredistributableshares) * [`StrategyManager.clearBurnOrRedistributableShares`](#clearBurnOrRedistributableShares) * [`StrategyManager.clearBurnOrRedistributableSharesByStrategy](#clearburnorredistributableshares) @@ -284,7 +284,7 @@ Anyone can then convert the shares to tokens and trigger a burn via `burnShares` *Effects*: * Sets `burnOrRedistributableShares` for the given `operatorSet`, `slashId`, and `strategy` * Emits a `BurnOrRedistributableSharesIncreased` event -* See [`SlashEscrowFactory.initiateSlashEscrow`](./SlashEscrowFactory.md#initiateslashescrow) + *Requirements*: @@ -320,12 +320,12 @@ function clearBurnOrRedistributableSharesByStrategy( ) external returns (uint256); ``` -Anyone can call this method to transfer slashed shares to the slash's `SlashEscrow` contract. This method sets the `burnOrRedistributableShares` for the given `slashId` and `operatorSet` to 0. To accommodate the unlimited number of strategies that can be added to an operatorSet, users can also pass in a strategy to clear via `clearBurnOrRedistributableSharesByStrategy`. The strategies that haven not been cleared can be retrieved by calling `getBurnOrRedistributableShares(operatorSet, slashId)`. +Anyone can call this method to transfer slashed shares to the slash's `redistributionRecipient`. This method sets the `burnOrRedistributableShares` for the given `slashId` and `operatorSet` to 0. To accommodate the unlimited number of strategies that can be added to an operatorSet, users can also pass in a strategy to clear via `clearBurnOrRedistributableSharesByStrategy`. The strategies that haven not been cleared can be retrieved by calling `getBurnOrRedistributableShares(operatorSet, slashId)`. *Effects*: * Resets the strategy's burn or redistributable shares for the operatorSet and slashId to 0 * If the shares to remove are nonzero: - * Calls `withdraw` on the `strategy`, withdrawing shares and sending a corresponding amount of tokens to the slash's `slashEscrow` contract + * Calls `withdraw` on the `strategy`, withdrawing shares and sending a corresponding amount of tokens to the slash's `redistributionRecipient` * Emits a `BurnOrRedistributableSharesDecreased` #### `burnShares` From ba75cecac404b790aed13a2daaa4b1cc683501e7 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:02:43 -0400 Subject: [PATCH 14/16] refactor: remove duplicate check --- src/contracts/core/AllocationManager.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/contracts/core/AllocationManager.sol b/src/contracts/core/AllocationManager.sol index bf4cd5253c..902572dbde 100644 --- a/src/contracts/core/AllocationManager.sol +++ b/src/contracts/core/AllocationManager.sol @@ -463,10 +463,6 @@ contract AllocationManager is } for (uint256 j = 0; j < params.strategies.length; j++) { - if (isRedistributing) { - require(params.strategies[j] != eigenStrategy, InvalidStrategy()); - } - _addStrategyToOperatorSet(operatorSet, params.strategies[j], isRedistributing); } } From 7ff0cfbb46ca3e3eece6ffdfd3e972671519e9d2 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:04:15 -0400 Subject: [PATCH 15/16] nit: condense checks --- src/contracts/core/AllocationManager.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/contracts/core/AllocationManager.sol b/src/contracts/core/AllocationManager.sol index 902572dbde..e75aad44d8 100644 --- a/src/contracts/core/AllocationManager.sol +++ b/src/contracts/core/AllocationManager.sol @@ -427,8 +427,7 @@ contract AllocationManager is ) internal { // We do not currently support redistributing beaconchain ETH. if (isRedistributing) { - require(strategy != BEACONCHAIN_ETH_STRAT, InvalidStrategy()); - require(strategy != eigenStrategy, InvalidStrategy()); + require(strategy != BEACONCHAIN_ETH_STRAT && strategy != eigenStrategy, InvalidStrategy()); } require(_operatorSetStrategies[operatorSet.key()].add(address(strategy)), StrategyAlreadyInOperatorSet()); From 30ceb83ebea9a4d1972e0e123de8af234b1625dd Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:13:07 -0400 Subject: [PATCH 16/16] docs: note use of harness --- src/test/unit/libraries/SlashingLibUnit.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/unit/libraries/SlashingLibUnit.t.sol b/src/test/unit/libraries/SlashingLibUnit.t.sol index 142ef32fa5..8ea6710cf9 100644 --- a/src/test/unit/libraries/SlashingLibUnit.t.sol +++ b/src/test/unit/libraries/SlashingLibUnit.t.sol @@ -17,6 +17,7 @@ contract SlashingLibHarness { } contract SlashingLibUnitTests is Test { + /// @dev We use a harness so that `vm.expectRevert()` can be used. SlashingLibHarness harness; function setUp() public {