diff --git a/docs/storage-report/AVSDirectory.md b/docs/storage-report/AVSDirectory.md index 016ce43b4..349366030 100644 --- a/docs/storage-report/AVSDirectory.md +++ b/docs/storage-report/AVSDirectory.md @@ -1,22 +1,16 @@ -| Name | Type | Slot | Offset | Bytes | Contract | -|-------------------------------|--------------------------------------------------------------------------------------------------------------------|------|--------|-------|--------------------------------------------------| -| _initialized | uint8 | 0 | 0 | 1 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| _initializing | bool | 0 | 1 | 1 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| __gap | uint256[50] | 1 | 0 | 1600 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| _owner | address | 51 | 0 | 20 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| __gap | uint256[49] | 52 | 0 | 1568 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| __deprecated_pauserRegistry | contract IPauserRegistry | 101 | 0 | 20 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| _paused | uint256 | 102 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| __gap | uint256[48] | 103 | 0 | 1536 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| __deprecated_DOMAIN_SEPARATOR | bytes32 | 151 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| avsOperatorStatus | mapping(address => mapping(address => enum IAVSDirectoryTypes.OperatorAVSRegistrationStatus)) | 152 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| operatorSaltIsSpent | mapping(address => mapping(bytes32 => bool)) | 153 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| isOperatorSetAVS | mapping(address => bool) | 154 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| isOperatorSet | mapping(address => mapping(uint32 => bool)) | 155 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| _operatorSetsMemberOf | mapping(address => struct EnumerableSet.Bytes32Set) | 156 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| _operatorSetMembers | mapping(bytes32 => struct EnumerableSet.AddressSet) | 157 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| _operatorSetStrategies | mapping(bytes32 => struct EnumerableSet.AddressSet) | 158 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| operatorSetStatus | mapping(address => mapping(address => mapping(uint32 => struct IAVSDirectoryTypes.OperatorSetRegistrationStatus))) | 159 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| __gap | uint256[41] | 160 | 0 | 1312 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| _status | uint256 | 201 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | -| __gap | uint256[49] | 202 | 0 | 1568 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| Name | Type | Slot | Offset | Bytes | Contract | +|-------------------------------|-----------------------------------------------------------------------------------------------|------|--------|-------|--------------------------------------------------| +| _initialized | uint8 | 0 | 0 | 1 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| _initializing | bool | 0 | 1 | 1 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| __gap | uint256[50] | 1 | 0 | 1600 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| _owner | address | 51 | 0 | 20 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| __gap | uint256[49] | 52 | 0 | 1568 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| __deprecated_pauserRegistry | contract IPauserRegistry | 101 | 0 | 20 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| _paused | uint256 | 102 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| __gap | uint256[48] | 103 | 0 | 1536 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| __deprecated_DOMAIN_SEPARATOR | bytes32 | 151 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| avsOperatorStatus | mapping(address => mapping(address => enum IAVSDirectoryTypes.OperatorAVSRegistrationStatus)) | 152 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| operatorSaltIsSpent | mapping(address => mapping(bytes32 => bool)) | 153 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| __gap | uint256[41] | 154 | 0 | 1312 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| _status | uint256 | 195 | 0 | 32 | src/contracts/core/AVSDirectory.sol:AVSDirectory | +| __gap | uint256[49] | 196 | 0 | 1568 | src/contracts/core/AVSDirectory.sol:AVSDirectory | diff --git a/docs/storage-report/AVSDirectoryStorage.md b/docs/storage-report/AVSDirectoryStorage.md index 7012326d8..3e02bf7f2 100644 --- a/docs/storage-report/AVSDirectoryStorage.md +++ b/docs/storage-report/AVSDirectoryStorage.md @@ -1,12 +1,6 @@ -| Name | Type | Slot | Offset | Bytes | Contract | -|-------------------------------|--------------------------------------------------------------------------------------------------------------------|------|--------|-------|----------------------------------------------------------------| -| __deprecated_DOMAIN_SEPARATOR | bytes32 | 0 | 0 | 32 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | -| avsOperatorStatus | mapping(address => mapping(address => enum IAVSDirectoryTypes.OperatorAVSRegistrationStatus)) | 1 | 0 | 32 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | -| operatorSaltIsSpent | mapping(address => mapping(bytes32 => bool)) | 2 | 0 | 32 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | -| isOperatorSetAVS | mapping(address => bool) | 3 | 0 | 32 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | -| isOperatorSet | mapping(address => mapping(uint32 => bool)) | 4 | 0 | 32 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | -| _operatorSetsMemberOf | mapping(address => struct EnumerableSet.Bytes32Set) | 5 | 0 | 32 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | -| _operatorSetMembers | mapping(bytes32 => struct EnumerableSet.AddressSet) | 6 | 0 | 32 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | -| _operatorSetStrategies | mapping(bytes32 => struct EnumerableSet.AddressSet) | 7 | 0 | 32 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | -| operatorSetStatus | mapping(address => mapping(address => mapping(uint32 => struct IAVSDirectoryTypes.OperatorSetRegistrationStatus))) | 8 | 0 | 32 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | -| __gap | uint256[41] | 9 | 0 | 1312 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | +| Name | Type | Slot | Offset | Bytes | Contract | +|-------------------------------|-----------------------------------------------------------------------------------------------|------|--------|-------|----------------------------------------------------------------| +| __deprecated_DOMAIN_SEPARATOR | bytes32 | 0 | 0 | 32 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | +| avsOperatorStatus | mapping(address => mapping(address => enum IAVSDirectoryTypes.OperatorAVSRegistrationStatus)) | 1 | 0 | 32 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | +| operatorSaltIsSpent | mapping(address => mapping(bytes32 => bool)) | 2 | 0 | 32 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | +| __gap | uint256[41] | 3 | 0 | 1312 | src/contracts/core/AVSDirectoryStorage.sol:AVSDirectoryStorage | diff --git a/docs/storage-report/AllocationManager.md b/docs/storage-report/AllocationManager.md index 597eb3974..5af24bfff 100644 --- a/docs/storage-report/AllocationManager.md +++ b/docs/storage-report/AllocationManager.md @@ -1,18 +1,26 @@ -| Name | Type | Slot | Offset | Bytes | Contract | -|-----------------------------|---------------------------------------------------------------------------------------------------------------------|------|--------|-------|------------------------------------------------------------| -| _initialized | uint8 | 0 | 0 | 1 | src/contracts/core/AllocationManager.sol:AllocationManager | -| _initializing | bool | 0 | 1 | 1 | src/contracts/core/AllocationManager.sol:AllocationManager | -| __gap | uint256[50] | 1 | 0 | 1600 | src/contracts/core/AllocationManager.sol:AllocationManager | -| _owner | address | 51 | 0 | 20 | src/contracts/core/AllocationManager.sol:AllocationManager | -| __gap | uint256[49] | 52 | 0 | 1568 | src/contracts/core/AllocationManager.sol:AllocationManager | -| __deprecated_pauserRegistry | contract IPauserRegistry | 101 | 0 | 20 | src/contracts/core/AllocationManager.sol:AllocationManager | -| _paused | uint256 | 102 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | -| __gap | uint256[48] | 103 | 0 | 1536 | src/contracts/core/AllocationManager.sol:AllocationManager | -| _maxMagnitudeHistory | mapping(address => mapping(contract IStrategy => struct Snapshots.DefaultWadHistory)) | 151 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | -| encumberedMagnitude | mapping(address => mapping(contract IStrategy => uint64)) | 152 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | -| _operatorMagnitudeInfo | mapping(address => mapping(contract IStrategy => mapping(bytes32 => struct IAllocationManagerTypes.MagnitudeInfo))) | 153 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | -| deallocationQueue | mapping(address => mapping(contract IStrategy => struct DoubleEndedQueue.Bytes32Deque)) | 154 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | -| _allocationDelayInfo | mapping(address => struct IAllocationManagerTypes.AllocationDelayInfo) | 155 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | -| __gap | uint256[45] | 156 | 0 | 1440 | src/contracts/core/AllocationManager.sol:AllocationManager | -| _status | uint256 | 201 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | -| __gap | uint256[49] | 202 | 0 | 1568 | src/contracts/core/AllocationManager.sol:AllocationManager | +| Name | Type | Slot | Offset | Bytes | Contract | +|-----------------------------|------------------------------------------------------------------------------------------------------------------|------|--------|-------|------------------------------------------------------------| +| _initialized | uint8 | 0 | 0 | 1 | src/contracts/core/AllocationManager.sol:AllocationManager | +| _initializing | bool | 0 | 1 | 1 | src/contracts/core/AllocationManager.sol:AllocationManager | +| __gap | uint256[50] | 1 | 0 | 1600 | src/contracts/core/AllocationManager.sol:AllocationManager | +| _owner | address | 51 | 0 | 20 | src/contracts/core/AllocationManager.sol:AllocationManager | +| __gap | uint256[49] | 52 | 0 | 1568 | src/contracts/core/AllocationManager.sol:AllocationManager | +| __deprecated_pauserRegistry | contract IPauserRegistry | 101 | 0 | 20 | src/contracts/core/AllocationManager.sol:AllocationManager | +| _paused | uint256 | 102 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| __gap | uint256[48] | 103 | 0 | 1536 | src/contracts/core/AllocationManager.sol:AllocationManager | +| _avsRegistrar | mapping(address => contract IAVSRegistrar) | 151 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| _operatorSets | mapping(address => struct EnumerableSet.UintSet) | 152 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| _operatorSetStrategies | mapping(bytes32 => struct EnumerableSet.AddressSet) | 153 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| _operatorSetMembers | mapping(bytes32 => struct EnumerableSet.AddressSet) | 154 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| _allocationDelayInfo | mapping(address => struct IAllocationManagerTypes.AllocationDelayInfo) | 155 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| registeredSets | mapping(address => struct EnumerableSet.Bytes32Set) | 156 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| allocatedSets | mapping(address => struct EnumerableSet.Bytes32Set) | 157 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| registrationStatus | mapping(address => mapping(bytes32 => struct IAllocationManagerTypes.RegistrationStatus)) | 158 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| allocatedStrategies | mapping(address => mapping(bytes32 => struct EnumerableSet.AddressSet)) | 159 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| allocations | mapping(address => mapping(bytes32 => mapping(contract IStrategy => struct IAllocationManagerTypes.Allocation))) | 160 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| _maxMagnitudeHistory | mapping(address => mapping(contract IStrategy => struct Snapshots.DefaultWadHistory)) | 161 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| encumberedMagnitude | mapping(address => mapping(contract IStrategy => uint64)) | 162 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| deallocationQueue | mapping(address => mapping(contract IStrategy => struct DoubleEndedQueue.Bytes32Deque)) | 163 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| __gap | uint256[37] | 164 | 0 | 1184 | src/contracts/core/AllocationManager.sol:AllocationManager | +| _status | uint256 | 201 | 0 | 32 | src/contracts/core/AllocationManager.sol:AllocationManager | +| __gap | uint256[49] | 202 | 0 | 1568 | src/contracts/core/AllocationManager.sol:AllocationManager | diff --git a/docs/storage-report/AllocationManagerStorage.md b/docs/storage-report/AllocationManagerStorage.md index eea12782d..6d099d44f 100644 --- a/docs/storage-report/AllocationManagerStorage.md +++ b/docs/storage-report/AllocationManagerStorage.md @@ -1,8 +1,16 @@ -| Name | Type | Slot | Offset | Bytes | Contract | -|------------------------|---------------------------------------------------------------------------------------------------------------------|------|--------|-------|--------------------------------------------------------------------------| -| _maxMagnitudeHistory | mapping(address => mapping(contract IStrategy => struct Snapshots.DefaultWadHistory)) | 0 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | -| encumberedMagnitude | mapping(address => mapping(contract IStrategy => uint64)) | 1 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | -| _operatorMagnitudeInfo | mapping(address => mapping(contract IStrategy => mapping(bytes32 => struct IAllocationManagerTypes.MagnitudeInfo))) | 2 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | -| deallocationQueue | mapping(address => mapping(contract IStrategy => struct DoubleEndedQueue.Bytes32Deque)) | 3 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | -| _allocationDelayInfo | mapping(address => struct IAllocationManagerTypes.AllocationDelayInfo) | 4 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | -| __gap | uint256[45] | 5 | 0 | 1440 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| Name | Type | Slot | Offset | Bytes | Contract | +|------------------------|------------------------------------------------------------------------------------------------------------------|------|--------|-------|--------------------------------------------------------------------------| +| _avsRegistrar | mapping(address => contract IAVSRegistrar) | 0 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| _operatorSets | mapping(address => struct EnumerableSet.UintSet) | 1 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| _operatorSetStrategies | mapping(bytes32 => struct EnumerableSet.AddressSet) | 2 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| _operatorSetMembers | mapping(bytes32 => struct EnumerableSet.AddressSet) | 3 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| _allocationDelayInfo | mapping(address => struct IAllocationManagerTypes.AllocationDelayInfo) | 4 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| registeredSets | mapping(address => struct EnumerableSet.Bytes32Set) | 5 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| allocatedSets | mapping(address => struct EnumerableSet.Bytes32Set) | 6 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| registrationStatus | mapping(address => mapping(bytes32 => struct IAllocationManagerTypes.RegistrationStatus)) | 7 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| allocatedStrategies | mapping(address => mapping(bytes32 => struct EnumerableSet.AddressSet)) | 8 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| allocations | mapping(address => mapping(bytes32 => mapping(contract IStrategy => struct IAllocationManagerTypes.Allocation))) | 9 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| _maxMagnitudeHistory | mapping(address => mapping(contract IStrategy => struct Snapshots.DefaultWadHistory)) | 10 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| encumberedMagnitude | mapping(address => mapping(contract IStrategy => uint64)) | 11 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| deallocationQueue | mapping(address => mapping(contract IStrategy => struct DoubleEndedQueue.Bytes32Deque)) | 12 | 0 | 32 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | +| __gap | uint256[37] | 13 | 0 | 1184 | src/contracts/core/AllocationManagerStorage.sol:AllocationManagerStorage | diff --git a/docs/storage-report/DelegationManager.md b/docs/storage-report/DelegationManager.md index 854ac8c84..fe037fae0 100644 --- a/docs/storage-report/DelegationManager.md +++ b/docs/storage-report/DelegationManager.md @@ -1,25 +1,29 @@ -| Name | Type | Slot | Offset | Bytes | Contract | -|--------------------------------------------|--------------------------------------------------------------------------------|------|--------|-------|------------------------------------------------------------| -| _initialized | uint8 | 0 | 0 | 1 | src/contracts/core/DelegationManager.sol:DelegationManager | -| _initializing | bool | 0 | 1 | 1 | src/contracts/core/DelegationManager.sol:DelegationManager | -| __gap | uint256[50] | 1 | 0 | 1600 | src/contracts/core/DelegationManager.sol:DelegationManager | -| _owner | address | 51 | 0 | 20 | src/contracts/core/DelegationManager.sol:DelegationManager | -| __gap | uint256[49] | 52 | 0 | 1568 | src/contracts/core/DelegationManager.sol:DelegationManager | -| __deprecated_pauserRegistry | contract IPauserRegistry | 101 | 0 | 20 | src/contracts/core/DelegationManager.sol:DelegationManager | -| _paused | uint256 | 102 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| __gap | uint256[48] | 103 | 0 | 1536 | src/contracts/core/DelegationManager.sol:DelegationManager | -| __deprecated_DOMAIN_SEPARATOR | bytes32 | 151 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| operatorShares | mapping(address => mapping(contract IStrategy => uint256)) | 152 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| _operatorDetails | mapping(address => struct IDelegationManagerTypes.OperatorDetails) | 153 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| delegatedTo | mapping(address => address) | 154 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| stakerNonce | mapping(address => uint256) | 155 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| delegationApproverSaltIsSpent | mapping(address => mapping(bytes32 => bool)) | 156 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| __deprecated_minWithdrawalDelayBlocks | uint256 | 157 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| pendingWithdrawals | mapping(bytes32 => bool) | 158 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| cumulativeWithdrawalsQueued | mapping(address => uint256) | 159 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| __deprecated_stakeRegistry | address | 160 | 0 | 20 | src/contracts/core/DelegationManager.sol:DelegationManager | -| __deprecated_strategyWithdrawalDelayBlocks | mapping(contract IStrategy => uint256) | 161 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| stakerScalingFactor | mapping(address => mapping(contract IStrategy => struct StakerScalingFactors)) | 162 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| __gap | uint256[38] | 163 | 0 | 1216 | src/contracts/core/DelegationManager.sol:DelegationManager | -| _status | uint256 | 201 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | -| __gap | uint256[49] | 202 | 0 | 1568 | src/contracts/core/DelegationManager.sol:DelegationManager | +| Name | Type | Slot | Offset | Bytes | Contract | +|--------------------------------------------|---------------------------------------------------------------------------------------|------|--------|-------|------------------------------------------------------------| +| _initialized | uint8 | 0 | 0 | 1 | src/contracts/core/DelegationManager.sol:DelegationManager | +| _initializing | bool | 0 | 1 | 1 | src/contracts/core/DelegationManager.sol:DelegationManager | +| __gap | uint256[50] | 1 | 0 | 1600 | src/contracts/core/DelegationManager.sol:DelegationManager | +| _owner | address | 51 | 0 | 20 | src/contracts/core/DelegationManager.sol:DelegationManager | +| __gap | uint256[49] | 52 | 0 | 1568 | src/contracts/core/DelegationManager.sol:DelegationManager | +| __deprecated_pauserRegistry | contract IPauserRegistry | 101 | 0 | 20 | src/contracts/core/DelegationManager.sol:DelegationManager | +| _paused | uint256 | 102 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| __gap | uint256[48] | 103 | 0 | 1536 | src/contracts/core/DelegationManager.sol:DelegationManager | +| __deprecated_DOMAIN_SEPARATOR | bytes32 | 151 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| operatorShares | mapping(address => mapping(contract IStrategy => uint256)) | 152 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| _operatorDetails | mapping(address => struct IDelegationManagerTypes.OperatorDetails) | 153 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| delegatedTo | mapping(address => address) | 154 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| __deprecated_stakerNonce | mapping(address => uint256) | 155 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| delegationApproverSaltIsSpent | mapping(address => mapping(bytes32 => bool)) | 156 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| __deprecated_minWithdrawalDelayBlocks | uint256 | 157 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| pendingWithdrawals | mapping(bytes32 => bool) | 158 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| cumulativeWithdrawalsQueued | mapping(address => uint256) | 159 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| __deprecated_stakeRegistry | address | 160 | 0 | 20 | src/contracts/core/DelegationManager.sol:DelegationManager | +| __deprecated_strategyWithdrawalDelayBlocks | mapping(contract IStrategy => uint256) | 161 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| _depositScalingFactor | mapping(address => mapping(contract IStrategy => struct DepositScalingFactor)) | 162 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| _beaconChainSlashingFactor | mapping(address => struct IDelegationManagerTypes.BeaconChainSlashingFactor) | 163 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| _stakerQueuedWithdrawalRoots | mapping(address => struct EnumerableSet.Bytes32Set) | 164 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| queuedWithdrawals | mapping(bytes32 => struct IDelegationManagerTypes.Withdrawal) | 165 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| _cumulativeScaledSharesHistory | mapping(address => mapping(contract IStrategy => struct Snapshots.WithdrawalHistory)) | 166 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| __gap | uint256[34] | 167 | 0 | 1088 | src/contracts/core/DelegationManager.sol:DelegationManager | +| _status | uint256 | 201 | 0 | 32 | src/contracts/core/DelegationManager.sol:DelegationManager | +| __gap | uint256[49] | 202 | 0 | 1568 | src/contracts/core/DelegationManager.sol:DelegationManager | diff --git a/docs/storage-report/DelegationManagerStorage.md b/docs/storage-report/DelegationManagerStorage.md index f30caf772..c91985146 100644 --- a/docs/storage-report/DelegationManagerStorage.md +++ b/docs/storage-report/DelegationManagerStorage.md @@ -1,15 +1,19 @@ -| Name | Type | Slot | Offset | Bytes | Contract | -|--------------------------------------------|--------------------------------------------------------------------------------|------|--------|-------|--------------------------------------------------------------------------| -| __deprecated_DOMAIN_SEPARATOR | bytes32 | 0 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | -| operatorShares | mapping(address => mapping(contract IStrategy => uint256)) | 1 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | -| _operatorDetails | mapping(address => struct IDelegationManagerTypes.OperatorDetails) | 2 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | -| delegatedTo | mapping(address => address) | 3 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | -| stakerNonce | mapping(address => uint256) | 4 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | -| delegationApproverSaltIsSpent | mapping(address => mapping(bytes32 => bool)) | 5 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | -| __deprecated_minWithdrawalDelayBlocks | uint256 | 6 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | -| pendingWithdrawals | mapping(bytes32 => bool) | 7 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | -| cumulativeWithdrawalsQueued | mapping(address => uint256) | 8 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | -| __deprecated_stakeRegistry | address | 9 | 0 | 20 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | -| __deprecated_strategyWithdrawalDelayBlocks | mapping(contract IStrategy => uint256) | 10 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | -| stakerScalingFactor | mapping(address => mapping(contract IStrategy => struct StakerScalingFactors)) | 11 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | -| __gap | uint256[38] | 12 | 0 | 1216 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| Name | Type | Slot | Offset | Bytes | Contract | +|--------------------------------------------|---------------------------------------------------------------------------------------|------|--------|-------|--------------------------------------------------------------------------| +| __deprecated_DOMAIN_SEPARATOR | bytes32 | 0 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| operatorShares | mapping(address => mapping(contract IStrategy => uint256)) | 1 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| _operatorDetails | mapping(address => struct IDelegationManagerTypes.OperatorDetails) | 2 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| delegatedTo | mapping(address => address) | 3 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| __deprecated_stakerNonce | mapping(address => uint256) | 4 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| delegationApproverSaltIsSpent | mapping(address => mapping(bytes32 => bool)) | 5 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| __deprecated_minWithdrawalDelayBlocks | uint256 | 6 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| pendingWithdrawals | mapping(bytes32 => bool) | 7 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| cumulativeWithdrawalsQueued | mapping(address => uint256) | 8 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| __deprecated_stakeRegistry | address | 9 | 0 | 20 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| __deprecated_strategyWithdrawalDelayBlocks | mapping(contract IStrategy => uint256) | 10 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| _depositScalingFactor | mapping(address => mapping(contract IStrategy => struct DepositScalingFactor)) | 11 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| _beaconChainSlashingFactor | mapping(address => struct IDelegationManagerTypes.BeaconChainSlashingFactor) | 12 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| _stakerQueuedWithdrawalRoots | mapping(address => struct EnumerableSet.Bytes32Set) | 13 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| queuedWithdrawals | mapping(bytes32 => struct IDelegationManagerTypes.Withdrawal) | 14 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| _cumulativeScaledSharesHistory | mapping(address => mapping(contract IStrategy => struct Snapshots.WithdrawalHistory)) | 15 | 0 | 32 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | +| __gap | uint256[34] | 16 | 0 | 1088 | src/contracts/core/DelegationManagerStorage.sol:DelegationManagerStorage | diff --git a/lib/forge-std b/lib/forge-std index 4f57c59f0..1eea5bae1 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 4f57c59f066a03d13de8c65bb34fca8247f5fcb2 +Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 diff --git a/script/tasks/complete_withdrawal_from_strategy.s.sol b/script/tasks/complete_withdrawal_from_strategy.s.sol index c1f15a59b..281b233f7 100644 --- a/script/tasks/complete_withdrawal_from_strategy.s.sol +++ b/script/tasks/complete_withdrawal_from_strategy.s.sol @@ -67,7 +67,7 @@ contract CompleteWithdrawFromStrategy is Script, Test { // Get scaled shares for the given amount uint256[] memory scaledShares = new uint256[](1); - scaledShares[0] = SlashingLib.scaleSharesForQueuedWithdrawal({ + scaledShares[0] = SlashingLib.scaleForQueueWithdrawal({ sharesToWithdraw: sharesToWithdraw, slashingFactor: slashingFactor }); diff --git a/src/contracts/core/AllocationManager.sol b/src/contracts/core/AllocationManager.sol index a6310bf1d..390ce0f52 100644 --- a/src/contracts/core/AllocationManager.sol +++ b/src/contracts/core/AllocationManager.sol @@ -88,6 +88,7 @@ contract AllocationManager is uint64 slashedMagnitude = uint64(uint256(allocation.currentMagnitude).mulWadRoundUp(params.wadToSlash)); uint256 sharesWadSlashed = uint256(slashedMagnitude).divWad(info.maxMagnitude); wadSlashed[i] = sharesWadSlashed; + uint64 prevMaxMagnitude = info.maxMagnitude; allocation.currentMagnitude -= slashedMagnitude; info.maxMagnitude -= slashedMagnitude; @@ -113,11 +114,12 @@ contract AllocationManager is _updateAllocationInfo(params.operator, operatorSet.key(), strategy, info, allocation); _updateMaxMagnitude(params.operator, strategy, info.maxMagnitude); - // 6. Decrease operators shares in the DelegationManager - delegation.decreaseOperatorShares({ + // 6. Decrease and burn operators shares in the DelegationManager + delegation.burnOperatorShares({ operator: params.operator, strategy: strategy, - wadSlashed: sharesWadSlashed + prevMaxMagnitude: prevMaxMagnitude, + newMaxMagnitude: info.maxMagnitude }); } @@ -662,7 +664,7 @@ contract AllocationManager is uint64[] memory maxMagnitudes = new uint64[](strategies.length); for (uint256 i = 0; i < strategies.length; ++i) { - maxMagnitudes[i] = _maxMagnitudeHistory[operator][strategies[i]].upperLookup(blockNumber); + maxMagnitudes[i] = _maxMagnitudeHistory[operator][strategies[i]].upperLookup({key: blockNumber}); } return maxMagnitudes; diff --git a/src/contracts/core/DelegationManager.sol b/src/contracts/core/DelegationManager.sol index b4d933724..a02961179 100644 --- a/src/contracts/core/DelegationManager.sol +++ b/src/contracts/core/DelegationManager.sol @@ -8,6 +8,7 @@ import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol import "../mixins/SignatureUtils.sol"; import "../permissions/Pausable.sol"; import "../libraries/SlashingLib.sol"; +import "../libraries/Snapshots.sol"; import "./DelegationManagerStorage.sol"; /** @@ -29,6 +30,7 @@ contract DelegationManager is SignatureUtils { using SlashingLib for *; + using Snapshots for Snapshots.DefaultZeroHistory; using EnumerableSet for EnumerableSet.Bytes32Set; // @notice Simple permission for functions that are only callable by the StrategyManager contract OR by the EigenPodManagerContract @@ -338,23 +340,41 @@ contract DelegationManager is } /// @inheritdoc IDelegationManager - function decreaseOperatorShares( + function burnOperatorShares( address operator, IStrategy strategy, - uint256 wadSlashed + uint64 prevMaxMagnitude, + uint64 newMaxMagnitude ) external onlyAllocationManager { + require(newMaxMagnitude < prevMaxMagnitude, MaxMagnitudeCantIncrease()); + /// forgefmt: disable-next-item - uint256 amountSlashed = SlashingLib.calcSlashedAmount({ - operatorShares: operatorShares[operator][strategy], - wadSlashed: wadSlashed + uint256 sharesToDecrement = SlashingLib.calcSlashedAmount({ + operatorShares: operatorShares[operator][strategy], + prevMaxMagnitude: prevMaxMagnitude, + newMaxMagnitude: newMaxMagnitude }); + // While `sharesToDecrement` describes the amount we should directly remove from the operator's delegated + // shares, `sharesToBurn` also includes any shares that have been queued for withdrawal and are still + // slashable given the withdrawal delay. + uint256 sharesToBurn = + sharesToDecrement + _getSlashedSharesInQueue(operator, strategy, prevMaxMagnitude, newMaxMagnitude); + + // Remove shares from operator _decreaseDelegation({ operator: operator, staker: address(0), // we treat this as a decrease for the zero address staker strategy: strategy, - sharesToDecrease: amountSlashed + sharesToDecrease: sharesToDecrement }); + + /// TODO: implement EPM.burnShares interface. Likely requires more complex interface than just shares + /// so not adding a burnShares method in IShareManager + if (strategy != beaconChainETHStrategy) { + strategyManager.burnShares(strategy, sharesToBurn); + emit OperatorSharesBurned(operator, strategy, sharesToBurn); + } } /** @@ -479,7 +499,7 @@ contract DelegationManager is IShareManager shareManager = _getShareManager(withdrawal.strategies[i]); // Calculate how much slashing to apply, as well as shares to withdraw - uint256 sharesToWithdraw = SlashingLib.scaleSharesForCompleteWithdrawal({ + uint256 sharesToWithdraw = SlashingLib.scaleForCompleteWithdrawal({ scaledShares: withdrawal.scaledShares[i], slashingFactor: prevSlashingFactors[i] }); @@ -627,13 +647,18 @@ contract DelegationManager is sharesToWithdraw[i] = dsf.calcWithdrawable(depositSharesToWithdraw[i], slashingFactors[i]); // Apply slashing. If the staker or operator has been fully slashed, this will return 0 - scaledShares[i] = SlashingLib.scaleSharesForQueuedWithdrawal({ + scaledShares[i] = SlashingLib.scaleForQueueWithdrawal({ sharesToWithdraw: sharesToWithdraw[i], slashingFactor: slashingFactors[i] }); // Remove delegated shares from the operator if (operator != address(0)) { + // Staker was delegated and remains slashable during the withdrawal delay period + // Cumulative withdrawn scaled shares are updated for the strategy, this is for accounting + // purposes for burning shares if slashed + _addQueuedSlashableShares(operator, strategies[i], scaledShares[i]); + // forgefmt: disable-next-item _decreaseDelegation({ operator: operator, @@ -732,6 +757,48 @@ contract DelegationManager is _beaconChainSlashingFactor[staker] = bsf; } + /** + * @dev Calculate amount of slashable shares that would be slashed from the queued withdrawals from an operator for a strategy + * given the previous maxMagnitude and the new maxMagnitude. + * Note: To get the total amount of slashable shares in the queue withdrawable, set newMaxMagnitude to 0 and prevMaxMagnitude + * is the current maxMagnitude of the operator. + */ + function _getSlashedSharesInQueue( + address operator, + IStrategy strategy, + uint64 prevMaxMagnitude, + uint64 newMaxMagnitude + ) internal view returns (uint256) { + // Fetch the cumulative scaled shares sitting in the withdrawal queue both now and before + // the withdrawal delay. + uint256 curCumulativeScaledShares = _cumulativeScaledSharesHistory[operator][strategy].latest(); + uint256 prevCumulativeScaledShares = _cumulativeScaledSharesHistory[operator][strategy].upperLookup({ + key: uint32(block.number) - MIN_WITHDRAWAL_DELAY_BLOCKS + }); + + // The difference between these values represents the number of scaled shares that entered the + // withdrawal queue less than `MIN_WITHDRAWAL_DELAY_BLOCKS` ago. These shares are still slashable, + // so we use them to calculate the number of slashable shares in the withdrawal queue. + uint256 slashableScaledShares = curCumulativeScaledShares - prevCumulativeScaledShares; + + return SlashingLib.scaleForBurning({ + scaledShares: slashableScaledShares, + prevMaxMagnitude: prevMaxMagnitude, + newMaxMagnitude: newMaxMagnitude + }); + } + + /// @dev Add to the cumulative withdrawn scaled shares from an operator for a given strategy + function _addQueuedSlashableShares(address operator, IStrategy strategy, uint256 scaledShares) internal { + if (strategy != beaconChainETHStrategy) { + uint256 currCumulativeScaledShares = _cumulativeScaledSharesHistory[operator][strategy].latest(); + _cumulativeScaledSharesHistory[operator][strategy].push({ + key: uint32(block.number), + value: currCumulativeScaledShares + scaledShares + }); + } + } + /// @dev Depending on the strategy used, determine which ShareManager contract to make external calls to function _getShareManager( IStrategy strategy @@ -812,6 +879,20 @@ contract DelegationManager is return shares; } + /// @inheritdoc IDelegationManager + function getSlashableSharesInQueue(address operator, IStrategy strategy) public view returns (uint256) { + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy; + uint64 maxMagnitude = allocationManager.getMaxMagnitudes(operator, strategies)[0]; + // Return amount of shares slashed if all remaining magnitude were to be slashed + return _getSlashedSharesInQueue({ + operator: operator, + strategy: strategy, + prevMaxMagnitude: maxMagnitude, + newMaxMagnitude: 0 + }); + } + /// @inheritdoc IDelegationManager function getWithdrawableShares( address staker, @@ -884,7 +965,7 @@ contract DelegationManager is uint256[] memory slashingFactors = _getSlashingFactors(staker, operator, withdrawals[i].strategies); for (uint256 j; j < withdrawals[i].strategies.length; ++j) { - shares[i][j] = SlashingLib.scaleSharesForCompleteWithdrawal({ + shares[i][j] = SlashingLib.scaleForCompleteWithdrawal({ scaledShares: withdrawals[i].scaledShares[j], slashingFactor: slashingFactors[i] }); diff --git a/src/contracts/core/DelegationManagerStorage.sol b/src/contracts/core/DelegationManagerStorage.sol index 40e07893c..b36a54715 100644 --- a/src/contracts/core/DelegationManagerStorage.sol +++ b/src/contracts/core/DelegationManagerStorage.sol @@ -9,6 +9,8 @@ import "../interfaces/IAVSDirectory.sol"; import "../interfaces/IEigenPodManager.sol"; import "../interfaces/IAllocationManager.sol"; +import {Snapshots} from "../libraries/Snapshots.sol"; + /** * @title Storage variables for the `DelegationManager` contract. * @author Layr Labs, Inc. @@ -16,6 +18,8 @@ import "../interfaces/IAllocationManager.sol"; * @notice This storage contract is separate from the logic to simplify the upgrade process. */ abstract contract DelegationManagerStorage is IDelegationManager { + using Snapshots for Snapshots.DefaultZeroHistory; + // Constants /// @notice The EIP-712 typehash for the `DelegationApproval` struct used by the contract @@ -115,6 +119,12 @@ abstract contract DelegationManagerStorage is IDelegationManager { /// @dev This variable only reflects withdrawals that were made after the slashing release. mapping(bytes32 withdrawalRoot => Withdrawal withdrawal) public queuedWithdrawals; + /// @notice Contains history of the total cumulative staker withdrawals for an operator and a given strategy. + /// Used to calculate burned StrategyManager shares when an operator is slashed. + /// @dev Stores scaledShares instead of total withdrawn shares to track current slashable shares, dependent on the maxMagnitude + mapping(address operator => mapping(IStrategy strategy => Snapshots.DefaultZeroHistory)) internal + _cumulativeScaledSharesHistory; + // Construction constructor( @@ -136,5 +146,5 @@ abstract contract DelegationManagerStorage is IDelegationManager { * 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[34] private __gap; } diff --git a/src/contracts/core/StrategyManager.sol b/src/contracts/core/StrategyManager.sol index 901420154..ead92c167 100644 --- a/src/contracts/core/StrategyManager.sol +++ b/src/contracts/core/StrategyManager.sol @@ -140,6 +140,12 @@ contract StrategyManager is strategy.withdraw(staker, token, shares); } + /// @inheritdoc IStrategyManager + function burnShares(IStrategy strategy, uint256 sharesToBurn) external onlyDelegationManager { + // burning shares is functionally the same as withdrawing but with different destination address + strategy.withdraw(DEFAULT_BURN_ADDRESS, strategy.underlyingToken(), sharesToBurn); + } + /// @inheritdoc IStrategyManager function setStrategyWhitelister( address newStrategyWhitelister diff --git a/src/contracts/core/StrategyManagerStorage.sol b/src/contracts/core/StrategyManagerStorage.sol index 541c8527b..c02fd77b7 100644 --- a/src/contracts/core/StrategyManagerStorage.sol +++ b/src/contracts/core/StrategyManagerStorage.sol @@ -26,6 +26,9 @@ abstract contract StrategyManagerStorage is IStrategyManager { // index for flag that pauses deposits when set uint8 internal constant PAUSED_DEPOSITS = 0; + /// @notice default address for burning slashed shares and transferring underlying tokens + address public constant DEFAULT_BURN_ADDRESS = 0x00000000000000000000000000000000DeaDBeef; + // Immutables IDelegationManager public immutable delegation; diff --git a/src/contracts/interfaces/IDelegationManager.sol b/src/contracts/interfaces/IDelegationManager.sol index 93c1d0eb2..f07a2aa25 100644 --- a/src/contracts/interfaces/IDelegationManager.sol +++ b/src/contracts/interfaces/IDelegationManager.sol @@ -43,6 +43,8 @@ interface IDelegationManagerErrors { /// @dev Thrown when an operator has been fully slashed(maxMagnitude is 0) for a strategy. /// or if the staker has had been natively slashed to the point of their beaconChainScalingFactor equalling 0. error FullySlashed(); + /// @dev Thrown when an operator has been slashed but their new magnitude is higher than previously set. + error MaxMagnitudeCantIncrease(); /// Signatures @@ -159,6 +161,9 @@ interface IDelegationManagerEvents is IDelegationManagerTypes { /// @notice Emitted whenever an operator's shares are decreased for a given strategy. Note that shares is the delta in the operator's shares. event OperatorSharesDecreased(address indexed operator, address staker, IStrategy strategy, uint256 shares); + /// @notice Emitted whenever an operator's shares are burned for a given strategy + event OperatorSharesBurned(address indexed operator, IStrategy strategy, uint256 shares); + /// @notice Emitted when @param staker delegates to @param operator. event StakerDelegated(address indexed staker, address indexed operator); @@ -370,13 +375,20 @@ interface IDelegationManager is ISignatureUtils, IDelegationManagerErrors, IDele ) external; /** - * @notice Decreases the operators shares in storage after a slash + * @notice Decreases the operators shares in storage after a slash and burns the corresponding Strategy shares + * by calling into the StrategyManager or EigenPodManager to burn the shares. * @param operator The operator to decrease shares for * @param strategy The strategy to decrease shares for - * @param wadSlashed The proportion of 1e18 slashed + * @param prevMaxMagnitude the previous maxMagnitude of the operator + * @param newMaxMagnitude the new maxMagnitude of the operator * @dev Callable only by the AllocationManager */ - function decreaseOperatorShares(address operator, IStrategy strategy, uint256 wadSlashed) external; + function burnOperatorShares( + address operator, + IStrategy strategy, + uint64 prevMaxMagnitude, + uint64 newMaxMagnitude + ) external; /** * @@ -479,6 +491,16 @@ interface IDelegationManager is ISignatureUtils, IDelegationManagerErrors, IDele IStrategy[] memory strategies ) external view returns (uint256[][] memory); + /** + * @notice Returns amount of withdrawable shares from an operator for a strategy that is still in the queue + * and therefore slashable. Note that the *actual* slashable amount could be less than this value as this doesn't account + * for amounts that have already been slashed. This assumes that none of the shares have been slashed. + * @param operator the operator to get shares for + * @param strategy the strategy to get shares for + * @return the amount of shares that are slashable in the withdrawal queue for an operator and a strategy + */ + function getSlashableSharesInQueue(address operator, IStrategy strategy) external view returns (uint256); + /** * @notice Given a staker and a set of strategies, return the shares they can queue for withdrawal and the * corresponding depositShares. diff --git a/src/contracts/interfaces/IStrategyManager.sol b/src/contracts/interfaces/IStrategyManager.sol index b0e879723..da5d0a813 100644 --- a/src/contracts/interfaces/IStrategyManager.sol +++ b/src/contracts/interfaces/IStrategyManager.sol @@ -110,6 +110,14 @@ interface IStrategyManager is IStrategyManagerErrors, IStrategyManagerEvents, IS bytes memory signature ) external returns (uint256 shares); + /** + * @notice Burns Strategy shares for the given strategy by calling into the strategy to transfer to the default burn address. + * @param strategy The strategy to burn shares in. + * @param sharesToBurn The amount of shares to burn. + * @dev This function is only called by the DelegationManager when an operator is slashed. + */ + function burnShares(IStrategy strategy, uint256 sharesToBurn) external; + /** * @notice Owner-only function to change the `strategyWhitelister` address. * @param newStrategyWhitelister new address for the `strategyWhitelister`. diff --git a/src/contracts/libraries/SlashingLib.sol b/src/contracts/libraries/SlashingLib.sol index f330df2c1..dfe715f7a 100644 --- a/src/contracts/libraries/SlashingLib.sol +++ b/src/contracts/libraries/SlashingLib.sol @@ -62,7 +62,7 @@ library SlashingLib { return dsf._scalingFactor == 0 ? WAD : dsf._scalingFactor; } - function scaleSharesForQueuedWithdrawal( + function scaleForQueueWithdrawal( uint256 sharesToWithdraw, uint256 slashingFactor ) internal pure returns (uint256) { @@ -73,11 +73,22 @@ library SlashingLib { return sharesToWithdraw.divWad(slashingFactor); } - function scaleSharesForCompleteWithdrawal( + function scaleForCompleteWithdrawal(uint256 scaledShares, uint256 slashingFactor) internal pure returns (uint256) { + return scaledShares.mulWad(slashingFactor); + } + + /** + * @notice Scales shares according to the difference in an operator's magnitude before and + * after being slashed. This is used to calculate the number of slashable shares in the + * withdrawal queue. + * NOTE: max magnitude is guaranteed to only ever decrease. + */ + function scaleForBurning( uint256 scaledShares, - uint256 slashingFactor + uint64 prevMaxMagnitude, + uint64 newMaxMagnitude ) internal pure returns (uint256) { - return scaledShares.mulWad(slashingFactor); + return scaledShares.mulWad(prevMaxMagnitude - newMaxMagnitude); } function update( @@ -141,7 +152,12 @@ library SlashingLib { .mulWad(slashingFactor); } - function calcSlashedAmount(uint256 operatorShares, uint256 wadSlashed) internal pure returns (uint256) { - return operatorShares.mulWad(wadSlashed); + function calcSlashedAmount( + uint256 operatorShares, + uint256 prevMaxMagnitude, + uint256 newMaxMagnitude + ) internal pure returns (uint256) { + // round up mulDiv so we don't overslash + return operatorShares - operatorShares.mulDiv(newMaxMagnitude, prevMaxMagnitude, Math.Rounding.Up); } } diff --git a/src/contracts/libraries/Snapshots.sol b/src/contracts/libraries/Snapshots.sol index dbf510c60..839aec0d4 100644 --- a/src/contracts/libraries/Snapshots.sol +++ b/src/contracts/libraries/Snapshots.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import "@openzeppelin-upgrades/contracts/utils/math/MathUpgradeable.sol"; -import "@openzeppelin-upgrades/contracts/utils/math/SafeCastUpgradeable.sol"; import "./SlashingLib.sol"; @@ -11,13 +10,15 @@ import "./SlashingLib.sol"; * @title Library for handling snapshots as part of allocating and slashing. * @notice This library is using OpenZeppelin's CheckpointsUpgradeable library (v4.9.0) * and removes structs and functions that are unessential. - * Interfaces and structs are renamed for clarity and usage (timestamps, etc). + * Interfaces and structs are renamed for clarity and usage. * Some additional functions have also been added for convenience. - * @dev This library defines the `DefaultWadHistory` struct, for snapshotting values as they change at different points in + * @dev This library defines the `DefaultWadHistory` and `DefaultZeroHistory` struct, for snapshotting values as they change at different points in * time, and later looking up past values by block number. See {Votes} as an example. * - * To create a history of snapshots define a variable type `Snapshots.DefaultWadHistory` in your contract, and store a new - * snapshot for the current transaction block using the {push} function. If there is no history yet, the value is WAD. + * To create a history of snapshots define a variable type `Snapshots.DefaultWadHistory` or `Snapshots.DefaultZeroHistory` in your contract, + * and store a new snapshot for the current transaction block using the {push} function. If there is no history yet, the value is either WAD or 0, + * depending on the type of History struct used. This is implemented because for the AllocationManager we want the + * the default value to be WAD(1e18) but when used in the DelegationManager we want the default value to be 0. * * _Available since v4.5._ */ @@ -26,27 +27,46 @@ library Snapshots { Snapshot[] _snapshots; } + struct DefaultZeroHistory { + Snapshot[] _snapshots; + } + struct Snapshot { uint32 _key; - uint64 _value; + uint224 _value; } + error InvalidSnapshotOrdering(); + /** * @dev Pushes a (`key`, `value`) pair into a DefaultWadHistory so that it is stored as the snapshot. - * - * Returns previous value and new value. */ - function push(DefaultWadHistory storage self, uint32 key, uint64 value) internal returns (uint64, uint64) { - return _insert(self._snapshots, key, value); + function push(DefaultWadHistory storage self, uint32 key, uint64 value) internal { + _insert(self._snapshots, key, value); } /** - * @dev Returns the value in the last (most recent) snapshot with key lower or equal than the search key, or zero if there is none. + * @dev Pushes a (`key`, `value`) pair into a DefaultZeroHistory so that it is stored as the snapshot. + * `value` is cast to uint224. Responsibility for the safety of this operation falls outside of this library. + */ + function push(DefaultZeroHistory storage self, uint32 key, uint256 value) internal { + _insert(self._snapshots, key, uint224(value)); + } + + /** + * @dev Return default value of WAD if there are no snapshots for DefaultWadHistory. + * This is used for looking up maxMagnitudes in the AllocationManager. */ function upperLookup(DefaultWadHistory storage self, uint32 key) internal view returns (uint64) { - uint256 len = self._snapshots.length; - uint256 pos = _upperBinaryLookup(self._snapshots, key, 0, len); - return pos == 0 ? WAD : _unsafeAccess(self._snapshots, pos - 1)._value; + return uint64(_upperLookup(self._snapshots, key, WAD)); + } + + /** + * @dev Return default value of 0 if there are no snapshots for DefaultZeroHistory. + * This is used for looking up cumulative scaled shares in the DelegationManager. + */ + function upperLookup(DefaultZeroHistory storage self, uint32 key) internal view returns (uint256) { + return _upperLookup(self._snapshots, key, 0); } /** @@ -55,8 +75,16 @@ library Snapshots { function latest( DefaultWadHistory storage self ) internal view returns (uint64) { - uint256 pos = self._snapshots.length; - return pos == 0 ? WAD : _unsafeAccess(self._snapshots, pos - 1)._value; + return uint64(_latest(self._snapshots, WAD)); + } + + /** + * @dev Returns the value in the most recent snapshot, or 0 if there are no snapshots. + */ + function latest( + DefaultZeroHistory storage self + ) internal view returns (uint256) { + return uint256(_latest(self._snapshots, 0)); } /** @@ -68,31 +96,57 @@ library Snapshots { return self._snapshots.length; } + /** + * @dev Returns the number of snapshots. + */ + function length( + DefaultZeroHistory storage self + ) internal view returns (uint256) { + return self._snapshots.length; + } + /** * @dev Pushes a (`key`, `value`) pair into an ordered list of snapshots, either by inserting a new snapshot, * or by updating the last one. */ - function _insert(Snapshot[] storage self, uint32 key, uint64 value) private returns (uint64, uint64) { + function _insert(Snapshot[] storage self, uint32 key, uint224 value) private { uint256 pos = self.length; if (pos > 0) { - // Copying to memory is important here. + // Validate that inserted keys are always >= the previous key Snapshot memory last = _unsafeAccess(self, pos - 1); + require(last._key <= key, InvalidSnapshotOrdering()); - // Snapshot keys must be non-decreasing. - require(last._key <= key, "Snapshot: decreasing keys"); - - // Update or push new snapshot + // Update existing snapshot if `key` matches if (last._key == key) { _unsafeAccess(self, pos - 1)._value = value; - } else { - self.push(Snapshot({_key: key, _value: value})); + return; } - return (last._value, value); - } else { - self.push(Snapshot({_key: key, _value: value})); - return (0, value); } + + // `key` was not in the list; push as a new entry + self.push(Snapshot({_key: key, _value: value})); + } + + /** + * @dev Returns the value in the last (most recent) snapshot with key lower or equal than the search key, or `defaultValue` if there is none. + */ + function _upperLookup( + Snapshot[] storage snapshots, + uint32 key, + uint224 defaultValue + ) private view returns (uint224) { + uint256 len = snapshots.length; + uint256 pos = _upperBinaryLookup(snapshots, key, 0, len); + return pos == 0 ? defaultValue : _unsafeAccess(snapshots, pos - 1)._value; + } + + /** + * @dev Returns the value in the most recent snapshot, or `defaultValue` if there are no snapshots. + */ + function _latest(Snapshot[] storage snapshots, uint224 defaultValue) private view returns (uint224) { + uint256 pos = snapshots.length; + return pos == 0 ? defaultValue : _unsafeAccess(snapshots, pos - 1)._value; } /** diff --git a/src/test/mocks/AllocationManagerMock.sol b/src/test/mocks/AllocationManagerMock.sol index cdfd319a4..810565058 100644 --- a/src/test/mocks/AllocationManagerMock.sol +++ b/src/test/mocks/AllocationManagerMock.sol @@ -62,7 +62,9 @@ contract AllocationManagerMock is Test { uint64[] memory maxMagnitudes = new uint64[](strategies.length); for (uint256 i = 0; i < strategies.length; ++i) { - maxMagnitudes[i] = _maxMagnitudeHistory[operator][strategies[i]].upperLookup(blockNumber); + maxMagnitudes[i] = _maxMagnitudeHistory[operator][strategies[i]].upperLookup({ + key: blockNumber + }); } return maxMagnitudes; diff --git a/src/test/mocks/DelegationManagerMock.sol b/src/test/mocks/DelegationManagerMock.sol index dc0c82281..771c4e502 100644 --- a/src/test/mocks/DelegationManagerMock.sol +++ b/src/test/mocks/DelegationManagerMock.sol @@ -25,10 +25,16 @@ contract DelegationManagerMock is Test { isOperator[operator] = _isOperatorReturnValue; } - function decreaseOperatorShares(address operator, IStrategy strategy, uint256 wadSlashed) external { + function burnOperatorShares( + address operator, + IStrategy strategy, + uint64 prevMaxMagnitude, + uint64 newMaxMagnitude + ) external { uint256 amountSlashed = SlashingLib.calcSlashedAmount({ operatorShares: operatorShares[operator][strategy], - wadSlashed: wadSlashed + prevMaxMagnitude: prevMaxMagnitude, + newMaxMagnitude: newMaxMagnitude }); operatorShares[operator][strategy] -= amountSlashed; diff --git a/src/test/mocks/StrategyManagerMock.sol b/src/test/mocks/StrategyManagerMock.sol index 21ac64370..188e6e22f 100644 --- a/src/test/mocks/StrategyManagerMock.sol +++ b/src/test/mocks/StrategyManagerMock.sol @@ -107,6 +107,8 @@ contract StrategyManagerMock is Test { return (existingShares, addedShares); } + function burnShares(IStrategy strategy, uint256 sharesToBurn) external {} + function _getStrategyIndex(address staker, IStrategy strategy) internal view returns (uint256) { IStrategy[] memory strategies = strategiesToReturn[staker]; uint256 strategyIndex = type(uint256).max; diff --git a/src/test/unit/DelegationUnit.t.sol b/src/test/unit/DelegationUnit.t.sol index 2a8bb0213..6d545a203 100644 --- a/src/test/unit/DelegationUnit.t.sol +++ b/src/test/unit/DelegationUnit.t.sol @@ -38,6 +38,7 @@ contract DelegationManagerUnitTests is EigenLayerUnitTestSetup, IDelegationManag uint256 tokenMockInitialSupply = 10e50; uint32 constant MIN_WITHDRAWAL_DELAY_BLOCKS = 126_000; // 17.5 days in blocks + uint256 MAX_STRATEGY_SHARES = 1e38 - 1; // Delegation signer uint256 delegationSignerPrivateKey = uint256(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); @@ -121,6 +122,10 @@ contract DelegationManagerUnitTests is EigenLayerUnitTestSetup, IDelegationManag ) ); + // Roll blocks forward so that block.number - MIN_WITHDRAWAL_DELAY_BLOCKS doesn't revert + // in _getSlashableSharesInQueue + cheats.roll(MIN_WITHDRAWAL_DELAY_BLOCKS); + // Exclude delegation manager from fuzzed tests isExcludedFuzzAddress[address(delegationManager)] = true; isExcludedFuzzAddress[address(strategyManagerMock)] = true; @@ -265,10 +270,17 @@ contract DelegationManagerUnitTests is EigenLayerUnitTestSetup, IDelegationManag * @notice Using this helper function to fuzz withdrawalAmounts since fuzzing two dynamic sized arrays of equal lengths * reject too many inputs. */ - function _fuzzWithdrawalAmounts(uint256[] memory depositAmounts) internal view returns (uint256[] memory) { - uint256[] memory withdrawalAmounts = new uint256[](depositAmounts.length); - for (uint256 i = 0; i < depositAmounts.length; i++) { - cheats.assume(depositAmounts[i] > 0); + function _fuzzDepositWithdrawalAmounts(uint256[] memory fuzzAmounts) internal view returns (uint256[] memory, uint256[] memory) { + cheats.assume(fuzzAmounts.length > 0); + uint256[] memory withdrawalAmounts = new uint256[](fuzzAmounts.length); + // We want to bound deposits amounts as well + uint256[] memory depositAmounts = new uint256[](fuzzAmounts.length); + for (uint256 i = 0; i < fuzzAmounts.length; i++) { + depositAmounts[i] = bound( + uint256(keccak256(abi.encodePacked(fuzzAmounts[i]))), + 1, + 1e38 - 1 + ); // generate withdrawal amount within range s.t withdrawAmount <= depositAmount withdrawalAmounts[i] = bound( uint256(keccak256(abi.encodePacked(depositAmounts[i]))), @@ -276,7 +288,7 @@ contract DelegationManagerUnitTests is EigenLayerUnitTestSetup, IDelegationManag depositAmounts[i] ); } - return withdrawalAmounts; + return (depositAmounts, withdrawalAmounts); } function _setUpQueueWithdrawalsSingleStrat( @@ -369,7 +381,7 @@ contract DelegationManagerUnitTests is EigenLayerUnitTestSetup, IDelegationManag DepositScalingFactor memory _dsf = DepositScalingFactor(delegationManager.depositScalingFactor(staker, strategy)); uint256 sharesToWithdraw = _dsf.calcWithdrawable(depositSharesToWithdraw, slashingFactor); - uint256 scaledShares = SlashingLib.scaleSharesForQueuedWithdrawal({ + uint256 scaledShares = SlashingLib.scaleForQueueWithdrawal({ sharesToWithdraw: sharesToWithdraw, slashingFactor: slashingFactor }); @@ -2177,15 +2189,9 @@ contract DelegationManagerUnitTests_ShareAdjustment is DelegationManagerUnitTest ); if (delegationManager.isDelegated(staker)) { - uint256 slashingFactor = _getSlashingFactor(staker, strategyMock, magnitude); - dsf.update(0, shares, slashingFactor); - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit DepositScalingFactorUpdated(staker, strategyMock, dsf.scalingFactor()); - cheats.expectEmit(true, true, true, true, address(delegationManager)); emit OperatorSharesIncreased(defaultOperator, staker, strategyMock, shares); - } else { - uint256 slashingFactor = _getSlashingFactor(staker, strategyMock, WAD); + uint256 slashingFactor = _getSlashingFactor(staker, strategyMock, magnitude); dsf.update(0, shares, slashingFactor); cheats.expectEmit(true, true, true, true, address(delegationManager)); emit DepositScalingFactorUpdated(staker, strategyMock, dsf.scalingFactor()); @@ -2380,40 +2386,43 @@ contract DelegationManagerUnitTests_ShareAdjustment is DelegationManagerUnitTest ); } - /// @notice Verifies that `DelegationManager.decreaseOperatorShares` reverts if not called by the AllocationManager - function testFuzz_decreaseOperatorShares_revert_invalidCaller( + /// @notice Verifies that `DelegationManager.burnOperatorShares` reverts if not called by the AllocationManager + function testFuzz_burnOperatorShares_revert_invalidCaller( address invalidCaller ) public filterFuzzedAddressInputs(invalidCaller) { cheats.assume(invalidCaller != address(allocationManagerMock)); cheats.startPrank(invalidCaller); - cheats.expectRevert(OnlyAllocationManager.selector); - delegationManager.decreaseOperatorShares(invalidCaller, strategyMock, 0); + cheats.expectRevert(IDelegationManagerErrors.OnlyAllocationManager.selector); + delegationManager.burnOperatorShares(invalidCaller, strategyMock, 0, 0); } /// @notice Verifies that there is no change in shares if the staker is not delegatedd - function testFuzz_decreaseOperatorShares_noop() public { + function testFuzz_burnOperatorShares_noop() public { _registerOperatorWithBaseDetails(defaultOperator); cheats.prank(address(allocationManagerMock)); - delegationManager.decreaseOperatorShares(defaultOperator, strategyMock, WAD); + delegationManager.burnOperatorShares(defaultOperator, strategyMock, WAD, WAD/2); assertEq(delegationManager.operatorShares(defaultOperator, strategyMock), 0, "shares should not have changed"); } - /** - * @notice Verifies that `DelegationManager.decreaseOperatorShares` properly decreases the delegated `shares` that the operator + /** + * @notice Verifies that `DelegationManager.burnOperatorShares` properly decreases the delegated `shares` that the operator * who the `defaultStaker` is delegated to has in the strategies * @dev Checks that there is no change if the staker is not delegated * TODO: fuzz magnitude */ - function testFuzz_decreaseOperatorShares_slashedOperator( - Randomness r - ) public rand(r) { - uint256 numStrats = r.Uint256(1, 16); - IStrategy[] memory strategies = r.StrategyArray(16); - uint128 shares = r.Uint128(); - bool delegateFromStakerToOperator = r.Boolean(); - + function testFuzz_burnOperatorShares_slashedOperator( + IStrategy[] memory strategies, + uint128 shares, + bool delegateFromStakerToOperator + ) public { + // sanity-filtering on fuzzed input length & staker + cheats.assume(strategies.length <= 16); + // TODO: remove, handles rounding on division + cheats.assume(shares % 2 == 0); + + uint256 numStrats = strategies.length; bool hasBeaconChainStrategy = false; for(uint256 i = 0; i < numStrats; i++) { if (strategies[i] == beaconChainETHStrategy) { @@ -2482,14 +2491,14 @@ contract DelegationManagerUnitTests_ShareAdjustment is DelegationManagerUnitTest strategies[i], currentShares / 2 ); - delegationManager.decreaseOperatorShares(defaultOperator, strategies[i], WAD / 2); + delegationManager.burnOperatorShares(defaultOperator, strategies[i], WAD, WAD / 2); totalSharesDecreasedForStrategy[strategies[i]] += currentShares / 2; } } cheats.stopPrank(); } - // check shares after call to `decreaseOperatorShares` + // check shares after call to `burnOperatorShares` (uint256[] memory withdrawableShares, ) = delegationManager.getWithdrawableShares(defaultStaker, strategies); for (uint256 i = 0; i < strategies.length; ++i) { uint256 delegatedSharesAfter = delegationManager.operatorShares(delegatedTo, strategies[i]); @@ -2811,7 +2820,7 @@ contract DelegationManagerUnitTests_Undelegate is DelegationManagerUnitTests { uint256 operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock); _setOperatorMagnitude(defaultOperator, strategyMock, operatorMagnitude); cheats.prank(address(allocationManagerMock)); - delegationManager.decreaseOperatorShares(defaultOperator, strategyMock, operatorMagnitude); + delegationManager.burnOperatorShares(defaultOperator, strategyMock, WAD, operatorMagnitude); operatorSharesAfterSlash = delegationManager.operatorShares(defaultOperator, strategyMock); assertEq(operatorSharesAfterSlash, operatorSharesBefore / 2, "operator shares not properly updated"); } @@ -2880,7 +2889,7 @@ contract DelegationManagerUnitTests_Undelegate is DelegationManagerUnitTests { { _setOperatorMagnitude(defaultOperator, strategyMock, operatorMagnitude); cheats.prank(address(allocationManagerMock)); - delegationManager.decreaseOperatorShares(defaultOperator, strategyMock, WAD); + delegationManager.burnOperatorShares(defaultOperator, strategyMock, WAD, 0); operatorSharesAfterSlash = delegationManager.operatorShares(defaultOperator, strategyMock); assertEq(operatorSharesAfterSlash, 0, "operator shares not fully slashed"); } @@ -3210,7 +3219,7 @@ contract DelegationManagerUnitTests_queueWithdrawals is DelegationManagerUnitTes uint64 operatorMagnitude = 5e17; _setOperatorMagnitude(defaultOperator, strategyMock, operatorMagnitude); cheats.prank(address(allocationManagerMock)); - delegationManager.decreaseOperatorShares(defaultOperator, strategyMock, operatorMagnitude); + delegationManager.burnOperatorShares(defaultOperator, strategyMock, WAD, operatorMagnitude); ( QueuedWithdrawalParams[] memory queuedWithdrawalParams, @@ -3279,7 +3288,7 @@ contract DelegationManagerUnitTests_queueWithdrawals is DelegationManagerUnitTes uint64 operatorMagnitude = 0; _setOperatorMagnitude(defaultOperator, strategyMock, operatorMagnitude); cheats.prank(address(allocationManagerMock)); - delegationManager.decreaseOperatorShares(defaultOperator, strategyMock, WAD); + delegationManager.burnOperatorShares(defaultOperator, strategyMock, WAD, 0); // Attempt to withdraw for the strategy that was slashed 100% for the operator QueuedWithdrawalParams[] memory queuedWithdrawalParams = new QueuedWithdrawalParams[](1); @@ -3325,15 +3334,12 @@ contract DelegationManagerUnitTests_queueWithdrawals is DelegationManagerUnitTes * - Checks that event was emitted with correct withdrawalRoot and withdrawal */ function testFuzz_queueWithdrawal_MultipleStrats__nonSlashedOperator( - uint128[] memory depositAmountsUint128 + uint256[] memory fuzzAmounts ) public { - cheats.assume(depositAmountsUint128.length > 0 && depositAmountsUint128.length <= 32); - - uint256[] memory depositAmounts = new uint256[](depositAmountsUint128.length); - for (uint256 i = 0; i < depositAmountsUint128.length; i++) { - depositAmounts[i] = depositAmountsUint128[i]; - } - uint256[] memory withdrawalAmounts = _fuzzWithdrawalAmounts(depositAmounts); + ( + uint256[] memory depositAmounts, + uint256[] memory withdrawalAmounts + ) = _fuzzDepositWithdrawalAmounts(fuzzAmounts); IStrategy[] memory strategies = _deployAndDepositIntoStrategies(defaultStaker, depositAmounts); _registerOperatorWithBaseDetails(defaultOperator); @@ -3497,11 +3503,13 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage * then it should revert if the validBlockNumber has not passed either. */ function test_Revert_WhenWithdrawalDelayNotPassed( - uint256[] memory depositAmounts, + uint256[] memory fuzzAmounts, bool receiveAsTokens ) public { - cheats.assume(depositAmounts.length > 0 && depositAmounts.length <= 32); - uint256[] memory withdrawalAmounts = _fuzzWithdrawalAmounts(depositAmounts); + ( + uint256[] memory depositAmounts, + uint256[] memory withdrawalAmounts + ) = _fuzzDepositWithdrawalAmounts(fuzzAmounts); _registerOperatorWithBaseDetails(defaultOperator); ( @@ -3519,7 +3527,7 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage cheats.roll(withdrawal.startBlock + minWithdrawalDelayBlocks - 1); cheats.expectRevert(WithdrawalDelayNotElapsed.selector); cheats.prank(defaultStaker); - delegationManager.completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens); + delegationManager.completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens); IERC20[][] memory tokensArray = new IERC20[][](1); tokensArray[0] = tokens; @@ -3727,7 +3735,7 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage uint64 operatorMagnitude = 5e17; _setOperatorMagnitude(defaultOperator, withdrawal.strategies[0], operatorMagnitude); cheats.prank(address(allocationManagerMock)); - delegationManager.decreaseOperatorShares(defaultOperator, withdrawal.strategies[0], operatorMagnitude); + delegationManager.burnOperatorShares(defaultOperator, withdrawal.strategies[0], WAD, operatorMagnitude); uint256 operatorSharesAfterSlash = delegationManager.operatorShares(defaultOperator, strategyMock); assertApproxEqAbs(operatorSharesAfterSlash, operatorSharesAfterQueue / 2, 1, "operator shares should be decreased after slash"); @@ -3871,7 +3879,7 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage uint64 operatorMagnitude = 5e17; _setOperatorMagnitude(defaultOperator, withdrawal.strategies[0], operatorMagnitude); cheats.prank(address(allocationManagerMock)); - delegationManager.decreaseOperatorShares(defaultOperator, withdrawal.strategies[0], operatorMagnitude); + delegationManager.burnOperatorShares(defaultOperator, withdrawal.strategies[0], WAD, operatorMagnitude); operatorSharesAfterAVSSlash = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy); assertApproxEqAbs(operatorSharesAfterAVSSlash, operatorSharesAfterBeaconSlash / 2, 1, "operator shares should be decreased after AVS slash"); } @@ -3944,6 +3952,562 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage } } +contract DelegationManagerUnitTests_burningShares is DelegationManagerUnitTests { + using SingleItemArrayLib for *; + + /** + * @notice Test burning shares for an operator with no queued withdrawals + * - Asserts slashable shares before and after in queue is 0 + * - Asserts operator shares are decreased by half + */ + function testFuzz_burnOperatorShares_NoQueuedWithdrawals(Randomness r) public { + address operator = r.Address(); + address staker = r.Address(); + uint64 initMagnitude = WAD; + uint64 newMagnitude = 5e17; + uint256 shares = r.Uint256(1, MAX_STRATEGY_SHARES); + + // register *this contract* as an operator + _registerOperatorWithBaseDetails(operator); + _setOperatorMagnitude(operator, strategyMock, initMagnitude); + // Set the staker deposits in the strategies + IStrategy[] memory strategyArray = strategyMock.toArray(); + uint256[] memory sharesArray = shares.toArrayU256(); + strategyManagerMock.setDeposits(staker, strategyArray, sharesArray); + // delegate from the `staker` to the operator + _delegateToOperatorWhoAcceptsAllStakers(staker, operator); + uint256 operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock); + uint256 queuedSlashableSharesBefore = delegationManager.getSlashableSharesInQueue(operator, strategyMock); + + // calculate burned shares, should be halved + uint256 sharesToBurn = shares/2; + + // Burn shares + cheats.prank(address(allocationManagerMock)); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesDecreased(operator, address(0), strategyMock, sharesToBurn); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesBurned(operator, strategyMock, sharesToBurn); + delegationManager.burnOperatorShares({ + operator: operator, + strategy: strategyMock, + prevMaxMagnitude: initMagnitude, + newMaxMagnitude: newMagnitude + }); + + uint256 queuedSlashableSharesAfter = delegationManager.getSlashableSharesInQueue(operator, strategyMock); + uint256 operatorSharesAfter = delegationManager.operatorShares(operator, strategyMock); + assertEq(queuedSlashableSharesBefore, 0, "there should be no slashable shares in queue"); + assertEq(queuedSlashableSharesAfter, 0, "there should be no slashable shares in queue"); + assertEq(operatorSharesAfter, operatorSharesBefore - sharesToBurn, "operator shares should be decreased by sharesToBurn"); + } + + /** + * @notice Test burning shares for an operator with no slashable queued withdrawals in past MIN_WITHDRAWAL_DELAY_BLOCKS window. + * There does exist past queued withdrawals but nothing in the queue is slashable. + * - Asserts slashable shares in queue right after queuing a withdrawal is the withdrawal amount + * and then checks that after the withdrawal window the slashable shares is 0 again. + * - Asserts operator shares are decreased by half after burning + * - Asserts that the slashable shares in queue before/after burning are 0 + */ + function testFuzz_burnOperatorShares_NoQueuedWithdrawalsInWindow(Randomness r) public { + // 1. Randomize operator and staker info + // Operator info + address operator = r.Address(); + uint64 newMagnitude = 5e17; + // First staker + address staker1 = r.Address(); + uint256 shares = r.Uint256(1, MAX_STRATEGY_SHARES); + // Second Staker, will queue withdraw shares + address staker2 = r.Address(); + uint256 depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES); + uint256 withdrawAmount = r.Uint256(1, depositAmount); + + // 2. Register the operator, set the staker deposits, and delegate the 2 stakers to them + _registerOperatorWithBaseDetails(operator); + { + // Set the first staker deposits in the strategies + IStrategy[] memory strategyArray = strategyMock.toArray(); + uint256[] memory sharesArray = shares.toArrayU256(); + uint256[] memory depositArray = depositAmount.toArrayU256(); + strategyManagerMock.setDeposits(staker1, strategyArray, sharesArray); + // Set the second staker's deposits in the strategies + strategyManagerMock.setDeposits(staker2, strategyArray, depositArray); + } + _delegateToOperatorWhoAcceptsAllStakers(staker1, operator); + _delegateToOperatorWhoAcceptsAllStakers(staker2, operator); + + // 3. Queue withdrawal for staker2 and roll blocks forward so that the withdrawal is not slashable + { + ( + QueuedWithdrawalParams[] memory queuedWithdrawalParams, + Withdrawal memory withdrawal, + ) = _setUpQueueWithdrawalsSingleStrat({ + staker: staker2, + withdrawer: staker2, + strategy: strategyMock, + depositSharesToWithdraw: withdrawAmount + }); + cheats.prank(staker2); + delegationManager.queueWithdrawals(queuedWithdrawalParams); + assertEq( + delegationManager.getSlashableSharesInQueue(operator, strategyMock), + withdrawAmount, + "there should be withdrawAmount slashable shares in queue" + ); + cheats.roll(withdrawal.startBlock + delegationManager.MIN_WITHDRAWAL_DELAY_BLOCKS()); + } + + uint256 operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock); + uint256 queuedSlashableSharesBefore = delegationManager.getSlashableSharesInQueue(operator, strategyMock); + + // calculate burned shares, should be halved + // staker2 queue withdraws shares and we roll blocks to after the withdrawal is no longer slashable. + // Therefore amount of shares to burn should be what the staker still has remaining + staker1 shares and then + // divided by 2 since the operator was slashed 50% + uint256 sharesToBurn = (shares + depositAmount - withdrawAmount) / 2; + + // 4. Burn shares + _setOperatorMagnitude(operator, strategyMock, newMagnitude); + cheats.prank(address(allocationManagerMock)); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesDecreased(operator, address(0), strategyMock, sharesToBurn); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesBurned(operator, strategyMock, sharesToBurn); + delegationManager.burnOperatorShares({ + operator: operator, + strategy: strategyMock, + prevMaxMagnitude: WAD, + newMaxMagnitude: newMagnitude + }); + + // 5. Assert expected values + uint256 queuedSlashableSharesAfter = delegationManager.getSlashableSharesInQueue(operator, strategyMock); + uint256 operatorSharesAfter = delegationManager.operatorShares(operator, strategyMock); + assertEq(queuedSlashableSharesBefore, 0, "there should be no slashable shares in queue"); + assertEq(queuedSlashableSharesAfter, 0, "there should be no slashable shares in queue"); + assertEq(operatorSharesAfter, operatorSharesBefore - sharesToBurn, "operator shares should be decreased by sharesToBurn"); + } + + /** + * @notice Test burning shares for an operator with slashable queued withdrawals in past MIN_WITHDRAWAL_DELAY_BLOCKS window. + * There exists a single withdrawal that is slashable. + */ + function testFuzz_burnOperatorShares_SingleSlashableWithdrawal(Randomness r) public { + // 1. Randomize operator and staker info + // Operator info + address operator = r.Address(); + uint64 newMagnitude = 25e16; + // First staker + address staker1 = r.Address(); + uint256 shares = r.Uint256(1, MAX_STRATEGY_SHARES); + // Second Staker, will queue withdraw shares + address staker2 = r.Address(); + uint256 depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES); + uint256 withdrawAmount = r.Uint256(1, depositAmount); + + // 2. Register the operator, set the staker deposits, and delegate the 2 stakers to them + _registerOperatorWithBaseDetails(operator); + { + // Set the first staker deposits in the strategies + IStrategy[] memory strategyArray = strategyMock.toArray(); + uint256[] memory sharesArray = shares.toArrayU256(); + uint256[] memory depositArray = depositAmount.toArrayU256(); + strategyManagerMock.setDeposits(staker1, strategyArray, sharesArray); + // Set the second staker's deposits in the strategies + strategyManagerMock.setDeposits(staker2, strategyArray, depositArray); + } + _delegateToOperatorWhoAcceptsAllStakers(staker1, operator); + _delegateToOperatorWhoAcceptsAllStakers(staker2, operator); + + // 3. Queue withdrawal for staker2 so that the withdrawal is slashable + { + ( + QueuedWithdrawalParams[] memory queuedWithdrawalParams, + Withdrawal memory withdrawal, + ) = _setUpQueueWithdrawalsSingleStrat({ + staker: staker2, + withdrawer: staker2, + strategy: strategyMock, + depositSharesToWithdraw: withdrawAmount + }); + cheats.prank(staker2); + delegationManager.queueWithdrawals(queuedWithdrawalParams); + assertEq( + delegationManager.getSlashableSharesInQueue(operator, strategyMock), + withdrawAmount, + "there should be withdrawAmount slashable shares in queue" + ); + } + + uint256 operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock); + uint256 queuedSlashableSharesBefore = delegationManager.getSlashableSharesInQueue(operator, strategyMock); + + // calculate burned shares, should be 3/4 of the original shares + // staker2 queue withdraws shares + // Therefore amount of shares to burn should be what the staker still has remaining + staker1 shares and then + // divided by 2 since the operator was slashed 50% + uint256 sharesToDecrease = (shares + depositAmount - withdrawAmount) * 3 / 4; + uint256 sharesToBurn = sharesToDecrease + withdrawAmount * 3 / 4; + + // 4. Burn shares + _setOperatorMagnitude(operator, strategyMock, newMagnitude); + cheats.prank(address(allocationManagerMock)); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesDecreased(operator, address(0), strategyMock, sharesToDecrease); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesBurned(operator, strategyMock, sharesToBurn); + delegationManager.burnOperatorShares({ + operator: operator, + strategy: strategyMock, + prevMaxMagnitude: WAD, + newMaxMagnitude: newMagnitude + }); + + // 5. Assert expected values + uint256 queuedSlashableSharesAfter = delegationManager.getSlashableSharesInQueue(operator, strategyMock); + uint256 operatorSharesAfter = delegationManager.operatorShares(operator, strategyMock); + assertEq(queuedSlashableSharesBefore, withdrawAmount, "Slashable shares in queue should be full withdraw amount"); + assertEq(queuedSlashableSharesAfter, withdrawAmount / 4, "Slashable shares in queue should be 1/4 withdraw amount after slashing"); + assertEq(operatorSharesAfter, operatorSharesBefore - sharesToDecrease, "operator shares should be decreased by sharesToBurn"); + } + + /** + * @notice Test burning shares for an operator with slashable queued withdrawals in past MIN_WITHDRAWAL_DELAY_BLOCKS window. + * There exists multiple withdrawals that are slashable. + */ + function testFuzz_burnOperatorShares_MultipleSlashableWithdrawals(Randomness r) public { + // 1. Randomize operator and staker info + // Operator info + address operator = r.Address(); + uint64 newMagnitude = 25e16; + // Staker and withdrawing amounts + address staker = r.Address(); + uint256 depositAmount = r.Uint256(3, MAX_STRATEGY_SHARES); + uint256 withdrawAmount1 = r.Uint256(2, depositAmount); + uint256 withdrawAmount2 = r.Uint256(1, depositAmount - withdrawAmount1); + + // 2. Register the operator, set the staker deposits, and delegate the 2 stakers to them + _registerOperatorWithBaseDetails(operator); + { + // Set the first staker deposits in the strategies + IStrategy[] memory strategyArray = strategyMock.toArray(); + uint256[] memory sharesArray = depositAmount.toArrayU256(); + strategyManagerMock.setDeposits(staker, strategyArray, sharesArray); + } + _delegateToOperatorWhoAcceptsAllStakers(staker, operator); + + // 3. Queue withdrawal for staker and roll blocks forward so that the withdrawal is not slashable + { + ( + QueuedWithdrawalParams[] memory queuedWithdrawalParams, + Withdrawal memory withdrawal, + ) = _setUpQueueWithdrawalsSingleStrat({ + staker: staker, + withdrawer: staker, + strategy: strategyMock, + depositSharesToWithdraw: withdrawAmount1 + }); + cheats.prank(staker); + delegationManager.queueWithdrawals(queuedWithdrawalParams); + assertEq( + delegationManager.getSlashableSharesInQueue(operator, strategyMock), + withdrawAmount1, + "there should be withdrawAmount slashable shares in queue" + ); + + ( + queuedWithdrawalParams, + withdrawal, + ) = _setUpQueueWithdrawalsSingleStrat({ + staker: staker, + withdrawer: staker, + strategy: strategyMock, + depositSharesToWithdraw: withdrawAmount2 + }); + cheats.prank(staker); + delegationManager.queueWithdrawals(queuedWithdrawalParams); + assertEq( + delegationManager.getSlashableSharesInQueue(operator, strategyMock), + withdrawAmount2 + withdrawAmount1, + "there should be withdrawAmount slashable shares in queue" + ); + } + + uint256 operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock); + uint256 queuedSlashableSharesBefore = delegationManager.getSlashableSharesInQueue(operator, strategyMock); + + // calculate burned shares, should be halved for both operatorShares and slashable shares in queue + // staker queue withdraws shares twice and both withdrawals should be slashed 75%. + uint256 sharesToDecrease = (depositAmount - withdrawAmount1 - withdrawAmount2) * 3 / 4; + uint256 sharesToBurn = sharesToDecrease + (delegationManager.getSlashableSharesInQueue(operator, strategyMock) * 3 / 4); + + // 4. Burn shares + _setOperatorMagnitude(operator, strategyMock, newMagnitude); + cheats.prank(address(allocationManagerMock)); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesDecreased(operator, address(0), strategyMock, sharesToDecrease); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesBurned(operator, strategyMock, sharesToBurn); + delegationManager.burnOperatorShares({ + operator: operator, + strategy: strategyMock, + prevMaxMagnitude: WAD, + newMaxMagnitude: newMagnitude + }); + + // 5. Assert expected values + uint256 queuedSlashableSharesAfter = delegationManager.getSlashableSharesInQueue(operator, strategyMock); + uint256 operatorSharesAfter = delegationManager.operatorShares(operator, strategyMock); + assertEq(queuedSlashableSharesBefore, (withdrawAmount1 + withdrawAmount2), "Slashable shares in queue should be full withdraw amount"); + assertEq(queuedSlashableSharesAfter, (withdrawAmount1 + withdrawAmount2) / 4, "Slashable shares in queue should be 1/4 withdraw amount after slashing"); + assertEq(operatorSharesAfter, operatorSharesBefore - sharesToDecrease, "operator shares should be decreased by sharesToBurn"); + } + + /** + * @notice TODO Test burning shares for an operator with slashable queued withdrawals in past MIN_WITHDRAWAL_DELAY_BLOCKS window. + * There exists multiple withdrawals that are slashable but queued with different maxMagnitudes at + * time of queuing. + * + * Test Setup: + * - staker1 deposits, queues withdrawal for some amount, + * - operator slashed 50% + * - staker 2 deposits, queues withdrawal for some amount + * - operator is then slashed another 50% + * slashed amount for staker 1 should be 75% and staker 2 should be 50% where the total + * slashed amount is the sum of both + */ + function testFuzz_burnOperatorShares_MultipleWithdrawalsMultipleSlashings(Randomness r) public { + address operator = r.Address(); + address staker = r.Address(); + uint256 depositAmount = r.Uint256(3, MAX_STRATEGY_SHARES); + uint256 depositSharesToWithdraw1 = r.Uint256(1, depositAmount); + uint256 depositSharesToWithdraw2 = r.Uint256(1, depositAmount - depositSharesToWithdraw1); + + uint64 newMagnitude = 5e17; + + // 2. Register the operator, set the staker deposits, and delegate the 2 stakers to them + _registerOperatorWithBaseDetails(operator); + { + // Set the first staker deposits in the strategies + IStrategy[] memory strategyArray = strategyMock.toArray(); + uint256[] memory depositArray = depositAmount.toArrayU256(); + strategyManagerMock.setDeposits(staker, strategyArray, depositArray); + } + _delegateToOperatorWhoAcceptsAllStakers(staker, operator); + + // 3. Queue withdrawal for staker and slash operator for 50% + { + ( + QueuedWithdrawalParams[] memory queuedWithdrawalParams, + Withdrawal memory withdrawal, + ) = _setUpQueueWithdrawalsSingleStrat({ + staker: staker, + withdrawer: staker, + strategy: strategyMock, + depositSharesToWithdraw: depositSharesToWithdraw1 + }); + + // 3.1 queue a withdrawal for the staker + cheats.prank(staker); + delegationManager.queueWithdrawals(queuedWithdrawalParams); + uint256 operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock); + uint256 queuedSlashableSharesBefore = delegationManager.getSlashableSharesInQueue(operator, strategyMock); + + uint256 sharesToDecrease = (depositAmount - depositSharesToWithdraw1) / 2; + uint256 sharesToBurn = sharesToDecrease + depositSharesToWithdraw1/2; + + // 3.2 Burn shares + _setOperatorMagnitude(operator, strategyMock, newMagnitude); + cheats.prank(address(allocationManagerMock)); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesDecreased(operator, address(0), strategyMock, sharesToDecrease); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesBurned(operator, strategyMock, sharesToBurn); + delegationManager.burnOperatorShares({ + operator: operator, + strategy: strategyMock, + prevMaxMagnitude: WAD, + newMaxMagnitude: newMagnitude + }); + + // 3.3 Assert slashable shares and operator shares + assertEq( + queuedSlashableSharesBefore, + depositSharesToWithdraw1, + "Slashable shares in queue should be full withdraw1 amount" + ); + assertEq( + delegationManager.getSlashableSharesInQueue(operator, strategyMock), + depositSharesToWithdraw1 / 2, + "Slashable shares in queue should be 1/2 withdraw1 amount after slashing" + ); + assertEq( + delegationManager.operatorShares(operator, strategyMock), + operatorSharesBefore - sharesToDecrease, + "operator shares should be decreased by sharesToBurn" + ); + } + + // 4. Queue withdrawal for staker and slash operator for 50% again + newMagnitude = newMagnitude/2; + { + ( + QueuedWithdrawalParams[] memory queuedWithdrawalParams, + Withdrawal memory withdrawal, + ) = _setUpQueueWithdrawalsSingleStrat({ + staker: staker, + withdrawer: staker, + strategy: strategyMock, + depositSharesToWithdraw: depositSharesToWithdraw2 + }); + + // actual withdrawn shares are half of the deposit shares because of first slashing + uint256 withdrawAmount2 = depositSharesToWithdraw2 / 2; + + // 4.1 queue a withdrawal for the staker + cheats.prank(staker); + delegationManager.queueWithdrawals(queuedWithdrawalParams); + uint256 operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock); + uint256 queuedSlashableSharesBefore = delegationManager.getSlashableSharesInQueue(operator, strategyMock); + + uint256 sharesToDecrease = operatorSharesBefore / 2; + uint256 sharesToBurn = sharesToDecrease + (withdrawAmount2 + depositSharesToWithdraw1/2)/2; + + // 4.2 Burn shares + _setOperatorMagnitude(operator, strategyMock, newMagnitude); + cheats.prank(address(allocationManagerMock)); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesDecreased(operator, address(0), strategyMock, sharesToDecrease); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesBurned(operator, strategyMock, sharesToBurn); + delegationManager.burnOperatorShares({ + operator: operator, + strategy: strategyMock, + prevMaxMagnitude: newMagnitude*2, + newMaxMagnitude: newMagnitude + }); + + // 4.3 Assert slashable shares and operator shares + assertEq( + queuedSlashableSharesBefore, + withdrawAmount2 + depositSharesToWithdraw1/2, + "Slashable shares in queue before should be withdrawAmount1 / 2 + withdrawAmount2" + ); + assertEq( + delegationManager.getSlashableSharesInQueue(operator, strategyMock), + (withdrawAmount2 + depositSharesToWithdraw1/2)/2, + "Slashable shares in queue should be (withdrawAmount2 + depositSharesToWithdraw1/2)/2 after slashing" + ); + assertEq( + delegationManager.operatorShares(operator, strategyMock), + operatorSharesBefore - sharesToDecrease, + "operator shares should be decreased by sharesToBurn" + ); + } + + } + + /** + * @notice Ensure that when a withdrawal is completable then there are no slashable shares in the queue. + * However if the withdrawal is not completable and the withdrawal delay hasn't elapsed, then the withdrawal + * should be counted as slashable. + */ + function testFuzz_burnOperatorShares_Timings(Randomness r) public { + // 1. Randomize operator and staker info + // Operator info + address operator = r.Address(); + uint64 newMagnitude = 25e16; + // staker + address staker = r.Address(); + uint256 depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES); + + // 2. Register the operator, set the staker deposits, and delegate the staker to them + _registerOperatorWithBaseDetails(operator); + { + // Set the first staker deposits in the strategies + IStrategy[] memory strategyArray = strategyMock.toArray(); + uint256[] memory depositArray = depositAmount.toArrayU256(); + strategyManagerMock.setDeposits(staker, strategyArray, depositArray); + } + _delegateToOperatorWhoAcceptsAllStakers(staker, operator); + + // 3. Queue withdrawal for staker and roll blocks forward so that the withdrawal is completable + uint256 completableBlock; + { + ( + QueuedWithdrawalParams[] memory queuedWithdrawalParams, + Withdrawal memory withdrawal, + ) = _setUpQueueWithdrawalsSingleStrat({ + staker: staker, + withdrawer: staker, + strategy: strategyMock, + depositSharesToWithdraw: depositAmount + }); + cheats.startPrank(staker); + delegationManager.queueWithdrawals(queuedWithdrawalParams); + // 3.1 after queuing the withdrawal, check that there are slashable shares in queue + assertEq( + delegationManager.getSlashableSharesInQueue(operator, strategyMock), + depositAmount, + "there should be depositAmount slashable shares in queue" + ); + // Check slashable shares in queue before and when the withdrawal is completable + completableBlock = withdrawal.startBlock + delegationManager.MIN_WITHDRAWAL_DELAY_BLOCKS(); + IERC20[] memory tokenArray = strategyMock.underlyingToken().toArray(); + + // 3.2 roll to right before withdrawal is completable, check that slashable shares are still there + // attempting to complete a withdrawal should revert + cheats.roll(completableBlock - 1); + cheats.expectRevert(WithdrawalDelayNotElapsed.selector); + delegationManager.completeQueuedWithdrawal(withdrawal, tokenArray, true); + assertEq( + delegationManager.getSlashableSharesInQueue(operator, strategyMock), + depositAmount, + "there should still be depositAmount slashable shares in queue" + ); + + // 3.3 roll to blocknumber that the withdrawal is completable, there should be no slashable shares in queue + cheats.roll(completableBlock); + delegationManager.completeQueuedWithdrawal(withdrawal, tokenArray, true); + assertEq( + delegationManager.getSlashableSharesInQueue(operator, strategyMock), + 0, + "there should be no slashable shares in queue when the withdrawal is completable" + ); + + cheats.stopPrank(); + + } + + uint256 operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock); + + // 4. Burn 0 shares when new magnitude is set + _setOperatorMagnitude(operator, strategyMock, newMagnitude); + cheats.prank(address(allocationManagerMock)); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesDecreased(operator, address(0), strategyMock, 0); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesBurned(operator, strategyMock, 0); + delegationManager.burnOperatorShares({ + operator: operator, + strategy: strategyMock, + prevMaxMagnitude: WAD, + newMaxMagnitude: newMagnitude + }); + + // 5. Assert expected values + uint256 operatorSharesAfter = delegationManager.operatorShares(operator, strategyMock); + assertEq( + delegationManager.getSlashableSharesInQueue(operator, strategyMock), + 0, + "there should still be no slashable shares in queue after burning 0 shares" + ); + assertEq(operatorSharesAfter, operatorSharesBefore, "operator shares should be unchanged and equal to 0"); + assertEq(operatorSharesBefore, 0, "operator shares should be unchanged and equal to 0"); + } +} + /** * @notice TODO Lifecycle tests - These tests combine multiple functionalities of the DelegationManager 1. Old SigP test - registerAsOperator, separate staker delegate to operator, as operator undelegate (reverts), diff --git a/src/test/unit/StrategyManagerUnit.t.sol b/src/test/unit/StrategyManagerUnit.t.sol index bfa67ff61..f51502d3c 100644 --- a/src/test/unit/StrategyManagerUnit.t.sol +++ b/src/test/unit/StrategyManagerUnit.t.sol @@ -1416,6 +1416,50 @@ contract StrategyManagerUnitTests_withdrawSharesAsTokens is StrategyManagerUnitT } } +contract StrategyManagerUnitTests_burnShares is StrategyManagerUnitTests { + function test_Revert_DelegationManagerModifier() external { + DelegationManagerMock invalidDelegationManager = new DelegationManagerMock(); + cheats.expectRevert(IStrategyManagerErrors.OnlyDelegationManager.selector); + strategyManager.burnShares(dummyStrat, 1); + } + + /** + * @notice deposits a single strategy and withdrawSharesAsTokens() function reverts when sharesAmount is + * higher than depositAmount + */ + function testFuzz_Revert_ShareAmountTooHigh( + address staker, + uint256 depositAmount, + uint256 sharesToBurn + ) external filterFuzzedAddressInputs(staker) { + cheats.assume(staker != address(0)); + cheats.assume(depositAmount > 0 && depositAmount < dummyToken.totalSupply() && depositAmount < sharesToBurn); + IStrategy strategy = dummyStrat; + IERC20 token = dummyToken; + _depositIntoStrategySuccessfully(strategy, staker, depositAmount); + cheats.expectRevert(IStrategyErrors.WithdrawalAmountExceedsTotalDeposits.selector); + cheats.prank(address(delegationManagerMock)); + strategyManager.burnShares(strategy, sharesToBurn); + } + + function testFuzz_SingleStrategyDeposited( + address staker, + uint256 depositAmount, + uint256 sharesToBurn + ) external filterFuzzedAddressInputs(staker) { + cheats.assume(staker != address(0)); + cheats.assume(sharesToBurn > 0 && sharesToBurn < dummyToken.totalSupply() && depositAmount >= sharesToBurn); + IStrategy strategy = dummyStrat; + IERC20 token = dummyToken; + _depositIntoStrategySuccessfully(strategy, staker, depositAmount); + uint256 balanceBefore = token.balanceOf(strategyManager.DEFAULT_BURN_ADDRESS()); + cheats.prank(address(delegationManagerMock)); + strategyManager.burnShares(strategy, sharesToBurn); + uint256 balanceAfter = token.balanceOf(strategyManager.DEFAULT_BURN_ADDRESS()); + assertEq(balanceAfter, balanceBefore + sharesToBurn, "balanceAfter != balanceBefore + sharesAmount"); + } +} + contract StrategyManagerUnitTests_setStrategyWhitelister is StrategyManagerUnitTests { function testFuzz_SetStrategyWhitelister( address newWhitelister