repomix-output-eigenlayer-contracts-src.md•2.84 MB
Sourced from: https://github.com/Layr-Labs/eigenlayer-contracts/
This file is a merged representation of a subset of the codebase, containing specifically included files, combined into a single document by Repomix. The content has been processed where security check has been disabled.
# File Summary
## Purpose
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.
## File Format
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Multiple file entries, each consisting of:
a. A header with the file path (## File: path/to/file)
b. The full contents of the file in a code block
## Usage Guidelines
- This file should be treated as read-only. Any changes should be made to the
original repository files, not this packed version.
- When processing this file, use the file path to distinguish
between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
the same level of security as you would the original repository.
## Notes
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
- Only files matching these patterns are included: src/**
- Files matching patterns in .gitignore are excluded
- Files matching default ignore patterns are excluded
- Security check has been disabled - content may contain sensitive information
## Additional Info
# Directory Structure
```
src/
contracts/
core/
AllocationManager.sol
AllocationManagerStorage.sol
AVSDirectory.sol
AVSDirectoryStorage.sol
DelegationManager.sol
DelegationManagerStorage.sol
RewardsCoordinator.sol
RewardsCoordinatorStorage.sol
StrategyManager.sol
StrategyManagerStorage.sol
interfaces/
IAllocationManager.sol
IAVSDirectory.sol
IAVSRegistrar.sol
IBackingEigen.sol
IDelegationManager.sol
IEigen.sol
IEigenPod.sol
IEigenPodManager.sol
IETHPOSDeposit.sol
IPausable.sol
IPauserRegistry.sol
IPermissionController.sol
IRewardsCoordinator.sol
ISemVerMixin.sol
IShareManager.sol
ISignatureUtilsMixin.sol
IStrategy.sol
IStrategyFactory.sol
IStrategyManager.sol
libraries/
BeaconChainProofs.sol
BytesLib.sol
Endian.sol
Merkle.sol
OperatorSetLib.sol
SlashingLib.sol
Snapshots.sol
mixins/
PermissionControllerMixin.sol
SemVerMixin.sol
SignatureUtilsMixin.sol
permissions/
Pausable.sol
PauserRegistry.sol
PermissionController.sol
PermissionControllerStorage.sol
pods/
EigenPod.sol
EigenPodManager.sol
EigenPodManagerStorage.sol
EigenPodPausingConstants.sol
EigenPodStorage.sol
strategies/
EigenStrategy.sol
StrategyBase.sol
StrategyBaseTVLLimits.sol
StrategyFactory.sol
StrategyFactoryStorage.sol
token/
BackingEigen.sol
Eigen.sol
test/
harnesses/
AllocationManagerHarness.sol
DelegationManagerHarness.sol
EigenHarness.sol
EigenPodHarness.sol
EigenPodManagerWrapper.sol
PausableHarness.sol
integration/
deprecatedInterfaces/
mainnet/
BeaconChainProofs.sol
IBeaconChainOracle.sol
IDelayedWithdrawalRouter.sol
IDelegationManager.sol
IEigenPod.sol
IEigenPodManager.sol
IStrategyManager.sol
mocks/
BeaconChainMock_Deneb.t.sol
BeaconChainMock.t.sol
EIP_4788_Oracle_Mock.t.sol
tests/
eigenpod/
FullySlashed_EigenPod.t.sol
Register_Allocate_Slash_VerifyWC_.t.sol
SlashBC_OneBCSF.t.sol
Slashed_Eigenpod_BC.t.sol
VerifyWC_StartCP_CompleteCP.t.sol
upgrade/
Complete_PreSlashing_Withdrawal.t.sol
Delegate_Upgrade_Allocate.t.sol
EigenPod_Slashing_Migration.t.sol
EigenPod.t.sol
Prooftra.t.sol
README.md
ALM_Multi.t.sol
ALM_RegisterAndModify.t.sol
Delegate_Deposit_Queue_Complete.t.sol
Deposit_Delegate_Allocate_Slash_Queue_Redeposit.t.sol
Deposit_Delegate_Queue_Complete.t.sol
Deposit_Delegate_Redelegate_Complete.t.sol
Deposit_Delegate_Undelegate_Complete.t.sol
Deposit_Delegate_UpdateBalance.t.sol
Deposit_Queue_Complete.t.sol
Deposit_Register_QueueWithdrawal_Complete.t.sol
DualSlashing.t.sol
FullySlashed_Operator.t.sol
HighDSF_Multiple_Deposits.t.sol
Slashed_Eigenpod_AVS.t.sol
SlashingWithdrawals.t.sol
Timing.t.sol
Upgrade_Setup.t.sol
users/
AVS.t.sol
User_M1.t.sol
User_M2.t.sol
User.t.sol
IntegrationBase.t.sol
IntegrationChecks.t.sol
IntegrationDeployer.t.sol
README.md
TimeMachine.t.sol
TypeImporter.t.sol
UpgradeTest.t.sol
mocks/
AllocationManagerMock.sol
AVSDirectoryMock.sol
DelegationManagerMock.sol
Dummy.sol
EigenPodManagerMock.sol
EigenPodMock.sol
EmptyContract.sol
ERC20_OneWeiFeeOnTransfer.sol
ERC20_SetTransferReverting_Mock.sol
ERC20Mock.sol
ETHDepositMock.sol
LiquidStakingToken.sol
MockAVSRegistrar.sol
MockDecimals.sol
OwnableMock.sol
Reenterer.sol
Reverter.sol
StrategyManagerMock.sol
test-data/
rewardsCoordinator/
processClaim_Preprod_Test.json
processClaimProofs_MaxEarnerAndLeafIndices.json
processClaimProofs_Root1.json
processClaimProofs_Root2.json
processClaimProofs_Root3.json
processClaimProofs_SingleEarnerLeaf.json
processClaimProofs_SingleTokenLeaf.json
slashedProofs/
balanceUpdateProof_notOvercommitted_61511_incrementedBlockBy100.json
balanceUpdateProof_notOvercommitted_61511.json
balanceUpdateProof_Overcommitted_61511.json
balanceUpdateProof_balance28ETH_302913.json
balanceUpdateProof_notOverCommitted_302913_incrementedBlockBy100.json
balanceUpdateProof_notOverCommitted_302913.json
balanceUpdateProof_updated_to_0ETH_302913.json
balanceUpdateProof_updated_to_30ETH_302913.json
fullWithdrawalCapellaAgainstDenebRoot.json
fullWithdrawalDeneb.json
fullWithdrawalProof_Latest_1SlotAdvanced.json
fullWithdrawalProof_Latest_28ETH.json
fullWithdrawalProof_Latest.json
operators.json
owners.json
partialWithdrawalProof_Latest.json
reputedOwners.json
withdrawal_credential_proof_302913_30ETHBalance.json
withdrawal_credential_proof_302913_exited.json
withdrawal_credential_proof_302913.json
withdrawal_credential_proof_510257.json
withdrawalCredentialAndBalanceProof_61068.json
withdrawalCredentialAndBalanceProof_61336.json
token/
bEIGEN.t.sol
EigenTransferRestrictions.t.sol
EigenWrapping.t.sol
tree/
AllocationManagerUnit.tree
DelegationManagerUnit.tree
EigenPodManagerUnit.tree
EigenPodUnit.tree
PermissionControllerUnit.tree
StrategyManagerUnit.tree
unit/
libraries/
BytesLibUnit.t.sol
SnapshotsUnit.t.sol
mixins/
SemVerMixin.t.sol
SignatureUtilsUnit.t.sol
AllocationManagerUnit.t.sol
AVSDirectoryUnit.t.sol
DelegationUnit.t.sol
DeployFromScratch.t.sol
EigenPodManagerUnit.t.sol
EigenPodUnit.t.sol
PausableUnit.t.sol
PauserRegistryUnit.t.sol
PermissionControllerUnit.t.sol
RewardsCoordinatorUnit.t.sol
StrategyBaseTVLLimitsUnit.sol
StrategyBaseUnit.t.sol
StrategyFactoryUnit.t.sol
StrategyManagerUnit.t.sol
utils/
ArrayLib.sol
EigenLayerUnitTestSetup.sol
EigenPodUser.t.sol
Logger.t.sol
ProofParsing.sol
Random.sol
DevnetLifecycle.t.sol
```
# Files
## File: src/contracts/core/AllocationManager.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol";
import "../mixins/PermissionControllerMixin.sol";
import "../mixins/SemVerMixin.sol";
import "../permissions/Pausable.sol";
import "../libraries/SlashingLib.sol";
import "../libraries/OperatorSetLib.sol";
import "./AllocationManagerStorage.sol";
contract AllocationManager is
Initializable,
OwnableUpgradeable,
Pausable,
AllocationManagerStorage,
ReentrancyGuardUpgradeable,
PermissionControllerMixin,
SemVerMixin
{
using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque;
using EnumerableSet for *;
using SafeCast for *;
using Snapshots for Snapshots.DefaultWadHistory;
using OperatorSetLib for OperatorSet;
using SlashingLib for uint256;
/**
*
* INITIALIZING FUNCTIONS
*
*/
/**
* @dev Initializes the DelegationManager address, the deallocation delay, and the allocation configuration delay.
*/
constructor(
IDelegationManager _delegation,
IPauserRegistry _pauserRegistry,
IPermissionController _permissionController,
uint32 _DEALLOCATION_DELAY,
uint32 _ALLOCATION_CONFIGURATION_DELAY,
string memory _version
)
AllocationManagerStorage(_delegation, _DEALLOCATION_DELAY, _ALLOCATION_CONFIGURATION_DELAY)
Pausable(_pauserRegistry)
PermissionControllerMixin(_permissionController)
SemVerMixin(_version)
{
_disableInitializers();
}
/// @inheritdoc IAllocationManager
function initialize(address initialOwner, uint256 initialPausedStatus) external initializer {
_setPausedStatus(initialPausedStatus);
_transferOwnership(initialOwner);
}
/// @inheritdoc IAllocationManager
function slashOperator(
address avs,
SlashingParams calldata params
) external onlyWhenNotPaused(PAUSED_OPERATOR_SLASHING) checkCanCall(avs) {
// Check that the operator set exists and the operator is registered to it
OperatorSet memory operatorSet = OperatorSet(avs, params.operatorSetId);
require(params.strategies.length == params.wadsToSlash.length, InputArrayLengthMismatch());
require(_operatorSets[operatorSet.avs].contains(operatorSet.id), InvalidOperatorSet());
require(isOperatorSlashable(params.operator, operatorSet), OperatorNotSlashable());
uint256[] memory wadSlashed = new uint256[](params.strategies.length);
// For each strategy in the operator set, slash any existing allocation
for (uint256 i = 0; i < params.strategies.length; i++) {
// Check that `strategies` is in ascending order.
require(
i == 0 || uint160(address(params.strategies[i])) > uint160(address(params.strategies[i - 1])),
StrategiesMustBeInAscendingOrder()
);
// Check that `wadToSlash` is within acceptable bounds.
require(0 < params.wadsToSlash[i] && params.wadsToSlash[i] <= WAD, InvalidWadToSlash());
// Check that the operator set contains the strategy.
require(
_operatorSetStrategies[operatorSet.key()].contains(address(params.strategies[i])),
StrategyNotInOperatorSet()
);
// 1. Get the operator's allocation info for the strategy and operator set
(StrategyInfo memory info, Allocation memory allocation) =
_getUpdatedAllocation(params.operator, operatorSet.key(), params.strategies[i]);
// 2. Skip if the operator does not have a slashable allocation
// NOTE: this "if" is equivalent to: `if (!_isAllocationSlashable)`, because the other
// conditions in this method are already true (isOperatorSlashable + operatorSetStrategies.contains)
if (allocation.currentMagnitude == 0) {
continue;
}
// 3. Calculate the amount of magnitude being slashed, and subtract from
// the operator's currently-allocated magnitude, as well as the strategy's
// max and encumbered magnitudes
uint64 slashedMagnitude = uint64(uint256(allocation.currentMagnitude).mulWadRoundUp(params.wadsToSlash[i]));
uint64 prevMaxMagnitude = info.maxMagnitude;
wadSlashed[i] = uint256(slashedMagnitude).divWad(info.maxMagnitude);
allocation.currentMagnitude -= slashedMagnitude;
info.maxMagnitude -= slashedMagnitude;
info.encumberedMagnitude -= slashedMagnitude;
// 4. If there is a pending deallocation, reduce the pending deallocation proportionally.
// This ensures that when the deallocation is completed, less magnitude is freed.
if (allocation.pendingDiff < 0) {
uint64 slashedPending =
uint64(uint256(uint128(-allocation.pendingDiff)).mulWadRoundUp(params.wadsToSlash[i]));
allocation.pendingDiff += int128(uint128(slashedPending));
emit AllocationUpdated(
params.operator,
operatorSet,
params.strategies[i],
_addInt128(allocation.currentMagnitude, allocation.pendingDiff),
allocation.effectBlock
);
}
// 5. Update state
_updateAllocationInfo(params.operator, operatorSet.key(), params.strategies[i], info, allocation);
// Emit an event for the updated allocation
emit AllocationUpdated(
params.operator, operatorSet, params.strategies[i], allocation.currentMagnitude, uint32(block.number)
);
_updateMaxMagnitude(params.operator, params.strategies[i], info.maxMagnitude);
// 6. Slash operators shares in the DelegationManager
delegation.slashOperatorShares({
operator: params.operator,
strategy: params.strategies[i],
prevMaxMagnitude: prevMaxMagnitude,
newMaxMagnitude: info.maxMagnitude
});
}
emit OperatorSlashed(params.operator, operatorSet, params.strategies, wadSlashed, params.description);
}
/// @inheritdoc IAllocationManager
function modifyAllocations(
address operator,
AllocateParams[] memory params
) external onlyWhenNotPaused(PAUSED_MODIFY_ALLOCATIONS) {
// Check that the caller is allowed to modify allocations on behalf of the operator
// We do not use a modifier to avoid `stack too deep` errors
require(_checkCanCall(operator), InvalidCaller());
// Check that the operator exists and has configured an allocation delay
uint32 operatorAllocationDelay;
{
(bool isSet, uint32 delay) = getAllocationDelay(operator);
require(isSet, UninitializedAllocationDelay());
operatorAllocationDelay = delay;
}
for (uint256 i = 0; i < params.length; i++) {
require(params[i].strategies.length == params[i].newMagnitudes.length, InputArrayLengthMismatch());
// Check that the operator set exists and get the operator's registration status
// Operators do not need to be registered for an operator set in order to allocate
// slashable magnitude to the set. In fact, it is expected that operators will
// allocate magnitude before registering, as AVS's will likely only accept
// registrations from operators that are already slashable.
OperatorSet memory operatorSet = params[i].operatorSet;
require(_operatorSets[operatorSet.avs].contains(operatorSet.id), InvalidOperatorSet());
bool _isOperatorSlashable = isOperatorSlashable(operator, operatorSet);
for (uint256 j = 0; j < params[i].strategies.length; j++) {
IStrategy strategy = params[i].strategies[j];
// 1. If the operator has any pending deallocations for this strategy, clear them
// to free up magnitude for allocation. Fetch the operator's up to date allocation
// info and ensure there is no remaining pending modification.
_clearDeallocationQueue(operator, strategy, type(uint16).max);
(StrategyInfo memory info, Allocation memory allocation) =
_getUpdatedAllocation(operator, operatorSet.key(), strategy);
require(allocation.effectBlock == 0, ModificationAlreadyPending());
// 2. Check whether the operator's allocation is slashable. If not, we allow instant
// deallocation.
bool isSlashable = _isAllocationSlashable(operatorSet, strategy, allocation, _isOperatorSlashable);
// 3. Calculate the change in magnitude
allocation.pendingDiff = _calcDelta(allocation.currentMagnitude, params[i].newMagnitudes[j]);
require(allocation.pendingDiff != 0, SameMagnitude());
// 4. Handle deallocation/allocation
if (allocation.pendingDiff < 0) {
if (isSlashable) {
// If the operator is slashable, deallocated magnitude will be freed after
// the deallocation delay. This magnitude remains slashable until then.
deallocationQueue[operator][strategy].pushBack(operatorSet.key());
// deallocations are slashable in the window [block.number, block.number + deallocationDelay]
// therefore, the effectBlock is set to the block right after the slashable window
allocation.effectBlock = uint32(block.number) + DEALLOCATION_DELAY + 1;
} else {
// Deallocation immediately updates/frees magnitude if the operator is not slashable
info.encumberedMagnitude = _addInt128(info.encumberedMagnitude, allocation.pendingDiff);
allocation.currentMagnitude = params[i].newMagnitudes[j];
allocation.pendingDiff = 0;
allocation.effectBlock = uint32(block.number);
}
} else if (allocation.pendingDiff > 0) {
// Allocation immediately consumes available magnitude, but the additional
// magnitude does not become slashable until after the allocation delay
info.encumberedMagnitude = _addInt128(info.encumberedMagnitude, allocation.pendingDiff);
require(info.encumberedMagnitude <= info.maxMagnitude, InsufficientMagnitude());
allocation.effectBlock = uint32(block.number) + operatorAllocationDelay;
}
// 5. Update state
_updateAllocationInfo(operator, operatorSet.key(), strategy, info, allocation);
// 6. Emit an event for the updated allocation
emit AllocationUpdated(
operator,
operatorSet,
strategy,
_addInt128(allocation.currentMagnitude, allocation.pendingDiff),
allocation.effectBlock
);
}
}
}
/// @inheritdoc IAllocationManager
function clearDeallocationQueue(
address operator,
IStrategy[] calldata strategies,
uint16[] calldata numToClear
) external onlyWhenNotPaused(PAUSED_MODIFY_ALLOCATIONS) {
require(strategies.length == numToClear.length, InputArrayLengthMismatch());
for (uint256 i = 0; i < strategies.length; ++i) {
_clearDeallocationQueue({operator: operator, strategy: strategies[i], numToClear: numToClear[i]});
}
}
/// @inheritdoc IAllocationManager
function registerForOperatorSets(
address operator,
RegisterParams calldata params
) external onlyWhenNotPaused(PAUSED_OPERATOR_SET_REGISTRATION_AND_DEREGISTRATION) checkCanCall(operator) {
// Check that the operator exists
require(delegation.isOperator(operator), InvalidOperator());
for (uint256 i = 0; i < params.operatorSetIds.length; i++) {
// Check the operator set exists and the operator is not currently registered to it
OperatorSet memory operatorSet = OperatorSet(params.avs, params.operatorSetIds[i]);
require(_operatorSets[operatorSet.avs].contains(operatorSet.id), InvalidOperatorSet());
require(!isOperatorSlashable(operator, operatorSet), AlreadyMemberOfSet());
// Add operator to operator set
registeredSets[operator].add(operatorSet.key());
_operatorSetMembers[operatorSet.key()].add(operator);
emit OperatorAddedToOperatorSet(operator, operatorSet);
// Mark the operator registered
registrationStatus[operator][operatorSet.key()].registered = true;
}
// Call the AVS to complete registration. If the AVS reverts, registration will fail.
getAVSRegistrar(params.avs).registerOperator(operator, params.avs, params.operatorSetIds, params.data);
}
/// @inheritdoc IAllocationManager
function deregisterFromOperatorSets(
DeregisterParams calldata params
) external onlyWhenNotPaused(PAUSED_OPERATOR_SET_REGISTRATION_AND_DEREGISTRATION) {
// Check that the caller is either authorized on behalf of the operator or AVS
require(_checkCanCall(params.operator) || _checkCanCall(params.avs), InvalidCaller());
for (uint256 i = 0; i < params.operatorSetIds.length; i++) {
// Check the operator set exists and the operator is registered to it
OperatorSet memory operatorSet = OperatorSet(params.avs, params.operatorSetIds[i]);
require(_operatorSets[params.avs].contains(operatorSet.id), InvalidOperatorSet());
require(registrationStatus[params.operator][operatorSet.key()].registered, NotMemberOfSet());
// Remove operator from operator set
registeredSets[params.operator].remove(operatorSet.key());
_operatorSetMembers[operatorSet.key()].remove(params.operator);
emit OperatorRemovedFromOperatorSet(params.operator, operatorSet);
// Mark operator deregistered until the DEALLOCATION_DELAY passes
// forgefmt: disable-next-item
registrationStatus[params.operator][operatorSet.key()] = RegistrationStatus({
registered: false,
slashableUntil: uint32(block.number) + DEALLOCATION_DELAY
});
}
// Call the AVS to complete deregistration
getAVSRegistrar(params.avs).deregisterOperator(params.operator, params.avs, params.operatorSetIds);
}
/// @inheritdoc IAllocationManager
function setAllocationDelay(address operator, uint32 delay) external {
if (msg.sender != address(delegation)) {
require(_checkCanCall(operator), InvalidCaller());
require(delegation.isOperator(operator), InvalidOperator());
}
_setAllocationDelay(operator, delay);
}
/// @inheritdoc IAllocationManager
function setAVSRegistrar(address avs, IAVSRegistrar registrar) external checkCanCall(avs) {
// Check that the registrar is correctly configured to prevent an AVSRegistrar contract
// from being used with the wrong AVS
require(registrar.supportsAVS(avs), InvalidAVSRegistrar());
_avsRegistrar[avs] = registrar;
emit AVSRegistrarSet(avs, getAVSRegistrar(avs));
}
/// @inheritdoc IAllocationManager
function updateAVSMetadataURI(address avs, string calldata metadataURI) external checkCanCall(avs) {
if (!_avsRegisteredMetadata[avs]) {
_avsRegisteredMetadata[avs] = true;
}
emit AVSMetadataURIUpdated(avs, metadataURI);
}
/// @inheritdoc IAllocationManager
function createOperatorSets(address avs, CreateSetParams[] calldata params) external checkCanCall(avs) {
// Check that the AVS exists and has registered metadata
require(_avsRegisteredMetadata[avs], NonexistentAVSMetadata());
for (uint256 i = 0; i < params.length; i++) {
OperatorSet memory operatorSet = OperatorSet(avs, params[i].operatorSetId);
// Create the operator set, ensuring it does not already exist
require(_operatorSets[avs].add(operatorSet.id), InvalidOperatorSet());
emit OperatorSetCreated(OperatorSet(avs, operatorSet.id));
// Add strategies to the operator set
bytes32 operatorSetKey = operatorSet.key();
for (uint256 j = 0; j < params[i].strategies.length; j++) {
_operatorSetStrategies[operatorSetKey].add(address(params[i].strategies[j]));
emit StrategyAddedToOperatorSet(operatorSet, params[i].strategies[j]);
}
}
}
/// @inheritdoc IAllocationManager
function addStrategiesToOperatorSet(
address avs,
uint32 operatorSetId,
IStrategy[] calldata strategies
) external checkCanCall(avs) {
OperatorSet memory operatorSet = OperatorSet(avs, operatorSetId);
bytes32 operatorSetKey = operatorSet.key();
require(_operatorSets[avs].contains(operatorSet.id), InvalidOperatorSet());
for (uint256 i = 0; i < strategies.length; i++) {
require(_operatorSetStrategies[operatorSetKey].add(address(strategies[i])), StrategyAlreadyInOperatorSet());
emit StrategyAddedToOperatorSet(operatorSet, strategies[i]);
}
}
/// @inheritdoc IAllocationManager
function removeStrategiesFromOperatorSet(
address avs,
uint32 operatorSetId,
IStrategy[] calldata strategies
) external checkCanCall(avs) {
OperatorSet memory operatorSet = OperatorSet(avs, operatorSetId);
require(_operatorSets[avs].contains(operatorSet.id), InvalidOperatorSet());
bytes32 operatorSetKey = operatorSet.key();
for (uint256 i = 0; i < strategies.length; i++) {
require(_operatorSetStrategies[operatorSetKey].remove(address(strategies[i])), StrategyNotInOperatorSet());
emit StrategyRemovedFromOperatorSet(operatorSet, strategies[i]);
}
}
/**
*
* INTERNAL FUNCTIONS
*
*/
/**
* @dev Clear one or more pending deallocations to a strategy's allocated magnitude
* @param operator the operator whose pending deallocations will be cleared
* @param strategy the strategy to update
* @param numToClear the number of pending deallocations to clear
*/
function _clearDeallocationQueue(address operator, IStrategy strategy, uint16 numToClear) internal {
uint256 numCleared;
uint256 length = deallocationQueue[operator][strategy].length();
while (length > 0 && numCleared < numToClear) {
bytes32 operatorSetKey = deallocationQueue[operator][strategy].front();
(StrategyInfo memory info, Allocation memory allocation) =
_getUpdatedAllocation(operator, operatorSetKey, strategy);
// If we've reached a pending deallocation that isn't completable yet,
// we can stop. Any subsequent deallocation will also be uncompletable.
if (block.number < allocation.effectBlock) {
break;
}
// Update state. This completes the deallocation, because `_getUpdatedAllocation`
// gave us strategy/allocation info as if the deallocation was already completed.
_updateAllocationInfo(operator, operatorSetKey, strategy, info, allocation);
// Remove the deallocation from the queue
deallocationQueue[operator][strategy].popFront();
++numCleared;
--length;
}
}
/**
* @dev Sets the operator's allocation delay. This is the number of blocks between an operator
* allocating magnitude to an operator set, and the magnitude becoming slashable.
* @param operator The operator to set the delay on behalf of.
* @param delay The allocation delay in blocks.
*/
function _setAllocationDelay(address operator, uint32 delay) internal {
AllocationDelayInfo memory info = _allocationDelayInfo[operator];
// If there is a pending delay that can be applied now, set it
if (info.effectBlock != 0 && block.number >= info.effectBlock) {
info.delay = info.pendingDelay;
info.isSet = true;
}
info.pendingDelay = delay;
info.effectBlock = uint32(block.number) + ALLOCATION_CONFIGURATION_DELAY + 1;
_allocationDelayInfo[operator] = info;
emit AllocationDelaySet(operator, delay, info.effectBlock);
}
/// @notice returns whether the operator's allocation is slashable in the given operator set
function _isAllocationSlashable(
OperatorSet memory operatorSet,
IStrategy strategy,
Allocation memory allocation,
bool _isOperatorSlashable
) internal view returns (bool) {
/// forgefmt: disable-next-item
return
// If the operator set does not use this strategy, any allocation from it is not slashable
_operatorSetStrategies[operatorSet.key()].contains(address(strategy)) &&
// If the operator is not slashable by the operatorSet, any allocation is not slashable
_isOperatorSlashable &&
// If there is nothing allocated, the allocation is not slashable
allocation.currentMagnitude != 0;
}
/**
* @dev For an operator set, get the operator's effective allocated magnitude.
* If the operator set has a pending deallocation that can be completed at the
* current block number, this method returns a view of the allocation as if the deallocation
* was completed.
* @return info the effective allocated and pending magnitude for the operator set, and
* the effective encumbered magnitude for all operator sets belonging to this strategy
*/
function _getUpdatedAllocation(
address operator,
bytes32 operatorSetKey,
IStrategy strategy
) internal view returns (StrategyInfo memory, Allocation memory) {
StrategyInfo memory info = StrategyInfo({
maxMagnitude: _maxMagnitudeHistory[operator][strategy].latest(),
encumberedMagnitude: encumberedMagnitude[operator][strategy]
});
Allocation memory allocation = allocations[operator][operatorSetKey][strategy];
// If the pending change can't be completed yet, return as-is
if (block.number < allocation.effectBlock) {
return (info, allocation);
}
// Otherwise, complete the pending change and return updated info
allocation.currentMagnitude = _addInt128(allocation.currentMagnitude, allocation.pendingDiff);
// If the completed change was a deallocation, update used magnitude
if (allocation.pendingDiff < 0) {
info.encumberedMagnitude = _addInt128(info.encumberedMagnitude, allocation.pendingDiff);
}
allocation.effectBlock = 0;
allocation.pendingDiff = 0;
return (info, allocation);
}
function _updateAllocationInfo(
address operator,
bytes32 operatorSetKey,
IStrategy strategy,
StrategyInfo memory info,
Allocation memory allocation
) internal {
// Update encumbered magnitude if it has changed
// The mapping should NOT be updated when there is a deallocation on a delay
if (encumberedMagnitude[operator][strategy] != info.encumberedMagnitude) {
encumberedMagnitude[operator][strategy] = info.encumberedMagnitude;
emit EncumberedMagnitudeUpdated(operator, strategy, info.encumberedMagnitude);
}
// Update allocation for this operator set from the strategy
// We emit an `AllocationUpdated` from the `modifyAllocations` and `slashOperator` functions.
// `clearDeallocationQueue` does not emit an `AllocationUpdated` event since it was
// emitted when the deallocation was queued
allocations[operator][operatorSetKey][strategy] = allocation;
// Note: these no-op if the sets already contain the added values (or do not contain removed ones)
if (allocation.pendingDiff != 0) {
// If we have a pending modification, ensure the allocation is in the operator's
// list of enumerable strategies/sets.
allocatedStrategies[operator][operatorSetKey].add(address(strategy));
allocatedSets[operator].add(operatorSetKey);
} else if (allocation.currentMagnitude == 0) {
// If we do NOT have a pending modification, and no existing magnitude, remove the
// allocation from the operator's lists.
allocatedStrategies[operator][operatorSetKey].remove(address(strategy));
if (allocatedStrategies[operator][operatorSetKey].length() == 0) {
allocatedSets[operator].remove(operatorSetKey);
}
}
}
/**
* @dev Returns the minimum allocated stake at the future block.
* @param operatorSet The operator set to get the minimum allocated stake for.
* @param operators The operators to get the minimum allocated stake for.
* @param strategies The strategies to get the minimum allocated stake for.
* @param futureBlock The future block to get the minimum allocated stake for.
*/
function _getMinimumAllocatedStake(
OperatorSet memory operatorSet,
address[] memory operators,
IStrategy[] memory strategies,
uint32 futureBlock
) internal view returns (uint256[][] memory allocatedStake) {
allocatedStake = new uint256[][](operators.length);
uint256[][] memory delegatedStake = delegation.getOperatorsShares(operators, strategies);
for (uint256 i = 0; i < operators.length; i++) {
address operator = operators[i];
allocatedStake[i] = new uint256[](strategies.length);
for (uint256 j = 0; j < strategies.length; j++) {
IStrategy strategy = strategies[j];
// Fetch the max magnitude and allocation for the operator/strategy.
// Prevent division by 0 if needed. This mirrors the "FullySlashed" checks
// in the DelegationManager
uint64 maxMagnitude = _maxMagnitudeHistory[operator][strategy].latest();
if (maxMagnitude == 0) {
continue;
}
Allocation memory alloc = getAllocation(operator, operatorSet, strategy);
// If the pending change takes effect before `futureBlock`, include it in `currentMagnitude`
// However, ONLY include the pending change if it is a deallocation, since this method
// is supposed to return the minimum slashable stake between now and `futureBlock`
if (alloc.effectBlock <= futureBlock && alloc.pendingDiff < 0) {
alloc.currentMagnitude = _addInt128(alloc.currentMagnitude, alloc.pendingDiff);
}
uint256 slashableProportion = uint256(alloc.currentMagnitude).divWad(maxMagnitude);
allocatedStake[i][j] = delegatedStake[i][j].mulWad(slashableProportion);
}
}
}
function _updateMaxMagnitude(address operator, IStrategy strategy, uint64 newMaxMagnitude) internal {
_maxMagnitudeHistory[operator][strategy].push({key: uint32(block.number), value: newMaxMagnitude});
emit MaxMagnitudeUpdated(operator, strategy, newMaxMagnitude);
}
function _calcDelta(uint64 currentMagnitude, uint64 newMagnitude) internal pure returns (int128) {
return int128(uint128(newMagnitude)) - int128(uint128(currentMagnitude));
}
/// @dev Use safe casting when downcasting to uint64
function _addInt128(uint64 a, int128 b) internal pure returns (uint64) {
return uint256(int256(int128(uint128(a)) + b)).toUint64();
}
/**
*
* VIEW FUNCTIONS
*
*/
/// @inheritdoc IAllocationManager
function getOperatorSetCount(
address avs
) external view returns (uint256) {
return _operatorSets[avs].length();
}
/// @inheritdoc IAllocationManager
function getAllocatedSets(
address operator
) external view returns (OperatorSet[] memory) {
uint256 length = allocatedSets[operator].length();
OperatorSet[] memory operatorSets = new OperatorSet[](length);
for (uint256 i = 0; i < length; i++) {
operatorSets[i] = OperatorSetLib.decode(allocatedSets[operator].at(i));
}
return operatorSets;
}
/// @inheritdoc IAllocationManager
function getAllocatedStrategies(
address operator,
OperatorSet memory operatorSet
) external view returns (IStrategy[] memory) {
address[] memory values = allocatedStrategies[operator][operatorSet.key()].values();
IStrategy[] memory strategies;
assembly {
strategies := values
}
return strategies;
}
/// @inheritdoc IAllocationManager
function getAllocation(
address operator,
OperatorSet memory operatorSet,
IStrategy strategy
) public view returns (Allocation memory) {
(, Allocation memory allocation) = _getUpdatedAllocation(operator, operatorSet.key(), strategy);
return allocation;
}
/// @inheritdoc IAllocationManager
function getAllocations(
address[] memory operators,
OperatorSet memory operatorSet,
IStrategy strategy
) external view returns (Allocation[] memory) {
Allocation[] memory _allocations = new Allocation[](operators.length);
for (uint256 i = 0; i < operators.length; i++) {
_allocations[i] = getAllocation(operators[i], operatorSet, strategy);
}
return _allocations;
}
/// @inheritdoc IAllocationManager
function getStrategyAllocations(
address operator,
IStrategy strategy
) external view returns (OperatorSet[] memory, Allocation[] memory) {
uint256 length = allocatedSets[operator].length();
OperatorSet[] memory operatorSets = new OperatorSet[](length);
Allocation[] memory _allocations = new Allocation[](length);
for (uint256 i = 0; i < length; i++) {
OperatorSet memory operatorSet = OperatorSetLib.decode(allocatedSets[operator].at(i));
operatorSets[i] = operatorSet;
_allocations[i] = getAllocation(operator, operatorSet, strategy);
}
return (operatorSets, _allocations);
}
/// @inheritdoc IAllocationManager
function getEncumberedMagnitude(address operator, IStrategy strategy) external view returns (uint64) {
(uint64 curEncumberedMagnitude,) = _getFreeAndUsedMagnitude(operator, strategy);
return curEncumberedMagnitude;
}
/// @inheritdoc IAllocationManager
function getAllocatableMagnitude(address operator, IStrategy strategy) external view returns (uint64) {
(, uint64 curAllocatableMagnitude) = _getFreeAndUsedMagnitude(operator, strategy);
return curAllocatableMagnitude;
}
/// @dev For an operator, returns up-to-date amounts for current encumbered and available
/// magnitude. Note that these two values will always add up to the operator's max magnitude
/// for the strategy
function _getFreeAndUsedMagnitude(
address operator,
IStrategy strategy
) internal view returns (uint64 curEncumberedMagnitude, uint64 curAllocatableMagnitude) {
// This method needs to simulate clearing any pending deallocations.
// This roughly mimics the calculations done in `_clearDeallocationQueue` and
// `_getUpdatedAllocation`, while operating on a `curEncumberedMagnitude`
// rather than continually reading/updating state.
curEncumberedMagnitude = encumberedMagnitude[operator][strategy];
uint256 length = deallocationQueue[operator][strategy].length();
for (uint256 i = 0; i < length; ++i) {
bytes32 operatorSetKey = deallocationQueue[operator][strategy].at(i);
Allocation memory allocation = allocations[operator][operatorSetKey][strategy];
// If we've reached a pending deallocation that isn't completable yet,
// we can stop. Any subsequent modifications will also be uncompletable.
if (block.number < allocation.effectBlock) {
break;
}
// The diff is a deallocation. Add to encumbered magnitude. Note that this is a deallocation
// queue and allocations aren't considered because encumbered magnitude
// is updated as soon as the allocation is created.
curEncumberedMagnitude = _addInt128(curEncumberedMagnitude, allocation.pendingDiff);
}
// The difference between the operator's max magnitude and its encumbered magnitude
// is the magnitude that can be allocated.
curAllocatableMagnitude = _maxMagnitudeHistory[operator][strategy].latest() - curEncumberedMagnitude;
return (curEncumberedMagnitude, curAllocatableMagnitude);
}
/// @inheritdoc IAllocationManager
function getMaxMagnitude(address operator, IStrategy strategy) public view returns (uint64) {
return _maxMagnitudeHistory[operator][strategy].latest();
}
/// @inheritdoc IAllocationManager
function getMaxMagnitudes(
address operator,
IStrategy[] memory strategies
) external view returns (uint64[] memory) {
uint64[] memory maxMagnitudes = new uint64[](strategies.length);
for (uint256 i = 0; i < strategies.length; ++i) {
maxMagnitudes[i] = getMaxMagnitude(operator, strategies[i]);
}
return maxMagnitudes;
}
/// @inheritdoc IAllocationManager
function getMaxMagnitudes(address[] memory operators, IStrategy strategy) external view returns (uint64[] memory) {
uint64[] memory maxMagnitudes = new uint64[](operators.length);
for (uint256 i = 0; i < operators.length; ++i) {
maxMagnitudes[i] = getMaxMagnitude(operators[i], strategy);
}
return maxMagnitudes;
}
/// @inheritdoc IAllocationManager
function getMaxMagnitudesAtBlock(
address operator,
IStrategy[] memory strategies,
uint32 blockNumber
) external view returns (uint64[] memory) {
uint64[] memory maxMagnitudes = new uint64[](strategies.length);
for (uint256 i = 0; i < strategies.length; ++i) {
maxMagnitudes[i] = _maxMagnitudeHistory[operator][strategies[i]].upperLookup({key: blockNumber});
}
return maxMagnitudes;
}
/// @inheritdoc IAllocationManager
function getAllocationDelay(
address operator
) public view returns (bool, uint32) {
AllocationDelayInfo memory info = _allocationDelayInfo[operator];
uint32 delay = info.delay;
bool isSet = info.isSet;
// If there is a pending delay that can be applied, apply it
if (info.effectBlock != 0 && block.number >= info.effectBlock) {
delay = info.pendingDelay;
isSet = true;
}
return (isSet, delay);
}
/// @inheritdoc IAllocationManager
function getRegisteredSets(
address operator
) public view returns (OperatorSet[] memory) {
uint256 length = registeredSets[operator].length();
OperatorSet[] memory operatorSets = new OperatorSet[](length);
for (uint256 i = 0; i < length; ++i) {
operatorSets[i] = OperatorSetLib.decode(registeredSets[operator].at(i));
}
return operatorSets;
}
/// @inheritdoc IAllocationManager
function isMemberOfOperatorSet(address operator, OperatorSet memory operatorSet) public view returns (bool) {
return _operatorSetMembers[operatorSet.key()].contains(operator);
}
/// @inheritdoc IAllocationManager
function isOperatorSet(
OperatorSet memory operatorSet
) external view returns (bool) {
return _operatorSets[operatorSet.avs].contains(operatorSet.id);
}
/// @inheritdoc IAllocationManager
function getMembers(
OperatorSet memory operatorSet
) external view returns (address[] memory) {
return _operatorSetMembers[operatorSet.key()].values();
}
/// @inheritdoc IAllocationManager
function getMemberCount(
OperatorSet memory operatorSet
) external view returns (uint256) {
return _operatorSetMembers[operatorSet.key()].length();
}
/// @inheritdoc IAllocationManager
function getAVSRegistrar(
address avs
) public view returns (IAVSRegistrar) {
IAVSRegistrar registrar = _avsRegistrar[avs];
return address(registrar) == address(0) ? IAVSRegistrar(avs) : registrar;
}
/// @inheritdoc IAllocationManager
function getStrategiesInOperatorSet(
OperatorSet memory operatorSet
) external view returns (IStrategy[] memory) {
address[] memory values = _operatorSetStrategies[operatorSet.key()].values();
IStrategy[] memory strategies;
assembly {
strategies := values
}
return strategies;
}
/// @inheritdoc IAllocationManager
function getMinimumSlashableStake(
OperatorSet memory operatorSet,
address[] memory operators,
IStrategy[] memory strategies,
uint32 futureBlock
) external view returns (uint256[][] memory slashableStake) {
slashableStake = _getMinimumAllocatedStake(operatorSet, operators, strategies, futureBlock);
for (uint256 i = 0; i < operators.length; i++) {
// If the operator is not slashable by the opSet, all strategies should have a slashable stake of 0
if (!isOperatorSlashable(operators[i], operatorSet)) {
for (uint256 j = 0; j < strategies.length; j++) {
slashableStake[i][j] = 0;
}
}
}
}
/// @inheritdoc IAllocationManager
function getAllocatedStake(
OperatorSet memory operatorSet,
address[] memory operators,
IStrategy[] memory strategies
) public view returns (uint256[][] memory) {
/// This helper function returns the minimum allocated stake by taking into account deallocations at some `futureBlock`.
/// We use the block.number, as the `futureBlock`, meaning that no **future** deallocations are considered.
return _getMinimumAllocatedStake(operatorSet, operators, strategies, uint32(block.number));
}
/// @inheritdoc IAllocationManager
function isOperatorSlashable(address operator, OperatorSet memory operatorSet) public view returns (bool) {
RegistrationStatus memory status = registrationStatus[operator][operatorSet.key()];
// slashableUntil returns the last block the operator is slashable in so we check for
// less than or equal to
return status.registered || block.number <= status.slashableUntil;
}
}
````
## File: src/contracts/core/AllocationManagerStorage.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol";
import "../interfaces/IAllocationManager.sol";
import "../interfaces/IDelegationManager.sol";
import {Snapshots} from "../libraries/Snapshots.sol";
abstract contract AllocationManagerStorage is IAllocationManager {
using Snapshots for Snapshots.DefaultWadHistory;
// Constants
/// @dev Index for flag that pauses operator allocations/deallocations when set.
uint8 internal constant PAUSED_MODIFY_ALLOCATIONS = 0;
/// @dev Index for flag that pauses operator register/deregister to operator sets when set.
uint8 internal constant PAUSED_OPERATOR_SLASHING = 1;
/// @dev Index for flag that pauses operator register/deregister to operator sets when set.
uint8 internal constant PAUSED_OPERATOR_SET_REGISTRATION_AND_DEREGISTRATION = 2;
// Immutables
/// @notice The DelegationManager contract for EigenLayer
IDelegationManager public immutable delegation;
/// @notice Delay before deallocations are clearable and can be added back into freeMagnitude
/// In this window, deallocations still remain slashable by the operatorSet they were allocated to.
uint32 public immutable DEALLOCATION_DELAY;
/// @notice Delay before alloaction delay modifications take effect.
uint32 public immutable ALLOCATION_CONFIGURATION_DELAY;
// Mutatables
/// AVS => OPERATOR SET
/// @dev Contains the AVS's configured registrar contract that handles registration/deregistration
/// Note: if set to 0, defaults to the AVS's address
mapping(address avs => IAVSRegistrar) internal _avsRegistrar;
/// @dev Lists the operator set ids an AVS has created
mapping(address avs => EnumerableSet.UintSet) internal _operatorSets;
/// @dev Lists the strategies an AVS supports for an operator set
mapping(bytes32 operatorSetKey => EnumerableSet.AddressSet) internal _operatorSetStrategies;
/// @dev Lists the members of an AVS's operator set
mapping(bytes32 operatorSetKey => EnumerableSet.AddressSet) internal _operatorSetMembers;
/// OPERATOR => OPERATOR SET (REGISTRATION/DEREGISTRATION)
/// @notice Returns the allocation delay info for each `operator`; the delay and whether or not it's previously been set.
mapping(address operator => AllocationDelayInfo) internal _allocationDelayInfo;
/// @dev Lists the operator sets the operator is registered for. Note that an operator
/// can be registered without allocated stake. Likewise, an operator can allocate
/// without being registered.
mapping(address operator => EnumerableSet.Bytes32Set) internal registeredSets;
/// @dev Lists the operator sets the operator has outstanding allocations in.
mapping(address operator => EnumerableSet.Bytes32Set) internal allocatedSets;
/// @dev Contains the operator's registration status for an operator set.
mapping(address operator => mapping(bytes32 operatorSetKey => RegistrationStatus)) internal registrationStatus;
/// @dev For an operator set, lists all strategies an operator has outstanding allocations from.
mapping(address operator => mapping(bytes32 operatorSetKey => EnumerableSet.AddressSet)) internal
allocatedStrategies;
/// @dev For an operator set and strategy, the current allocated magnitude and any pending modification
mapping(address operator => mapping(bytes32 operatorSetKey => mapping(IStrategy strategy => Allocation))) internal
allocations;
/// OPERATOR => STRATEGY (MAX/USED AND DEALLOCATIONS)
/// @dev Contains a history of the operator's maximum magnitude for a given strategy
mapping(address operator => mapping(IStrategy strategy => Snapshots.DefaultWadHistory)) internal
_maxMagnitudeHistory;
/// @dev For a strategy, contains the amount of magnitude an operator has allocated to operator sets
mapping(address operator => mapping(IStrategy strategy => uint64)) internal encumberedMagnitude;
/// @dev For a strategy, keeps an ordered queue of operator sets that have pending deallocations
/// These must be completed in order to free up magnitude for future allocation
mapping(address operator => mapping(IStrategy strategy => DoubleEndedQueue.Bytes32Deque)) internal deallocationQueue;
/// @dev Lists the AVSs who has registered metadata and claimed itself as an AVS
/// @notice bool is not used and is always true if the avs has registered metadata
mapping(address avs => bool) internal _avsRegisteredMetadata;
// Construction
constructor(IDelegationManager _delegation, uint32 _DEALLOCATION_DELAY, uint32 _ALLOCATION_CONFIGURATION_DELAY) {
delegation = _delegation;
DEALLOCATION_DELAY = _DEALLOCATION_DELAY;
ALLOCATION_CONFIGURATION_DELAY = _ALLOCATION_CONFIGURATION_DELAY;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[36] private __gap;
}
````
## File: src/contracts/core/AVSDirectory.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol";
import "../mixins/SignatureUtilsMixin.sol";
import "../permissions/Pausable.sol";
import "./AVSDirectoryStorage.sol";
contract AVSDirectory is
Initializable,
OwnableUpgradeable,
Pausable,
AVSDirectoryStorage,
ReentrancyGuardUpgradeable,
SignatureUtilsMixin
{
/**
*
* INITIALIZING FUNCTIONS
*
*/
/**
* @dev Initializes the immutable addresses of the strategy manager, delegationManager,
* and eigenpodManager contracts
*/
constructor(
IDelegationManager _delegation,
IPauserRegistry _pauserRegistry,
string memory _version
) AVSDirectoryStorage(_delegation) Pausable(_pauserRegistry) SignatureUtilsMixin(_version) {
_disableInitializers();
}
/// @inheritdoc IAVSDirectory
function initialize(address initialOwner, uint256 initialPausedStatus) external initializer {
_setPausedStatus(initialPausedStatus);
_transferOwnership(initialOwner);
}
/**
*
* EXTERNAL FUNCTIONS
*
*/
/// @inheritdoc IAVSDirectory
function updateAVSMetadataURI(
string calldata metadataURI
) external override {
emit AVSMetadataURIUpdated(msg.sender, metadataURI);
}
/// @inheritdoc IAVSDirectory
function cancelSalt(
bytes32 salt
) external override {
// Mutate `operatorSaltIsSpent` to `true` to prevent future spending.
operatorSaltIsSpent[msg.sender][salt] = true;
}
/**
*
* LEGACY EXTERNAL FUNCTIONS - SUPPORT DEPRECATED IN FUTURE RELEASE AFTER SLASHING RELEASE
*
*/
/// @inheritdoc IAVSDirectory
function registerOperatorToAVS(
address operator,
ISignatureUtilsMixinTypes.SignatureWithSaltAndExpiry memory operatorSignature
) external override onlyWhenNotPaused(PAUSED_OPERATOR_REGISTER_DEREGISTER_TO_AVS) {
// Assert that the `operator` is not actively registered to the AVS.
require(
avsOperatorStatus[msg.sender][operator] != OperatorAVSRegistrationStatus.REGISTERED,
OperatorAlreadyRegisteredToAVS()
);
// Assert `operator` has not already spent `operatorSignature.salt`.
require(!operatorSaltIsSpent[operator][operatorSignature.salt], SaltSpent());
// Assert `operator` is a registered operator.
require(delegation.isOperator(operator), OperatorNotRegisteredToEigenLayer());
// Assert that `operatorSignature.signature` is a valid signature for the operator AVS registration.
_checkIsValidSignatureNow({
signer: operator,
signableDigest: calculateOperatorAVSRegistrationDigestHash({
operator: operator,
avs: msg.sender,
salt: operatorSignature.salt,
expiry: operatorSignature.expiry
}),
signature: operatorSignature.signature,
expiry: operatorSignature.expiry
});
// Mutate `operatorSaltIsSpent` to `true` to prevent future respending.
operatorSaltIsSpent[operator][operatorSignature.salt] = true;
// Set the operator as registered
avsOperatorStatus[msg.sender][operator] = OperatorAVSRegistrationStatus.REGISTERED;
emit OperatorAVSRegistrationStatusUpdated(operator, msg.sender, OperatorAVSRegistrationStatus.REGISTERED);
}
/// @inheritdoc IAVSDirectory
function deregisterOperatorFromAVS(
address operator
) external override onlyWhenNotPaused(PAUSED_OPERATOR_REGISTER_DEREGISTER_TO_AVS) {
// Assert that operator is registered for the AVS.
require(
avsOperatorStatus[msg.sender][operator] == OperatorAVSRegistrationStatus.REGISTERED,
OperatorNotRegisteredToAVS()
);
// Set the operator as deregistered
avsOperatorStatus[msg.sender][operator] = OperatorAVSRegistrationStatus.UNREGISTERED;
emit OperatorAVSRegistrationStatusUpdated(operator, msg.sender, OperatorAVSRegistrationStatus.UNREGISTERED);
}
/**
*
* VIEW FUNCTIONS
*
*/
/// @inheritdoc IAVSDirectory
function calculateOperatorAVSRegistrationDigestHash(
address operator,
address avs,
bytes32 salt,
uint256 expiry
) public view override returns (bytes32) {
return _calculateSignableDigest(
keccak256(abi.encode(OPERATOR_AVS_REGISTRATION_TYPEHASH, operator, avs, salt, expiry))
);
}
}
````
## File: src/contracts/core/AVSDirectoryStorage.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IAVSDirectory.sol";
import "../interfaces/IDelegationManager.sol";
abstract contract AVSDirectoryStorage is IAVSDirectory {
// Constants
/// @notice The EIP-712 typehash for the `Registration` struct used by the contract
bytes32 public constant OPERATOR_AVS_REGISTRATION_TYPEHASH =
keccak256("OperatorAVSRegistration(address operator,address avs,bytes32 salt,uint256 expiry)");
/// @notice The EIP-712 typehash for the `OperatorSetRegistration` struct used by the contract
bytes32 public constant OPERATOR_SET_REGISTRATION_TYPEHASH =
keccak256("OperatorSetRegistration(address avs,uint32[] operatorSetIds,bytes32 salt,uint256 expiry)");
/// @notice The EIP-712 typehash for the `OperatorSetMembership` struct used by the contract
bytes32 public constant OPERATOR_SET_FORCE_DEREGISTRATION_TYPEHASH =
keccak256("OperatorSetForceDeregistration(address avs,uint32[] operatorSetIds,bytes32 salt,uint256 expiry)");
/// @dev Index for flag that pauses operator register/deregister to avs when set.
uint8 internal constant PAUSED_OPERATOR_REGISTER_DEREGISTER_TO_AVS = 0;
/// @dev Index for flag that pauses operator register/deregister to operator sets when set.
uint8 internal constant PAUSED_OPERATOR_SET_REGISTRATION_AND_DEREGISTRATION = 1;
// Immutables
/// @notice The DelegationManager contract for EigenLayer
IDelegationManager public immutable delegation;
// Mutatables
/// @dev Do not remove, deprecated storage.
bytes32 internal __deprecated_DOMAIN_SEPARATOR;
/// @notice Returns the registration status of each `operator` for a given `avs`.
/// @dev This storage will be deprecated once M2-based deregistration is removed.
mapping(address avs => mapping(address operator => OperatorAVSRegistrationStatus)) public avsOperatorStatus;
/// @notice Returns whether a `salt` has been used by a given `operator`.
mapping(address operator => mapping(bytes32 salt => bool isSpent)) public operatorSaltIsSpent;
// Construction
constructor(
IDelegationManager _delegation
) {
delegation = _delegation;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[47] private __gap;
}
````
## File: src/contracts/core/DelegationManager.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol";
import "../mixins/SignatureUtilsMixin.sol";
import "../mixins/PermissionControllerMixin.sol";
import "../permissions/Pausable.sol";
import "../libraries/SlashingLib.sol";
import "../libraries/Snapshots.sol";
import "./DelegationManagerStorage.sol";
/**
* @title DelegationManager
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice This is the contract for delegation in EigenLayer. The main functionalities of this contract are
* - enabling anyone to register as an operator in EigenLayer
* - allowing operators to specify parameters related to stakers who delegate to them
* - enabling any staker to delegate its stake to the operator of its choice (a given staker can only delegate to a single operator at a time)
* - enabling a staker to undelegate its assets from the operator it is delegated to (performed as part of the withdrawal process, initiated through the StrategyManager)
*/
contract DelegationManager is
Initializable,
OwnableUpgradeable,
Pausable,
DelegationManagerStorage,
ReentrancyGuardUpgradeable,
PermissionControllerMixin,
SignatureUtilsMixin
{
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
modifier onlyStrategyManagerOrEigenPodManager() {
require(
(msg.sender == address(strategyManager) || msg.sender == address(eigenPodManager)),
OnlyStrategyManagerOrEigenPodManager()
);
_;
}
modifier onlyEigenPodManager() {
require(msg.sender == address(eigenPodManager), OnlyEigenPodManager());
_;
}
modifier onlyAllocationManager() {
require(msg.sender == address(allocationManager), OnlyAllocationManager());
_;
}
/**
*
* INITIALIZING FUNCTIONS
*
*/
/**
* @dev Initializes the immutable addresses of the strategy manager, eigenpod manager, and allocation manager.
*/
constructor(
IStrategyManager _strategyManager,
IEigenPodManager _eigenPodManager,
IAllocationManager _allocationManager,
IPauserRegistry _pauserRegistry,
IPermissionController _permissionController,
uint32 _MIN_WITHDRAWAL_DELAY,
string memory _version
)
DelegationManagerStorage(_strategyManager, _eigenPodManager, _allocationManager, _MIN_WITHDRAWAL_DELAY)
Pausable(_pauserRegistry)
PermissionControllerMixin(_permissionController)
SignatureUtilsMixin(_version)
{
_disableInitializers();
}
function initialize(address initialOwner, uint256 initialPausedStatus) external initializer {
_setPausedStatus(initialPausedStatus);
_transferOwnership(initialOwner);
}
/**
*
* EXTERNAL FUNCTIONS
*
*/
/// @inheritdoc IDelegationManager
function registerAsOperator(
address initDelegationApprover,
uint32 allocationDelay,
string calldata metadataURI
) external nonReentrant {
require(!isDelegated(msg.sender), ActivelyDelegated());
allocationManager.setAllocationDelay(msg.sender, allocationDelay);
_setDelegationApprover(msg.sender, initDelegationApprover);
// delegate from the operator to themselves
_delegate(msg.sender, msg.sender);
emit OperatorRegistered(msg.sender, initDelegationApprover);
emit OperatorMetadataURIUpdated(msg.sender, metadataURI);
}
/// @inheritdoc IDelegationManager
function modifyOperatorDetails(
address operator,
address newDelegationApprover
) external checkCanCall(operator) nonReentrant {
require(isOperator(operator), OperatorNotRegistered());
_setDelegationApprover(operator, newDelegationApprover);
}
/// @inheritdoc IDelegationManager
function updateOperatorMetadataURI(address operator, string calldata metadataURI) external checkCanCall(operator) {
require(isOperator(operator), OperatorNotRegistered());
emit OperatorMetadataURIUpdated(operator, metadataURI);
}
/// @inheritdoc IDelegationManager
function delegateTo(
address operator,
SignatureWithExpiry memory approverSignatureAndExpiry,
bytes32 approverSalt
) public nonReentrant {
require(!isDelegated(msg.sender), ActivelyDelegated());
require(isOperator(operator), OperatorNotRegistered());
// If the operator has a `delegationApprover`, check the provided signature
_checkApproverSignature({
staker: msg.sender,
operator: operator,
signature: approverSignatureAndExpiry,
salt: approverSalt
});
// Delegate msg.sender to the operator
_delegate(msg.sender, operator);
}
/// @inheritdoc IDelegationManager
function undelegate(
address staker
) public nonReentrant returns (bytes32[] memory withdrawalRoots) {
// Check that the `staker` can undelegate
require(isDelegated(staker), NotActivelyDelegated());
require(!isOperator(staker), OperatorsCannotUndelegate());
// If the action is not being initiated by the staker, validate that it is initiated
// by the operator or their delegationApprover.
if (msg.sender != staker) {
address operator = delegatedTo[staker];
require(_checkCanCall(operator) || msg.sender == delegationApprover(operator), CallerCannotUndelegate());
emit StakerForceUndelegated(staker, operator);
}
return _undelegate(staker);
}
/// @inheritdoc IDelegationManager
function redelegate(
address newOperator,
SignatureWithExpiry memory newOperatorApproverSig,
bytes32 approverSalt
) external returns (bytes32[] memory withdrawalRoots) {
withdrawalRoots = undelegate(msg.sender);
// delegateTo uses msg.sender as staker
delegateTo(newOperator, newOperatorApproverSig, approverSalt);
}
/// @inheritdoc IDelegationManager
function queueWithdrawals(
QueuedWithdrawalParams[] calldata params
) external onlyWhenNotPaused(PAUSED_ENTER_WITHDRAWAL_QUEUE) nonReentrant returns (bytes32[] memory) {
bytes32[] memory withdrawalRoots = new bytes32[](params.length);
address operator = delegatedTo[msg.sender];
for (uint256 i = 0; i < params.length; i++) {
require(params[i].strategies.length == params[i].depositShares.length, InputArrayLengthMismatch());
uint256[] memory slashingFactors = _getSlashingFactors(msg.sender, operator, params[i].strategies);
// Remove shares from staker's strategies and place strategies/shares in queue.
// If the staker is delegated to an operator, the operator's delegated shares are also reduced
// NOTE: This will fail if the staker doesn't have the shares implied by the input parameters.
// The view function getWithdrawableShares() can be used to check what shares are available for withdrawal.
withdrawalRoots[i] = _removeSharesAndQueueWithdrawal({
staker: msg.sender,
operator: operator,
strategies: params[i].strategies,
depositSharesToWithdraw: params[i].depositShares,
slashingFactors: slashingFactors
});
}
return withdrawalRoots;
}
/// @inheritdoc IDelegationManager
function completeQueuedWithdrawal(
Withdrawal calldata withdrawal,
IERC20[] calldata tokens,
bool receiveAsTokens
) external onlyWhenNotPaused(PAUSED_EXIT_WITHDRAWAL_QUEUE) nonReentrant {
_completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens);
}
/// @inheritdoc IDelegationManager
function completeQueuedWithdrawals(
Withdrawal[] calldata withdrawals,
IERC20[][] calldata tokens,
bool[] calldata receiveAsTokens
) external onlyWhenNotPaused(PAUSED_EXIT_WITHDRAWAL_QUEUE) nonReentrant {
uint256 n = withdrawals.length;
for (uint256 i; i < n; ++i) {
_completeQueuedWithdrawal(withdrawals[i], tokens[i], receiveAsTokens[i]);
}
}
/// @inheritdoc IDelegationManager
function increaseDelegatedShares(
address staker,
IStrategy strategy,
uint256 prevDepositShares,
uint256 addedShares
) external onlyStrategyManagerOrEigenPodManager nonReentrant {
/// Note: Unlike `decreaseDelegatedShares`, we don't return early if the staker has no operator.
/// This is because `_increaseDelegation` updates the staker's deposit scaling factor, which we
/// need to do even if not delegated.
address operator = delegatedTo[staker];
uint64 maxMagnitude = allocationManager.getMaxMagnitude(operator, strategy);
uint256 slashingFactor = _getSlashingFactor(staker, strategy, maxMagnitude);
// Increase the staker's deposit scaling factor and delegate shares to the operator
_increaseDelegation({
operator: operator,
staker: staker,
strategy: strategy,
prevDepositShares: prevDepositShares,
addedShares: addedShares,
slashingFactor: slashingFactor
});
}
/// @inheritdoc IDelegationManager
function decreaseDelegatedShares(
address staker,
uint256 curDepositShares,
uint64 beaconChainSlashingFactorDecrease
) external onlyEigenPodManager nonReentrant {
if (!isDelegated(staker)) {
return;
}
address operator = delegatedTo[staker];
// Calculate the shares to remove from the operator by calculating difference in shares
// from the newly updated beaconChainSlashingFactor
uint64 maxMagnitude = allocationManager.getMaxMagnitude(operator, beaconChainETHStrategy);
DepositScalingFactor memory dsf = _depositScalingFactor[staker][beaconChainETHStrategy];
uint256 sharesToRemove = dsf.calcWithdrawable({
depositShares: curDepositShares,
slashingFactor: maxMagnitude.mulWad(beaconChainSlashingFactorDecrease)
});
// Decrease the operator's shares
_decreaseDelegation({
operator: operator,
staker: staker,
strategy: beaconChainETHStrategy,
sharesToDecrease: sharesToRemove
});
}
/// @inheritdoc IDelegationManager
function slashOperatorShares(
address operator,
IStrategy strategy,
uint64 prevMaxMagnitude,
uint64 newMaxMagnitude
) external onlyAllocationManager nonReentrant {
/// forgefmt: disable-next-item
uint256 operatorSharesSlashed = SlashingLib.calcSlashedAmount({
operatorShares: operatorShares[operator][strategy],
prevMaxMagnitude: prevMaxMagnitude,
newMaxMagnitude: newMaxMagnitude
});
uint256 scaledSharesSlashedFromQueue = _getSlashableSharesInQueue({
operator: operator,
strategy: strategy,
prevMaxMagnitude: prevMaxMagnitude,
newMaxMagnitude: newMaxMagnitude
});
// Calculate the total deposit shares to burn - slashed operator shares plus still-slashable
// shares sitting in the withdrawal queue.
uint256 totalDepositSharesToBurn = operatorSharesSlashed + scaledSharesSlashedFromQueue;
// Remove shares from operator
_decreaseDelegation({
operator: operator,
staker: address(0), // we treat this as a decrease for the 0-staker (only used for events)
strategy: strategy,
sharesToDecrease: operatorSharesSlashed
});
// Emit event for operator shares being slashed
emit OperatorSharesSlashed(operator, strategy, totalDepositSharesToBurn);
IShareManager shareManager = _getShareManager(strategy);
// NOTE: for beaconChainETHStrategy, increased burnable shares currently have no mechanism for burning
shareManager.increaseBurnableShares(strategy, totalDepositSharesToBurn);
}
/**
*
* INTERNAL FUNCTIONS
*
*/
/**
* @notice Sets operator parameters in the `_operatorDetails` mapping.
* @param operator The account registered as an operator updating their operatorDetails
* @param newDelegationApprover The new parameters for the operator
*/
function _setDelegationApprover(address operator, address newDelegationApprover) internal {
_operatorDetails[operator].delegationApprover = newDelegationApprover;
emit DelegationApproverUpdated(operator, newDelegationApprover);
}
/**
* @notice Delegates *from* a `staker` *to* an `operator`.
* @param staker The address to delegate *from* -- this address is delegating control of its own assets.
* @param operator The address to delegate *to* -- this address is being given power to place the `staker`'s assets at risk on services
* @dev Assumes the following is checked before calling this function:
* 1) the `staker` is not already delegated to an operator
* 2) the `operator` has indeed registered as an operator in EigenLayer
* 3) if applicable, the `operator's` `delegationApprover` signed off on delegation
* Ensures that:
* 1) new delegations are not paused (PAUSED_NEW_DELEGATION)
*/
function _delegate(address staker, address operator) internal onlyWhenNotPaused(PAUSED_NEW_DELEGATION) {
// When a staker is not delegated to an operator, their deposit shares are equal to their
// withdrawable shares -- except for the beaconChainETH strategy, which is handled below
(IStrategy[] memory strategies, uint256[] memory withdrawableShares) = getDepositedShares(staker);
// Retrieve the amount of slashing experienced by the operator in each strategy so far.
// When delegating, we "forgive" the staker for this slashing by adjusting their
// deposit scaling factor.
uint256[] memory operatorSlashingFactors = _getSlashingFactors(address(0), operator, strategies);
// Delegate to the operator
delegatedTo[staker] = operator;
emit StakerDelegated(staker, operator);
for (uint256 i = 0; i < strategies.length; ++i) {
// Special case for beacon chain slashing - ensure the staker's beacon chain slashing is
// reflected in the number of shares they delegate.
if (strategies[i] == beaconChainETHStrategy) {
uint64 stakerBeaconChainSlashing = eigenPodManager.beaconChainSlashingFactor(staker);
DepositScalingFactor memory dsf = _depositScalingFactor[staker][strategies[i]];
withdrawableShares[i] = dsf.calcWithdrawable(withdrawableShares[i], stakerBeaconChainSlashing);
}
// forgefmt: disable-next-item
_increaseDelegation({
operator: operator,
staker: staker,
strategy: strategies[i],
prevDepositShares: uint256(0),
addedShares: withdrawableShares[i],
slashingFactor: operatorSlashingFactors[i]
});
}
}
/**
* @dev Undelegates `staker` from their operator, queueing a withdrawal for all
* their deposited shares in the process.
* @dev Assumes the following is checked before calling this function:
* 1) the `staker` is currently delegated to an operator
* 2) the `staker` is not an operator themselves
* Ensures that:
* 1) the withdrawal queue is not paused (PAUSED_ENTER_WITHDRAWAL_QUEUE)
*/
function _undelegate(
address staker
) internal onlyWhenNotPaused(PAUSED_ENTER_WITHDRAWAL_QUEUE) returns (bytes32[] memory withdrawalRoots) {
// Undelegate the staker
address operator = delegatedTo[staker];
delegatedTo[staker] = address(0);
emit StakerUndelegated(staker, operator);
// Get all of the staker's deposited strategies/shares. These will be removed from the operator
// and queued for withdrawal.
(IStrategy[] memory strategies, uint256[] memory depositedShares) = getDepositedShares(staker);
if (strategies.length == 0) {
return withdrawalRoots;
}
// For the operator and each of the staker's strategies, get the slashing factors to apply
// when queueing for withdrawal
withdrawalRoots = new bytes32[](strategies.length);
uint256[] memory slashingFactors = _getSlashingFactors(staker, operator, strategies);
// Queue a withdrawal for each strategy independently. This is done for UX reasons.
for (uint256 i = 0; i < strategies.length; i++) {
IStrategy[] memory singleStrategy = new IStrategy[](1);
uint256[] memory singleDepositShares = new uint256[](1);
uint256[] memory singleSlashingFactor = new uint256[](1);
singleStrategy[0] = strategies[i];
singleDepositShares[0] = depositedShares[i];
singleSlashingFactor[0] = slashingFactors[i];
// Remove shares from staker's strategies and place strategies/shares in queue.
// The operator's delegated shares are also reduced.
withdrawalRoots[i] = _removeSharesAndQueueWithdrawal({
staker: staker,
operator: operator,
strategies: singleStrategy,
depositSharesToWithdraw: singleDepositShares,
slashingFactors: singleSlashingFactor
});
}
return withdrawalRoots;
}
/**
* @notice Removes `sharesToWithdraw` in `strategies` from `staker` who is currently delegated to `operator` and queues a withdrawal to the `withdrawer`.
* @param staker The staker queuing a withdrawal
* @param operator The operator the staker is delegated to
* @param strategies The strategies to queue a withdrawal for
* @param depositSharesToWithdraw The amount of deposit shares the staker wishes to withdraw, must be less than staker's depositShares in storage
* @param slashingFactors The corresponding slashing factor for the staker/operator for each strategy
*
* @dev The amount withdrawable by the staker may not actually be the same as the depositShares that are in storage in the StrategyManager/EigenPodManager.
* This is a result of any slashing that has occurred during the time the staker has been delegated to an operator. So the proportional amount that is withdrawn
* out of the amount withdrawable for the staker has to also be decremented from the staker's deposit shares.
* So the amount of depositShares withdrawn out has to be proportionally scaled down depending on the slashing that has occurred.
* Ex. Suppose as a staker, I have 100 depositShares for a strategy thats sitting in the StrategyManager in the `stakerDepositShares` mapping but I actually have been slashed 50%
* and my real withdrawable amount is 50 shares.
* Now when I go to withdraw 40 depositShares, I'm proportionally withdrawing 40% of my withdrawable shares. We calculate below the actual shares withdrawn via the `toShares()` function to
* get 20 shares to queue withdraw. The end state is that I have 60 depositShares and 30 withdrawable shares now, this still accurately reflects a 50% slashing that has occurred on my existing stake.
* @dev depositSharesToWithdraw are converted to sharesToWithdraw using the `toShares` library function. sharesToWithdraw are then divided by the current maxMagnitude of the operator (at queue time)
* and this value is stored in the Withdrawal struct as `scaledShares.
* Upon completion the `scaledShares` are then multiplied by the maxMagnitude of the operator at completion time. This is how we factor in any slashing events
* that occurred during the withdrawal delay period. Shares in a withdrawal are no longer slashable once the withdrawal is completable.
* @dev If the `operator` is indeed an operator, then the operator's delegated shares in the `strategies` are also decreased appropriately.
*/
function _removeSharesAndQueueWithdrawal(
address staker,
address operator,
IStrategy[] memory strategies,
uint256[] memory depositSharesToWithdraw,
uint256[] memory slashingFactors
) internal returns (bytes32) {
require(staker != address(0), InputAddressZero());
require(strategies.length != 0, InputArrayLengthZero());
uint256[] memory scaledShares = new uint256[](strategies.length);
uint256[] memory withdrawableShares = new uint256[](strategies.length);
// Remove shares from staker and operator
// Each of these operations fail if we attempt to remove more shares than exist
for (uint256 i = 0; i < strategies.length; ++i) {
IShareManager shareManager = _getShareManager(strategies[i]);
DepositScalingFactor memory dsf = _depositScalingFactor[staker][strategies[i]];
// Calculate how many shares can be withdrawn after factoring in slashing
withdrawableShares[i] = dsf.calcWithdrawable(depositSharesToWithdraw[i], slashingFactors[i]);
// Scale shares for queue withdrawal
scaledShares[i] = dsf.scaleForQueueWithdrawal(depositSharesToWithdraw[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,
staker: staker,
strategy: strategies[i],
sharesToDecrease: withdrawableShares[i]
});
}
// Remove deposit shares from EigenPodManager/StrategyManager
uint256 sharesAfter = shareManager.removeDepositShares(staker, strategies[i], depositSharesToWithdraw[i]);
if (sharesAfter == 0) {
_depositScalingFactor[staker][strategies[i]].reset();
}
}
// Create queue entry and increment withdrawal nonce
uint256 nonce = cumulativeWithdrawalsQueued[staker];
cumulativeWithdrawalsQueued[staker]++;
Withdrawal memory withdrawal = Withdrawal({
staker: staker,
delegatedTo: operator,
withdrawer: staker,
nonce: nonce,
startBlock: uint32(block.number),
strategies: strategies,
scaledShares: scaledShares
});
bytes32 withdrawalRoot = calculateWithdrawalRoot(withdrawal);
pendingWithdrawals[withdrawalRoot] = true;
_queuedWithdrawals[withdrawalRoot] = withdrawal;
_stakerQueuedWithdrawalRoots[staker].add(withdrawalRoot);
emit SlashingWithdrawalQueued(withdrawalRoot, withdrawal, withdrawableShares);
return withdrawalRoot;
}
/**
* @dev This function completes a queued withdrawal for a staker.
* This will apply any slashing that has occurred since the the withdrawal was queued by multiplying the withdrawal's
* scaledShares by the operator's maxMagnitude for each strategy. This ensures that any slashing that has occurred
* during the period the withdrawal was queued until its slashableUntil block is applied to the withdrawal amount.
* If receiveAsTokens is true, then these shares will be withdrawn as tokens.
* If receiveAsTokens is false, then they will be redeposited according to the current operator the staker is delegated to,
* and added back to the operator's delegatedShares.
*/
function _completeQueuedWithdrawal(
Withdrawal memory withdrawal,
IERC20[] calldata tokens,
bool receiveAsTokens
) internal {
require(tokens.length == withdrawal.strategies.length, InputArrayLengthMismatch());
require(msg.sender == withdrawal.withdrawer, WithdrawerNotCaller());
bytes32 withdrawalRoot = calculateWithdrawalRoot(withdrawal);
require(pendingWithdrawals[withdrawalRoot], WithdrawalNotQueued());
uint256[] memory prevSlashingFactors;
{
// slashableUntil is block inclusive so we need to check if the current block is strictly greater than the slashableUntil block
// meaning the withdrawal can be completed.
uint32 slashableUntil = withdrawal.startBlock + MIN_WITHDRAWAL_DELAY_BLOCKS;
require(uint32(block.number) > slashableUntil, WithdrawalDelayNotElapsed());
// Given the max magnitudes of the operator the staker was originally delegated to, calculate
// the slashing factors for each of the withdrawal's strategies.
prevSlashingFactors = _getSlashingFactorsAtBlock({
staker: withdrawal.staker,
operator: withdrawal.delegatedTo,
strategies: withdrawal.strategies,
blockNumber: slashableUntil
});
}
// Remove the withdrawal from the queue. Note that for legacy withdrawals, the removals
// from `_stakerQueuedWithdrawalRoots` and `queuedWithdrawals` will no-op.
_stakerQueuedWithdrawalRoots[withdrawal.staker].remove(withdrawalRoot);
delete _queuedWithdrawals[withdrawalRoot];
delete pendingWithdrawals[withdrawalRoot];
emit SlashingWithdrawalCompleted(withdrawalRoot);
// Given the max magnitudes of the operator the staker is now delegated to, calculate the current
// slashing factors to apply to each withdrawal if it is received as shares.
address newOperator = delegatedTo[withdrawal.staker];
uint256[] memory newSlashingFactors = _getSlashingFactors(withdrawal.staker, newOperator, withdrawal.strategies);
for (uint256 i = 0; i < withdrawal.strategies.length; i++) {
IShareManager shareManager = _getShareManager(withdrawal.strategies[i]);
// Calculate how much slashing to apply, as well as shares to withdraw
uint256 sharesToWithdraw = SlashingLib.scaleForCompleteWithdrawal({
scaledShares: withdrawal.scaledShares[i],
slashingFactor: prevSlashingFactors[i]
});
//Do nothing if 0 shares to withdraw
if (sharesToWithdraw == 0) {
continue;
}
if (receiveAsTokens) {
// Withdraws `shares` in `strategy` to `withdrawer`. If the shares are virtual beaconChainETH shares,
// then a call is ultimately forwarded to the `staker`s EigenPod; otherwise a call is ultimately forwarded
// to the `strategy` with info on the `token`.
shareManager.withdrawSharesAsTokens({
staker: withdrawal.staker,
strategy: withdrawal.strategies[i],
token: tokens[i],
shares: sharesToWithdraw
});
} else {
// Award shares back in StrategyManager/EigenPodManager.
(uint256 prevDepositShares, uint256 addedShares) = shareManager.addShares({
staker: withdrawal.staker,
strategy: withdrawal.strategies[i],
shares: sharesToWithdraw
});
// Update the staker's deposit scaling factor and delegate shares to their operator
_increaseDelegation({
operator: newOperator,
staker: withdrawal.staker,
strategy: withdrawal.strategies[i],
prevDepositShares: prevDepositShares,
addedShares: addedShares,
slashingFactor: newSlashingFactors[i]
});
}
}
}
/**
* @notice Increases `operator`s depositedShares in `strategy` based on staker's addedDepositShares
* and updates the staker's depositScalingFactor for the strategy.
* @param operator The operator to increase the delegated delegatedShares for
* @param staker The staker to increase the depositScalingFactor for
* @param strategy The strategy to increase the delegated delegatedShares and the depositScalingFactor for
* @param prevDepositShares The number of delegated deposit shares the staker had in the strategy prior to the increase
* @param addedShares The shares added to the staker in the StrategyManager/EigenPodManager
* @param slashingFactor The current slashing factor for the staker/operator/strategy
*/
function _increaseDelegation(
address operator,
address staker,
IStrategy strategy,
uint256 prevDepositShares,
uint256 addedShares,
uint256 slashingFactor
) internal {
// Ensure that the operator has not been fully slashed for a strategy
// and that the staker has not been fully slashed if it is the beaconChainStrategy
// This is to prevent a divWad by 0 when updating the depositScalingFactor
require(slashingFactor != 0, FullySlashed());
// If `addedShares` is 0, do nothing
if (addedShares == 0) {
return;
}
// Update the staker's depositScalingFactor. This only results in an update
// if the slashing factor has changed for this strategy.
DepositScalingFactor storage dsf = _depositScalingFactor[staker][strategy];
dsf.update(prevDepositShares, addedShares, slashingFactor);
emit DepositScalingFactorUpdated(staker, strategy, dsf.scalingFactor());
// If the staker is delegated to an operator, update the operator's shares
if (isDelegated(staker)) {
operatorShares[operator][strategy] += addedShares;
emit OperatorSharesIncreased(operator, staker, strategy, addedShares);
}
}
/**
* @notice Decreases `operator`s shares in `strategy` based on staker's removed shares
* @param operator The operator to decrease the delegated delegated shares for
* @param staker The staker to decrease the delegated delegated shares for
* @param strategy The strategy to decrease the delegated delegated shares for
* @param sharesToDecrease The shares to remove from the operator's delegated shares
*/
function _decreaseDelegation(
address operator,
address staker,
IStrategy strategy,
uint256 sharesToDecrease
) internal {
// Decrement operator shares
operatorShares[operator][strategy] -= sharesToDecrease;
emit OperatorSharesDecreased(operator, staker, strategy, sharesToDecrease);
}
/// @dev If `operator` has configured a `delegationApprover`, check that `signature` and `salt`
/// are a valid approval for `staker` delegating to `operator`.
function _checkApproverSignature(
address staker,
address operator,
SignatureWithExpiry memory signature,
bytes32 salt
) internal {
address approver = _operatorDetails[operator].delegationApprover;
if (approver == address(0)) {
return;
}
// Check that the salt hasn't been used previously, then mark the salt as spent
require(!delegationApproverSaltIsSpent[approver][salt], SaltSpent());
delegationApproverSaltIsSpent[approver][salt] = true;
// Validate the signature
_checkIsValidSignatureNow({
signer: approver,
signableDigest: calculateDelegationApprovalDigestHash(staker, operator, approver, salt, signature.expiry),
signature: signature.signature,
expiry: signature.expiry
});
}
/// @dev Calculate the amount of slashing to apply to the staker's shares.
/// @dev Be mindful of rounding in `mulWad()`, it's possible for the slashing factor to round down to 0
/// even when both operatorMaxMagnitude and beaconChainSlashingFactor are non-zero. This is only possible
/// in an edge case where the operator has a very low maxMagnitude.
function _getSlashingFactor(
address staker,
IStrategy strategy,
uint64 operatorMaxMagnitude
) internal view returns (uint256) {
if (strategy == beaconChainETHStrategy) {
uint64 beaconChainSlashingFactor = eigenPodManager.beaconChainSlashingFactor(staker);
return operatorMaxMagnitude.mulWad(beaconChainSlashingFactor);
}
return operatorMaxMagnitude;
}
/// @dev Calculate the amount of slashing to apply to the staker's shares across multiple strategies
function _getSlashingFactors(
address staker,
address operator,
IStrategy[] memory strategies
) internal view returns (uint256[] memory) {
uint256[] memory slashingFactors = new uint256[](strategies.length);
uint64[] memory maxMagnitudes = allocationManager.getMaxMagnitudes(operator, strategies);
for (uint256 i = 0; i < strategies.length; i++) {
slashingFactors[i] = _getSlashingFactor(staker, strategies[i], maxMagnitudes[i]);
}
return slashingFactors;
}
/// @dev Calculate the amount of slashing to apply to the staker's shares across multiple strategies
/// Note: specifically checks the operator's magnitude at a prior block, used for completing withdrawals
function _getSlashingFactorsAtBlock(
address staker,
address operator,
IStrategy[] memory strategies,
uint32 blockNumber
) internal view returns (uint256[] memory) {
uint256[] memory slashingFactors = new uint256[](strategies.length);
uint64[] memory maxMagnitudes = allocationManager.getMaxMagnitudesAtBlock({
operator: operator,
strategies: strategies,
blockNumber: blockNumber
});
for (uint256 i = 0; i < strategies.length; i++) {
slashingFactors[i] = _getSlashingFactor(staker, strategies[i], maxMagnitudes[i]);
}
return slashingFactors;
}
/**
* @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 _getSlashableSharesInQueue(
address operator,
IStrategy strategy,
uint64 prevMaxMagnitude,
uint64 newMaxMagnitude
) internal view returns (uint256) {
// We want ALL shares added to the withdrawal queue in the window [block.number - MIN_WITHDRAWAL_DELAY_BLOCKS, block.number]
//
// To get this, we take the current shares in the withdrawal queue and subtract the number of shares
// that were in the queue before MIN_WITHDRAWAL_DELAY_BLOCKS.
uint256 curQueuedScaledShares = _cumulativeScaledSharesHistory[operator][strategy].latest();
uint256 prevQueuedScaledShares = _cumulativeScaledSharesHistory[operator][strategy].upperLookup({
key: uint32(block.number) - MIN_WITHDRAWAL_DELAY_BLOCKS - 1
});
// The difference between these values is the number of scaled shares that entered the withdrawal queue
// less than or equal to MIN_WITHDRAWAL_DELAY_BLOCKS ago. These shares are still slashable.
uint256 scaledSharesAdded = curQueuedScaledShares - prevQueuedScaledShares;
return SlashingLib.scaleForBurning({
scaledShares: scaledSharesAdded,
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 {
uint256 currCumulativeScaledShares = _cumulativeScaledSharesHistory[operator][strategy].latest();
_cumulativeScaledSharesHistory[operator][strategy].push({
key: uint32(block.number),
value: currCumulativeScaledShares + scaledShares
});
}
/// @dev Get the shares from a queued withdrawal.
function _getSharesByWithdrawalRoot(
bytes32 withdrawalRoot
) internal view returns (Withdrawal memory withdrawal, uint256[] memory shares) {
withdrawal = _queuedWithdrawals[withdrawalRoot];
shares = new uint256[](withdrawal.strategies.length);
uint32 slashableUntil = withdrawal.startBlock + MIN_WITHDRAWAL_DELAY_BLOCKS;
// If the slashableUntil block is in the past, read the slashing factors at that block.
// Otherwise, read the current slashing factors. Note that if the slashableUntil block is the current block
// or in the future, then the slashing factors are still subject to change before the withdrawal is completable,
// which may result in fewer shares being withdrawn.
uint256[] memory slashingFactors = slashableUntil < uint32(block.number)
? _getSlashingFactorsAtBlock({
staker: withdrawal.staker,
operator: withdrawal.delegatedTo,
strategies: withdrawal.strategies,
blockNumber: slashableUntil
})
: _getSlashingFactors({
staker: withdrawal.staker,
operator: withdrawal.delegatedTo,
strategies: withdrawal.strategies
});
for (uint256 j; j < withdrawal.strategies.length; ++j) {
shares[j] = SlashingLib.scaleForCompleteWithdrawal({
scaledShares: withdrawal.scaledShares[j],
slashingFactor: slashingFactors[j]
});
}
}
/// @dev Depending on the strategy used, determine which ShareManager contract to make external calls to
function _getShareManager(
IStrategy strategy
) internal view returns (IShareManager) {
return strategy == beaconChainETHStrategy
? IShareManager(address(eigenPodManager))
: IShareManager(address(strategyManager));
}
/**
*
* VIEW FUNCTIONS
*
*/
/// @inheritdoc IDelegationManager
function isDelegated(
address staker
) public view returns (bool) {
return (delegatedTo[staker] != address(0));
}
/// @inheritdoc IDelegationManager
function isOperator(
address operator
) public view returns (bool) {
return operator != address(0) && delegatedTo[operator] == operator;
}
/// @inheritdoc IDelegationManager
function delegationApprover(
address operator
) public view returns (address) {
return _operatorDetails[operator].delegationApprover;
}
/// @inheritdoc IDelegationManager
function depositScalingFactor(address staker, IStrategy strategy) external view returns (uint256) {
return _depositScalingFactor[staker][strategy].scalingFactor();
}
/// @inheritdoc IDelegationManager
function getOperatorShares(
address operator,
IStrategy[] memory strategies
) public view returns (uint256[] memory) {
uint256[] memory shares = new uint256[](strategies.length);
for (uint256 i = 0; i < strategies.length; ++i) {
shares[i] = operatorShares[operator][strategies[i]];
}
return shares;
}
/// @inheritdoc IDelegationManager
function getOperatorsShares(
address[] memory operators,
IStrategy[] memory strategies
) public view returns (uint256[][] memory) {
uint256[][] memory shares = new uint256[][](operators.length);
for (uint256 i = 0; i < operators.length; ++i) {
shares[i] = getOperatorShares(operators[i], strategies);
}
return shares;
}
/// @inheritdoc IDelegationManager
function getSlashableSharesInQueue(address operator, IStrategy strategy) public view returns (uint256) {
uint64 maxMagnitude = allocationManager.getMaxMagnitude(operator, strategy);
// Return amount of slashable scaled shares remaining
return _getSlashableSharesInQueue({
operator: operator,
strategy: strategy,
prevMaxMagnitude: maxMagnitude,
newMaxMagnitude: 0
});
}
/// @inheritdoc IDelegationManager
function getWithdrawableShares(
address staker,
IStrategy[] memory strategies
) public view returns (uint256[] memory withdrawableShares, uint256[] memory depositShares) {
withdrawableShares = new uint256[](strategies.length);
depositShares = new uint256[](strategies.length);
// Get the slashing factors for the staker/operator/strategies
address operator = delegatedTo[staker];
uint256[] memory slashingFactors = _getSlashingFactors(staker, operator, strategies);
for (uint256 i = 0; i < strategies.length; ++i) {
IShareManager shareManager = _getShareManager(strategies[i]);
depositShares[i] = shareManager.stakerDepositShares(staker, strategies[i]);
// Calculate the withdrawable shares based on the slashing factor
DepositScalingFactor memory dsf = _depositScalingFactor[staker][strategies[i]];
withdrawableShares[i] = dsf.calcWithdrawable(depositShares[i], slashingFactors[i]);
}
return (withdrawableShares, depositShares);
}
/// @inheritdoc IDelegationManager
function getDepositedShares(
address staker
) public view returns (IStrategy[] memory, uint256[] memory) {
// Get a list of the staker's deposited strategies/shares in the strategy manager
(IStrategy[] memory tokenStrategies, uint256[] memory tokenDeposits) = strategyManager.getDeposits(staker);
// If the staker has no beacon chain ETH shares, return any shares from the strategy manager
uint256 podOwnerShares = eigenPodManager.stakerDepositShares(staker, beaconChainETHStrategy);
if (podOwnerShares == 0) {
return (tokenStrategies, tokenDeposits);
}
// Allocate extra space for beaconChainETHStrategy and shares
IStrategy[] memory strategies = new IStrategy[](tokenStrategies.length + 1);
uint256[] memory shares = new uint256[](tokenStrategies.length + 1);
strategies[tokenStrategies.length] = beaconChainETHStrategy;
shares[tokenStrategies.length] = podOwnerShares;
// Copy any strategy manager shares to complete array
for (uint256 i = 0; i < tokenStrategies.length; i++) {
strategies[i] = tokenStrategies[i];
shares[i] = tokenDeposits[i];
}
return (strategies, shares);
}
function queuedWithdrawals(
bytes32 withdrawalRoot
) external view returns (Withdrawal memory withdrawal) {
return _queuedWithdrawals[withdrawalRoot];
}
/// @inheritdoc IDelegationManager
function getQueuedWithdrawal(
bytes32 withdrawalRoot
) external view returns (Withdrawal memory withdrawal, uint256[] memory shares) {
(withdrawal, shares) = _getSharesByWithdrawalRoot(withdrawalRoot);
}
/// @inheritdoc IDelegationManager
function getQueuedWithdrawals(
address staker
) external view returns (Withdrawal[] memory withdrawals, uint256[][] memory shares) {
bytes32[] memory withdrawalRoots = getQueuedWithdrawalRoots(staker);
uint256 totalQueued = withdrawalRoots.length;
withdrawals = new Withdrawal[](totalQueued);
shares = new uint256[][](totalQueued);
for (uint256 i; i < totalQueued; ++i) {
(withdrawals[i], shares[i]) = _getSharesByWithdrawalRoot(withdrawalRoots[i]);
}
}
/// @inheritdoc IDelegationManager
function getQueuedWithdrawalRoots(
address staker
) public view returns (bytes32[] memory) {
return _stakerQueuedWithdrawalRoots[staker].values();
}
/// @inheritdoc IDelegationManager
function convertToDepositShares(
address staker,
IStrategy[] memory strategies,
uint256[] memory withdrawableShares
) external view returns (uint256[] memory) {
// Get the slashing factors for the staker/operator/strategies
address operator = delegatedTo[staker];
uint256[] memory slashingFactors = _getSlashingFactors(staker, operator, strategies);
// Calculate the deposit shares based on the slashing factor
uint256[] memory depositShares = new uint256[](strategies.length);
for (uint256 i = 0; i < strategies.length; ++i) {
DepositScalingFactor memory dsf = _depositScalingFactor[staker][strategies[i]];
depositShares[i] = dsf.calcDepositShares(withdrawableShares[i], slashingFactors[i]);
}
return depositShares;
}
/// @inheritdoc IDelegationManager
function calculateWithdrawalRoot(
Withdrawal memory withdrawal
) public pure returns (bytes32) {
return keccak256(abi.encode(withdrawal));
}
/// @inheritdoc IDelegationManager
function minWithdrawalDelayBlocks() external view returns (uint32) {
return MIN_WITHDRAWAL_DELAY_BLOCKS;
}
/// @inheritdoc IDelegationManager
function calculateDelegationApprovalDigestHash(
address staker,
address operator,
address approver,
bytes32 approverSalt,
uint256 expiry
) public view returns (bytes32) {
/// forgefmt: disable-next-item
return _calculateSignableDigest(
keccak256(
abi.encode(
DELEGATION_APPROVAL_TYPEHASH,
approver,
staker,
operator,
approverSalt,
expiry
)
)
);
}
}
````
## File: src/contracts/core/DelegationManagerStorage.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "../libraries/SlashingLib.sol";
import "../interfaces/IDelegationManager.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.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @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
bytes32 public constant DELEGATION_APPROVAL_TYPEHASH = keccak256(
"DelegationApproval(address delegationApprover,address staker,address operator,bytes32 salt,uint256 expiry)"
);
/// @dev Index for flag that pauses new delegations when set
uint8 internal constant PAUSED_NEW_DELEGATION = 0;
/// @dev Index for flag that pauses queuing new withdrawals when set.
uint8 internal constant PAUSED_ENTER_WITHDRAWAL_QUEUE = 1;
/// @dev Index for flag that pauses completing existing withdrawals when set.
uint8 internal constant PAUSED_EXIT_WITHDRAWAL_QUEUE = 2;
/// @notice Canonical, virtual beacon chain ETH strategy
IStrategy public constant beaconChainETHStrategy = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);
// Immutables
/// @notice The StrategyManager contract for EigenLayer
IStrategyManager public immutable strategyManager;
/// @notice The EigenPodManager contract for EigenLayer
IEigenPodManager public immutable eigenPodManager;
/// @notice The AllocationManager contract for EigenLayer
IAllocationManager public immutable allocationManager;
/// @notice Minimum withdrawal delay in blocks until a queued withdrawal can be completed.
uint32 internal immutable MIN_WITHDRAWAL_DELAY_BLOCKS;
// Mutatables
/// @dev Do not remove, deprecated storage.
bytes32 internal __deprecated_DOMAIN_SEPARATOR;
/**
* @notice Tracks the current balance of shares an `operator` is delegated according to each `strategy`.
* Updated by both the `StrategyManager` and `EigenPodManager` when a staker's delegatable balance changes,
* and by the `AllocationManager` when the `operator` is slashed.
*
* @dev The following invariant should hold for each `strategy`:
*
* operatorShares[operator] = sum(withdrawable shares of all stakers delegated to operator)
*/
mapping(address operator => mapping(IStrategy strategy => uint256 shares)) public operatorShares;
/// @notice Returns the operator details for a given `operator`.
/// Note: two of the `OperatorDetails` fields are deprecated. The only relevant field
/// is `OperatorDetails.delegationApprover`.
mapping(address operator => OperatorDetails) internal _operatorDetails;
/// @notice Returns the `operator` a `staker` is delegated to, or address(0) if not delegated.
/// Note: operators are delegated to themselves
mapping(address staker => address operator) public delegatedTo;
/// @notice Do not remove, deprecated storage.
mapping(address staker => uint256 nonce) private __deprecated_stakerNonce;
/// @notice Returns whether `delegationApprover` has already used the given `salt`.
mapping(address delegationApprover => mapping(bytes32 salt => bool spent)) public delegationApproverSaltIsSpent;
/// @dev Do not remove, deprecated storage.
uint256 private __deprecated_minWithdrawalDelayBlocks;
/// @dev Returns whether a withdrawal is pending for a given `withdrawalRoot`.
/// @dev This variable will be deprecated in the future, values should only be read or deleted.
mapping(bytes32 withdrawalRoot => bool pending) public pendingWithdrawals;
/// @notice Returns the total number of withdrawals that have been queued for a given `staker`.
/// @dev This only increments (doesn't decrement), and is used to help ensure that otherwise identical withdrawals have unique hashes.
mapping(address staker => uint256 totalQueued) public cumulativeWithdrawalsQueued;
/// @dev Do not remove, deprecated storage.
/// See conversation here: https://github.com/Layr-Labs/eigenlayer-contracts/pull/365/files#r1417525270
address private __deprecated_stakeRegistry;
/// @dev Do not remove, deprecated storage.
mapping(IStrategy strategy => uint256 delayBlocks) private __deprecated_strategyWithdrawalDelayBlocks;
/// @notice Returns the scaling factor applied to a `staker` for a given `strategy`
mapping(address staker => mapping(IStrategy strategy => DepositScalingFactor)) internal _depositScalingFactor;
/// @notice Returns a list of queued withdrawals for a given `staker`.
/// @dev Entries are removed when the withdrawal is completed.
/// @dev This variable only reflects withdrawals that were made after the slashing release.
mapping(address staker => EnumerableSet.Bytes32Set withdrawalRoots) internal _stakerQueuedWithdrawalRoots;
/// @notice Returns the details of a queued withdrawal given by `withdrawalRoot`.
/// @dev This variable only reflects withdrawals that were made after the slashing release.
mapping(bytes32 withdrawalRoot => Withdrawal withdrawal) internal _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(
IStrategyManager _strategyManager,
IEigenPodManager _eigenPodManager,
IAllocationManager _allocationManager,
uint32 _MIN_WITHDRAWAL_DELAY_BLOCKS
) {
strategyManager = _strategyManager;
eigenPodManager = _eigenPodManager;
allocationManager = _allocationManager;
MIN_WITHDRAWAL_DELAY_BLOCKS = _MIN_WITHDRAWAL_DELAY_BLOCKS;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[35] private __gap;
}
````
## File: src/contracts/core/RewardsCoordinator.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../libraries/Merkle.sol";
import "../permissions/Pausable.sol";
import "./RewardsCoordinatorStorage.sol";
import "../mixins/PermissionControllerMixin.sol";
import "../mixins/SemVerMixin.sol";
/**
* @title RewardsCoordinator
* @author Eigen Labs Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice This is the contract for rewards in EigenLayer. The main functionalities of this contract are
* - enabling any ERC20 rewards from AVSs to their operators and stakers for a given time range
* - allowing stakers and operators to claim their earnings including a split bips for operators
* - allowing the protocol to provide ERC20 tokens to stakers over a specified time range
*/
contract RewardsCoordinator is
Initializable,
OwnableUpgradeable,
Pausable,
ReentrancyGuardUpgradeable,
RewardsCoordinatorStorage,
PermissionControllerMixin,
SemVerMixin
{
using SafeERC20 for IERC20;
using OperatorSetLib for OperatorSet;
modifier onlyRewardsUpdater() {
require(msg.sender == rewardsUpdater, UnauthorizedCaller());
_;
}
modifier onlyRewardsForAllSubmitter() {
require(isRewardsForAllSubmitter[msg.sender], UnauthorizedCaller());
_;
}
/// @dev Sets the immutable variables for the contract
constructor(
RewardsCoordinatorConstructorParams memory params
)
RewardsCoordinatorStorage(
params.delegationManager,
params.strategyManager,
params.allocationManager,
params.CALCULATION_INTERVAL_SECONDS,
params.MAX_REWARDS_DURATION,
params.MAX_RETROACTIVE_LENGTH,
params.MAX_FUTURE_LENGTH,
params.GENESIS_REWARDS_TIMESTAMP
)
Pausable(params.pauserRegistry)
PermissionControllerMixin(params.permissionController)
SemVerMixin(params.version)
{
_disableInitializers();
}
/**
* @dev Initializes the addresses of the initial owner, pauser registry, rewardsUpdater and
* configures the initial paused status, activationDelay, and defaultOperatorSplitBips.
*/
/// @inheritdoc IRewardsCoordinator
function initialize(
address initialOwner,
uint256 initialPausedStatus,
address _rewardsUpdater,
uint32 _activationDelay,
uint16 _defaultSplitBips
) external initializer {
_setPausedStatus(initialPausedStatus);
_transferOwnership(initialOwner);
_setRewardsUpdater(_rewardsUpdater);
_setActivationDelay(_activationDelay);
_setDefaultOperatorSplit(_defaultSplitBips);
}
/**
*
* EXTERNAL FUNCTIONS
*
*/
/// @inheritdoc IRewardsCoordinator
function createAVSRewardsSubmission(
RewardsSubmission[] calldata rewardsSubmissions
) external onlyWhenNotPaused(PAUSED_AVS_REWARDS_SUBMISSION) nonReentrant {
for (uint256 i = 0; i < rewardsSubmissions.length; i++) {
RewardsSubmission calldata rewardsSubmission = rewardsSubmissions[i];
uint256 nonce = submissionNonce[msg.sender];
bytes32 rewardsSubmissionHash = keccak256(abi.encode(msg.sender, nonce, rewardsSubmission));
_validateRewardsSubmission(rewardsSubmission);
isAVSRewardsSubmissionHash[msg.sender][rewardsSubmissionHash] = true;
submissionNonce[msg.sender] = nonce + 1;
emit AVSRewardsSubmissionCreated(msg.sender, nonce, rewardsSubmissionHash, rewardsSubmission);
rewardsSubmission.token.safeTransferFrom(msg.sender, address(this), rewardsSubmission.amount);
}
}
/// @inheritdoc IRewardsCoordinator
function createRewardsForAllSubmission(
RewardsSubmission[] calldata rewardsSubmissions
) external onlyWhenNotPaused(PAUSED_REWARDS_FOR_ALL_SUBMISSION) onlyRewardsForAllSubmitter nonReentrant {
for (uint256 i = 0; i < rewardsSubmissions.length; i++) {
RewardsSubmission calldata rewardsSubmission = rewardsSubmissions[i];
uint256 nonce = submissionNonce[msg.sender];
bytes32 rewardsSubmissionForAllHash = keccak256(abi.encode(msg.sender, nonce, rewardsSubmission));
_validateRewardsSubmission(rewardsSubmission);
isRewardsSubmissionForAllHash[msg.sender][rewardsSubmissionForAllHash] = true;
submissionNonce[msg.sender] = nonce + 1;
emit RewardsSubmissionForAllCreated(msg.sender, nonce, rewardsSubmissionForAllHash, rewardsSubmission);
rewardsSubmission.token.safeTransferFrom(msg.sender, address(this), rewardsSubmission.amount);
}
}
/// @inheritdoc IRewardsCoordinator
function createRewardsForAllEarners(
RewardsSubmission[] calldata rewardsSubmissions
) external onlyWhenNotPaused(PAUSED_REWARD_ALL_STAKERS_AND_OPERATORS) onlyRewardsForAllSubmitter nonReentrant {
for (uint256 i = 0; i < rewardsSubmissions.length; i++) {
RewardsSubmission calldata rewardsSubmission = rewardsSubmissions[i];
uint256 nonce = submissionNonce[msg.sender];
bytes32 rewardsSubmissionForAllEarnersHash = keccak256(abi.encode(msg.sender, nonce, rewardsSubmission));
_validateRewardsSubmission(rewardsSubmission);
isRewardsSubmissionForAllEarnersHash[msg.sender][rewardsSubmissionForAllEarnersHash] = true;
submissionNonce[msg.sender] = nonce + 1;
emit RewardsSubmissionForAllEarnersCreated(
msg.sender, nonce, rewardsSubmissionForAllEarnersHash, rewardsSubmission
);
rewardsSubmission.token.safeTransferFrom(msg.sender, address(this), rewardsSubmission.amount);
}
}
/// @inheritdoc IRewardsCoordinator
function createOperatorDirectedAVSRewardsSubmission(
address avs,
OperatorDirectedRewardsSubmission[] calldata operatorDirectedRewardsSubmissions
) external onlyWhenNotPaused(PAUSED_OPERATOR_DIRECTED_AVS_REWARDS_SUBMISSION) checkCanCall(avs) nonReentrant {
for (uint256 i = 0; i < operatorDirectedRewardsSubmissions.length; i++) {
OperatorDirectedRewardsSubmission calldata operatorDirectedRewardsSubmission =
operatorDirectedRewardsSubmissions[i];
uint256 nonce = submissionNonce[avs];
bytes32 operatorDirectedRewardsSubmissionHash =
keccak256(abi.encode(avs, nonce, operatorDirectedRewardsSubmission));
uint256 totalAmount = _validateOperatorDirectedRewardsSubmission(operatorDirectedRewardsSubmission);
isOperatorDirectedAVSRewardsSubmissionHash[avs][operatorDirectedRewardsSubmissionHash] = true;
submissionNonce[avs] = nonce + 1;
emit OperatorDirectedAVSRewardsSubmissionCreated(
msg.sender, avs, operatorDirectedRewardsSubmissionHash, nonce, operatorDirectedRewardsSubmission
);
operatorDirectedRewardsSubmission.token.safeTransferFrom(msg.sender, address(this), totalAmount);
}
}
/// @inheritdoc IRewardsCoordinator
function createOperatorDirectedOperatorSetRewardsSubmission(
OperatorSet calldata operatorSet,
OperatorDirectedRewardsSubmission[] calldata operatorDirectedRewardsSubmissions
)
external
onlyWhenNotPaused(PAUSED_OPERATOR_DIRECTED_OPERATOR_SET_REWARDS_SUBMISSION)
checkCanCall(operatorSet.avs)
nonReentrant
{
require(allocationManager.isOperatorSet(operatorSet), InvalidOperatorSet());
for (uint256 i = 0; i < operatorDirectedRewardsSubmissions.length; i++) {
OperatorDirectedRewardsSubmission calldata operatorDirectedRewardsSubmission =
operatorDirectedRewardsSubmissions[i];
uint256 nonce = submissionNonce[operatorSet.avs];
bytes32 operatorDirectedRewardsSubmissionHash =
keccak256(abi.encode(operatorSet.avs, nonce, operatorDirectedRewardsSubmission));
uint256 totalAmount = _validateOperatorDirectedRewardsSubmission(operatorDirectedRewardsSubmission);
isOperatorDirectedOperatorSetRewardsSubmissionHash[operatorSet.avs][operatorDirectedRewardsSubmissionHash] =
true;
submissionNonce[operatorSet.avs] = nonce + 1;
emit OperatorDirectedOperatorSetRewardsSubmissionCreated(
msg.sender, operatorDirectedRewardsSubmissionHash, operatorSet, nonce, operatorDirectedRewardsSubmission
);
operatorDirectedRewardsSubmission.token.safeTransferFrom(msg.sender, address(this), totalAmount);
}
}
/// @inheritdoc IRewardsCoordinator
function processClaim(
RewardsMerkleClaim calldata claim,
address recipient
) external onlyWhenNotPaused(PAUSED_PROCESS_CLAIM) nonReentrant {
_processClaim(claim, recipient);
}
/// @inheritdoc IRewardsCoordinator
function processClaims(
RewardsMerkleClaim[] calldata claims,
address recipient
) external onlyWhenNotPaused(PAUSED_PROCESS_CLAIM) nonReentrant {
for (uint256 i = 0; i < claims.length; i++) {
_processClaim(claims[i], recipient);
}
}
/// @inheritdoc IRewardsCoordinator
function submitRoot(
bytes32 root,
uint32 rewardsCalculationEndTimestamp
) external onlyWhenNotPaused(PAUSED_SUBMIT_DISABLE_ROOTS) onlyRewardsUpdater {
require(
rewardsCalculationEndTimestamp > currRewardsCalculationEndTimestamp, NewRootMustBeForNewCalculatedPeriod()
);
require(rewardsCalculationEndTimestamp < block.timestamp, RewardsEndTimestampNotElapsed());
uint32 rootIndex = uint32(_distributionRoots.length);
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
_distributionRoots.push(
DistributionRoot({
root: root,
activatedAt: activatedAt,
rewardsCalculationEndTimestamp: rewardsCalculationEndTimestamp,
disabled: false
})
);
currRewardsCalculationEndTimestamp = rewardsCalculationEndTimestamp;
emit DistributionRootSubmitted(rootIndex, root, rewardsCalculationEndTimestamp, activatedAt);
}
/// @inheritdoc IRewardsCoordinator
function disableRoot(
uint32 rootIndex
) external onlyWhenNotPaused(PAUSED_SUBMIT_DISABLE_ROOTS) onlyRewardsUpdater {
require(rootIndex < _distributionRoots.length, InvalidRootIndex());
DistributionRoot storage root = _distributionRoots[rootIndex];
require(!root.disabled, RootDisabled());
require(block.timestamp < root.activatedAt, RootAlreadyActivated());
root.disabled = true;
emit DistributionRootDisabled(rootIndex);
}
/// @inheritdoc IRewardsCoordinator
function setClaimerFor(
address claimer
) external {
address earner = msg.sender;
_setClaimer(earner, claimer);
}
/// @inheritdoc IRewardsCoordinator
function setClaimerFor(address earner, address claimer) external checkCanCall(earner) {
// Require that the earner is an operator or AVS
require(
delegationManager.isOperator(earner) || allocationManager.getOperatorSetCount(earner) > 0, InvalidEarner()
);
_setClaimer(earner, claimer);
}
/// @inheritdoc IRewardsCoordinator
function setActivationDelay(
uint32 _activationDelay
) external onlyOwner {
_setActivationDelay(_activationDelay);
}
/// @inheritdoc IRewardsCoordinator
function setDefaultOperatorSplit(
uint16 split
) external onlyOwner {
_setDefaultOperatorSplit(split);
}
/// @inheritdoc IRewardsCoordinator
function setOperatorAVSSplit(
address operator,
address avs,
uint16 split
) external onlyWhenNotPaused(PAUSED_OPERATOR_AVS_SPLIT) checkCanCall(operator) {
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
uint16 oldSplit = _getOperatorSplit(_operatorAVSSplitBips[operator][avs]);
_setOperatorSplit(_operatorAVSSplitBips[operator][avs], split, activatedAt);
emit OperatorAVSSplitBipsSet(msg.sender, operator, avs, activatedAt, oldSplit, split);
}
/// @inheritdoc IRewardsCoordinator
function setOperatorPISplit(
address operator,
uint16 split
) external onlyWhenNotPaused(PAUSED_OPERATOR_PI_SPLIT) checkCanCall(operator) {
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
uint16 oldSplit = _getOperatorSplit(_operatorPISplitBips[operator]);
_setOperatorSplit(_operatorPISplitBips[operator], split, activatedAt);
emit OperatorPISplitBipsSet(msg.sender, operator, activatedAt, oldSplit, split);
}
/// @inheritdoc IRewardsCoordinator
function setOperatorSetSplit(
address operator,
OperatorSet calldata operatorSet,
uint16 split
) external onlyWhenNotPaused(PAUSED_OPERATOR_SET_SPLIT) checkCanCall(operator) {
require(allocationManager.isOperatorSet(operatorSet), InvalidOperatorSet());
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
uint16 oldSplit = _getOperatorSplit(_operatorSetSplitBips[operator][operatorSet.key()]);
_setOperatorSplit(_operatorSetSplitBips[operator][operatorSet.key()], split, activatedAt);
emit OperatorSetSplitBipsSet(msg.sender, operator, operatorSet, activatedAt, oldSplit, split);
}
/// @inheritdoc IRewardsCoordinator
function setRewardsUpdater(
address _rewardsUpdater
) external onlyOwner {
_setRewardsUpdater(_rewardsUpdater);
}
/// @inheritdoc IRewardsCoordinator
function setRewardsForAllSubmitter(address _submitter, bool _newValue) external onlyOwner {
bool prevValue = isRewardsForAllSubmitter[_submitter];
emit RewardsForAllSubmitterSet(_submitter, prevValue, _newValue);
isRewardsForAllSubmitter[_submitter] = _newValue;
}
/**
*
* INTERNAL FUNCTIONS
*
*/
/**
* @notice Internal helper to process reward claims.
* @param claim The RewardsMerkleClaims to be processed.
* @param recipient The address recipient that receives the ERC20 rewards
*/
function _processClaim(RewardsMerkleClaim calldata claim, address recipient) internal {
DistributionRoot memory root = _distributionRoots[claim.rootIndex];
_checkClaim(claim, root);
// If claimerFor earner is not set, claimer is by default the earner. Else set to claimerFor
address earner = claim.earnerLeaf.earner;
address claimer = claimerFor[earner];
if (claimer == address(0)) {
claimer = earner;
}
require(msg.sender == claimer, UnauthorizedCaller());
for (uint256 i = 0; i < claim.tokenIndices.length; i++) {
TokenTreeMerkleLeaf calldata tokenLeaf = claim.tokenLeaves[i];
uint256 currCumulativeClaimed = cumulativeClaimed[earner][tokenLeaf.token];
require(tokenLeaf.cumulativeEarnings > currCumulativeClaimed, EarningsNotGreaterThanClaimed());
// Calculate amount to claim and update cumulativeClaimed
uint256 claimAmount = tokenLeaf.cumulativeEarnings - currCumulativeClaimed;
cumulativeClaimed[earner][tokenLeaf.token] = tokenLeaf.cumulativeEarnings;
tokenLeaf.token.safeTransfer(recipient, claimAmount);
emit RewardsClaimed(root.root, earner, claimer, recipient, tokenLeaf.token, claimAmount);
}
}
function _setActivationDelay(
uint32 _activationDelay
) internal {
emit ActivationDelaySet(activationDelay, _activationDelay);
activationDelay = _activationDelay;
}
function _setDefaultOperatorSplit(
uint16 split
) internal {
emit DefaultOperatorSplitBipsSet(defaultOperatorSplitBips, split);
defaultOperatorSplitBips = split;
}
function _setRewardsUpdater(
address _rewardsUpdater
) internal {
emit RewardsUpdaterSet(rewardsUpdater, _rewardsUpdater);
rewardsUpdater = _rewardsUpdater;
}
function _setClaimer(address earner, address claimer) internal {
address prevClaimer = claimerFor[earner];
claimerFor[earner] = claimer;
emit ClaimerForSet(earner, prevClaimer, claimer);
}
/**
* @notice Internal helper to set the operator split.
* @param operatorSplit The split struct for an Operator
* @param split The split in basis points.
* @param activatedAt The timestamp when the split is activated.
*/
function _setOperatorSplit(OperatorSplit storage operatorSplit, uint16 split, uint32 activatedAt) internal {
require(split <= ONE_HUNDRED_IN_BIPS, SplitExceedsMax());
require(block.timestamp > operatorSplit.activatedAt, PreviousSplitPending());
if (operatorSplit.activatedAt == 0) {
// If the operator split has not been initialized yet, set the old split to `type(uint16).max` as a flag.
operatorSplit.oldSplitBips = type(uint16).max;
} else {
operatorSplit.oldSplitBips = operatorSplit.newSplitBips;
}
operatorSplit.newSplitBips = split;
operatorSplit.activatedAt = activatedAt;
}
/**
* @notice Common checks for all RewardsSubmissions.
*/
function _validateCommonRewardsSubmission(
StrategyAndMultiplier[] calldata strategiesAndMultipliers,
uint32 startTimestamp,
uint32 duration
) internal view {
require(strategiesAndMultipliers.length > 0, InputArrayLengthZero());
require(duration <= MAX_REWARDS_DURATION, DurationExceedsMax());
require(duration % CALCULATION_INTERVAL_SECONDS == 0, InvalidDurationRemainder());
require(duration > 0, DurationIsZero());
require(startTimestamp % CALCULATION_INTERVAL_SECONDS == 0, InvalidStartTimestampRemainder());
require(
block.timestamp - MAX_RETROACTIVE_LENGTH <= startTimestamp && GENESIS_REWARDS_TIMESTAMP <= startTimestamp,
StartTimestampTooFarInPast()
);
// Require reward submission is for whitelisted strategy or beaconChainETHStrategy
address currAddress = address(0);
for (uint256 i = 0; i < strategiesAndMultipliers.length; ++i) {
IStrategy strategy = strategiesAndMultipliers[i].strategy;
require(
strategyManager.strategyIsWhitelistedForDeposit(strategy) || strategy == beaconChainETHStrategy,
StrategyNotWhitelisted()
);
require(currAddress < address(strategy), StrategiesNotInAscendingOrder());
currAddress = address(strategy);
}
}
/**
* @notice Validate a RewardsSubmission. Called from both `createAVSRewardsSubmission` and `createRewardsForAllSubmission`
*/
function _validateRewardsSubmission(
RewardsSubmission calldata rewardsSubmission
) internal view {
_validateCommonRewardsSubmission(
rewardsSubmission.strategiesAndMultipliers, rewardsSubmission.startTimestamp, rewardsSubmission.duration
);
require(rewardsSubmission.amount > 0, AmountIsZero());
require(rewardsSubmission.amount <= MAX_REWARDS_AMOUNT, AmountExceedsMax());
require(rewardsSubmission.startTimestamp <= block.timestamp + MAX_FUTURE_LENGTH, StartTimestampTooFarInFuture());
}
/**
* @notice Validate a OperatorDirectedRewardsSubmission. Called from `createOperatorDirectedAVSRewardsSubmission`.
* @dev Not checking for `MAX_FUTURE_LENGTH` (Since operator-directed reward submissions are strictly retroactive).
* @param submission OperatorDirectedRewardsSubmission to validate.
* @return total amount to be transferred from the avs to the contract.
*/
function _validateOperatorDirectedRewardsSubmission(
OperatorDirectedRewardsSubmission calldata submission
) internal view returns (uint256) {
_validateCommonRewardsSubmission(
submission.strategiesAndMultipliers, submission.startTimestamp, submission.duration
);
require(submission.operatorRewards.length > 0, InputArrayLengthZero());
require(submission.startTimestamp + submission.duration < block.timestamp, SubmissionNotRetroactive());
uint256 totalAmount = 0;
address currOperatorAddress = address(0);
for (uint256 i = 0; i < submission.operatorRewards.length; ++i) {
OperatorReward calldata operatorReward = submission.operatorRewards[i];
require(operatorReward.operator != address(0), InvalidAddressZero());
require(currOperatorAddress < operatorReward.operator, OperatorsNotInAscendingOrder());
require(operatorReward.amount > 0, AmountIsZero());
currOperatorAddress = operatorReward.operator;
totalAmount += operatorReward.amount;
}
require(totalAmount <= MAX_REWARDS_AMOUNT, AmountExceedsMax());
return totalAmount;
}
function _checkClaim(RewardsMerkleClaim calldata claim, DistributionRoot memory root) internal view {
require(!root.disabled, RootDisabled());
require(block.timestamp >= root.activatedAt, RootNotActivated());
require(claim.tokenIndices.length == claim.tokenTreeProofs.length, InputArrayLengthMismatch());
require(claim.tokenTreeProofs.length == claim.tokenLeaves.length, InputArrayLengthMismatch());
// Verify inclusion of earners leaf (earner, earnerTokenRoot) in the distribution root
_verifyEarnerClaimProof({
root: root.root,
earnerLeafIndex: claim.earnerIndex,
earnerProof: claim.earnerTreeProof,
earnerLeaf: claim.earnerLeaf
});
// For each of the tokenLeaf proofs, verify inclusion of token tree leaf again the earnerTokenRoot
for (uint256 i = 0; i < claim.tokenIndices.length; ++i) {
_verifyTokenClaimProof({
earnerTokenRoot: claim.earnerLeaf.earnerTokenRoot,
tokenLeafIndex: claim.tokenIndices[i],
tokenProof: claim.tokenTreeProofs[i],
tokenLeaf: claim.tokenLeaves[i]
});
}
}
/**
* @notice verify inclusion of the token claim proof in the earner token root hash (earnerTokenRoot).
* The token leaf comprises of the IERC20 token and cumulativeAmount of earnings.
* @param earnerTokenRoot root hash of the earner token subtree
* @param tokenLeafIndex index of the token leaf
* @param tokenProof proof of the token leaf in the earner token subtree
* @param tokenLeaf token leaf to be verified
*/
function _verifyTokenClaimProof(
bytes32 earnerTokenRoot,
uint32 tokenLeafIndex,
bytes calldata tokenProof,
TokenTreeMerkleLeaf calldata tokenLeaf
) internal pure {
// Validate index size so that there aren't multiple valid indices for the given proof
// index can't be greater than 2**(tokenProof/32)
require(tokenLeafIndex < (1 << (tokenProof.length / 32)), InvalidTokenLeafIndex());
// Verify inclusion of token leaf
bytes32 tokenLeafHash = calculateTokenLeafHash(tokenLeaf);
require(
Merkle.verifyInclusionKeccak({
root: earnerTokenRoot,
index: tokenLeafIndex,
proof: tokenProof,
leaf: tokenLeafHash
}),
InvalidClaimProof()
);
}
/**
* @notice verify inclusion of earner claim proof in the distribution root. This verifies
* the inclusion of the earner and earnerTokenRoot hash in the tree. The token claims are proven separately
* against the earnerTokenRoot hash (see _verifyTokenClaimProof). The earner leaf comprises of (earner, earnerTokenRoot)
* @param root distribution root that should be read from storage
* @param earnerLeafIndex index of the earner leaf
* @param earnerProof proof of the earners account root in the merkle tree
* @param earnerLeaf leaf of earner merkle tree containing the earner address and earner's token root hash
*/
function _verifyEarnerClaimProof(
bytes32 root,
uint32 earnerLeafIndex,
bytes calldata earnerProof,
EarnerTreeMerkleLeaf calldata earnerLeaf
) internal pure {
// Validate index size so that there aren't multiple valid indices for the given proof
// index can't be greater than 2**(earnerProof/32)
require(earnerLeafIndex < (1 << (earnerProof.length / 32)), InvalidEarnerLeafIndex());
// Verify inclusion of earner leaf
bytes32 earnerLeafHash = calculateEarnerLeafHash(earnerLeaf);
// forgefmt: disable-next-item
require(
Merkle.verifyInclusionKeccak({
root: root,
index: earnerLeafIndex,
proof: earnerProof,
leaf: earnerLeafHash
}),
InvalidClaimProof()
);
}
/**
* @notice Internal helper to get the operator split in basis points.
* @dev It takes default split and activation delay into account while calculating the split.
* @param operatorSplit The split struct for an Operator
* @return The split in basis points.
*/
function _getOperatorSplit(
OperatorSplit memory operatorSplit
) internal view returns (uint16) {
if (
(operatorSplit.activatedAt == 0)
|| (operatorSplit.oldSplitBips == type(uint16).max && block.timestamp < operatorSplit.activatedAt)
) {
// Return the Default Operator Split if the operator split has not been initialized.
// Also return the Default Operator Split if the operator split has been initialized but not activated yet. (i.e the first initialization)
return defaultOperatorSplitBips;
} else {
// Return the new split if the new split has been activated, else return the old split.
return
(block.timestamp >= operatorSplit.activatedAt) ? operatorSplit.newSplitBips : operatorSplit.oldSplitBips;
}
}
/**
*
* VIEW FUNCTIONS
*
*/
/// @inheritdoc IRewardsCoordinator
function calculateEarnerLeafHash(
EarnerTreeMerkleLeaf calldata leaf
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(EARNER_LEAF_SALT, leaf.earner, leaf.earnerTokenRoot));
}
/// @inheritdoc IRewardsCoordinator
function calculateTokenLeafHash(
TokenTreeMerkleLeaf calldata leaf
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(TOKEN_LEAF_SALT, leaf.token, leaf.cumulativeEarnings));
}
/// @inheritdoc IRewardsCoordinator
function checkClaim(
RewardsMerkleClaim calldata claim
) public view returns (bool) {
_checkClaim(claim, _distributionRoots[claim.rootIndex]);
return true;
}
/// @inheritdoc IRewardsCoordinator
function getOperatorAVSSplit(address operator, address avs) external view returns (uint16) {
return _getOperatorSplit(_operatorAVSSplitBips[operator][avs]);
}
/// @inheritdoc IRewardsCoordinator
function getOperatorPISplit(
address operator
) external view returns (uint16) {
return _getOperatorSplit(_operatorPISplitBips[operator]);
}
/// @inheritdoc IRewardsCoordinator
function getOperatorSetSplit(address operator, OperatorSet calldata operatorSet) external view returns (uint16) {
return _getOperatorSplit(_operatorSetSplitBips[operator][operatorSet.key()]);
}
/// @inheritdoc IRewardsCoordinator
function getDistributionRootsLength() public view returns (uint256) {
return _distributionRoots.length;
}
/// @inheritdoc IRewardsCoordinator
function getDistributionRootAtIndex(
uint256 index
) external view returns (DistributionRoot memory) {
return _distributionRoots[index];
}
/// @inheritdoc IRewardsCoordinator
function getCurrentDistributionRoot() external view returns (DistributionRoot memory) {
return _distributionRoots[_distributionRoots.length - 1];
}
/// @inheritdoc IRewardsCoordinator
function getCurrentClaimableDistributionRoot() external view returns (DistributionRoot memory) {
for (uint256 i = _distributionRoots.length; i > 0; i--) {
DistributionRoot memory root = _distributionRoots[i - 1];
if (!root.disabled && block.timestamp >= root.activatedAt) {
return root;
}
}
// Silence compiler warning.
return DistributionRoot(bytes32(0), 0, 0, false);
}
/// @inheritdoc IRewardsCoordinator
function getRootIndexFromHash(
bytes32 rootHash
) public view returns (uint32) {
for (uint32 i = uint32(_distributionRoots.length); i > 0; i--) {
if (_distributionRoots[i - 1].root == rootHash) {
return i - 1;
}
}
revert InvalidRoot();
}
}
````
## File: src/contracts/core/RewardsCoordinatorStorage.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IRewardsCoordinator.sol";
/**
* @title Storage variables for the `RewardsCoordinator` contract.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice This storage contract is separate from the logic to simplify the upgrade process.
*/
abstract contract RewardsCoordinatorStorage is IRewardsCoordinator {
// Constants
/// @dev Index for flag that pauses calling createAVSRewardsSubmission
uint8 internal constant PAUSED_AVS_REWARDS_SUBMISSION = 0;
/// @dev Index for flag that pauses calling createRewardsForAllSubmission
uint8 internal constant PAUSED_REWARDS_FOR_ALL_SUBMISSION = 1;
/// @dev Index for flag that pauses calling processClaim
uint8 internal constant PAUSED_PROCESS_CLAIM = 2;
/// @dev Index for flag that pauses submitRoots and disableRoot
uint8 internal constant PAUSED_SUBMIT_DISABLE_ROOTS = 3;
/// @dev Index for flag that pauses calling rewardAllStakersAndOperators
uint8 internal constant PAUSED_REWARD_ALL_STAKERS_AND_OPERATORS = 4;
/// @dev Index for flag that pauses calling createOperatorDirectedAVSRewardsSubmission
uint8 internal constant PAUSED_OPERATOR_DIRECTED_AVS_REWARDS_SUBMISSION = 5;
/// @dev Index for flag that pauses calling setOperatorAVSSplit
uint8 internal constant PAUSED_OPERATOR_AVS_SPLIT = 6;
/// @dev Index for flag that pauses calling setOperatorPISplit
uint8 internal constant PAUSED_OPERATOR_PI_SPLIT = 7;
/// @dev Index for flag that pauses calling setOperatorSetSplit
uint8 internal constant PAUSED_OPERATOR_SET_SPLIT = 8;
/// @dev Index for flag that pauses calling setOperatorSetPerformanceRewardsSubmission
uint8 internal constant PAUSED_OPERATOR_DIRECTED_OPERATOR_SET_REWARDS_SUBMISSION = 9;
/// @dev Salt for the earner leaf, meant to distinguish from tokenLeaf since they have the same sized data
uint8 internal constant EARNER_LEAF_SALT = 0;
/// @dev Salt for the token leaf, meant to distinguish from earnerLeaf since they have the same sized data
uint8 internal constant TOKEN_LEAF_SALT = 1;
/// @notice The maximum rewards token amount for a single rewards submission, constrained by off-chain calculation
uint256 internal constant MAX_REWARDS_AMOUNT = 1e38 - 1;
/// @notice Equivalent to 100%, but in basis points.
uint16 internal constant ONE_HUNDRED_IN_BIPS = 10_000;
/// @notice Canonical, virtual beacon chain ETH strategy
IStrategy public constant beaconChainETHStrategy = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);
// Immutables
/// @notice The DelegationManager contract for EigenLayer
IDelegationManager public immutable delegationManager;
/// @notice The StrategyManager contract for EigenLayer
IStrategyManager public immutable strategyManager;
/// @notice The AllocationManager contract for EigenLayer
IAllocationManager public immutable allocationManager;
/// @notice The interval in seconds at which the calculation for rewards distribution is done.
/// @dev RewardsSubmission durations must be multiples of this interval. This is going to be configured to 1 day
uint32 public immutable CALCULATION_INTERVAL_SECONDS;
/// @notice The maximum amount of time (seconds) that a rewards submission can span over
uint32 public immutable MAX_REWARDS_DURATION;
/// @notice max amount of time (seconds) that a rewards submission can start in the past
uint32 public immutable MAX_RETROACTIVE_LENGTH;
/// @notice max amount of time (seconds) that a rewards submission can start in the future
uint32 public immutable MAX_FUTURE_LENGTH;
/// @notice absolute min timestamp (seconds) that a rewards submission can start at
uint32 public immutable GENESIS_REWARDS_TIMESTAMP;
/// @notice The cadence at which a snapshot is taken offchain for calculating rewards distributions
uint32 internal constant SNAPSHOT_CADENCE = 1 days;
// Mutatables
/// @dev Do not remove, deprecated storage.
bytes32 internal __deprecated_DOMAIN_SEPARATOR;
/**
* @notice List of roots submitted by the rewardsUpdater
* @dev Array is internal with an external getter so we can return a `DistributionRoot[] memory` object
*/
DistributionRoot[] internal _distributionRoots;
/// Slot 2
/// @notice The address of the entity that can update the contract with new merkle roots
address public rewardsUpdater;
/// @notice Delay in timestamp (seconds) before a posted root can be claimed against
uint32 public activationDelay;
/// @notice Timestamp for last submitted DistributionRoot
uint32 public currRewardsCalculationEndTimestamp;
/// @notice the default split for all operators across all avss in bips.
uint16 public defaultOperatorSplitBips;
/// @notice Returns the `claimer` for a given `earner`.
/// @dev The claimer is able to call `processClaim` on behalf of the `earner`.
mapping(address earner => address claimer) public claimerFor;
/// @notice Returns the total claimed amount for an `earner` for a given `token`.
mapping(address earner => mapping(IERC20 token => uint256 totalClaimed)) public cumulativeClaimed;
/// @notice Returns the submission `nonce` for an `avs`.
mapping(address avs => uint256 nonce) public submissionNonce;
/// @notice Returns whether a `hash` is a `valid` rewards submission hash for a given `avs`.
mapping(address avs => mapping(bytes32 hash => bool valid)) public isAVSRewardsSubmissionHash;
/// @notice Returns whether a `hash` is a `valid` rewards submission for all hash for a given `avs`.
mapping(address avs => mapping(bytes32 hash => bool valid)) public isRewardsSubmissionForAllHash;
/// @notice Returns whether a `submitter` is a `valid` rewards for all submitter.
mapping(address submitter => bool valid) public isRewardsForAllSubmitter;
/// @notice Returns whether a `hash` is a `valid` rewards submission for all earners hash for a given `avs`.
mapping(address avs => mapping(bytes32 hash => bool valid)) public isRewardsSubmissionForAllEarnersHash;
/// @notice Returns whether a `hash` is a `valid` operator set performance rewards submission hash for a given `avs`.
mapping(address avs => mapping(bytes32 hash => bool valid)) public isOperatorDirectedAVSRewardsSubmissionHash;
/// @notice Returns the `split` an `operator` takes for an `avs`.
mapping(address operator => mapping(address avs => OperatorSplit split)) internal _operatorAVSSplitBips;
/// @notice Returns the `split` an `operator` takes for Programmatic Incentives.
mapping(address operator => OperatorSplit split) internal _operatorPISplitBips;
/// @notice Returns the `split` an `operator` takes for a given operator set.
mapping(address operator => mapping(bytes32 operatorSetKey => OperatorSplit split)) internal _operatorSetSplitBips;
/// @notice Returns whether a `hash` is a `valid` operator set performance rewards submission hash for a given `avs`.
mapping(address avs => mapping(bytes32 hash => bool valid)) public
isOperatorDirectedOperatorSetRewardsSubmissionHash;
// Construction
constructor(
IDelegationManager _delegationManager,
IStrategyManager _strategyManager,
IAllocationManager _allocationManager,
uint32 _CALCULATION_INTERVAL_SECONDS,
uint32 _MAX_REWARDS_DURATION,
uint32 _MAX_RETROACTIVE_LENGTH,
uint32 _MAX_FUTURE_LENGTH,
uint32 _GENESIS_REWARDS_TIMESTAMP
) {
require(
_GENESIS_REWARDS_TIMESTAMP % _CALCULATION_INTERVAL_SECONDS == 0, InvalidGenesisRewardsTimestampRemainder()
);
require(_CALCULATION_INTERVAL_SECONDS % SNAPSHOT_CADENCE == 0, InvalidCalculationIntervalSecondsRemainder());
delegationManager = _delegationManager;
strategyManager = _strategyManager;
allocationManager = _allocationManager;
CALCULATION_INTERVAL_SECONDS = _CALCULATION_INTERVAL_SECONDS;
MAX_REWARDS_DURATION = _MAX_REWARDS_DURATION;
MAX_RETROACTIVE_LENGTH = _MAX_RETROACTIVE_LENGTH;
MAX_FUTURE_LENGTH = _MAX_FUTURE_LENGTH;
GENESIS_REWARDS_TIMESTAMP = _GENESIS_REWARDS_TIMESTAMP;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[35] private __gap;
}
````
## File: src/contracts/core/StrategyManager.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../mixins/SignatureUtilsMixin.sol";
import "../interfaces/IEigenPodManager.sol";
import "../permissions/Pausable.sol";
import "./StrategyManagerStorage.sol";
/**
* @title The primary entry- and exit-point for funds into and out of EigenLayer.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice This contract is for managing deposits in different strategies. The main
* functionalities are:
* - adding and removing strategies that any delegator can deposit into
* - enabling deposit of assets into specified strategy(s)
*/
contract StrategyManager is
Initializable,
OwnableUpgradeable,
ReentrancyGuardUpgradeable,
Pausable,
StrategyManagerStorage,
SignatureUtilsMixin
{
using SlashingLib for *;
using SafeERC20 for IERC20;
modifier onlyStrategyWhitelister() {
require(msg.sender == strategyWhitelister, OnlyStrategyWhitelister());
_;
}
modifier onlyStrategiesWhitelistedForDeposit(
IStrategy strategy
) {
require(strategyIsWhitelistedForDeposit[strategy], StrategyNotWhitelisted());
_;
}
modifier onlyDelegationManager() {
require(msg.sender == address(delegation), OnlyDelegationManager());
_;
}
/**
* @param _delegation The delegation contract of EigenLayer.
*/
constructor(
IDelegationManager _delegation,
IPauserRegistry _pauserRegistry,
string memory _version
) StrategyManagerStorage(_delegation) Pausable(_pauserRegistry) SignatureUtilsMixin(_version) {
_disableInitializers();
}
// EXTERNAL FUNCTIONS
/**
* @notice Initializes the strategy manager contract. Sets the `pauserRegistry` (currently **not** modifiable after being set),
* and transfers contract ownership to the specified `initialOwner`.
* @param initialOwner Ownership of this contract is transferred to this address.
* @param initialStrategyWhitelister The initial value of `strategyWhitelister` to set.
*/
function initialize(
address initialOwner,
address initialStrategyWhitelister,
uint256 initialPausedStatus
) external initializer {
_setPausedStatus(initialPausedStatus);
_transferOwnership(initialOwner);
_setStrategyWhitelister(initialStrategyWhitelister);
}
/// @inheritdoc IStrategyManager
function depositIntoStrategy(
IStrategy strategy,
IERC20 token,
uint256 amount
) external onlyWhenNotPaused(PAUSED_DEPOSITS) nonReentrant returns (uint256 depositShares) {
depositShares = _depositIntoStrategy(msg.sender, strategy, token, amount);
}
/// @inheritdoc IStrategyManager
function depositIntoStrategyWithSignature(
IStrategy strategy,
IERC20 token,
uint256 amount,
address staker,
uint256 expiry,
bytes memory signature
) external onlyWhenNotPaused(PAUSED_DEPOSITS) nonReentrant returns (uint256 depositShares) {
// Cache staker's nonce to avoid sloads.
uint256 nonce = nonces[staker];
// Assert that the signature is valid.
_checkIsValidSignatureNow({
signer: staker,
signableDigest: calculateStrategyDepositDigestHash(staker, strategy, token, amount, nonce, expiry),
signature: signature,
expiry: expiry
});
// Increment the nonce for the staker.
unchecked {
nonces[staker] = nonce + 1;
}
// deposit the tokens (from the `msg.sender`) and credit the new shares to the `staker`
depositShares = _depositIntoStrategy(staker, strategy, token, amount);
}
/// @inheritdoc IShareManager
function removeDepositShares(
address staker,
IStrategy strategy,
uint256 depositSharesToRemove
) external onlyDelegationManager nonReentrant returns (uint256) {
(, uint256 sharesAfter) = _removeDepositShares(staker, strategy, depositSharesToRemove);
return sharesAfter;
}
/// @inheritdoc IShareManager
function addShares(
address staker,
IStrategy strategy,
uint256 shares
) external onlyDelegationManager nonReentrant returns (uint256, uint256) {
return _addShares(staker, strategy, shares);
}
/// @inheritdoc IShareManager
function withdrawSharesAsTokens(
address staker,
IStrategy strategy,
IERC20 token,
uint256 shares
) external onlyDelegationManager nonReentrant {
strategy.withdraw(staker, token, shares);
}
/// @inheritdoc IShareManager
function increaseBurnableShares(
IStrategy strategy,
uint256 addedSharesToBurn
) external onlyDelegationManager nonReentrant {
(, uint256 currentShares) = EnumerableMap.tryGet(burnableShares, address(strategy));
EnumerableMap.set(burnableShares, address(strategy), currentShares + addedSharesToBurn);
emit BurnableSharesIncreased(strategy, addedSharesToBurn);
}
/// @inheritdoc IStrategyManager
function burnShares(
IStrategy strategy
) external nonReentrant {
(, uint256 sharesToBurn) = EnumerableMap.tryGet(burnableShares, address(strategy));
EnumerableMap.remove(burnableShares, address(strategy));
emit BurnableSharesDecreased(strategy, sharesToBurn);
// Burning acts like withdrawing, except that the destination is to the burn address.
// If we have no shares to burn, we don't need to call the strategy.
if (sharesToBurn != 0) {
strategy.withdraw(DEFAULT_BURN_ADDRESS, strategy.underlyingToken(), sharesToBurn);
}
}
/// @inheritdoc IStrategyManager
function setStrategyWhitelister(
address newStrategyWhitelister
) external onlyOwner nonReentrant {
_setStrategyWhitelister(newStrategyWhitelister);
}
/// @inheritdoc IStrategyManager
function addStrategiesToDepositWhitelist(
IStrategy[] calldata strategiesToWhitelist
) external onlyStrategyWhitelister nonReentrant {
uint256 strategiesToWhitelistLength = strategiesToWhitelist.length;
for (uint256 i = 0; i < strategiesToWhitelistLength; ++i) {
// change storage and emit event only if strategy is not already in whitelist
if (!strategyIsWhitelistedForDeposit[strategiesToWhitelist[i]]) {
strategyIsWhitelistedForDeposit[strategiesToWhitelist[i]] = true;
emit StrategyAddedToDepositWhitelist(strategiesToWhitelist[i]);
}
}
}
/// @inheritdoc IStrategyManager
function removeStrategiesFromDepositWhitelist(
IStrategy[] calldata strategiesToRemoveFromWhitelist
) external onlyStrategyWhitelister nonReentrant {
uint256 strategiesToRemoveFromWhitelistLength = strategiesToRemoveFromWhitelist.length;
for (uint256 i = 0; i < strategiesToRemoveFromWhitelistLength; ++i) {
// change storage and emit event only if strategy is already in whitelist
if (strategyIsWhitelistedForDeposit[strategiesToRemoveFromWhitelist[i]]) {
strategyIsWhitelistedForDeposit[strategiesToRemoveFromWhitelist[i]] = false;
emit StrategyRemovedFromDepositWhitelist(strategiesToRemoveFromWhitelist[i]);
}
}
}
// INTERNAL FUNCTIONS
/**
* @notice This function adds `shares` for a given `strategy` to the `staker` and runs through the necessary update logic.
* @param staker The address to add shares to
* @param strategy The Strategy in which the `staker` is receiving shares
* @param shares The amount of shares to grant to the `staker`
* @dev In particular, this function calls `delegation.increaseDelegatedShares(staker, strategy, shares)` to ensure that all
* delegated shares are tracked, increases the stored share amount in `stakerDepositShares[staker][strategy]`, and adds `strategy`
* to the `staker`'s list of strategies, if it is not in the list already.
*/
function _addShares(address staker, IStrategy strategy, uint256 shares) internal returns (uint256, uint256) {
// sanity checks on inputs
require(staker != address(0), StakerAddressZero());
require(shares != 0, SharesAmountZero());
uint256 prevDepositShares = stakerDepositShares[staker][strategy];
// if they dont have prevDepositShares of this strategy, add it to their strats
if (prevDepositShares == 0) {
require(stakerStrategyList[staker].length < MAX_STAKER_STRATEGY_LIST_LENGTH, MaxStrategiesExceeded());
stakerStrategyList[staker].push(strategy);
}
// add the returned depositedShares to their existing shares for this strategy
stakerDepositShares[staker][strategy] = prevDepositShares + shares;
emit Deposit(staker, strategy, shares);
return (prevDepositShares, shares);
}
/**
* @notice Internal function in which `amount` of ERC20 `token` is transferred from `msg.sender` to the Strategy-type contract
* `strategy`, with the resulting shares credited to `staker`.
* @param staker The address that will be credited with the new shares.
* @param strategy The Strategy contract to deposit into.
* @param token The ERC20 token to deposit.
* @param amount The amount of `token` to deposit.
* @return shares The amount of *new* shares in `strategy` that have been credited to the `staker`.
*/
function _depositIntoStrategy(
address staker,
IStrategy strategy,
IERC20 token,
uint256 amount
) internal onlyStrategiesWhitelistedForDeposit(strategy) returns (uint256 shares) {
// transfer tokens from the sender to the strategy
token.safeTransferFrom(msg.sender, address(strategy), amount);
// deposit the assets into the specified strategy and get the equivalent amount of shares in that strategy
shares = strategy.deposit(token, amount);
// add the returned shares to the staker's existing shares for this strategy
(uint256 prevDepositShares, uint256 addedShares) = _addShares(staker, strategy, shares);
// Increase shares delegated to operator
delegation.increaseDelegatedShares({
staker: staker,
strategy: strategy,
prevDepositShares: prevDepositShares,
addedShares: addedShares
});
return shares;
}
/**
* @notice Decreases the shares that `staker` holds in `strategy` by `depositSharesToRemove`.
* @param staker The address to decrement shares from
* @param strategy The strategy for which the `staker`'s shares are being decremented
* @param depositSharesToRemove The amount of deposit shares to decrement
* @dev If the amount of shares represents all of the staker`s shares in said strategy,
* then the strategy is removed from stakerStrategyList[staker] and 'true' is returned. Otherwise 'false' is returned.
* Also returns the user's updated deposit shares after decrement.
*/
function _removeDepositShares(
address staker,
IStrategy strategy,
uint256 depositSharesToRemove
) internal returns (bool, uint256) {
// sanity checks on inputs
require(depositSharesToRemove != 0, SharesAmountZero());
//check that the user has sufficient shares
uint256 userDepositShares = stakerDepositShares[staker][strategy];
// This check technically shouldn't actually ever revert because depositSharesToRemove is already
// checked to not exceed max amount of shares when the withdrawal was queued in the DelegationManager
require(depositSharesToRemove <= userDepositShares, SharesAmountTooHigh());
userDepositShares = userDepositShares - depositSharesToRemove;
// subtract the shares from the staker's existing shares for this strategy
stakerDepositShares[staker][strategy] = userDepositShares;
// if no existing shares, remove the strategy from the staker's dynamic array of strategies
if (userDepositShares == 0) {
_removeStrategyFromStakerStrategyList(staker, strategy);
// return true in the event that the strategy was removed from stakerStrategyList[staker]
return (true, userDepositShares);
}
// return false in the event that the strategy was *not* removed from stakerStrategyList[staker]
return (false, userDepositShares);
}
/**
* @notice Removes `strategy` from `staker`'s dynamic array of strategies, i.e. from `stakerStrategyList[staker]`
* @param staker The user whose array will have an entry removed
* @param strategy The Strategy to remove from `stakerStrategyList[staker]`
*/
function _removeStrategyFromStakerStrategyList(address staker, IStrategy strategy) internal {
//loop through all of the strategies, find the right one, then replace
uint256 stratsLength = stakerStrategyList[staker].length;
uint256 j = 0;
for (; j < stratsLength; ++j) {
if (stakerStrategyList[staker][j] == strategy) {
//replace the strategy with the last strategy in the list
stakerStrategyList[staker][j] = stakerStrategyList[staker][stakerStrategyList[staker].length - 1];
break;
}
}
// if we didn't find the strategy, revert
require(j != stratsLength, StrategyNotFound());
// pop off the last entry in the list of strategies
stakerStrategyList[staker].pop();
}
/**
* @notice Internal function for modifying the `strategyWhitelister`. Used inside of the `setStrategyWhitelister` and `initialize` functions.
* @param newStrategyWhitelister The new address for the `strategyWhitelister` to take.
*/
function _setStrategyWhitelister(
address newStrategyWhitelister
) internal {
emit StrategyWhitelisterChanged(strategyWhitelister, newStrategyWhitelister);
strategyWhitelister = newStrategyWhitelister;
}
// VIEW FUNCTIONS
/// @inheritdoc IStrategyManager
function getDeposits(
address staker
) external view returns (IStrategy[] memory, uint256[] memory) {
uint256 strategiesLength = stakerStrategyList[staker].length;
uint256[] memory depositedShares = new uint256[](strategiesLength);
for (uint256 i = 0; i < strategiesLength; ++i) {
depositedShares[i] = stakerDepositShares[staker][stakerStrategyList[staker][i]];
}
return (stakerStrategyList[staker], depositedShares);
}
function getStakerStrategyList(
address staker
) external view returns (IStrategy[] memory) {
return stakerStrategyList[staker];
}
/// @inheritdoc IStrategyManager
function stakerStrategyListLength(
address staker
) external view returns (uint256) {
return stakerStrategyList[staker].length;
}
/// @inheritdoc IStrategyManager
function calculateStrategyDepositDigestHash(
address staker,
IStrategy strategy,
IERC20 token,
uint256 amount,
uint256 nonce,
uint256 expiry
) public view returns (bytes32) {
/// forgefmt: disable-next-item
return _calculateSignableDigest(
keccak256(
abi.encode(
DEPOSIT_TYPEHASH,
staker,
strategy,
token,
amount,
nonce,
expiry
)
)
);
}
/// @inheritdoc IStrategyManager
function getBurnableShares(
IStrategy strategy
) external view returns (uint256) {
(, uint256 shares) = EnumerableMap.tryGet(burnableShares, address(strategy));
return shares;
}
/// @inheritdoc IStrategyManager
function getStrategiesWithBurnableShares() external view returns (address[] memory, uint256[] memory) {
uint256 totalEntries = EnumerableMap.length(burnableShares);
address[] memory strategies = new address[](totalEntries);
uint256[] memory shares = new uint256[](totalEntries);
for (uint256 i = 0; i < totalEntries; i++) {
(address strategy, uint256 shareAmount) = EnumerableMap.at(burnableShares, i);
strategies[i] = strategy;
shares[i] = shareAmount;
}
return (strategies, shares);
}
}
````
## File: src/contracts/core/StrategyManagerStorage.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
import "../interfaces/IStrategyManager.sol";
import "../interfaces/IStrategy.sol";
import "../interfaces/IEigenPodManager.sol";
import "../interfaces/IDelegationManager.sol";
import "../interfaces/IAVSDirectory.sol";
/**
* @title Storage variables for the `StrategyManager` contract.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice This storage contract is separate from the logic to simplify the upgrade process.
*/
abstract contract StrategyManagerStorage is IStrategyManager {
// Constants
/// @notice The EIP-712 typehash for the deposit struct used by the contract
bytes32 public constant DEPOSIT_TYPEHASH =
keccak256("Deposit(address staker,address strategy,address token,uint256 amount,uint256 nonce,uint256 expiry)");
// maximum length of dynamic arrays in `stakerStrategyList` mapping, for sanity's sake
uint8 internal constant MAX_STAKER_STRATEGY_LIST_LENGTH = 32;
// 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 = 0x00000000000000000000000000000000000E16E4;
// Immutables
IDelegationManager public immutable delegation;
// Mutatables
/// @dev Do not remove, deprecated storage.
bytes32 internal __deprecated_DOMAIN_SEPARATOR;
/// @notice Returns the signature `nonce` for each `signer`.
mapping(address signer => uint256 nonce) public nonces;
/// @notice Returns the permissioned address that can whitelist strategies.
address public strategyWhitelister;
/// @dev Do not remove, deprecated storage.
uint256 private __deprecated_withdrawalDelayBlocks;
/// @notice Returns the number of deposited `shares` for a `staker` for a given `strategy`.
/// @dev All of these shares may not be withdrawable if the staker has delegated to an operator that has been slashed.
mapping(address staker => mapping(IStrategy strategy => uint256 shares)) public stakerDepositShares;
/// @notice Returns a list of the `strategies` that a `staker` is currently staking in.
mapping(address staker => IStrategy[] strategies) public stakerStrategyList;
/// @dev Do not remove, deprecated storage.
mapping(bytes32 withdrawalRoot => bool pending) private __deprecated_withdrawalRootPending;
/// @dev Do not remove, deprecated storage.
mapping(address staker => uint256 totalQueued) private __deprecated_numWithdrawalsQueued;
/// @notice Returns whether a `strategy` is `whitelisted` for deposits.
mapping(IStrategy strategy => bool whitelisted) public strategyIsWhitelistedForDeposit;
/// @dev Do not remove, deprecated storage.
mapping(address avs => uint256 shares) private __deprecated_beaconChainETHSharesToDecrementOnWithdrawal;
/// @dev Do not remove, deprecated storage.
mapping(IStrategy strategy => bool) private __deprecated_thirdPartyTransfersForbidden;
/// @notice Returns the amount of `shares` that have been slashed on EigenLayer but not burned yet. Takes 3 storage slots.
EnumerableMap.AddressToUintMap internal burnableShares;
// Construction
/**
* @param _delegation The delegation contract of EigenLayer.
*/
constructor(
IDelegationManager _delegation
) {
delegation = _delegation;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[36] private __gap;
}
````
## File: src/contracts/interfaces/IAllocationManager.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import {OperatorSet} from "../libraries/OperatorSetLib.sol";
import "./IPauserRegistry.sol";
import "./IStrategy.sol";
import "./IAVSRegistrar.sol";
import "./ISemVerMixin.sol";
interface IAllocationManagerErrors {
/// Input Validation
/// @dev Thrown when `wadToSlash` is zero or greater than 1e18
error InvalidWadToSlash();
/// @dev Thrown when two array parameters have mismatching lengths.
error InputArrayLengthMismatch();
/// @dev Thrown when the AVSRegistrar is not correctly configured to prevent an AVSRegistrar contract
/// from being used with the wrong AVS
error InvalidAVSRegistrar();
/// Caller
/// @dev Thrown when caller is not authorized to call a function.
error InvalidCaller();
/// Operator Status
/// @dev Thrown when an invalid operator is provided.
error InvalidOperator();
/// @dev Thrown when an invalid avs whose metadata is not registered is provided.
error NonexistentAVSMetadata();
/// @dev Thrown when an operator's allocation delay has yet to be set.
error UninitializedAllocationDelay();
/// @dev Thrown when attempting to slash an operator when they are not slashable.
error OperatorNotSlashable();
/// @dev Thrown when trying to add an operator to a set they are already a member of
error AlreadyMemberOfSet();
/// @dev Thrown when trying to slash/remove an operator from a set they are not a member of
error NotMemberOfSet();
/// Operator Set Status
/// @dev Thrown when an invalid operator set is provided.
error InvalidOperatorSet();
/// @dev Thrown when provided `strategies` are not in ascending order.
error StrategiesMustBeInAscendingOrder();
/// @dev Thrown when trying to add a strategy to an operator set that already contains it.
error StrategyAlreadyInOperatorSet();
/// @dev Thrown when a strategy is referenced that does not belong to an operator set.
error StrategyNotInOperatorSet();
/// Modifying Allocations
/// @dev Thrown when an operator attempts to set their allocation for an operatorSet to the same value
error SameMagnitude();
/// @dev Thrown when an allocation is attempted for a given operator when they have pending allocations or deallocations.
error ModificationAlreadyPending();
/// @dev Thrown when an allocation is attempted that exceeds a given operators total allocatable magnitude.
error InsufficientMagnitude();
}
interface IAllocationManagerTypes {
/**
* @notice Defines allocation information from a strategy to an operator set, for an operator
* @param currentMagnitude the current magnitude allocated from the strategy to the operator set
* @param pendingDiff a pending change in magnitude, if it exists (0 otherwise)
* @param effectBlock the block at which the pending magnitude diff will take effect
*/
struct Allocation {
uint64 currentMagnitude;
int128 pendingDiff;
uint32 effectBlock;
}
/**
* @notice Struct containing allocation delay metadata for a given operator.
* @param delay Current allocation delay
* @param isSet Whether the operator has initially set an allocation delay. Note that this could be false but the
* block.number >= effectBlock in which we consider their delay to be configured and active.
* @param pendingDelay The delay that will take effect after `effectBlock`
* @param effectBlock The block number after which a pending delay will take effect
*/
struct AllocationDelayInfo {
uint32 delay;
bool isSet;
uint32 pendingDelay;
uint32 effectBlock;
}
/**
* @notice Contains registration details for an operator pertaining to an operator set
* @param registered Whether the operator is currently registered for the operator set
* @param slashableUntil If the operator is not registered, they are still slashable until
* this block is reached.
*/
struct RegistrationStatus {
bool registered;
uint32 slashableUntil;
}
/**
* @notice Contains allocation info for a specific strategy
* @param maxMagnitude the maximum magnitude that can be allocated between all operator sets
* @param encumberedMagnitude the currently-allocated magnitude for the strategy
*/
struct StrategyInfo {
uint64 maxMagnitude;
uint64 encumberedMagnitude;
}
/**
* @notice Struct containing parameters to slashing
* @param operator the address to slash
* @param operatorSetId the ID of the operatorSet the operator is being slashed on behalf of
* @param strategies the set of strategies to slash
* @param wadsToSlash the parts in 1e18 to slash, this will be proportional to the operator's
* slashable stake allocation for the operatorSet
* @param description the description of the slashing provided by the AVS for legibility
*/
struct SlashingParams {
address operator;
uint32 operatorSetId;
IStrategy[] strategies;
uint256[] wadsToSlash;
string description;
}
/**
* @notice struct used to modify the allocation of slashable magnitude to an operator set
* @param operatorSet the operator set to modify the allocation for
* @param strategies the strategies to modify allocations for
* @param newMagnitudes the new magnitude to allocate for each strategy to this operator set
*/
struct AllocateParams {
OperatorSet operatorSet;
IStrategy[] strategies;
uint64[] newMagnitudes;
}
/**
* @notice Parameters used to register for an AVS's operator sets
* @param avs the AVS being registered for
* @param operatorSetIds the operator sets within the AVS to register for
* @param data extra data to be passed to the AVS to complete registration
*/
struct RegisterParams {
address avs;
uint32[] operatorSetIds;
bytes data;
}
/**
* @notice Parameters used to deregister from an AVS's operator sets
* @param operator the operator being deregistered
* @param avs the avs being deregistered from
* @param operatorSetIds the operator sets within the AVS being deregistered from
*/
struct DeregisterParams {
address operator;
address avs;
uint32[] operatorSetIds;
}
/**
* @notice Parameters used by an AVS to create new operator sets
* @param operatorSetId the id of the operator set to create
* @param strategies the strategies to add as slashable to the operator set
*/
struct CreateSetParams {
uint32 operatorSetId;
IStrategy[] strategies;
}
}
interface IAllocationManagerEvents is IAllocationManagerTypes {
/// @notice Emitted when operator updates their allocation delay.
event AllocationDelaySet(address operator, uint32 delay, uint32 effectBlock);
/// @notice Emitted when an operator's magnitude is updated for a given operatorSet and strategy
event AllocationUpdated(
address operator, OperatorSet operatorSet, IStrategy strategy, uint64 magnitude, uint32 effectBlock
);
/// @notice Emitted when operator's encumbered magnitude is updated for a given strategy
event EncumberedMagnitudeUpdated(address operator, IStrategy strategy, uint64 encumberedMagnitude);
/// @notice Emitted when an operator's max magnitude is updated for a given strategy
event MaxMagnitudeUpdated(address operator, IStrategy strategy, uint64 maxMagnitude);
/// @notice Emitted when an operator is slashed by an operator set for a strategy
/// `wadSlashed` is the proportion of the operator's total delegated stake that was slashed
event OperatorSlashed(
address operator, OperatorSet operatorSet, IStrategy[] strategies, uint256[] wadSlashed, string description
);
/// @notice Emitted when an AVS configures the address that will handle registration/deregistration
event AVSRegistrarSet(address avs, IAVSRegistrar registrar);
/// @notice Emitted when an AVS updates their metadata URI (Uniform Resource Identifier).
/// @dev The URI is never stored; it is simply emitted through an event for off-chain indexing.
event AVSMetadataURIUpdated(address indexed avs, string metadataURI);
/// @notice Emitted when an operator set is created by an AVS.
event OperatorSetCreated(OperatorSet operatorSet);
/// @notice Emitted when an operator is added to an operator set.
event OperatorAddedToOperatorSet(address indexed operator, OperatorSet operatorSet);
/// @notice Emitted when an operator is removed from an operator set.
event OperatorRemovedFromOperatorSet(address indexed operator, OperatorSet operatorSet);
/// @notice Emitted when a strategy is added to an operator set.
event StrategyAddedToOperatorSet(OperatorSet operatorSet, IStrategy strategy);
/// @notice Emitted when a strategy is removed from an operator set.
event StrategyRemovedFromOperatorSet(OperatorSet operatorSet, IStrategy strategy);
}
interface IAllocationManager is IAllocationManagerErrors, IAllocationManagerEvents, ISemVerMixin {
/**
* @dev Initializes the initial owner and paused status.
*/
function initialize(address initialOwner, uint256 initialPausedStatus) external;
/**
* @notice Called by an AVS to slash an operator in a given operator set. The operator must be registered
* and have slashable stake allocated to the operator set.
*
* @param avs The AVS address initiating the slash.
* @param params The slashing parameters, containing:
* - operator: The operator to slash.
* - operatorSetId: The ID of the operator set the operator is being slashed from.
* - strategies: Array of strategies to slash allocations from (must be in ascending order).
* - wadsToSlash: Array of proportions to slash from each strategy (must be between 0 and 1e18).
* - description: Description of why the operator was slashed.
*
* @dev For each strategy:
* 1. Reduces the operator's current allocation magnitude by wadToSlash proportion.
* 2. Reduces the strategy's max and encumbered magnitudes proportionally.
* 3. If there is a pending deallocation, reduces it proportionally.
* 4. Updates the operator's shares in the DelegationManager.
*
* @dev Small slashing amounts may not result in actual token burns due to
* rounding, which will result in small amounts of tokens locked in the contract
* rather than fully burning through the burn mechanism.
*/
function slashOperator(address avs, SlashingParams calldata params) external;
/**
* @notice Modifies the proportions of slashable stake allocated to an operator set from a list of strategies
* Note that deallocations remain slashable for DEALLOCATION_DELAY blocks therefore when they are cleared they may
* free up less allocatable magnitude than initially deallocated.
* @param operator the operator to modify allocations for
* @param params array of magnitude adjustments for one or more operator sets
* @dev Updates encumberedMagnitude for the updated strategies
*/
function modifyAllocations(address operator, AllocateParams[] calldata params) external;
/**
* @notice This function takes a list of strategies and for each strategy, removes from the deallocationQueue
* all clearable deallocations up to max `numToClear` number of deallocations, updating the encumberedMagnitude
* of the operator as needed.
*
* @param operator address to clear deallocations for
* @param strategies a list of strategies to clear deallocations for
* @param numToClear a list of number of pending deallocations to clear for each strategy
*
* @dev can be called permissionlessly by anyone
*/
function clearDeallocationQueue(
address operator,
IStrategy[] calldata strategies,
uint16[] calldata numToClear
) external;
/**
* @notice Allows an operator to register for one or more operator sets for an AVS. If the operator
* has any stake allocated to these operator sets, it immediately becomes slashable.
* @dev After registering within the ALM, this method calls the AVS Registrar's `IAVSRegistrar.
* registerOperator` method to complete registration. This call MUST succeed in order for
* registration to be successful.
*/
function registerForOperatorSets(address operator, RegisterParams calldata params) external;
/**
* @notice Allows an operator or AVS to deregister the operator from one or more of the AVS's operator sets.
* If the operator has any slashable stake allocated to the AVS, it remains slashable until the
* DEALLOCATION_DELAY has passed.
* @dev After deregistering within the ALM, this method calls the AVS Registrar's `IAVSRegistrar.
* deregisterOperator` method to complete deregistration. This call MUST succeed in order for
* deregistration to be successful.
*/
function deregisterFromOperatorSets(
DeregisterParams calldata params
) external;
/**
* @notice Called by the delegation manager OR an operator to set an operator's allocation delay.
* This is set when the operator first registers, and is the number of blocks between an operator
* allocating magnitude to an operator set, and the magnitude becoming slashable.
* @param operator The operator to set the delay on behalf of.
* @param delay the allocation delay in blocks
*/
function setAllocationDelay(address operator, uint32 delay) external;
/**
* @notice Called by an AVS to configure the address that is called when an operator registers
* or is deregistered from the AVS's operator sets. If not set (or set to 0), defaults
* to the AVS's address.
* @param registrar the new registrar address
*/
function setAVSRegistrar(address avs, IAVSRegistrar registrar) external;
/**
* @notice Called by an AVS to emit an `AVSMetadataURIUpdated` event indicating the information has updated.
*
* @param metadataURI The URI for metadata associated with an AVS.
*
* @dev Note that the `metadataURI` is *never stored* and is only emitted in the `AVSMetadataURIUpdated` event.
*/
function updateAVSMetadataURI(address avs, string calldata metadataURI) external;
/**
* @notice Allows an AVS to create new operator sets, defining strategies that the operator set uses
*/
function createOperatorSets(address avs, CreateSetParams[] calldata params) external;
/**
* @notice Allows an AVS to add strategies to an operator set
* @dev Strategies MUST NOT already exist in the operator set
* @param avs the avs to set strategies for
* @param operatorSetId the operator set to add strategies to
* @param strategies the strategies to add
*/
function addStrategiesToOperatorSet(address avs, uint32 operatorSetId, IStrategy[] calldata strategies) external;
/**
* @notice Allows an AVS to remove strategies from an operator set
* @dev Strategies MUST already exist in the operator set
* @param avs the avs to remove strategies for
* @param operatorSetId the operator set to remove strategies from
* @param strategies the strategies to remove
*/
function removeStrategiesFromOperatorSet(
address avs,
uint32 operatorSetId,
IStrategy[] calldata strategies
) external;
/**
*
* VIEW FUNCTIONS
*
*/
/**
* @notice Returns the number of operator sets for the AVS
* @param avs the AVS to query
*/
function getOperatorSetCount(
address avs
) external view returns (uint256);
/**
* @notice Returns the list of operator sets the operator has current or pending allocations/deallocations in
* @param operator the operator to query
* @return the list of operator sets the operator has current or pending allocations/deallocations in
*/
function getAllocatedSets(
address operator
) external view returns (OperatorSet[] memory);
/**
* @notice Returns the list of strategies an operator has current or pending allocations/deallocations from
* given a specific operator set.
* @param operator the operator to query
* @param operatorSet the operator set to query
* @return the list of strategies
*/
function getAllocatedStrategies(
address operator,
OperatorSet memory operatorSet
) external view returns (IStrategy[] memory);
/**
* @notice Returns the current/pending stake allocation an operator has from a strategy to an operator set
* @param operator the operator to query
* @param operatorSet the operator set to query
* @param strategy the strategy to query
* @return the current/pending stake allocation
*/
function getAllocation(
address operator,
OperatorSet memory operatorSet,
IStrategy strategy
) external view returns (Allocation memory);
/**
* @notice Returns the current/pending stake allocations for multiple operators from a strategy to an operator set
* @param operators the operators to query
* @param operatorSet the operator set to query
* @param strategy the strategy to query
* @return each operator's allocation
*/
function getAllocations(
address[] memory operators,
OperatorSet memory operatorSet,
IStrategy strategy
) external view returns (Allocation[] memory);
/**
* @notice Given a strategy, returns a list of operator sets and corresponding stake allocations.
* @dev Note that this returns a list of ALL operator sets the operator has allocations in. This means
* some of the returned allocations may be zero.
* @param operator the operator to query
* @param strategy the strategy to query
* @return the list of all operator sets the operator has allocations for
* @return the corresponding list of allocations from the specific `strategy`
*/
function getStrategyAllocations(
address operator,
IStrategy strategy
) external view returns (OperatorSet[] memory, Allocation[] memory);
/**
* @notice For a strategy, get the amount of magnitude that is allocated across one or more operator sets
* @param operator the operator to query
* @param strategy the strategy to get allocatable magnitude for
* @return currently allocated magnitude
*/
function getEncumberedMagnitude(address operator, IStrategy strategy) external view returns (uint64);
/**
* @notice For a strategy, get the amount of magnitude not currently allocated to any operator set
* @param operator the operator to query
* @param strategy the strategy to get allocatable magnitude for
* @return magnitude available to be allocated to an operator set
*/
function getAllocatableMagnitude(address operator, IStrategy strategy) external view returns (uint64);
/**
* @notice Returns the maximum magnitude an operator can allocate for the given strategy
* @dev The max magnitude of an operator starts at WAD (1e18), and is decreased anytime
* the operator is slashed. This value acts as a cap on the max magnitude of the operator.
* @param operator the operator to query
* @param strategy the strategy to get the max magnitude for
* @return the max magnitude for the strategy
*/
function getMaxMagnitude(address operator, IStrategy strategy) external view returns (uint64);
/**
* @notice Returns the maximum magnitude an operator can allocate for the given strategies
* @dev The max magnitude of an operator starts at WAD (1e18), and is decreased anytime
* the operator is slashed. This value acts as a cap on the max magnitude of the operator.
* @param operator the operator to query
* @param strategies the strategies to get the max magnitudes for
* @return the max magnitudes for each strategy
*/
function getMaxMagnitudes(
address operator,
IStrategy[] calldata strategies
) external view returns (uint64[] memory);
/**
* @notice Returns the maximum magnitudes each operator can allocate for the given strategy
* @dev The max magnitude of an operator starts at WAD (1e18), and is decreased anytime
* the operator is slashed. This value acts as a cap on the max magnitude of the operator.
* @param operators the operators to query
* @param strategy the strategy to get the max magnitudes for
* @return the max magnitudes for each operator
*/
function getMaxMagnitudes(
address[] calldata operators,
IStrategy strategy
) external view returns (uint64[] memory);
/**
* @notice Returns the maximum magnitude an operator can allocate for the given strategies
* at a given block number
* @dev The max magnitude of an operator starts at WAD (1e18), and is decreased anytime
* the operator is slashed. This value acts as a cap on the max magnitude of the operator.
* @param operator the operator to query
* @param strategies the strategies to get the max magnitudes for
* @param blockNumber the blockNumber at which to check the max magnitudes
* @return the max magnitudes for each strategy
*/
function getMaxMagnitudesAtBlock(
address operator,
IStrategy[] calldata strategies,
uint32 blockNumber
) external view returns (uint64[] memory);
/**
* @notice Returns the time in blocks between an operator allocating slashable magnitude
* and the magnitude becoming slashable. If the delay has not been set, `isSet` will be false.
* @dev The operator must have a configured delay before allocating magnitude
* @param operator The operator to query
* @return isSet Whether the operator has configured a delay
* @return delay The time in blocks between allocating magnitude and magnitude becoming slashable
*/
function getAllocationDelay(
address operator
) external view returns (bool isSet, uint32 delay);
/**
* @notice Returns a list of all operator sets the operator is registered for
* @param operator The operator address to query.
*/
function getRegisteredSets(
address operator
) external view returns (OperatorSet[] memory operatorSets);
/**
* @notice Returns whether the operator is registered for the operator set
* @param operator The operator to query
* @param operatorSet The operator set to query
*/
function isMemberOfOperatorSet(address operator, OperatorSet memory operatorSet) external view returns (bool);
/**
* @notice Returns whether the operator set exists
*/
function isOperatorSet(
OperatorSet memory operatorSet
) external view returns (bool);
/**
* @notice Returns all the operators registered to an operator set
* @param operatorSet The operatorSet to query.
*/
function getMembers(
OperatorSet memory operatorSet
) external view returns (address[] memory operators);
/**
* @notice Returns the number of operators registered to an operatorSet.
* @param operatorSet The operatorSet to get the member count for
*/
function getMemberCount(
OperatorSet memory operatorSet
) external view returns (uint256);
/**
* @notice Returns the address that handles registration/deregistration for the AVS
* If not set, defaults to the input address (`avs`)
*/
function getAVSRegistrar(
address avs
) external view returns (IAVSRegistrar);
/**
* @notice Returns an array of strategies in the operatorSet.
* @param operatorSet The operatorSet to query.
*/
function getStrategiesInOperatorSet(
OperatorSet memory operatorSet
) external view returns (IStrategy[] memory strategies);
/**
* @notice Returns the minimum amount of stake that will be slashable as of some future block,
* according to each operator's allocation from each strategy to the operator set. Note that this function
* will return 0 for the slashable stake if the operator is not slashable at the time of the call.
* @dev This method queries actual delegated stakes in the DelegationManager and applies
* each operator's allocation to the stake to produce the slashable stake each allocation
* represents. This method does not consider slashable stake in the withdrawal queue even though there could be
* slashable stake in the queue.
* @dev This minimum takes into account `futureBlock`, and will omit any pending magnitude
* diffs that will not be in effect as of `futureBlock`. NOTE that in order to get the true
* minimum slashable stake as of some future block, `futureBlock` MUST be greater than block.number
* @dev NOTE that `futureBlock` should be fewer than `DEALLOCATION_DELAY` blocks in the future,
* or the values returned from this method may not be accurate due to deallocations.
* @param operatorSet the operator set to query
* @param operators the list of operators whose slashable stakes will be returned
* @param strategies the strategies that each slashable stake corresponds to
* @param futureBlock the block at which to get allocation information. Should be a future block.
*/
function getMinimumSlashableStake(
OperatorSet memory operatorSet,
address[] memory operators,
IStrategy[] memory strategies,
uint32 futureBlock
) external view returns (uint256[][] memory slashableStake);
/**
* @notice Returns the current allocated stake, irrespective of the operator's slashable status for the operatorSet.
* @param operatorSet the operator set to query
* @param operators the operators to query
* @param strategies the strategies to query
*/
function getAllocatedStake(
OperatorSet memory operatorSet,
address[] memory operators,
IStrategy[] memory strategies
) external view returns (uint256[][] memory slashableStake);
/**
* @notice Returns whether an operator is slashable by an operator set.
* This returns true if the operator is registered or their slashableUntil block has not passed.
* This is because even when operators are deregistered, they still remain slashable for a period of time.
* @param operator the operator to check slashability for
* @param operatorSet the operator set to check slashability for
*/
function isOperatorSlashable(address operator, OperatorSet memory operatorSet) external view returns (bool);
}
````
## File: src/contracts/interfaces/IAVSDirectory.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import "./ISignatureUtilsMixin.sol";
import "./IPauserRegistry.sol";
import "./IStrategy.sol";
interface IAVSDirectoryErrors {
/// Operator Status
/// @dev Thrown when an operator does not exist in the DelegationManager
error OperatorNotRegisteredToEigenLayer();
/// @dev Thrown when an operator is already registered to an AVS.
error OperatorNotRegisteredToAVS();
/// @dev Thrown when `operator` is already registered to the AVS.
error OperatorAlreadyRegisteredToAVS();
/// @dev Thrown when attempting to spend a spent eip-712 salt.
error SaltSpent();
}
interface IAVSDirectoryTypes {
/// @notice Enum representing the registration status of an operator with an AVS.
/// @notice Only used by legacy M2 AVSs that have not integrated with operatorSets.
enum OperatorAVSRegistrationStatus {
UNREGISTERED, // Operator not registered to AVS
REGISTERED // Operator registered to AVS
}
/**
* @notice Struct representing the registration status of an operator with an operator set.
* Keeps track of last deregistered timestamp for slashability concerns.
* @param registered whether the operator is registered with the operator set
* @param lastDeregisteredTimestamp the timestamp at which the operator was last deregistered
*/
struct OperatorSetRegistrationStatus {
bool registered;
uint32 lastDeregisteredTimestamp;
}
}
interface IAVSDirectoryEvents is IAVSDirectoryTypes {
/**
* @notice Emitted when an operator's registration status with an AVS id updated
* @notice Only used by legacy M2 AVSs that have not integrated with operatorSets.
*/
event OperatorAVSRegistrationStatusUpdated(
address indexed operator, address indexed avs, OperatorAVSRegistrationStatus status
);
/// @notice Emitted when an AVS updates their metadata URI (Uniform Resource Identifier).
/// @dev The URI is never stored; it is simply emitted through an event for off-chain indexing.
event AVSMetadataURIUpdated(address indexed avs, string metadataURI);
}
interface IAVSDirectory is IAVSDirectoryEvents, IAVSDirectoryErrors, ISignatureUtilsMixin {
/**
*
* EXTERNAL FUNCTIONS
*
*/
/**
* @dev Initializes the addresses of the initial owner and paused status.
*/
function initialize(address initialOwner, uint256 initialPausedStatus) external;
/**
* @notice Called by an AVS to emit an `AVSMetadataURIUpdated` event indicating the information has updated.
*
* @param metadataURI The URI for metadata associated with an AVS.
*
* @dev Note that the `metadataURI` is *never stored* and is only emitted in the `AVSMetadataURIUpdated` event.
*/
function updateAVSMetadataURI(
string calldata metadataURI
) external;
/**
* @notice Called by an operator to cancel a salt that has been used to register with an AVS.
*
* @param salt A unique and single use value associated with the approver signature.
*/
function cancelSalt(
bytes32 salt
) external;
/**
* @notice Legacy function called by the AVS's service manager contract
* to register an operator with the AVS. NOTE: this function will be deprecated in a future release
* after the slashing release. New AVSs should use `registerForOperatorSets` instead.
*
* @param operator The address of the operator to register.
* @param operatorSignature The signature, salt, and expiry of the operator's signature.
*
* @dev msg.sender must be the AVS.
* @dev Only used by legacy M2 AVSs that have not integrated with operator sets.
*/
function registerOperatorToAVS(
address operator,
ISignatureUtilsMixinTypes.SignatureWithSaltAndExpiry memory operatorSignature
) external;
/**
* @notice Legacy function called by an AVS to deregister an operator from the AVS.
* NOTE: this function will be deprecated in a future release after the slashing release.
* New AVSs integrating should use `deregisterOperatorFromOperatorSets` instead.
*
* @param operator The address of the operator to deregister.
*
* @dev Only used by legacy M2 AVSs that have not integrated with operator sets.
*/
function deregisterOperatorFromAVS(
address operator
) external;
/**
*
* VIEW FUNCTIONS
*
*/
function operatorSaltIsSpent(address operator, bytes32 salt) external view returns (bool);
/**
* @notice Calculates the digest hash to be signed by an operator to register with an AVS.
*
* @param operator The account registering as an operator.
* @param avs The AVS the operator is registering with.
* @param salt A unique and single-use value associated with the approver's signature.
* @param expiry The time after which the approver's signature becomes invalid.
*/
function calculateOperatorAVSRegistrationDigestHash(
address operator,
address avs,
bytes32 salt,
uint256 expiry
) external view returns (bytes32);
/// @notice The EIP-712 typehash for the Registration struct used by the contract.
function OPERATOR_AVS_REGISTRATION_TYPEHASH() external view returns (bytes32);
/// @notice The EIP-712 typehash for the OperatorSetRegistration struct used by the contract.
function OPERATOR_SET_REGISTRATION_TYPEHASH() external view returns (bytes32);
}
````
## File: src/contracts/interfaces/IAVSRegistrar.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
interface IAVSRegistrar {
/**
* @notice Called by the AllocationManager when an operator wants to register
* for one or more operator sets. This method should revert if registration
* is unsuccessful.
* @param operator the registering operator
* @param avs the AVS the operator is registering for. This should be the same as IAVSRegistrar.avs()
* @param operatorSetIds the list of operator set ids being registered for
* @param data arbitrary data the operator can provide as part of registration
*/
function registerOperator(
address operator,
address avs,
uint32[] calldata operatorSetIds,
bytes calldata data
) external;
/**
* @notice Called by the AllocationManager when an operator is deregistered from
* one or more operator sets. If this method reverts, it is ignored.
* @param operator the deregistering operator
* @param avs the AVS the operator is deregistering from. This should be the same as IAVSRegistrar.avs()
* @param operatorSetIds the list of operator set ids being deregistered from
*/
function deregisterOperator(address operator, address avs, uint32[] calldata operatorSetIds) external;
/**
* @notice Returns true if the AVS is supported by the registrar
* @param avs the AVS to check
* @return true if the AVS is supported, false otherwise
*/
function supportsAVS(
address avs
) external view returns (bool);
}
````
## File: src/contracts/interfaces/IBackingEigen.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IBackingEigen is IERC20 {
/**
* @notice This function allows the owner to set the allowedFrom status of an address
* @param from the address whose allowedFrom status is being set
* @param isAllowedFrom the new allowedFrom status
*/
function setAllowedFrom(address from, bool isAllowedFrom) external;
/**
* @notice This function allows the owner to set the allowedTo status of an address
* @param to the address whose allowedTo status is being set
* @param isAllowedTo the new allowedTo status
*/
function setAllowedTo(address to, bool isAllowedTo) external;
/**
* @notice Allows the owner to disable transfer restrictions
*/
function disableTransferRestrictions() external;
/**
* @notice An initializer function that sets initial values for the contract's state variables.
*/
function initialize(
address initialOwner
) external;
// @notice Allows the contract owner to modify an entry in the `isMinter` mapping.
function setIsMinter(address minterAddress, bool newStatus) external;
/**
* @notice Allows any privileged address to mint `amount` new tokens to the address `to`.
* @dev Callable only by an address that has `isMinter` set to true.
*/
function mint(address to, uint256 amount) external;
/**
* @dev Destroys `amount` tokens from the caller.
*
* See {ERC20-_burn}.
*/
function burn(
uint256 amount
) external;
/// @notice the address of the wrapped Eigen token EIGEN
function EIGEN() external view returns (IERC20);
/// @notice the timestamp after which transfer restrictions are disabled
function transferRestrictionsDisabledAfter() external view returns (uint256);
/**
* @dev Clock used for flagging checkpoints. Has been overridden to implement timestamp based
* checkpoints (and voting).
*/
function clock() external view returns (uint48);
/**
* @dev Machine-readable description of the clock as specified in EIP-6372.
* Has been overridden to inform callers that this contract uses timestamps instead of block numbers, to match `clock()`
*/
// solhint-disable-next-line func-name-mixedcase
function CLOCK_MODE() external pure returns (string memory);
}
````
## File: src/contracts/interfaces/IDelegationManager.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import "./IStrategy.sol";
import "./IPauserRegistry.sol";
import "./ISignatureUtilsMixin.sol";
import "../libraries/SlashingLib.sol";
interface IDelegationManagerErrors {
/// @dev Thrown when caller is neither the StrategyManager or EigenPodManager contract.
error OnlyStrategyManagerOrEigenPodManager();
/// @dev Thrown when msg.sender is not the EigenPodManager
error OnlyEigenPodManager();
/// @dev Throw when msg.sender is not the AllocationManager
error OnlyAllocationManager();
/// Delegation Status
/// @dev Thrown when an operator attempts to undelegate.
error OperatorsCannotUndelegate();
/// @dev Thrown when an account is actively delegated.
error ActivelyDelegated();
/// @dev Thrown when an account is not actively delegated.
error NotActivelyDelegated();
/// @dev Thrown when `operator` is not a registered operator.
error OperatorNotRegistered();
/// Invalid Inputs
/// @dev Thrown when attempting to execute an action that was not queued.
error WithdrawalNotQueued();
/// @dev Thrown when caller cannot undelegate on behalf of a staker.
error CallerCannotUndelegate();
/// @dev Thrown when two array parameters have mismatching lengths.
error InputArrayLengthMismatch();
/// @dev Thrown when input arrays length is zero.
error InputArrayLengthZero();
/// Slashing
/// @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();
/// Signatures
/// @dev Thrown when attempting to spend a spent eip-712 salt.
error SaltSpent();
/// Withdrawal Processing
/// @dev Thrown when attempting to withdraw before delay has elapsed.
error WithdrawalDelayNotElapsed();
/// @dev Thrown when withdrawer is not the current caller.
error WithdrawerNotCaller();
}
interface IDelegationManagerTypes {
// @notice Struct used for storing information about a single operator who has registered with EigenLayer
struct OperatorDetails {
/// @notice DEPRECATED -- this field is no longer used, payments are handled in RewardsCoordinator.sol
address __deprecated_earningsReceiver;
/**
* @notice Address to verify signatures when a staker wishes to delegate to the operator, as well as controlling "forced undelegations".
* @dev Signature verification follows these rules:
* 1) If this address is left as address(0), then any staker will be free to delegate to the operator, i.e. no signature verification will be performed.
* 2) If this address is an EOA (i.e. it has no code), then we follow standard ECDSA signature verification for delegations to the operator.
* 3) If this address is a contract (i.e. it has code) then we forward a call to the contract and verify that it returns the correct EIP-1271 "magic value".
*/
address delegationApprover;
/// @notice DEPRECATED -- this field is no longer used. An analogous field is the `allocationDelay` stored in the AllocationManager
uint32 __deprecated_stakerOptOutWindowBlocks;
}
/**
* @notice Abstract struct used in calculating an EIP712 signature for an operator's delegationApprover to approve that a specific staker delegate to the operator.
* @dev Used in computing the `DELEGATION_APPROVAL_TYPEHASH` and as a reference in the computation of the approverDigestHash in the `_delegate` function.
*/
struct DelegationApproval {
// the staker who is delegating
address staker;
// the operator being delegated to
address operator;
// the operator's provided salt
bytes32 salt;
// the expiration timestamp (UTC) of the signature
uint256 expiry;
}
/**
* @dev A struct representing an existing queued withdrawal. After the withdrawal delay has elapsed, this withdrawal can be completed via `completeQueuedWithdrawal`.
* A `Withdrawal` is created by the `DelegationManager` when `queueWithdrawals` is called. The `withdrawalRoots` hashes returned by `queueWithdrawals` can be used
* to fetch the corresponding `Withdrawal` from storage (via `getQueuedWithdrawal`).
*
* @param staker The address that queued the withdrawal
* @param delegatedTo The address that the staker was delegated to at the time the withdrawal was queued. Used to determine if additional slashing occurred before
* this withdrawal became completable.
* @param withdrawer The address that will call the contract to complete the withdrawal. Note that this will always equal `staker`; alternate withdrawers are not
* supported at this time.
* @param nonce The staker's `cumulativeWithdrawalsQueued` at time of queuing. Used to ensure withdrawals have unique hashes.
* @param startBlock The block number when the withdrawal was queued.
* @param strategies The strategies requested for withdrawal when the withdrawal was queued
* @param scaledShares The staker's deposit shares requested for withdrawal, scaled by the staker's `depositScalingFactor`. Upon completion, these will be
* scaled by the appropriate slashing factor as of the withdrawal's completable block. The result is what is actually withdrawable.
*/
struct Withdrawal {
address staker;
address delegatedTo;
address withdrawer;
uint256 nonce;
uint32 startBlock;
IStrategy[] strategies;
uint256[] scaledShares;
}
/**
* @param strategies The strategies to withdraw from
* @param depositShares For each strategy, the number of deposit shares to withdraw. Deposit shares can
* be queried via `getDepositedShares`.
* NOTE: The number of shares ultimately received when a withdrawal is completed may be lower depositShares
* if the staker or their delegated operator has experienced slashing.
* @param __deprecated_withdrawer This field is ignored. The only party that may complete a withdrawal
* is the staker that originally queued it. Alternate withdrawers are not supported.
*/
struct QueuedWithdrawalParams {
IStrategy[] strategies;
uint256[] depositShares;
address __deprecated_withdrawer;
}
}
interface IDelegationManagerEvents is IDelegationManagerTypes {
// @notice Emitted when a new operator registers in EigenLayer and provides their delegation approver.
event OperatorRegistered(address indexed operator, address delegationApprover);
/// @notice Emitted when an operator updates their delegation approver
event DelegationApproverUpdated(address indexed operator, address newDelegationApprover);
/**
* @notice Emitted when @param operator indicates that they are updating their MetadataURI string
* @dev Note that these strings are *never stored in storage* and are instead purely emitted in events for off-chain indexing
*/
event OperatorMetadataURIUpdated(address indexed operator, string metadataURI);
/// @notice Emitted whenever an operator's shares are increased for a given strategy. Note that shares is the delta in the operator's shares.
event OperatorSharesIncreased(address indexed operator, address staker, IStrategy strategy, uint256 shares);
/// @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 when @param staker delegates to @param operator.
event StakerDelegated(address indexed staker, address indexed operator);
/// @notice Emitted when @param staker undelegates from @param operator.
event StakerUndelegated(address indexed staker, address indexed operator);
/// @notice Emitted when @param staker is undelegated via a call not originating from the staker themself
event StakerForceUndelegated(address indexed staker, address indexed operator);
/// @notice Emitted when a staker's depositScalingFactor is updated
event DepositScalingFactorUpdated(address staker, IStrategy strategy, uint256 newDepositScalingFactor);
/**
* @notice Emitted when a new withdrawal is queued.
* @param withdrawalRoot Is the hash of the `withdrawal`.
* @param withdrawal Is the withdrawal itself.
* @param sharesToWithdraw Is an array of the expected shares that were queued for withdrawal corresponding to the strategies in the `withdrawal`.
*/
event SlashingWithdrawalQueued(bytes32 withdrawalRoot, Withdrawal withdrawal, uint256[] sharesToWithdraw);
/// @notice Emitted when a queued withdrawal is completed
event SlashingWithdrawalCompleted(bytes32 withdrawalRoot);
/// @notice Emitted whenever an operator's shares are slashed for a given strategy
event OperatorSharesSlashed(address indexed operator, IStrategy strategy, uint256 totalSlashedShares);
}
/**
* @title DelegationManager
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice This is the contract for delegation in EigenLayer. The main functionalities of this contract are
* - enabling anyone to register as an operator in EigenLayer
* - allowing operators to specify parameters related to stakers who delegate to them
* - enabling any staker to delegate its stake to the operator of its choice (a given staker can only delegate to a single operator at a time)
* - enabling a staker to undelegate its assets from the operator it is delegated to (performed as part of the withdrawal process, initiated through the StrategyManager)
*/
interface IDelegationManager is ISignatureUtilsMixin, IDelegationManagerErrors, IDelegationManagerEvents {
/**
* @dev Initializes the initial owner and paused status.
*/
function initialize(address initialOwner, uint256 initialPausedStatus) external;
/**
* @notice Registers the caller as an operator in EigenLayer.
* @param initDelegationApprover is an address that, if set, must provide a signature when stakers delegate
* to an operator.
* @param allocationDelay The delay before allocations take effect.
* @param metadataURI is a URI for the operator's metadata, i.e. a link providing more details on the operator.
*
* @dev Once an operator is registered, they cannot 'deregister' as an operator, and they will forever be considered "delegated to themself".
* @dev This function will revert if the caller is already delegated to an operator.
* @dev Note that the `metadataURI` is *never stored * and is only emitted in the `OperatorMetadataURIUpdated` event
*/
function registerAsOperator(
address initDelegationApprover,
uint32 allocationDelay,
string calldata metadataURI
) external;
/**
* @notice Updates an operator's stored `delegationApprover`.
* @param operator is the operator to update the delegationApprover for
* @param newDelegationApprover is the new delegationApprover for the operator
*
* @dev The caller must have previously registered as an operator in EigenLayer.
*/
function modifyOperatorDetails(address operator, address newDelegationApprover) external;
/**
* @notice Called by an operator to emit an `OperatorMetadataURIUpdated` event indicating the information has updated.
* @param operator The operator to update metadata for
* @param metadataURI The URI for metadata associated with an operator
* @dev Note that the `metadataURI` is *never stored * and is only emitted in the `OperatorMetadataURIUpdated` event
*/
function updateOperatorMetadataURI(address operator, string calldata metadataURI) external;
/**
* @notice Caller delegates their stake to an operator.
* @param operator The account (`msg.sender`) is delegating its assets to for use in serving applications built on EigenLayer.
* @param approverSignatureAndExpiry (optional) Verifies the operator approves of this delegation
* @param approverSalt (optional) A unique single use value tied to an individual signature.
* @dev The signature/salt are used ONLY if the operator has configured a delegationApprover.
* If they have not, these params can be left empty.
*/
function delegateTo(
address operator,
SignatureWithExpiry memory approverSignatureAndExpiry,
bytes32 approverSalt
) external;
/**
* @notice Undelegates the staker from their operator and queues a withdrawal for all of their shares
* @param staker The account to be undelegated
* @return withdrawalRoots The roots of the newly queued withdrawals, if a withdrawal was queued. Returns
* an empty array if none was queued.
*
* @dev Reverts if the `staker` is also an operator, since operators are not allowed to undelegate from themselves.
* @dev Reverts if the caller is not the staker, nor the operator who the staker is delegated to, nor the operator's specified "delegationApprover"
* @dev Reverts if the `staker` is not delegated to an operator
*/
function undelegate(
address staker
) external returns (bytes32[] memory withdrawalRoots);
/**
* @notice Undelegates the staker from their current operator, and redelegates to `newOperator`
* Queues a withdrawal for all of the staker's withdrawable shares. These shares will only be
* delegated to `newOperator` AFTER the withdrawal is completed.
* @dev This method acts like a call to `undelegate`, then `delegateTo`
* @param newOperator the new operator that will be delegated all assets
* @dev NOTE: the following 2 params are ONLY checked if `newOperator` has a `delegationApprover`.
* If not, they can be left empty.
* @param newOperatorApproverSig A signature from the operator's `delegationApprover`
* @param approverSalt A unique single use value tied to the approver's signature
*/
function redelegate(
address newOperator,
SignatureWithExpiry memory newOperatorApproverSig,
bytes32 approverSalt
) external returns (bytes32[] memory withdrawalRoots);
/**
* @notice Allows a staker to queue a withdrawal of their deposit shares. The withdrawal can be
* completed after the MIN_WITHDRAWAL_DELAY_BLOCKS via either of the completeQueuedWithdrawal methods.
*
* While in the queue, these shares are removed from the staker's balance, as well as from their operator's
* delegated share balance (if applicable). Note that while in the queue, deposit shares are still subject
* to slashing. If any slashing has occurred, the shares received may be less than the queued deposit shares.
*
* @dev To view all the staker's strategies/deposit shares that can be queued for withdrawal, see `getDepositedShares`
* @dev To view the current conversion between a staker's deposit shares and withdrawable shares, see `getWithdrawableShares`
*/
function queueWithdrawals(
QueuedWithdrawalParams[] calldata params
) external returns (bytes32[] memory);
/**
* @notice Used to complete a queued withdrawal
* @param withdrawal The withdrawal to complete
* @param tokens Array in which the i-th entry specifies the `token` input to the 'withdraw' function of the i-th Strategy in the `withdrawal.strategies` array.
* @param tokens For each `withdrawal.strategies`, the underlying token of the strategy
* NOTE: if `receiveAsTokens` is false, the `tokens` array is unused and can be filled with default values. However, `tokens.length` MUST still be equal to `withdrawal.strategies.length`.
* NOTE: For the `beaconChainETHStrategy`, the corresponding `tokens` value is ignored (can be 0).
* @param receiveAsTokens If true, withdrawn shares will be converted to tokens and sent to the caller. If false, the caller receives shares that can be delegated to an operator.
* NOTE: if the caller receives shares and is currently delegated to an operator, the received shares are
* automatically delegated to the caller's current operator.
*/
function completeQueuedWithdrawal(
Withdrawal calldata withdrawal,
IERC20[] calldata tokens,
bool receiveAsTokens
) external;
/**
* @notice Used to complete multiple queued withdrawals
* @param withdrawals Array of Withdrawals to complete. See `completeQueuedWithdrawal` for the usage of a single Withdrawal.
* @param tokens Array of tokens for each Withdrawal. See `completeQueuedWithdrawal` for the usage of a single array.
* @param receiveAsTokens Whether or not to complete each withdrawal as tokens. See `completeQueuedWithdrawal` for the usage of a single boolean.
* @dev See `completeQueuedWithdrawal` for relevant dev tags
*/
function completeQueuedWithdrawals(
Withdrawal[] calldata withdrawals,
IERC20[][] calldata tokens,
bool[] calldata receiveAsTokens
) external;
/**
* @notice Called by a share manager when a staker's deposit share balance in a strategy increases.
* This method delegates any new shares to an operator (if applicable), and updates the staker's
* deposit scaling factor regardless.
* @param staker The address whose deposit shares have increased
* @param strategy The strategy in which shares have been deposited
* @param prevDepositShares The number of deposit shares the staker had in the strategy prior to the increase
* @param addedShares The number of deposit shares added by the staker
*
* @dev Note that if the either the staker's current operator has been slashed 100% for `strategy`, OR the
* staker has been slashed 100% on the beacon chain such that the calculated slashing factor is 0, this
* method WILL REVERT.
*/
function increaseDelegatedShares(
address staker,
IStrategy strategy,
uint256 prevDepositShares,
uint256 addedShares
) external;
/**
* @notice If the staker is delegated, decreases its operator's shares in response to
* a decrease in balance in the beaconChainETHStrategy
* @param staker the staker whose operator's balance will be decreased
* @param curDepositShares the current deposit shares held by the staker
* @param beaconChainSlashingFactorDecrease the amount that the staker's beaconChainSlashingFactor has decreased by
* @dev Note: `beaconChainSlashingFactorDecrease` are assumed to ALWAYS be < 1 WAD.
* These invariants are maintained in the EigenPodManager.
*/
function decreaseDelegatedShares(
address staker,
uint256 curDepositShares,
uint64 beaconChainSlashingFactorDecrease
) external;
/**
* @notice Decreases the operators shares in storage after a slash and increases the burnable shares by calling
* into either the StrategyManager or EigenPodManager (if the strategy is beaconChainETH).
* @param operator The operator to decrease shares for
* @param strategy The strategy to decrease shares for
* @param prevMaxMagnitude the previous maxMagnitude of the operator
* @param newMaxMagnitude the new maxMagnitude of the operator
* @dev Callable only by the AllocationManager
* @dev Note: Assumes `prevMaxMagnitude <= newMaxMagnitude`. This invariant is maintained in
* the AllocationManager.
*/
function slashOperatorShares(
address operator,
IStrategy strategy,
uint64 prevMaxMagnitude,
uint64 newMaxMagnitude
) external;
/**
*
* VIEW FUNCTIONS
*
*/
/**
* @notice returns the address of the operator that `staker` is delegated to.
* @notice Mapping: staker => operator whom the staker is currently delegated to.
* @dev Note that returning address(0) indicates that the staker is not actively delegated to any operator.
*/
function delegatedTo(
address staker
) external view returns (address);
/**
* @notice Mapping: delegationApprover => 32-byte salt => whether or not the salt has already been used by the delegationApprover.
* @dev Salts are used in the `delegateTo` function. Note that this function only processes the delegationApprover's
* signature + the provided salt if the operator being delegated to has specified a nonzero address as their `delegationApprover`.
*/
function delegationApproverSaltIsSpent(address _delegationApprover, bytes32 salt) external view returns (bool);
/// @notice Mapping: staker => cumulative number of queued withdrawals they have ever initiated.
/// @dev This only increments (doesn't decrement), and is used to help ensure that otherwise identical withdrawals have unique hashes.
function cumulativeWithdrawalsQueued(
address staker
) external view returns (uint256);
/**
* @notice Returns 'true' if `staker` *is* actively delegated, and 'false' otherwise.
*/
function isDelegated(
address staker
) external view returns (bool);
/**
* @notice Returns true is an operator has previously registered for delegation.
*/
function isOperator(
address operator
) external view returns (bool);
/**
* @notice Returns the delegationApprover account for an operator
*/
function delegationApprover(
address operator
) external view returns (address);
/**
* @notice Returns the shares that an operator has delegated to them in a set of strategies
* @param operator the operator to get shares for
* @param strategies the strategies to get shares for
*/
function getOperatorShares(
address operator,
IStrategy[] memory strategies
) external view returns (uint256[] memory);
/**
* @notice Returns the shares that a set of operators have delegated to them in a set of strategies
* @param operators the operators to get shares for
* @param strategies the strategies to get shares for
*/
function getOperatorsShares(
address[] memory operators,
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.
* This value depends on which operator the staker is delegated to.
* The shares amount returned is the actual amount of Strategy shares the staker would receive (subject
* to each strategy's underlying shares to token ratio).
*/
function getWithdrawableShares(
address staker,
IStrategy[] memory strategies
) external view returns (uint256[] memory withdrawableShares, uint256[] memory depositShares);
/**
* @notice Returns the number of shares in storage for a staker and all their strategies
*/
function getDepositedShares(
address staker
) external view returns (IStrategy[] memory, uint256[] memory);
/**
* @notice Returns the scaling factor applied to a staker's deposits for a given strategy
*/
function depositScalingFactor(address staker, IStrategy strategy) external view returns (uint256);
/**
* @notice Returns the Withdrawal and corresponding shares associated with a `withdrawalRoot`
* @param withdrawalRoot The hash identifying the queued withdrawal
* @return withdrawal The withdrawal details
* @return shares Array of shares corresponding to each strategy in the withdrawal
* @dev The shares are what a user would receive from completing a queued withdrawal, assuming all slashings are applied
* @dev Withdrawals queued before the slashing release cannot be queried with this method
*/
function getQueuedWithdrawal(
bytes32 withdrawalRoot
) external view returns (Withdrawal memory withdrawal, uint256[] memory shares);
/**
* @notice Returns all queued withdrawals and their corresponding shares for a staker.
* @param staker The address of the staker to query withdrawals for.
* @return withdrawals Array of Withdrawal structs containing details about each queued withdrawal.
* @return shares 2D array of shares, where each inner array corresponds to the strategies in the withdrawal.
* @dev The shares are what a user would receive from completing a queued withdrawal, assuming all slashings are applied.
*/
function getQueuedWithdrawals(
address staker
) external view returns (Withdrawal[] memory withdrawals, uint256[][] memory shares);
/// @notice Returns a list of queued withdrawal roots for the `staker`.
/// NOTE that this only returns withdrawals queued AFTER the slashing release.
function getQueuedWithdrawalRoots(
address staker
) external view returns (bytes32[] memory);
/**
* @notice Converts shares for a set of strategies to deposit shares, likely in order to input into `queueWithdrawals`.
* This function will revert from a division by 0 error if any of the staker's strategies have a slashing factor of 0.
* @param staker the staker to convert shares for
* @param strategies the strategies to convert shares for
* @param withdrawableShares the shares to convert
* @return the deposit shares
* @dev will be a few wei off due to rounding errors
*/
function convertToDepositShares(
address staker,
IStrategy[] memory strategies,
uint256[] memory withdrawableShares
) external view returns (uint256[] memory);
/// @notice Returns the keccak256 hash of `withdrawal`.
function calculateWithdrawalRoot(
Withdrawal memory withdrawal
) external pure returns (bytes32);
/**
* @notice Calculates the digest hash to be signed by the operator's delegationApprove and used in the `delegateTo` function.
* @param staker The account delegating their stake
* @param operator The account receiving delegated stake
* @param _delegationApprover the operator's `delegationApprover` who will be signing the delegationHash (in general)
* @param approverSalt A unique and single use value associated with the approver signature.
* @param expiry Time after which the approver's signature becomes invalid
*/
function calculateDelegationApprovalDigestHash(
address staker,
address operator,
address _delegationApprover,
bytes32 approverSalt,
uint256 expiry
) external view returns (bytes32);
/// @notice return address of the beaconChainETHStrategy
function beaconChainETHStrategy() external view returns (IStrategy);
/**
* @notice Returns the minimum withdrawal delay in blocks to pass for withdrawals queued to be completable.
* Also applies to legacy withdrawals so any withdrawals not completed prior to the slashing upgrade will be subject
* to this longer delay.
* @dev Backwards-compatible interface to return the internal `MIN_WITHDRAWAL_DELAY_BLOCKS` value
* @dev Previous value in storage was deprecated. See `__deprecated_minWithdrawalDelayBlocks`
*/
function minWithdrawalDelayBlocks() external view returns (uint32);
/// @notice The EIP-712 typehash for the DelegationApproval struct used by the contract
function DELEGATION_APPROVAL_TYPEHASH() external view returns (bytes32);
}
````
## File: src/contracts/interfaces/IEigen.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IEigen is IERC20 {
/**
* @notice This function allows the owner to set the allowedFrom status of an address
* @param from the address whose allowedFrom status is being set
* @param isAllowedFrom the new allowedFrom status
*/
function setAllowedFrom(address from, bool isAllowedFrom) external;
/**
* @notice This function allows the owner to set the allowedTo status of an address
* @param to the address whose allowedTo status is being set
* @param isAllowedTo the new allowedTo status
*/
function setAllowedTo(address to, bool isAllowedTo) external;
/**
* @notice Allows the owner to disable transfer restrictions
*/
function disableTransferRestrictions() external;
/**
* @notice This function allows minter to mint tokens
*/
function mint() external;
/**
* @notice This function allows bEIGEN holders to wrap their tokens into Eigen
*/
function wrap(
uint256 amount
) external;
/**
* @notice This function allows Eigen holders to unwrap their tokens into bEIGEN
*/
function unwrap(
uint256 amount
) external;
/**
* @dev Clock used for flagging checkpoints. Has been overridden to implement timestamp based
* checkpoints (and voting).
*/
function clock() external view returns (uint48);
/**
* @dev Machine-readable description of the clock as specified in EIP-6372.
* Has been overridden to inform callers that this contract uses timestamps instead of block numbers, to match `clock()`
*/
// solhint-disable-next-line func-name-mixedcase
function CLOCK_MODE() external pure returns (string memory);
}
````
## File: src/contracts/interfaces/IEigenPod.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../libraries/BeaconChainProofs.sol";
import "./ISemVerMixin.sol";
import "./IEigenPodManager.sol";
interface IEigenPodErrors {
/// @dev Thrown when msg.sender is not the EPM.
error OnlyEigenPodManager();
/// @dev Thrown when msg.sender is not the pod owner.
error OnlyEigenPodOwner();
/// @dev Thrown when msg.sender is not owner or the proof submitter.
error OnlyEigenPodOwnerOrProofSubmitter();
/// @dev Thrown when attempting an action that is currently paused.
error CurrentlyPaused();
/// Invalid Inputs
/// @dev Thrown when an address of zero is provided.
error InputAddressZero();
/// @dev Thrown when two array parameters have mismatching lengths.
error InputArrayLengthMismatch();
/// @dev Thrown when `validatorPubKey` length is not equal to 48-bytes.
error InvalidPubKeyLength();
/// @dev Thrown when provided timestamp is out of range.
error TimestampOutOfRange();
/// Checkpoints
/// @dev Thrown when no active checkpoints are found.
error NoActiveCheckpoint();
/// @dev Thrown if an uncompleted checkpoint exists.
error CheckpointAlreadyActive();
/// @dev Thrown if there's not a balance available to checkpoint.
error NoBalanceToCheckpoint();
/// @dev Thrown when attempting to create a checkpoint twice within a given block.
error CannotCheckpointTwiceInSingleBlock();
/// Withdrawing
/// @dev Thrown when amount exceeds `restakedExecutionLayerGwei`.
error InsufficientWithdrawableBalance();
/// Validator Status
/// @dev Thrown when a validator's withdrawal credentials have already been verified.
error CredentialsAlreadyVerified();
/// @dev Thrown if the provided proof is not valid for this EigenPod.
error WithdrawalCredentialsNotForEigenPod();
/// @dev Thrown when a validator is not in the ACTIVE status in the pod.
error ValidatorNotActiveInPod();
/// @dev Thrown when validator is not active yet on the beacon chain.
error ValidatorInactiveOnBeaconChain();
/// @dev Thrown if a validator is exiting the beacon chain.
error ValidatorIsExitingBeaconChain();
/// @dev Thrown when a validator has not been slashed on the beacon chain.
error ValidatorNotSlashedOnBeaconChain();
/// Misc
/// @dev Thrown when an invalid block root is returned by the EIP-4788 oracle.
error InvalidEIP4788Response();
/// @dev Thrown when attempting to send an invalid amount to the beacon deposit contract.
error MsgValueNot32ETH();
/// @dev Thrown when provided `beaconTimestamp` is too far in the past.
error BeaconTimestampTooFarInPast();
/// @dev Thrown when the pectraForkTimestamp returned from the EigenPodManager is zero
error ForkTimestampZero();
}
interface IEigenPodTypes {
enum VALIDATOR_STATUS {
INACTIVE, // doesnt exist
ACTIVE, // staked on ethpos and withdrawal credentials are pointed to the EigenPod
WITHDRAWN // withdrawn from the Beacon Chain
}
struct ValidatorInfo {
// index of the validator in the beacon chain
uint64 validatorIndex;
// amount of beacon chain ETH restaked on EigenLayer in gwei
uint64 restakedBalanceGwei;
//timestamp of the validator's most recent balance update
uint64 lastCheckpointedAt;
// status of the validator
VALIDATOR_STATUS status;
}
struct Checkpoint {
bytes32 beaconBlockRoot;
uint24 proofsRemaining;
uint64 podBalanceGwei;
int64 balanceDeltasGwei;
uint64 prevBeaconBalanceGwei;
}
}
interface IEigenPodEvents is IEigenPodTypes {
/// @notice Emitted when an ETH validator stakes via this eigenPod
event EigenPodStaked(bytes pubkey);
/// @notice Emitted when a pod owner updates the proof submitter address
event ProofSubmitterUpdated(address prevProofSubmitter, address newProofSubmitter);
/// @notice Emitted when an ETH validator's withdrawal credentials are successfully verified to be pointed to this eigenPod
event ValidatorRestaked(uint40 validatorIndex);
/// @notice Emitted when an ETH validator's balance is proven to be updated. Here newValidatorBalanceGwei
// is the validator's balance that is credited on EigenLayer.
event ValidatorBalanceUpdated(uint40 validatorIndex, uint64 balanceTimestamp, uint64 newValidatorBalanceGwei);
/// @notice Emitted when restaked beacon chain ETH is withdrawn from the eigenPod.
event RestakedBeaconChainETHWithdrawn(address indexed recipient, uint256 amount);
/// @notice Emitted when ETH is received via the `receive` fallback
event NonBeaconChainETHReceived(uint256 amountReceived);
/// @notice Emitted when a checkpoint is created
event CheckpointCreated(
uint64 indexed checkpointTimestamp, bytes32 indexed beaconBlockRoot, uint256 validatorCount
);
/// @notice Emitted when a checkpoint is finalized
event CheckpointFinalized(uint64 indexed checkpointTimestamp, int256 totalShareDeltaWei);
/// @notice Emitted when a validator is proven for a given checkpoint
event ValidatorCheckpointed(uint64 indexed checkpointTimestamp, uint40 indexed validatorIndex);
/// @notice Emitted when a validaor is proven to have 0 balance at a given checkpoint
event ValidatorWithdrawn(uint64 indexed checkpointTimestamp, uint40 indexed validatorIndex);
}
/**
* @title The implementation contract used for restaking beacon chain ETH on EigenLayer
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose
* to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts
*/
interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin {
/// @notice Used to initialize the pointers to contracts crucial to the pod's functionality, in beacon proxy construction from EigenPodManager
function initialize(
address owner
) external;
/// @notice Called by EigenPodManager when the owner wants to create another ETH validator.
/// @dev This function only supports staking to a 0x01 validator. For compounding validators, please interact directly with the deposit contract.
function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable;
/**
* @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address
* @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain.
* @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `restakedExecutionLayerGwei` exceeds the
* `amountWei` input (when converted to GWEI).
* @dev Reverts if `amountWei` is not a whole Gwei amount
*/
function withdrawRestakedBeaconChainETH(address recipient, uint256 amount) external;
/**
* @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed
* by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total
* change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`.
* @dev Once finalized, the pod owner is awarded shares corresponding to:
* - the total change in their ACTIVE validator balances
* - any ETH in the pod not already awarded shares
* @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If
* this is the case, the pod owner MUST complete the existing checkpoint before starting a new one.
* @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner
* to prevent accidentally starting a checkpoint that will not increase their shares
*/
function startCheckpoint(
bool revertIfNoBalance
) external;
/**
* @dev Progress the current checkpoint towards completion by submitting one or more validator
* checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint.
* For each validator proven, the current checkpoint's `proofsRemaining` decreases.
* @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized.
* (see `_updateCheckpoint` for more details)
* @dev This method can only be called when there is a currently-active checkpoint.
* @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot`
* @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot`
*/
function verifyCheckpointProofs(
BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof,
BeaconChainProofs.BalanceProof[] calldata proofs
) external;
/**
* @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award
* shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and
* future checkpoint proofs will need to include them.
* @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`.
* @dev Validators proven via this method MUST NOT have an exit epoch set already.
* @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds
* to the parent beacon block root against which the proof is verified.
* @param stateRootProof proves a beacon state root against a beacon block root
* @param validatorIndices a list of validator indices being proven
* @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root
* @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for
* details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator
*/
function verifyWithdrawalCredentials(
uint64 beaconTimestamp,
BeaconChainProofs.StateRootProof calldata stateRootProof,
uint40[] calldata validatorIndices,
bytes[] calldata validatorFieldsProofs,
bytes32[][] calldata validatorFields
) external;
/**
* @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful
* staleness proof allows the caller to start a checkpoint.
*
* @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed!
* (See `_startCheckpoint` for details)
*
* @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon
* chain. This is intended to make it easier to external watchers to keep a pod's balance up to date.
*
* @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event
* and the validator's final exit back to the execution layer. During this time, the validator's balance may or
* may not drop further due to a correlation penalty. This method allows proof of a slashed validator
* to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator
* has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven
* "stale" via this method.
* See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info.
*
* @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds
* to the parent beacon block root against which the proof is verified.
* @param stateRootProof proves a beacon state root against a beacon block root
* @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against
* the beacon state root. See the consensus specs for more details:
* https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator
*
* @dev Staleness conditions:
* - Validator's last checkpoint is older than `beaconTimestamp`
* - Validator MUST be in `ACTIVE` status in the pod
* - Validator MUST be slashed on the beacon chain
*/
function verifyStaleBalance(
uint64 beaconTimestamp,
BeaconChainProofs.StateRootProof calldata stateRootProof,
BeaconChainProofs.ValidatorProof calldata proof
) external;
/// @notice called by owner of a pod to remove any ERC20s deposited in the pod
function recoverTokens(IERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external;
/// @notice Allows the owner of a pod to update the proof submitter, a permissioned
/// address that can call `startCheckpoint` and `verifyWithdrawalCredentials`.
/// @dev Note that EITHER the podOwner OR proofSubmitter can access these methods,
/// so it's fine to set your proofSubmitter to 0 if you want the podOwner to be the
/// only address that can call these methods.
/// @param newProofSubmitter The new proof submitter address. If set to 0, only the
/// pod owner will be able to call `startCheckpoint` and `verifyWithdrawalCredentials`
function setProofSubmitter(
address newProofSubmitter
) external;
/**
*
* VIEW METHODS
*
*/
/// @notice An address with permissions to call `startCheckpoint` and `verifyWithdrawalCredentials`, set
/// by the podOwner. This role exists to allow a podOwner to designate a hot wallet that can call
/// these methods, allowing the podOwner to remain a cold wallet that is only used to manage funds.
/// @dev If this address is NOT set, only the podOwner can call `startCheckpoint` and `verifyWithdrawalCredentials`
function proofSubmitter() external view returns (address);
/// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from beaconchain but not EigenLayer),
function withdrawableRestakedExecutionLayerGwei() external view returns (uint64);
/// @notice The single EigenPodManager for EigenLayer
function eigenPodManager() external view returns (IEigenPodManager);
/// @notice The owner of this EigenPod
function podOwner() external view returns (address);
/// @notice Returns the validatorInfo struct for the provided pubkeyHash
function validatorPubkeyHashToInfo(
bytes32 validatorPubkeyHash
) external view returns (ValidatorInfo memory);
/// @notice Returns the validatorInfo struct for the provided pubkey
function validatorPubkeyToInfo(
bytes calldata validatorPubkey
) external view returns (ValidatorInfo memory);
/// @notice This returns the status of a given validator
function validatorStatus(
bytes32 pubkeyHash
) external view returns (VALIDATOR_STATUS);
/// @notice This returns the status of a given validator pubkey
function validatorStatus(
bytes calldata validatorPubkey
) external view returns (VALIDATOR_STATUS);
/// @notice Number of validators with proven withdrawal credentials, who do not have proven full withdrawals
function activeValidatorCount() external view returns (uint256);
/// @notice The timestamp of the last checkpoint finalized
function lastCheckpointTimestamp() external view returns (uint64);
/// @notice The timestamp of the currently-active checkpoint. Will be 0 if there is not active checkpoint
function currentCheckpointTimestamp() external view returns (uint64);
/// @notice Returns the currently-active checkpoint
function currentCheckpoint() external view returns (Checkpoint memory);
/// @notice For each checkpoint, the total balance attributed to exited validators, in gwei
///
/// NOTE that the values added to this mapping are NOT guaranteed to capture the entirety of a validator's
/// exit - rather, they capture the total change in a validator's balance when a checkpoint shows their
/// balance change from nonzero to zero. While a change from nonzero to zero DOES guarantee that a validator
/// has been fully exited, it is possible that the magnitude of this change does not capture what is
/// typically thought of as a "full exit."
///
/// For example:
/// 1. Consider a validator was last checkpointed at 32 ETH before exiting. Once the exit has been processed,
/// it is expected that the validator's exited balance is calculated to be `32 ETH`.
/// 2. However, before `startCheckpoint` is called, a deposit is made to the validator for 1 ETH. The beacon
/// chain will automatically withdraw this ETH, but not until the withdrawal sweep passes over the validator
/// again. Until this occurs, the validator's current balance (used for checkpointing) is 1 ETH.
/// 3. If `startCheckpoint` is called at this point, the balance delta calculated for this validator will be
/// `-31 ETH`, and because the validator has a nonzero balance, it is not marked WITHDRAWN.
/// 4. After the exit is processed by the beacon chain, a subsequent `startCheckpoint` and checkpoint proof
/// will calculate a balance delta of `-1 ETH` and attribute a 1 ETH exit to the validator.
///
/// If this edge case impacts your usecase, it should be possible to mitigate this by monitoring for deposits
/// to your exited validators, and waiting to call `startCheckpoint` until those deposits have been automatically
/// exited.
///
/// Additional edge cases this mapping does not cover:
/// - If a validator is slashed, their balance exited will reflect their original balance rather than the slashed amount
/// - The final partial withdrawal for an exited validator will be likely be included in this mapping.
/// i.e. if a validator was last checkpointed at 32.1 ETH before exiting, the next checkpoint will calculate their
/// "exited" amount to be 32.1 ETH rather than 32 ETH.
function checkpointBalanceExitedGwei(
uint64
) external view returns (uint64);
/// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp`
/// @param timestamp of the block for which the parent block root will be returned. MUST correspond
/// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method
/// will revert.
function getParentBlockRoot(
uint64 timestamp
) external view returns (bytes32);
}
````
## File: src/contracts/interfaces/IEigenPodManager.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import "@openzeppelin/contracts/proxy/beacon/IBeacon.sol";
import "./IETHPOSDeposit.sol";
import "./IStrategyManager.sol";
import "./IEigenPod.sol";
import "./IShareManager.sol";
import "./IPausable.sol";
import "./IStrategy.sol";
import "./ISemVerMixin.sol";
interface IEigenPodManagerErrors {
/// @dev Thrown when caller is not a EigenPod.
error OnlyEigenPod();
/// @dev Thrown when caller is not DelegationManager.
error OnlyDelegationManager();
/// @dev Thrown when caller already has an EigenPod.
error EigenPodAlreadyExists();
/// @dev Thrown when shares is not a multiple of gwei.
error SharesNotMultipleOfGwei();
/// @dev Thrown when shares would result in a negative integer.
error SharesNegative();
/// @dev Thrown when the strategy is not the beaconChainETH strategy.
error InvalidStrategy();
/// @dev Thrown when the pods shares are negative and a beacon chain balance update is attempted.
/// The podOwner should complete legacy withdrawal first.
error LegacyWithdrawalsNotCompleted();
/// @dev Thrown when caller is not the proof timestamp setter
error OnlyProofTimestampSetter();
}
interface IEigenPodManagerEvents {
/// @notice Emitted to notify the deployment of an EigenPod
event PodDeployed(address indexed eigenPod, address indexed podOwner);
/// @notice Emitted to notify a deposit of beacon chain ETH recorded in the strategy manager
event BeaconChainETHDeposited(address indexed podOwner, uint256 amount);
/// @notice Emitted when the balance of an EigenPod is updated
event PodSharesUpdated(address indexed podOwner, int256 sharesDelta);
/// @notice Emitted every time the total shares of a pod are updated
event NewTotalShares(address indexed podOwner, int256 newTotalShares);
/// @notice Emitted when a withdrawal of beacon chain ETH is completed
event BeaconChainETHWithdrawalCompleted(
address indexed podOwner,
uint256 shares,
uint96 nonce,
address delegatedAddress,
address withdrawer,
bytes32 withdrawalRoot
);
/// @notice Emitted when a staker's beaconChainSlashingFactor is updated
event BeaconChainSlashingFactorDecreased(
address staker, uint64 prevBeaconChainSlashingFactor, uint64 newBeaconChainSlashingFactor
);
/// @notice Emitted when an operator is slashed and shares to be burned are increased
event BurnableETHSharesIncreased(uint256 shares);
/// @notice Emitted when the Pectra fork timestamp is updated
event PectraForkTimestampSet(uint64 newPectraForkTimestamp);
/// @notice Emitted when the proof timestamp setter is updated
event ProofTimestampSetterSet(address newProofTimestampSetter);
}
interface IEigenPodManagerTypes {
/**
* @notice The amount of beacon chain slashing experienced by a pod owner as a proportion of WAD
* @param isSet whether the slashingFactor has ever been updated. Used to distinguish between
* a value of "0" and an uninitialized value.
* @param slashingFactor the proportion of the pod owner's balance that has been decreased due to
* slashing or other beacon chain balance decreases.
* @dev NOTE: if !isSet, `slashingFactor` should be treated as WAD. `slashingFactor` is monotonically
* decreasing and can hit 0 if fully slashed.
*/
struct BeaconChainSlashingFactor {
bool isSet;
uint64 slashingFactor;
}
}
/**
* @title Interface for factory that creates and manages solo staking pods that have their withdrawal credentials pointed to EigenLayer.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
interface IEigenPodManager is
IEigenPodManagerErrors,
IEigenPodManagerEvents,
IEigenPodManagerTypes,
IShareManager,
IPausable,
ISemVerMixin
{
/**
* @notice Creates an EigenPod for the sender.
* @dev Function will revert if the `msg.sender` already has an EigenPod.
* @dev Returns EigenPod address
*/
function createPod() external returns (address);
/**
* @notice Stakes for a new beacon chain validator on the sender's EigenPod.
* Also creates an EigenPod for the sender if they don't have one already.
* @param pubkey The 48 bytes public key of the beacon chain validator.
* @param signature The validator's signature of the deposit data.
* @param depositDataRoot The root/hash of the deposit data for the validator's deposit.
*/
function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable;
/**
* @notice Adds any positive share delta to the pod owner's deposit shares, and delegates them to the pod
* owner's operator (if applicable). A negative share delta does NOT impact the pod owner's deposit shares,
* but will reduce their beacon chain slashing factor and delegated shares accordingly.
* @param podOwner is the pod owner whose balance is being updated.
* @param prevRestakedBalanceWei is the total amount restaked through the pod before the balance update, including
* any amount currently in the withdrawal queue.
* @param balanceDeltaWei is the amount the balance changed
* @dev Callable only by the podOwner's EigenPod contract.
* @dev Reverts if `sharesDelta` is not a whole Gwei amount
*/
function recordBeaconChainETHBalanceUpdate(
address podOwner,
uint256 prevRestakedBalanceWei,
int256 balanceDeltaWei
) external;
/// @notice Sets the address that can set proof timestamps
function setProofTimestampSetter(
address newProofTimestampSetter
) external;
/// @notice Sets the Pectra fork timestamp, only callable by `proofTimestampSetter`
function setPectraForkTimestamp(
uint64 timestamp
) external;
/// @notice Returns the address of the `podOwner`'s EigenPod if it has been deployed.
function ownerToPod(
address podOwner
) external view returns (IEigenPod);
/// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not).
function getPod(
address podOwner
) external view returns (IEigenPod);
/// @notice The ETH2 Deposit Contract
function ethPOS() external view returns (IETHPOSDeposit);
/// @notice Beacon proxy to which the EigenPods point
function eigenPodBeacon() external view returns (IBeacon);
/// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise.
function hasPod(
address podOwner
) external view returns (bool);
/// @notice Returns the number of EigenPods that have been created
function numPods() external view returns (uint256);
/**
* @notice Mapping from Pod owner owner to the number of shares they have in the virtual beacon chain ETH strategy.
* @dev The share amount can become negative. This is necessary to accommodate the fact that a pod owner's virtual beacon chain ETH shares can
* decrease between the pod owner queuing and completing a withdrawal.
* When the pod owner's shares would otherwise increase, this "deficit" is decreased first _instead_.
* Likewise, when a withdrawal is completed, this "deficit" is decreased and the withdrawal amount is decreased; We can think of this
* as the withdrawal "paying off the deficit".
*/
function podOwnerDepositShares(
address podOwner
) external view returns (int256);
/// @notice returns canonical, virtual beaconChainETH strategy
function beaconChainETHStrategy() external view returns (IStrategy);
/**
* @notice Returns the historical sum of proportional balance decreases a pod owner has experienced when
* updating their pod's balance.
*/
function beaconChainSlashingFactor(
address staker
) external view returns (uint64);
/// @notice Returns the accumulated amount of beacon chain ETH Strategy shares
function burnableETHShares() external view returns (uint256);
/// @notice Returns the timestamp of the Pectra hard fork
/// @dev Specifically, this returns the timestamp of the first non-missed slot at or after the Pectra hard fork
function pectraForkTimestamp() external view returns (uint64);
}
````
## File: src/contracts/interfaces/IETHPOSDeposit.sol
````
// ┏━━━┓━┏┓━┏┓━━┏━━━┓━━┏━━━┓━━━━┏━━━┓━━━━━━━━━━━━━━━━━━━┏┓━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━━━━━━━┏┓━
// ┃┏━━┛┏┛┗┓┃┃━━┃┏━┓┃━━┃┏━┓┃━━━━┗┓┏┓┃━━━━━━━━━━━━━━━━━━┏┛┗┓━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━━━━━━┏┛┗┓
// ┃┗━━┓┗┓┏┛┃┗━┓┗┛┏┛┃━━┃┃━┃┃━━━━━┃┃┃┃┏━━┓┏━━┓┏━━┓┏━━┓┏┓┗┓┏┛━━━━┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓━┏━━┓┗┓┏┛
// ┃┏━━┛━┃┃━┃┏┓┃┏━┛┏┛━━┃┃━┃┃━━━━━┃┃┃┃┃┏┓┃┃┏┓┃┃┏┓┃┃━━┫┣┫━┃┃━━━━━┃┃━┏┓┃┏┓┃┃┏┓┓━┃┃━┃┏┛┗━┓┃━┃┏━┛━┃┃━
// ┃┗━━┓━┃┗┓┃┃┃┃┃┃┗━┓┏┓┃┗━┛┃━━━━┏┛┗┛┃┃┃━┫┃┗┛┃┃┗┛┃┣━━┃┃┃━┃┗┓━━━━┃┗━┛┃┃┗┛┃┃┃┃┃━┃┗┓┃┃━┃┗┛┗┓┃┗━┓━┃┗┓
// ┗━━━┛━┗━┛┗┛┗┛┗━━━┛┗┛┗━━━┛━━━━┗━━━┛┗━━┛┃┏━┛┗━━┛┗━━┛┗┛━┗━┛━━━━┗━━━┛┗━━┛┗┛┗┛━┗━┛┗┛━┗━━━┛┗━━┛━┗━┛
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.5.0;
// This interface is designed to be compatible with the Vyper version.
/// @notice This is the Ethereum 2.0 deposit contract interface.
/// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs
interface IETHPOSDeposit {
/// @notice A processed deposit event.
event DepositEvent(bytes pubkey, bytes withdrawal_credentials, bytes amount, bytes signature, bytes index);
/// @notice Submit a Phase 0 DepositData object.
/// @param pubkey A BLS12-381 public key.
/// @param withdrawal_credentials Commitment to a public key for withdrawals.
/// @param signature A BLS12-381 signature.
/// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object.
/// Used as a protection against malformed input.
function deposit(
bytes calldata pubkey,
bytes calldata withdrawal_credentials,
bytes calldata signature,
bytes32 deposit_data_root
) external payable;
/// @notice Query the current deposit root hash.
/// @return The deposit root hash.
function get_deposit_root() external view returns (bytes32);
/// @notice Query the current deposit count.
/// @return The deposit count encoded as a little endian 64-bit number.
function get_deposit_count() external view returns (bytes memory);
}
````
## File: src/contracts/interfaces/IPausable.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import "../interfaces/IPauserRegistry.sol";
/**
* @title Adds pausability to a contract, with pausing & unpausing controlled by the `pauser` and `unpauser` of a PauserRegistry contract.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice Contracts that inherit from this contract may define their own `pause` and `unpause` (and/or related) functions.
* These functions should be permissioned as "onlyPauser" which defers to a `PauserRegistry` for determining access control.
* @dev Pausability is implemented using a uint256, which allows up to 256 different single bit-flags; each bit can potentially pause different functionality.
* Inspiration for this was taken from the NearBridge design here https://etherscan.io/address/0x3FEFc5A4B1c02f21cBc8D3613643ba0635b9a873#code.
* For the `pause` and `unpause` functions we've implemented, if you pause, you can only flip (any number of) switches to on/1 (aka "paused"), and if you unpause,
* you can only flip (any number of) switches to off/0 (aka "paused").
* If you want a pauseXYZ function that just flips a single bit / "pausing flag", it will:
* 1) 'bit-wise and' (aka `&`) a flag with the current paused state (as a uint256)
* 2) update the paused state to this new value
* @dev We note as well that we have chosen to identify flags by their *bit index* as opposed to their numerical value, so, e.g. defining `DEPOSITS_PAUSED = 3`
* indicates specifically that if the *third bit* of `_paused` is flipped -- i.e. it is a '1' -- then deposits should be paused
*/
interface IPausable {
/// @dev Thrown when caller is not pauser.
error OnlyPauser();
/// @dev Thrown when caller is not unpauser.
error OnlyUnpauser();
/// @dev Thrown when currently paused.
error CurrentlyPaused();
/// @dev Thrown when invalid `newPausedStatus` is provided.
error InvalidNewPausedStatus();
/// @dev Thrown when a null address input is provided.
error InputAddressZero();
/// @notice Emitted when the pause is triggered by `account`, and changed to `newPausedStatus`.
event Paused(address indexed account, uint256 newPausedStatus);
/// @notice Emitted when the pause is lifted by `account`, and changed to `newPausedStatus`.
event Unpaused(address indexed account, uint256 newPausedStatus);
/// @notice Address of the `PauserRegistry` contract that this contract defers to for determining access control (for pausing).
function pauserRegistry() external view returns (IPauserRegistry);
/**
* @notice This function is used to pause an EigenLayer contract's functionality.
* It is permissioned to the `pauser` address, which is expected to be a low threshold multisig.
* @param newPausedStatus represents the new value for `_paused` to take, which means it may flip several bits at once.
* @dev This function can only pause functionality, and thus cannot 'unflip' any bit in `_paused` from 1 to 0.
*/
function pause(
uint256 newPausedStatus
) external;
/**
* @notice Alias for `pause(type(uint256).max)`.
*/
function pauseAll() external;
/**
* @notice This function is used to unpause an EigenLayer contract's functionality.
* It is permissioned to the `unpauser` address, which is expected to be a high threshold multisig or governance contract.
* @param newPausedStatus represents the new value for `_paused` to take, which means it may flip several bits at once.
* @dev This function can only unpause functionality, and thus cannot 'flip' any bit in `_paused` from 0 to 1.
*/
function unpause(
uint256 newPausedStatus
) external;
/// @notice Returns the current paused status as a uint256.
function paused() external view returns (uint256);
/// @notice Returns 'true' if the `indexed`th bit of `_paused` is 1, and 'false' otherwise
function paused(
uint8 index
) external view returns (bool);
}
````
## File: src/contracts/interfaces/IPauserRegistry.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
/**
* @title Interface for the `PauserRegistry` contract.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
interface IPauserRegistry {
error OnlyUnpauser();
error InputAddressZero();
event PauserStatusChanged(address pauser, bool canPause);
event UnpauserChanged(address previousUnpauser, address newUnpauser);
/// @notice Mapping of addresses to whether they hold the pauser role.
function isPauser(
address pauser
) external view returns (bool);
/// @notice Unique address that holds the unpauser role. Capable of changing *both* the pauser and unpauser addresses.
function unpauser() external view returns (address);
}
````
## File: src/contracts/interfaces/IPermissionController.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "./ISemVerMixin.sol";
interface IPermissionControllerErrors {
/// @notice Thrown when a non-admin caller attempts to perform an admin-only action.
error NotAdmin();
/// @notice Thrown when attempting to remove an admin that does not exist.
error AdminNotSet();
/// @notice Thrown when attempting to set an appointee for a function that already has one.
error AppointeeAlreadySet();
/// @notice Thrown when attempting to interact with a non-existent appointee.
error AppointeeNotSet();
/// @notice Thrown when attempting to remove the last remaining admin.
error CannotHaveZeroAdmins();
/// @notice Thrown when attempting to set an admin that is already registered.
error AdminAlreadySet();
/// @notice Thrown when attempting to interact with an admin that is not in pending status.
error AdminNotPending();
/// @notice Thrown when attempting to add an admin that is already pending.
error AdminAlreadyPending();
}
interface IPermissionControllerEvents {
/// @notice Emitted when an appointee is set for an account to handle specific function calls.
event AppointeeSet(address indexed account, address indexed appointee, address target, bytes4 selector);
/// @notice Emitted when an appointee's permission to handle function calls for an account is revoked.
event AppointeeRemoved(address indexed account, address indexed appointee, address target, bytes4 selector);
/// @notice Emitted when an address is set as a pending admin for an account, requiring acceptance.
event PendingAdminAdded(address indexed account, address admin);
/// @notice Emitted when a pending admin status is removed for an account before acceptance.
event PendingAdminRemoved(address indexed account, address admin);
/// @notice Emitted when an address accepts and becomes an active admin for an account.
event AdminSet(address indexed account, address admin);
/// @notice Emitted when an admin's permissions are removed from an account.
event AdminRemoved(address indexed account, address admin);
}
interface IPermissionController is IPermissionControllerErrors, IPermissionControllerEvents, ISemVerMixin {
/**
* @notice Sets a pending admin for an account.
* @param account The account to set the pending admin for.
* @param admin The address to set as pending admin.
* @dev The pending admin must accept the role before becoming an active admin.
* @dev Multiple admins can be set for a single account.
*/
function addPendingAdmin(address account, address admin) external;
/**
* @notice Removes a pending admin from an account before they have accepted the role.
* @param account The account to remove the pending admin from.
* @param admin The pending admin address to remove.
* @dev Only an existing admin of the account can remove a pending admin.
*/
function removePendingAdmin(address account, address admin) external;
/**
* @notice Allows a pending admin to accept their admin role for an account.
* @param account The account to accept the admin role for.
* @dev Only addresses that were previously set as pending admins can accept the role.
*/
function acceptAdmin(
address account
) external;
/**
* @notice Removes an active admin from an account.
* @param account The account to remove the admin from.
* @param admin The admin address to remove.
* @dev Only an existing admin of the account can remove another admin.
* @dev Will revert if removing this admin would leave the account with zero admins.
*/
function removeAdmin(address account, address admin) external;
/**
* @notice Sets an appointee who can call specific functions on behalf of an account.
* @param account The account to set the appointee for.
* @param appointee The address to be given permission.
* @param target The contract address the appointee can interact with.
* @param selector The function selector the appointee can call.
* @dev Only an admin of the account can set appointees.
*/
function setAppointee(address account, address appointee, address target, bytes4 selector) external;
/**
* @notice Removes an appointee's permission to call a specific function.
* @param account The account to remove the appointee from.
* @param appointee The appointee address to remove.
* @param target The contract address to remove permissions for.
* @param selector The function selector to remove permissions for.
* @dev Only an admin of the account can remove appointees.
*/
function removeAppointee(address account, address appointee, address target, bytes4 selector) external;
/**
* @notice Checks if a given address is an admin of an account.
* @param account The account to check admin status for.
* @param caller The address to check.
* @dev If the account has no admins, returns true only if the caller is the account itself.
* @return Returns true if the caller is an admin, false otherwise.
*/
function isAdmin(address account, address caller) external view returns (bool);
/**
* @notice Checks if an address is currently a pending admin for an account.
* @param account The account to check pending admin status for.
* @param pendingAdmin The address to check.
* @return Returns true if the address is a pending admin, false otherwise.
*/
function isPendingAdmin(address account, address pendingAdmin) external view returns (bool);
/**
* @notice Retrieves all active admins for an account.
* @param account The account to get the admins for.
* @dev If the account has no admins, returns an array containing only the account address.
* @return An array of admin addresses.
*/
function getAdmins(
address account
) external view returns (address[] memory);
/**
* @notice Retrieves all pending admins for an account.
* @param account The account to get the pending admins for.
* @return An array of pending admin addresses.
*/
function getPendingAdmins(
address account
) external view returns (address[] memory);
/**
* @notice Checks if a caller has permission to call a specific function.
* @param account The account to check permissions for.
* @param caller The address attempting to make the call.
* @param target The contract address being called.
* @param selector The function selector being called.
* @dev Returns true if the caller is either an admin or an appointed caller.
* @dev Be mindful that upgrades to the contract may invalidate the appointee's permissions.
* This is only possible if a function's selector changes (e.g. if a function's parameters are modified).
* @return Returns true if the caller has permission, false otherwise.
*/
function canCall(address account, address caller, address target, bytes4 selector) external returns (bool);
/**
* @notice Retrieves all permissions granted to an appointee for a given account.
* @param account The account to check appointee permissions for.
* @param appointee The appointee address to check.
* @return Two arrays: target contract addresses and their corresponding function selectors.
*/
function getAppointeePermissions(
address account,
address appointee
) external returns (address[] memory, bytes4[] memory);
/**
* @notice Retrieves all appointees that can call a specific function for an account.
* @param account The account to get appointees for.
* @param target The contract address to check.
* @param selector The function selector to check.
* @dev Does not include admins in the returned list, even though they have calling permission.
* @return An array of appointee addresses.
*/
function getAppointees(address account, address target, bytes4 selector) external returns (address[] memory);
}
````
## File: src/contracts/interfaces/IRewardsCoordinator.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../libraries/OperatorSetLib.sol";
import "./IAllocationManager.sol";
import "./IDelegationManager.sol";
import "./IStrategyManager.sol";
import "./IPauserRegistry.sol";
import "./IPermissionController.sol";
import "./IStrategy.sol";
import "./ISemVerMixin.sol";
interface IRewardsCoordinatorErrors {
/// @dev Thrown when msg.sender is not allowed to call a function
error UnauthorizedCaller();
/// @dev Thrown when a earner not an AVS or Operator
error InvalidEarner();
/// Invalid Inputs
/// @dev Thrown when an input address is zero
error InvalidAddressZero();
/// @dev Thrown when an invalid root is provided.
error InvalidRoot();
/// @dev Thrown when an invalid root index is provided.
error InvalidRootIndex();
/// @dev Thrown when input arrays length is zero.
error InputArrayLengthZero();
/// @dev Thrown when two array parameters have mismatching lengths.
error InputArrayLengthMismatch();
/// @dev Thrown when provided root is not for new calculated period.
error NewRootMustBeForNewCalculatedPeriod();
/// @dev Thrown when rewards end timestamp has not elapsed.
error RewardsEndTimestampNotElapsed();
/// @dev Thrown when an invalid operator set is provided.
error InvalidOperatorSet();
/// Rewards Submissions
/// @dev Thrown when input `amount` is zero.
error AmountIsZero();
/// @dev Thrown when input `amount` exceeds maximum.
error AmountExceedsMax();
/// @dev Thrown when input `split` exceeds `ONE_HUNDRED_IN_BIPS`
error SplitExceedsMax();
/// @dev Thrown when an operator attempts to set a split before the previous one becomes active
error PreviousSplitPending();
/// @dev Thrown when input `duration` exceeds maximum.
error DurationExceedsMax();
/// @dev Thrown when input `duration` is zero.
error DurationIsZero();
/// @dev Thrown when input `duration` is not evenly divisble by CALCULATION_INTERVAL_SECONDS.
error InvalidDurationRemainder();
/// @dev Thrown when GENESIS_REWARDS_TIMESTAMP is not evenly divisble by CALCULATION_INTERVAL_SECONDS.
error InvalidGenesisRewardsTimestampRemainder();
/// @dev Thrown when CALCULATION_INTERVAL_SECONDS is not evenly divisble by SNAPSHOT_CADENCE.
error InvalidCalculationIntervalSecondsRemainder();
/// @dev Thrown when `startTimestamp` is not evenly divisble by CALCULATION_INTERVAL_SECONDS.
error InvalidStartTimestampRemainder();
/// @dev Thrown when `startTimestamp` is too far in the future.
error StartTimestampTooFarInFuture();
/// @dev Thrown when `startTimestamp` is too far in the past.
error StartTimestampTooFarInPast();
/// @dev Thrown when an attempt to use a non-whitelisted strategy is made.
error StrategyNotWhitelisted();
/// @dev Thrown when `strategies` is not sorted in ascending order.
error StrategiesNotInAscendingOrder();
/// @dev Thrown when `operators` are not sorted in ascending order
error OperatorsNotInAscendingOrder();
/// @dev Thrown when an operator-directed rewards submission is not retroactive
error SubmissionNotRetroactive();
/// Claims
/// @dev Thrown when an invalid earner claim proof is provided.
error InvalidClaimProof();
/// @dev Thrown when an invalid token leaf index is provided.
error InvalidTokenLeafIndex();
/// @dev Thrown when an invalid earner leaf index is provided.
error InvalidEarnerLeafIndex();
/// @dev Thrown when cumulative earnings are not greater than cumulative claimed.
error EarningsNotGreaterThanClaimed();
/// Reward Root Checks
/// @dev Thrown if a root has already been disabled.
error RootDisabled();
/// @dev Thrown if a root has not been activated yet.
error RootNotActivated();
/// @dev Thrown if a root has already been activated.
error RootAlreadyActivated();
}
interface IRewardsCoordinatorTypes {
/**
* @notice A linear combination of strategies and multipliers for AVSs to weigh
* EigenLayer strategies.
* @param strategy The EigenLayer strategy to be used for the rewards submission
* @param multiplier The weight of the strategy in the rewards submission
*/
struct StrategyAndMultiplier {
IStrategy strategy;
uint96 multiplier;
}
/**
* @notice A reward struct for an operator
* @param operator The operator to be rewarded
* @param amount The reward amount for the operator
*/
struct OperatorReward {
address operator;
uint256 amount;
}
/**
* @notice A split struct for an Operator
* @param oldSplitBips The old split in basis points. This is the split that is active if `block.timestamp < activatedAt`
* @param newSplitBips The new split in basis points. This is the split that is active if `block.timestamp >= activatedAt`
* @param activatedAt The timestamp at which the split will be activated
*/
struct OperatorSplit {
uint16 oldSplitBips;
uint16 newSplitBips;
uint32 activatedAt;
}
/**
* Sliding Window for valid RewardsSubmission startTimestamp
*
* Scenario A: GENESIS_REWARDS_TIMESTAMP IS WITHIN RANGE
* <-----MAX_RETROACTIVE_LENGTH-----> t (block.timestamp) <---MAX_FUTURE_LENGTH--->
* <--------------------valid range for startTimestamp------------------------>
* ^
* GENESIS_REWARDS_TIMESTAMP
*
*
* Scenario B: GENESIS_REWARDS_TIMESTAMP IS OUT OF RANGE
* <-----MAX_RETROACTIVE_LENGTH-----> t (block.timestamp) <---MAX_FUTURE_LENGTH--->
* <------------------------valid range for startTimestamp------------------------>
* ^
* GENESIS_REWARDS_TIMESTAMP
* @notice RewardsSubmission struct submitted by AVSs when making rewards for their operators and stakers
* RewardsSubmission can be for a time range within the valid window for startTimestamp and must be within max duration.
* See `createAVSRewardsSubmission()` for more details.
* @param strategiesAndMultipliers The strategies and their relative weights
* cannot have duplicate strategies and need to be sorted in ascending address order
* @param token The rewards token to be distributed
* @param amount The total amount of tokens to be distributed
* @param startTimestamp The timestamp (seconds) at which the submission range is considered for distribution
* could start in the past or in the future but within a valid range. See the diagram above.
* @param duration The duration of the submission range in seconds. Must be <= MAX_REWARDS_DURATION
*/
struct RewardsSubmission {
StrategyAndMultiplier[] strategiesAndMultipliers;
IERC20 token;
uint256 amount;
uint32 startTimestamp;
uint32 duration;
}
/**
* @notice OperatorDirectedRewardsSubmission struct submitted by AVSs when making operator-directed rewards for their operators and stakers.
* @param strategiesAndMultipliers The strategies and their relative weights.
* @param token The rewards token to be distributed.
* @param operatorRewards The rewards for the operators.
* @param startTimestamp The timestamp (seconds) at which the submission range is considered for distribution.
* @param duration The duration of the submission range in seconds.
* @param description Describes what the rewards submission is for.
*/
struct OperatorDirectedRewardsSubmission {
StrategyAndMultiplier[] strategiesAndMultipliers;
IERC20 token;
OperatorReward[] operatorRewards;
uint32 startTimestamp;
uint32 duration;
string description;
}
/**
* @notice A distribution root is a merkle root of the distribution of earnings for a given period.
* The RewardsCoordinator stores all historical distribution roots so that earners can claim their earnings against older roots
* if they wish but the merkle tree contains the cumulative earnings of all earners and tokens for a given period so earners (or their claimers if set)
* only need to claim against the latest root to claim all available earnings.
* @param root The merkle root of the distribution
* @param rewardsCalculationEndTimestamp The timestamp (seconds) until which rewards have been calculated
* @param activatedAt The timestamp (seconds) at which the root can be claimed against
*/
struct DistributionRoot {
bytes32 root;
uint32 rewardsCalculationEndTimestamp;
uint32 activatedAt;
bool disabled;
}
/**
* @notice Internal leaf in the merkle tree for the earner's account leaf
* @param earner The address of the earner
* @param earnerTokenRoot The merkle root of the earner's token subtree
* Each leaf in the earner's token subtree is a TokenTreeMerkleLeaf
*/
struct EarnerTreeMerkleLeaf {
address earner;
bytes32 earnerTokenRoot;
}
/**
* @notice The actual leaves in the distribution merkle tree specifying the token earnings
* for the respective earner's subtree. Each leaf is a claimable amount of a token for an earner.
* @param token The token for which the earnings are being claimed
* @param cumulativeEarnings The cumulative earnings of the earner for the token
*/
struct TokenTreeMerkleLeaf {
IERC20 token;
uint256 cumulativeEarnings;
}
/**
* @notice A claim against a distribution root called by an
* earners claimer (could be the earner themselves). Each token claim will claim the difference
* between the cumulativeEarnings of the earner and the cumulativeClaimed of the claimer.
* Each claim can specify which of the earner's earned tokens they want to claim.
* See `processClaim()` for more details.
* @param rootIndex The index of the root in the list of DistributionRoots
* @param earnerIndex The index of the earner's account root in the merkle tree
* @param earnerTreeProof The proof of the earner's EarnerTreeMerkleLeaf against the merkle root
* @param earnerLeaf The earner's EarnerTreeMerkleLeaf struct, providing the earner address and earnerTokenRoot
* @param tokenIndices The indices of the token leaves in the earner's subtree
* @param tokenTreeProofs The proofs of the token leaves against the earner's earnerTokenRoot
* @param tokenLeaves The token leaves to be claimed
* @dev The merkle tree is structured with the merkle root at the top and EarnerTreeMerkleLeaf as internal leaves
* in the tree. Each earner leaf has its own subtree with TokenTreeMerkleLeaf as leaves in the subtree.
* To prove a claim against a specified rootIndex(which specifies the distributionRoot being used),
* the claim will first verify inclusion of the earner leaf in the tree against _distributionRoots[rootIndex].root.
* Then for each token, it will verify inclusion of the token leaf in the earner's subtree against the earner's earnerTokenRoot.
*/
struct RewardsMerkleClaim {
uint32 rootIndex;
uint32 earnerIndex;
bytes earnerTreeProof;
EarnerTreeMerkleLeaf earnerLeaf;
uint32[] tokenIndices;
bytes[] tokenTreeProofs;
TokenTreeMerkleLeaf[] tokenLeaves;
}
/**
* @notice Parameters for the RewardsCoordinator constructor
* @param delegationManager The address of the DelegationManager contract
* @param strategyManager The address of the StrategyManager contract
* @param allocationManager The address of the AllocationManager contract
* @param pauserRegistry The address of the PauserRegistry contract
* @param permissionController The address of the PermissionController contract
* @param CALCULATION_INTERVAL_SECONDS The interval at which rewards are calculated
* @param MAX_REWARDS_DURATION The maximum duration of a rewards submission
* @param MAX_RETROACTIVE_LENGTH The maximum retroactive length of a rewards submission
* @param MAX_FUTURE_LENGTH The maximum future length of a rewards submission
* @param GENESIS_REWARDS_TIMESTAMP The timestamp at which rewards are first calculated
* @param version The semantic version of the contract (e.g. "v1.2.3")
* @dev Needed to avoid stack-too-deep errors
*/
struct RewardsCoordinatorConstructorParams {
IDelegationManager delegationManager;
IStrategyManager strategyManager;
IAllocationManager allocationManager;
IPauserRegistry pauserRegistry;
IPermissionController permissionController;
uint32 CALCULATION_INTERVAL_SECONDS;
uint32 MAX_REWARDS_DURATION;
uint32 MAX_RETROACTIVE_LENGTH;
uint32 MAX_FUTURE_LENGTH;
uint32 GENESIS_REWARDS_TIMESTAMP;
string version;
}
}
interface IRewardsCoordinatorEvents is IRewardsCoordinatorTypes {
/// @notice emitted when an AVS creates a valid RewardsSubmission
event AVSRewardsSubmissionCreated(
address indexed avs,
uint256 indexed submissionNonce,
bytes32 indexed rewardsSubmissionHash,
RewardsSubmission rewardsSubmission
);
/// @notice emitted when a valid RewardsSubmission is created for all stakers by a valid submitter
event RewardsSubmissionForAllCreated(
address indexed submitter,
uint256 indexed submissionNonce,
bytes32 indexed rewardsSubmissionHash,
RewardsSubmission rewardsSubmission
);
/// @notice emitted when a valid RewardsSubmission is created when rewardAllStakersAndOperators is called
event RewardsSubmissionForAllEarnersCreated(
address indexed tokenHopper,
uint256 indexed submissionNonce,
bytes32 indexed rewardsSubmissionHash,
RewardsSubmission rewardsSubmission
);
/**
* @notice Emitted when an AVS creates a valid `OperatorDirectedRewardsSubmission`
* @param caller The address calling `createOperatorDirectedAVSRewardsSubmission`.
* @param avs The avs on behalf of which the operator-directed rewards are being submitted.
* @param operatorDirectedRewardsSubmissionHash Keccak256 hash of (`avs`, `submissionNonce` and `operatorDirectedRewardsSubmission`).
* @param submissionNonce Current nonce of the avs. Used to generate a unique submission hash.
* @param operatorDirectedRewardsSubmission The Operator-Directed Rewards Submission. Contains the token, start timestamp, duration, operator rewards, description and, strategy and multipliers.
*/
event OperatorDirectedAVSRewardsSubmissionCreated(
address indexed caller,
address indexed avs,
bytes32 indexed operatorDirectedRewardsSubmissionHash,
uint256 submissionNonce,
OperatorDirectedRewardsSubmission operatorDirectedRewardsSubmission
);
/**
* @notice Emitted when an AVS creates a valid `OperatorDirectedRewardsSubmission` for an operator set.
* @param caller The address calling `createOperatorDirectedOperatorSetRewardsSubmission`.
* @param operatorDirectedRewardsSubmissionHash Keccak256 hash of (`avs`, `submissionNonce` and `operatorDirectedRewardsSubmission`).
* @param operatorSet The operatorSet on behalf of which the operator-directed rewards are being submitted.
* @param submissionNonce Current nonce of the avs. Used to generate a unique submission hash.
* @param operatorDirectedRewardsSubmission The Operator-Directed Rewards Submission. Contains the token, start timestamp, duration, operator rewards, description and, strategy and multipliers.
*/
event OperatorDirectedOperatorSetRewardsSubmissionCreated(
address indexed caller,
bytes32 indexed operatorDirectedRewardsSubmissionHash,
OperatorSet operatorSet,
uint256 submissionNonce,
OperatorDirectedRewardsSubmission operatorDirectedRewardsSubmission
);
/// @notice rewardsUpdater is responsible for submitting DistributionRoots, only owner can set rewardsUpdater
event RewardsUpdaterSet(address indexed oldRewardsUpdater, address indexed newRewardsUpdater);
event RewardsForAllSubmitterSet(
address indexed rewardsForAllSubmitter, bool indexed oldValue, bool indexed newValue
);
event ActivationDelaySet(uint32 oldActivationDelay, uint32 newActivationDelay);
event DefaultOperatorSplitBipsSet(uint16 oldDefaultOperatorSplitBips, uint16 newDefaultOperatorSplitBips);
/**
* @notice Emitted when the operator split for an AVS is set.
* @param caller The address calling `setOperatorAVSSplit`.
* @param operator The operator on behalf of which the split is being set.
* @param avs The avs for which the split is being set by the operator.
* @param activatedAt The timestamp at which the split will be activated.
* @param oldOperatorAVSSplitBips The old split for the operator for the AVS.
* @param newOperatorAVSSplitBips The new split for the operator for the AVS.
*/
event OperatorAVSSplitBipsSet(
address indexed caller,
address indexed operator,
address indexed avs,
uint32 activatedAt,
uint16 oldOperatorAVSSplitBips,
uint16 newOperatorAVSSplitBips
);
/**
* @notice Emitted when the operator split for Programmatic Incentives is set.
* @param caller The address calling `setOperatorPISplit`.
* @param operator The operator on behalf of which the split is being set.
* @param activatedAt The timestamp at which the split will be activated.
* @param oldOperatorPISplitBips The old split for the operator for Programmatic Incentives.
* @param newOperatorPISplitBips The new split for the operator for Programmatic Incentives.
*/
event OperatorPISplitBipsSet(
address indexed caller,
address indexed operator,
uint32 activatedAt,
uint16 oldOperatorPISplitBips,
uint16 newOperatorPISplitBips
);
/**
* @notice Emitted when the operator split for a given operatorSet is set.
* @param caller The address calling `setOperatorSetSplit`.
* @param operator The operator on behalf of which the split is being set.
* @param operatorSet The operatorSet for which the split is being set.
* @param activatedAt The timestamp at which the split will be activated.
* @param oldOperatorSetSplitBips The old split for the operator for the operatorSet.
* @param newOperatorSetSplitBips The new split for the operator for the operatorSet.
*/
event OperatorSetSplitBipsSet(
address indexed caller,
address indexed operator,
OperatorSet operatorSet,
uint32 activatedAt,
uint16 oldOperatorSetSplitBips,
uint16 newOperatorSetSplitBips
);
event ClaimerForSet(address indexed earner, address indexed oldClaimer, address indexed claimer);
/// @notice rootIndex is the specific array index of the newly created root in the storage array
event DistributionRootSubmitted(
uint32 indexed rootIndex,
bytes32 indexed root,
uint32 indexed rewardsCalculationEndTimestamp,
uint32 activatedAt
);
event DistributionRootDisabled(uint32 indexed rootIndex);
/// @notice root is one of the submitted distribution roots that was claimed against
event RewardsClaimed(
bytes32 root,
address indexed earner,
address indexed claimer,
address indexed recipient,
IERC20 token,
uint256 claimedAmount
);
}
/**
* @title Interface for the `IRewardsCoordinator` contract.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice Allows AVSs to make "Rewards Submissions", which get distributed amongst the AVSs' confirmed
* Operators and the Stakers delegated to those Operators.
* Calculations are performed based on the completed RewardsSubmission, with the results posted in
* a Merkle root against which Stakers & Operators can make claims.
*/
interface IRewardsCoordinator is IRewardsCoordinatorErrors, IRewardsCoordinatorEvents, ISemVerMixin {
/**
* @dev Initializes the addresses of the initial owner, pauser registry, rewardsUpdater and
* configures the initial paused status, activationDelay, and defaultOperatorSplitBips.
*/
function initialize(
address initialOwner,
uint256 initialPausedStatus,
address _rewardsUpdater,
uint32 _activationDelay,
uint16 _defaultSplitBips
) external;
/**
* @notice Creates a new rewards submission on behalf of an AVS, to be split amongst the
* set of stakers delegated to operators who are registered to the `avs`
* @param rewardsSubmissions The rewards submissions being created
* @dev Expected to be called by the ServiceManager of the AVS on behalf of which the submission is being made
* @dev The duration of the `rewardsSubmission` cannot exceed `MAX_REWARDS_DURATION`
* @dev The duration of the `rewardsSubmission` cannot be 0 and must be a multiple of `CALCULATION_INTERVAL_SECONDS`
* @dev The tokens are sent to the `RewardsCoordinator` contract
* @dev Strategies must be in ascending order of addresses to check for duplicates
* @dev This function will revert if the `rewardsSubmission` is malformed,
* e.g. if the `strategies` and `weights` arrays are of non-equal lengths
*/
function createAVSRewardsSubmission(
RewardsSubmission[] calldata rewardsSubmissions
) external;
/**
* @notice similar to `createAVSRewardsSubmission` except the rewards are split amongst *all* stakers
* rather than just those delegated to operators who are registered to a single avs and is
* a permissioned call based on isRewardsForAllSubmitter mapping.
* @param rewardsSubmissions The rewards submissions being created
*/
function createRewardsForAllSubmission(
RewardsSubmission[] calldata rewardsSubmissions
) external;
/**
* @notice Creates a new rewards submission for all earners across all AVSs.
* Earners in this case indicating all operators and their delegated stakers. Undelegated stake
* is not rewarded from this RewardsSubmission. This interface is only callable
* by the token hopper contract from the Eigen Foundation
* @param rewardsSubmissions The rewards submissions being created
*/
function createRewardsForAllEarners(
RewardsSubmission[] calldata rewardsSubmissions
) external;
/**
* @notice Creates a new operator-directed rewards submission on behalf of an AVS, to be split amongst the operators and
* set of stakers delegated to operators who are registered to the `avs`.
* @param avs The AVS on behalf of which the reward is being submitted
* @param operatorDirectedRewardsSubmissions The operator-directed rewards submissions being created
* @dev Expected to be called by the ServiceManager of the AVS on behalf of which the submission is being made
* @dev The duration of the `rewardsSubmission` cannot exceed `MAX_REWARDS_DURATION`
* @dev The duration of the `rewardsSubmission` cannot be 0 and must be a multiple of `CALCULATION_INTERVAL_SECONDS`
* @dev The tokens are sent to the `RewardsCoordinator` contract
* @dev The `RewardsCoordinator` contract needs a token approval of sum of all `operatorRewards` in the `operatorDirectedRewardsSubmissions`, before calling this function.
* @dev Strategies must be in ascending order of addresses to check for duplicates
* @dev Operators must be in ascending order of addresses to check for duplicates.
* @dev This function will revert if the `operatorDirectedRewardsSubmissions` is malformed.
*/
function createOperatorDirectedAVSRewardsSubmission(
address avs,
OperatorDirectedRewardsSubmission[] calldata operatorDirectedRewardsSubmissions
) external;
/**
* @notice Creates a new operator-directed rewards submission for an operator set, to be split amongst the operators and
* set of stakers delegated to operators who are part of the operator set.
* @param operatorSet The operator set for which the rewards are being submitted
* @param operatorDirectedRewardsSubmissions The operator-directed rewards submissions being created
* @dev Expected to be called by the AVS that created the operator set
* @dev The duration of the `rewardsSubmission` cannot exceed `MAX_REWARDS_DURATION`
* @dev The duration of the `rewardsSubmission` cannot be 0 and must be a multiple of `CALCULATION_INTERVAL_SECONDS`
* @dev The tokens are sent to the `RewardsCoordinator` contract
* @dev The `RewardsCoordinator` contract needs a token approval of sum of all `operatorRewards` in the `operatorDirectedRewardsSubmissions`, before calling this function
* @dev Strategies must be in ascending order of addresses to check for duplicates
* @dev Operators must be in ascending order of addresses to check for duplicates
* @dev This function will revert if the `operatorDirectedRewardsSubmissions` is malformed
*/
function createOperatorDirectedOperatorSetRewardsSubmission(
OperatorSet calldata operatorSet,
OperatorDirectedRewardsSubmission[] calldata operatorDirectedRewardsSubmissions
) external;
/**
* @notice Claim rewards against a given root (read from _distributionRoots[claim.rootIndex]).
* Earnings are cumulative so earners don't have to claim against all distribution roots they have earnings for,
* they can simply claim against the latest root and the contract will calculate the difference between
* their cumulativeEarnings and cumulativeClaimed. This difference is then transferred to recipient address.
* @param claim The RewardsMerkleClaim to be processed.
* Contains the root index, earner, token leaves, and required proofs
* @param recipient The address recipient that receives the ERC20 rewards
* @dev only callable by the valid claimer, that is
* if claimerFor[claim.earner] is address(0) then only the earner can claim, otherwise only
* claimerFor[claim.earner] can claim the rewards.
*/
function processClaim(RewardsMerkleClaim calldata claim, address recipient) external;
/**
* @notice Batch claim rewards against a given root (read from _distributionRoots[claim.rootIndex]).
* Earnings are cumulative so earners don't have to claim against all distribution roots they have earnings for,
* they can simply claim against the latest root and the contract will calculate the difference between
* their cumulativeEarnings and cumulativeClaimed. This difference is then transferred to recipient address.
* @param claims The RewardsMerkleClaims to be processed.
* Contains the root index, earner, token leaves, and required proofs
* @param recipient The address recipient that receives the ERC20 rewards
* @dev only callable by the valid claimer, that is
* if claimerFor[claim.earner] is address(0) then only the earner can claim, otherwise only
* claimerFor[claim.earner] can claim the rewards.
* @dev This function may fail to execute with a large number of claims due to gas limits. Use a smaller array of claims if necessary.
*/
function processClaims(RewardsMerkleClaim[] calldata claims, address recipient) external;
/**
* @notice Creates a new distribution root. activatedAt is set to block.timestamp + activationDelay
* @param root The merkle root of the distribution
* @param rewardsCalculationEndTimestamp The timestamp until which rewards have been calculated
* @dev Only callable by the rewardsUpdater
*/
function submitRoot(bytes32 root, uint32 rewardsCalculationEndTimestamp) external;
/**
* @notice allow the rewardsUpdater to disable/cancel a pending root submission in case of an error
* @param rootIndex The index of the root to be disabled
*/
function disableRoot(
uint32 rootIndex
) external;
/**
* @notice Sets the address of the entity that can call `processClaim` on ehalf of an earner
* @param claimer The address of the entity that can call `processClaim` on behalf of the earner
* @dev Assumes msg.sender is the earner
*/
function setClaimerFor(
address claimer
) external;
/**
* @notice Sets the address of the entity that can call `processClaim` on behalf of an earner
* @param earner The address to set the claimer for
* @param claimer The address of the entity that can call `processClaim` on behalf of the earner
* @dev Only callable by operators or AVSs. We define an AVS that has created at least one
* operatorSet in the `AllocationManager`
*/
function setClaimerFor(address earner, address claimer) external;
/**
* @notice Sets the delay in timestamp before a posted root can be claimed against
* @dev Only callable by the contract owner
* @param _activationDelay The new value for activationDelay
*/
function setActivationDelay(
uint32 _activationDelay
) external;
/**
* @notice Sets the default split for all operators across all avss.
* @param split The default split for all operators across all avss in bips.
* @dev Only callable by the contract owner.
*/
function setDefaultOperatorSplit(
uint16 split
) external;
/**
* @notice Sets the split for a specific operator for a specific avs
* @param operator The operator who is setting the split
* @param avs The avs for which the split is being set by the operator
* @param split The split for the operator for the specific avs in bips.
* @dev Only callable by the operator
* @dev Split has to be between 0 and 10000 bips (inclusive)
* @dev The split will be activated after the activation delay
*/
function setOperatorAVSSplit(address operator, address avs, uint16 split) external;
/**
* @notice Sets the split for a specific operator for Programmatic Incentives.
* @param operator The operator on behalf of which the split is being set.
* @param split The split for the operator for Programmatic Incentives in bips.
* @dev Only callable by the operator
* @dev Split has to be between 0 and 10000 bips (inclusive)
* @dev The split will be activated after the activation delay
*/
function setOperatorPISplit(address operator, uint16 split) external;
/**
* @notice Sets the split for a specific operator for a specific operatorSet.
* @param operator The operator who is setting the split.
* @param operatorSet The operatorSet for which the split is being set by the operator.
* @param split The split for the operator for the specific operatorSet in bips.
* @dev Only callable by the operator
* @dev Split has to be between 0 and 10000 bips (inclusive)
* @dev The split will be activated after the activation delay
*/
function setOperatorSetSplit(address operator, OperatorSet calldata operatorSet, uint16 split) external;
/**
* @notice Sets the permissioned `rewardsUpdater` address which can post new roots
* @dev Only callable by the contract owner
* @param _rewardsUpdater The address of the new rewardsUpdater
*/
function setRewardsUpdater(
address _rewardsUpdater
) external;
/**
* @notice Sets the permissioned `rewardsForAllSubmitter` address which can submit createRewardsForAllSubmission
* @dev Only callable by the contract owner
* @param _submitter The address of the rewardsForAllSubmitter
* @param _newValue The new value for isRewardsForAllSubmitter
*/
function setRewardsForAllSubmitter(address _submitter, bool _newValue) external;
/**
*
* VIEW FUNCTIONS
*
*/
/// @notice Delay in timestamp (seconds) before a posted root can be claimed against
function activationDelay() external view returns (uint32);
/// @notice The timestamp until which RewardsSubmissions have been calculated
function currRewardsCalculationEndTimestamp() external view returns (uint32);
/// @notice Mapping: earner => the address of the entity who can call `processClaim` on behalf of the earner
function claimerFor(
address earner
) external view returns (address);
/// @notice Mapping: claimer => token => total amount claimed
function cumulativeClaimed(address claimer, IERC20 token) external view returns (uint256);
/// @notice the default split for all operators across all avss
function defaultOperatorSplitBips() external view returns (uint16);
/// @notice the split for a specific `operator` for a specific `avs`
function getOperatorAVSSplit(address operator, address avs) external view returns (uint16);
/// @notice the split for a specific `operator` for Programmatic Incentives
function getOperatorPISplit(
address operator
) external view returns (uint16);
/// @notice Returns the split for a specific `operator` for a given `operatorSet`
function getOperatorSetSplit(address operator, OperatorSet calldata operatorSet) external view returns (uint16);
/// @notice return the hash of the earner's leaf
function calculateEarnerLeafHash(
EarnerTreeMerkleLeaf calldata leaf
) external pure returns (bytes32);
/// @notice returns the hash of the earner's token leaf
function calculateTokenLeafHash(
TokenTreeMerkleLeaf calldata leaf
) external pure returns (bytes32);
/// @notice returns 'true' if the claim would currently pass the check in `processClaims`
/// but will revert if not valid
function checkClaim(
RewardsMerkleClaim calldata claim
) external view returns (bool);
/// @notice returns the number of distribution roots posted
function getDistributionRootsLength() external view returns (uint256);
/// @notice returns the distributionRoot at the specified index
function getDistributionRootAtIndex(
uint256 index
) external view returns (DistributionRoot memory);
/// @notice returns the current distributionRoot
function getCurrentDistributionRoot() external view returns (DistributionRoot memory);
/// @notice loop through the distribution roots from reverse and get latest root that is not disabled and activated
/// i.e. a root that can be claimed against
function getCurrentClaimableDistributionRoot() external view returns (DistributionRoot memory);
/// @notice loop through distribution roots from reverse and return index from hash
function getRootIndexFromHash(
bytes32 rootHash
) external view returns (uint32);
/// @notice The address of the entity that can update the contract with new merkle roots
function rewardsUpdater() external view returns (address);
/**
* @notice The interval in seconds at which the calculation for a RewardsSubmission distribution is done.
* @dev Rewards Submission durations must be multiples of this interval.
*/
function CALCULATION_INTERVAL_SECONDS() external view returns (uint32);
/// @notice The maximum amount of time (seconds) that a RewardsSubmission can span over
function MAX_REWARDS_DURATION() external view returns (uint32);
/// @notice max amount of time (seconds) that a submission can start in the past
function MAX_RETROACTIVE_LENGTH() external view returns (uint32);
/// @notice max amount of time (seconds) that a submission can start in the future
function MAX_FUTURE_LENGTH() external view returns (uint32);
/// @notice absolute min timestamp (seconds) that a submission can start at
function GENESIS_REWARDS_TIMESTAMP() external view returns (uint32);
}
````
## File: src/contracts/interfaces/ISemVerMixin.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
/// @title ISemVerMixin
/// @notice A mixin interface that provides semantic versioning functionality.
/// @dev Follows SemVer 2.0.0 specification (https://semver.org/)
interface ISemVerMixin {
/// @notice Returns the semantic version string of the contract.
/// @return The version string in SemVer format (e.g., "v1.1.1")
function version() external view returns (string memory);
}
````
## File: src/contracts/interfaces/IShareManager.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../libraries/SlashingLib.sol";
import "./IStrategy.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title Interface for a `IShareManager` contract.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice This contract is used by the DelegationManager as a unified interface to interact with the EigenPodManager and StrategyManager
*/
interface IShareManager {
/// @notice Used by the DelegationManager to remove a Staker's shares from a particular strategy when entering the withdrawal queue
/// @dev strategy must be beaconChainETH when talking to the EigenPodManager
/// @return updatedShares the staker's deposit shares after decrement
function removeDepositShares(
address staker,
IStrategy strategy,
uint256 depositSharesToRemove
) external returns (uint256);
/// @notice Used by the DelegationManager to award a Staker some shares that have passed through the withdrawal queue
/// @dev strategy must be beaconChainETH when talking to the EigenPodManager
/// @return existingDepositShares the shares the staker had before any were added
/// @return addedShares the new shares added to the staker's balance
function addShares(address staker, IStrategy strategy, uint256 shares) external returns (uint256, uint256);
/// @notice Used by the DelegationManager to convert deposit shares to tokens and send them to a staker
/// @dev strategy must be beaconChainETH when talking to the EigenPodManager
/// @dev token is not validated when talking to the EigenPodManager
function withdrawSharesAsTokens(address staker, IStrategy strategy, IERC20 token, uint256 shares) external;
/// @notice Returns the current shares of `user` in `strategy`
/// @dev strategy must be beaconChainETH when talking to the EigenPodManager
/// @dev returns 0 if the user has negative shares
function stakerDepositShares(address user, IStrategy strategy) external view returns (uint256 depositShares);
/**
* @notice Increase the amount of burnable shares for a given Strategy. This is called by the DelegationManager
* when an operator is slashed in EigenLayer.
* @param strategy The strategy to burn shares in.
* @param addedSharesToBurn The amount of added shares to burn.
* @dev This function is only called by the DelegationManager when an operator is slashed.
*/
function increaseBurnableShares(IStrategy strategy, uint256 addedSharesToBurn) external;
}
````
## File: src/contracts/interfaces/ISignatureUtilsMixin.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import "./ISemVerMixin.sol";
interface ISignatureUtilsMixinErrors {
/// @notice Thrown when a signature is invalid.
error InvalidSignature();
/// @notice Thrown when a signature has expired.
error SignatureExpired();
}
interface ISignatureUtilsMixinTypes {
/// @notice Struct that bundles together a signature and an expiration time for the signature.
/// @dev Used primarily for stack management.
struct SignatureWithExpiry {
// the signature itself, formatted as a single bytes object
bytes signature;
// the expiration timestamp (UTC) of the signature
uint256 expiry;
}
/// @notice Struct that bundles together a signature, a salt for uniqueness, and an expiration time for the signature.
/// @dev Used primarily for stack management.
struct SignatureWithSaltAndExpiry {
// the signature itself, formatted as a single bytes object
bytes signature;
// the salt used to generate the signature
bytes32 salt;
// the expiration timestamp (UTC) of the signature
uint256 expiry;
}
}
/**
* @title The interface for common signature utilities.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
interface ISignatureUtilsMixin is ISignatureUtilsMixinErrors, ISignatureUtilsMixinTypes, ISemVerMixin {
/// @notice Computes the EIP-712 domain separator used for signature validation.
/// @dev The domain separator is computed according to EIP-712 specification, using:
/// - The hardcoded name "EigenLayer"
/// - The contract's version string
/// - The current chain ID
/// - This contract's address
/// @return The 32-byte domain separator hash used in EIP-712 structured data signing.
/// @dev See https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator.
function domainSeparator() external view returns (bytes32);
}
````
## File: src/contracts/interfaces/IStrategy.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../libraries/SlashingLib.sol";
import "./ISemVerMixin.sol";
interface IStrategyErrors {
/// @dev Thrown when called by an account that is not strategy manager.
error OnlyStrategyManager();
/// @dev Thrown when new shares value is zero.
error NewSharesZero();
/// @dev Thrown when total shares exceeds max.
error TotalSharesExceedsMax();
/// @dev Thrown when amount shares is greater than total shares.
error WithdrawalAmountExceedsTotalDeposits();
/// @dev Thrown when attempting an action with a token that is not accepted.
error OnlyUnderlyingToken();
/// StrategyBaseWithTVLLimits
/// @dev Thrown when `maxPerDeposit` exceeds max.
error MaxPerDepositExceedsMax();
/// @dev Thrown when balance exceeds max total deposits.
error BalanceExceedsMaxTotalDeposits();
}
interface IStrategyEvents {
/**
* @notice Used to emit an event for the exchange rate between 1 share and underlying token in a strategy contract
* @param rate is the exchange rate in wad 18 decimals
* @dev Tokens that do not have 18 decimals must have offchain services scale the exchange rate by the proper magnitude
*/
event ExchangeRateEmitted(uint256 rate);
/**
* Used to emit the underlying token and its decimals on strategy creation
* @notice token
* @param token is the ERC20 token of the strategy
* @param decimals are the decimals of the ERC20 token in the strategy
*/
event StrategyTokenSet(IERC20 token, uint8 decimals);
}
/**
* @title Minimal interface for an `Strategy` contract.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice Custom `Strategy` implementations may expand extensively on this interface.
*/
interface IStrategy is IStrategyErrors, IStrategyEvents, ISemVerMixin {
/**
* @notice Used to deposit tokens into this Strategy
* @param token is the ERC20 token being deposited
* @param amount is the amount of token being deposited
* @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's
* `depositIntoStrategy` function, and individual share balances are recorded in the strategyManager as well.
* @return newShares is the number of new shares issued at the current exchange ratio.
*/
function deposit(IERC20 token, uint256 amount) external returns (uint256);
/**
* @notice Used to withdraw tokens from this Strategy, to the `recipient`'s address
* @param recipient is the address to receive the withdrawn funds
* @param token is the ERC20 token being transferred out
* @param amountShares is the amount of shares being withdrawn
* @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's
* other functions, and individual share balances are recorded in the strategyManager as well.
*/
function withdraw(address recipient, IERC20 token, uint256 amountShares) external;
/**
* @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy.
* For a staker using this function and trying to calculate the amount of underlying tokens they have in total they
* should input into `amountShares` their withdrawable shares read from the `DelegationManager` contract.
* @notice In contrast to `sharesToUnderlyingView`, this function **may** make state modifications
* @param amountShares is the amount of shares to calculate its conversion into the underlying token
* @return The amount of underlying tokens corresponding to the input `amountShares`
* @dev Implementation for these functions in particular may vary significantly for different strategies
*/
function sharesToUnderlying(
uint256 amountShares
) external returns (uint256);
/**
* @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy.
* @notice In contrast to `underlyingToSharesView`, this function **may** make state modifications
* @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares
* @return The amount of shares corresponding to the input `amountUnderlying`. This is used as deposit shares
* in the `StrategyManager` contract.
* @dev Implementation for these functions in particular may vary significantly for different strategies
*/
function underlyingToShares(
uint256 amountUnderlying
) external returns (uint256);
/**
* @notice convenience function for fetching the current underlying value of all of the `user`'s shares in
* this strategy. In contrast to `userUnderlyingView`, this function **may** make state modifications
*/
function userUnderlying(
address user
) external returns (uint256);
/**
* @notice convenience function for fetching the current total shares of `user` in this strategy, by
* querying the `strategyManager` contract
*/
function shares(
address user
) external view returns (uint256);
/**
* @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy.
* For a staker using this function and trying to calculate the amount of underlying tokens they have in total they
* should input into `amountShares` their withdrawable shares read from the `DelegationManager` contract.
* @notice In contrast to `sharesToUnderlying`, this function guarantees no state modifications
* @param amountShares is the amount of shares to calculate its conversion into the underlying token
* @return The amount of underlying tokens corresponding to the input `amountShares`
* @dev Implementation for these functions in particular may vary significantly for different strategies
*/
function sharesToUnderlyingView(
uint256 amountShares
) external view returns (uint256);
/**
* @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy.
* @notice In contrast to `underlyingToShares`, this function guarantees no state modifications
* @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares
* @return The amount of shares corresponding to the input `amountUnderlying`. This is used as deposit shares
* in the `StrategyManager` contract.
* @dev Implementation for these functions in particular may vary significantly for different strategies
*/
function underlyingToSharesView(
uint256 amountUnderlying
) external view returns (uint256);
/**
* @notice convenience function for fetching the current underlying value of all of the `user`'s shares in
* this strategy. In contrast to `userUnderlying`, this function guarantees no state modifications
*/
function userUnderlyingView(
address user
) external view returns (uint256);
/// @notice The underlying token for shares in this Strategy
function underlyingToken() external view returns (IERC20);
/// @notice The total number of extant shares in this Strategy
function totalShares() external view returns (uint256);
/// @notice Returns either a brief string explaining the strategy's goal & purpose, or a link to metadata that explains in more detail.
function explanation() external view returns (string memory);
}
````
## File: src/contracts/interfaces/IStrategyFactory.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/proxy/beacon/IBeacon.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./IStrategy.sol";
import "./ISemVerMixin.sol";
/**
* @title Interface for the `StrategyFactory` contract.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @dev This may not be compatible with non-standard ERC20 tokens. Caution is warranted.
*/
interface IStrategyFactory is ISemVerMixin {
/// @dev Thrown when attempting to deploy a strategy for a blacklisted token.
error BlacklistedToken();
/// @dev Thrown when attempting to deploy a strategy that already exists.
error StrategyAlreadyExists();
/// @dev Thrown when attempting to blacklist a token that is already blacklisted
error AlreadyBlacklisted();
event TokenBlacklisted(IERC20 token);
/// @notice Upgradeable beacon which new Strategies deployed by this contract point to
function strategyBeacon() external view returns (IBeacon);
/// @notice Mapping token => Strategy contract for the token
/// The strategies in this mapping are deployed by the StrategyFactory.
/// The factory can only deploy a single strategy per token address
/// These strategies MIGHT not be whitelisted in the StrategyManager,
/// though deployNewStrategy does whitelist by default.
/// These strategies MIGHT not be the only strategy for the underlying token
/// as additional strategies can be whitelisted by the owner of the factory.
function deployedStrategies(
IERC20 token
) external view returns (IStrategy);
/**
* @notice Deploy a new strategyBeacon contract for the ERC20 token.
* @param token the token to deploy a strategy for
* @dev A strategy contract must not yet exist for the token.
* $dev Immense caution is warranted for non-standard ERC20 tokens, particularly "reentrant" tokens
* like those that conform to ERC777.
*/
function deployNewStrategy(
IERC20 token
) external returns (IStrategy newStrategy);
/**
* @notice Owner-only function to pass through a call to `StrategyManager.addStrategiesToDepositWhitelist`
*/
function whitelistStrategies(
IStrategy[] calldata strategiesToWhitelist
) external;
/**
* @notice Owner-only function to pass through a call to `StrategyManager.removeStrategiesFromDepositWhitelist`
*/
function removeStrategiesFromWhitelist(
IStrategy[] calldata strategiesToRemoveFromWhitelist
) external;
/// @notice Emitted when the `strategyBeacon` is changed
event StrategyBeaconModified(IBeacon previousBeacon, IBeacon newBeacon);
/// @notice Emitted whenever a slot is set in the `tokenStrategy` mapping
event StrategySetForToken(IERC20 token, IStrategy strategy);
}
````
## File: src/contracts/interfaces/IStrategyManager.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;
import "./IStrategy.sol";
import "./IShareManager.sol";
import "./IDelegationManager.sol";
import "./IEigenPodManager.sol";
import "./ISemVerMixin.sol";
interface IStrategyManagerErrors {
/// @dev Thrown when total strategies deployed exceeds max.
error MaxStrategiesExceeded();
/// @dev Thrown when call attempted from address that's not delegation manager.
error OnlyDelegationManager();
/// @dev Thrown when call attempted from address that's not strategy whitelister.
error OnlyStrategyWhitelister();
/// @dev Thrown when provided `shares` amount is too high.
error SharesAmountTooHigh();
/// @dev Thrown when provided `shares` amount is zero.
error SharesAmountZero();
/// @dev Thrown when provided `staker` address is null.
error StakerAddressZero();
/// @dev Thrown when provided `strategy` not found.
error StrategyNotFound();
/// @dev Thrown when attempting to deposit to a non-whitelisted strategy.
error StrategyNotWhitelisted();
}
interface IStrategyManagerEvents {
/**
* @notice Emitted when a new deposit occurs on behalf of `staker`.
* @param staker Is the staker who is depositing funds into EigenLayer.
* @param strategy Is the strategy that `staker` has deposited into.
* @param shares Is the number of new shares `staker` has been granted in `strategy`.
*/
event Deposit(address staker, IStrategy strategy, uint256 shares);
/// @notice Emitted when the `strategyWhitelister` is changed
event StrategyWhitelisterChanged(address previousAddress, address newAddress);
/// @notice Emitted when a strategy is added to the approved list of strategies for deposit
event StrategyAddedToDepositWhitelist(IStrategy strategy);
/// @notice Emitted when a strategy is removed from the approved list of strategies for deposit
event StrategyRemovedFromDepositWhitelist(IStrategy strategy);
/// @notice Emitted when an operator is slashed and shares to be burned are increased
event BurnableSharesIncreased(IStrategy strategy, uint256 shares);
/// @notice Emitted when shares are burned
event BurnableSharesDecreased(IStrategy strategy, uint256 shares);
}
/**
* @title Interface for the primary entrypoint for funds into EigenLayer.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice See the `StrategyManager` contract itself for implementation details.
*/
interface IStrategyManager is IStrategyManagerErrors, IStrategyManagerEvents, IShareManager, ISemVerMixin {
/**
* @notice Initializes the strategy manager contract. Sets the `pauserRegistry` (currently **not** modifiable after being set),
* and transfers contract ownership to the specified `initialOwner`.
* @param initialOwner Ownership of this contract is transferred to this address.
* @param initialStrategyWhitelister The initial value of `strategyWhitelister` to set.
* @param initialPausedStatus The initial value of `_paused` to set.
*/
function initialize(
address initialOwner,
address initialStrategyWhitelister,
uint256 initialPausedStatus
) external;
/**
* @notice Deposits `amount` of `token` into the specified `strategy` and credits shares to the caller
* @param strategy the strategy that handles `token`
* @param token the token from which the `amount` will be transferred
* @param amount the number of tokens to deposit
* @return depositShares the number of deposit shares credited to the caller
* @dev The caller must have previously approved this contract to transfer at least `amount` of `token` on their behalf.
*
* WARNING: Be extremely cautious when depositing tokens that do not strictly adhere to ERC20 standards.
* Tokens that diverge significantly from ERC20 norms can cause unexpected behavior in token balances for
* that strategy, e.g. ERC-777 tokens allowing cross-contract reentrancy.
*/
function depositIntoStrategy(
IStrategy strategy,
IERC20 token,
uint256 amount
) external returns (uint256 depositShares);
/**
* @notice Deposits `amount` of `token` into the specified `strategy` and credits shares to the `staker`
* Note tokens are transferred from `msg.sender`, NOT from `staker`. This method allows the caller, using a
* signature, to deposit their tokens to another staker's balance.
* @param strategy the strategy that handles `token`
* @param token the token from which the `amount` will be transferred
* @param amount the number of tokens to transfer from the caller to the strategy
* @param staker the staker that the deposited assets will be credited to
* @param expiry the timestamp at which the signature expires
* @param signature a valid ECDSA or EIP-1271 signature from `staker`
* @return depositShares the number of deposit shares credited to `staker`
* @dev The caller must have previously approved this contract to transfer at least `amount` of `token` on their behalf.
*
* WARNING: Be extremely cautious when depositing tokens that do not strictly adhere to ERC20 standards.
* Tokens that diverge significantly from ERC20 norms can cause unexpected behavior in token balances for
* that strategy, e.g. ERC-777 tokens allowing cross-contract reentrancy.
*/
function depositIntoStrategyWithSignature(
IStrategy strategy,
IERC20 token,
uint256 amount,
address staker,
uint256 expiry,
bytes memory signature
) external returns (uint256 depositShares);
/**
* @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.
*/
function burnShares(
IStrategy strategy
) external;
/**
* @notice Owner-only function to change the `strategyWhitelister` address.
* @param newStrategyWhitelister new address for the `strategyWhitelister`.
*/
function setStrategyWhitelister(
address newStrategyWhitelister
) external;
/**
* @notice Owner-only function that adds the provided Strategies to the 'whitelist' of strategies that stakers can deposit into
* @param strategiesToWhitelist Strategies that will be added to the `strategyIsWhitelistedForDeposit` mapping (if they aren't in it already)
*/
function addStrategiesToDepositWhitelist(
IStrategy[] calldata strategiesToWhitelist
) external;
/**
* @notice Owner-only function that removes the provided Strategies from the 'whitelist' of strategies that stakers can deposit into
* @param strategiesToRemoveFromWhitelist Strategies that will be removed to the `strategyIsWhitelistedForDeposit` mapping (if they are in it)
*/
function removeStrategiesFromDepositWhitelist(
IStrategy[] calldata strategiesToRemoveFromWhitelist
) external;
/// @notice Returns bool for whether or not `strategy` is whitelisted for deposit
function strategyIsWhitelistedForDeposit(
IStrategy strategy
) external view returns (bool);
/**
* @notice Get all details on the staker's deposits and corresponding shares
* @return (staker's strategies, shares in these strategies)
*/
function getDeposits(
address staker
) external view returns (IStrategy[] memory, uint256[] memory);
function getStakerStrategyList(
address staker
) external view returns (IStrategy[] memory);
/// @notice Simple getter function that returns `stakerStrategyList[staker].length`.
function stakerStrategyListLength(
address staker
) external view returns (uint256);
/// @notice Returns the current shares of `user` in `strategy`
function stakerDepositShares(address user, IStrategy strategy) external view returns (uint256 shares);
/// @notice Returns the single, central Delegation contract of EigenLayer
function delegation() external view returns (IDelegationManager);
/// @notice Returns the address of the `strategyWhitelister`
function strategyWhitelister() external view returns (address);
/// @notice Returns the burnable shares of a strategy
function getBurnableShares(
IStrategy strategy
) external view returns (uint256);
/**
* @notice Gets every strategy with burnable shares and the amount of burnable shares in each said strategy
*
* WARNING: This operation can copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Users should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function getStrategiesWithBurnableShares() external view returns (address[] memory, uint256[] memory);
/**
* @param staker The address of the staker.
* @param strategy The strategy to deposit into.
* @param token The token to deposit.
* @param amount The amount of `token` to deposit.
* @param nonce The nonce of the staker.
* @param expiry The expiry of the signature.
* @return The EIP-712 signable digest hash.
*/
function calculateStrategyDepositDigestHash(
address staker,
IStrategy strategy,
IERC20 token,
uint256 amount,
uint256 nonce,
uint256 expiry
) external view returns (bytes32);
}
````
## File: src/contracts/libraries/BeaconChainProofs.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import "./Merkle.sol";
import "../libraries/Endian.sol";
//Utility library for parsing and PHASE0 beacon chain block headers
//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization
//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader
//BeaconState Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate
library BeaconChainProofs {
/// @dev Thrown when a proof is invalid.
error InvalidProof();
/// @dev Thrown when a proof with an invalid length is provided.
error InvalidProofLength();
/// @dev Thrown when a validator fields length is invalid.
error InvalidValidatorFieldsLength();
/// @notice Heights of various merkle trees in the beacon chain
/// - beaconBlockRoot
/// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT
/// -- beaconStateRoot
/// | HEIGHT: BEACON_STATE_TREE_HEIGHT
/// validatorContainerRoot, balanceContainerRoot
/// | | HEIGHT: BALANCE_TREE_HEIGHT
/// | individual balances
/// | HEIGHT: VALIDATOR_TREE_HEIGHT
/// individual validators
uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3;
uint256 internal constant DENEB_BEACON_STATE_TREE_HEIGHT = 5;
uint256 internal constant PECTRA_BEACON_STATE_TREE_HEIGHT = 6;
uint256 internal constant BALANCE_TREE_HEIGHT = 38;
uint256 internal constant VALIDATOR_TREE_HEIGHT = 40;
/// @notice Index of the beaconStateRoot in the `BeaconBlockHeader` container
///
/// BeaconBlockHeader = [..., state_root, ...]
/// 0... 3
///
/// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader)
uint256 internal constant STATE_ROOT_INDEX = 3;
/// @notice Indices for fields in the `BeaconState` container
///
/// BeaconState = [..., validators, balances, ...]
/// 0... 11 12
///
/// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate)
uint256 internal constant VALIDATOR_CONTAINER_INDEX = 11;
uint256 internal constant BALANCE_CONTAINER_INDEX = 12;
/// @notice Number of fields in the `Validator` container
/// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator)
uint256 internal constant VALIDATOR_FIELDS_LENGTH = 8;
/// @notice Indices for fields in the `Validator` container
uint256 internal constant VALIDATOR_PUBKEY_INDEX = 0;
uint256 internal constant VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1;
uint256 internal constant VALIDATOR_BALANCE_INDEX = 2;
uint256 internal constant VALIDATOR_SLASHED_INDEX = 3;
uint256 internal constant VALIDATOR_ACTIVATION_EPOCH_INDEX = 5;
uint256 internal constant VALIDATOR_EXIT_EPOCH_INDEX = 6;
/// @notice Slot/Epoch timings
uint64 internal constant SECONDS_PER_SLOT = 12;
uint64 internal constant SLOTS_PER_EPOCH = 32;
uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT;
/// @notice `FAR_FUTURE_EPOCH` is used as the default value for certain `Validator`
/// fields when a `Validator` is first created on the beacon chain
uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max;
bytes8 internal constant UINT64_MASK = 0xffffffffffffffff;
/// @notice The beacon chain version to validate against
enum ProofVersion {
DENEB,
PECTRA
}
/// @notice Contains a beacon state root and a merkle proof verifying its inclusion under a beacon block root
struct StateRootProof {
bytes32 beaconStateRoot;
bytes proof;
}
/// @notice Contains a validator's fields and a merkle proof of their inclusion under a beacon state root
struct ValidatorProof {
bytes32[] validatorFields;
bytes proof;
}
/// @notice Contains a beacon balance container root and a proof of this root under a beacon block root
struct BalanceContainerProof {
bytes32 balanceContainerRoot;
bytes proof;
}
/// @notice Contains a validator balance root and a proof of its inclusion under a balance container root
struct BalanceProof {
bytes32 pubkeyHash;
bytes32 balanceRoot;
bytes proof;
}
/**
*
* VALIDATOR FIELDS -> BEACON STATE ROOT -> BEACON BLOCK ROOT
*
*/
/// @notice Verify a merkle proof of the beacon state root against a beacon block root
/// @param beaconBlockRoot merkle root of the beacon block
/// @param proof the beacon state root and merkle proof of its inclusion under `beaconBlockRoot`
function verifyStateRoot(bytes32 beaconBlockRoot, StateRootProof calldata proof) internal view {
require(proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT), InvalidProofLength());
/// This merkle proof verifies the `beaconStateRoot` under the `beaconBlockRoot`
/// - beaconBlockRoot
/// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT
/// -- beaconStateRoot
require(
Merkle.verifyInclusionSha256({
proof: proof.proof,
root: beaconBlockRoot,
leaf: proof.beaconStateRoot,
index: STATE_ROOT_INDEX
}),
InvalidProof()
);
}
/// @notice Verify a merkle proof of a validator container against a `beaconStateRoot`
/// @dev This proof starts at a validator's container root, proves through the validator container root,
/// and continues proving to the root of the `BeaconState`
/// @dev See https://eth2book.info/capella/part3/containers/dependencies/#validator for info on `Validator` containers
/// @dev See https://eth2book.info/capella/part3/containers/state/#beaconstate for info on `BeaconState` containers
/// @param beaconStateRoot merkle root of the `BeaconState` container
/// @param validatorFields an individual validator's fields. These are merklized to form a `validatorRoot`,
/// which is used as the leaf to prove against `beaconStateRoot`
/// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot`
/// @param validatorIndex the validator's unique index
function verifyValidatorFields(
ProofVersion proofVersion,
bytes32 beaconStateRoot,
bytes32[] calldata validatorFields,
bytes calldata validatorFieldsProof,
uint40 validatorIndex
) internal view {
require(validatorFields.length == VALIDATOR_FIELDS_LENGTH, InvalidValidatorFieldsLength());
uint256 beaconStateTreeHeight = getBeaconStateTreeHeight(proofVersion);
/// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for
/// this container includes hashing the root of the validator tree with the length of the validator list
require(
validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + beaconStateTreeHeight),
InvalidProofLength()
);
// Merkleize `validatorFields` to get the leaf to prove
bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields);
/// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees:
/// - beaconStateRoot
/// | HEIGHT: BEACON_STATE_TREE_HEIGHT
/// -- validatorContainerRoot
/// | HEIGHT: VALIDATOR_TREE_HEIGHT + 1
/// ---- validatorRoot
uint256 index = (VALIDATOR_CONTAINER_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex);
require(
Merkle.verifyInclusionSha256({
proof: validatorFieldsProof,
root: beaconStateRoot,
leaf: validatorRoot,
index: index
}),
InvalidProof()
);
}
/**
*
* VALIDATOR BALANCE -> BALANCE CONTAINER ROOT -> BEACON BLOCK ROOT
*
*/
/// @notice Verify a merkle proof of the beacon state's balances container against the beacon block root
/// @dev This proof starts at the balance container root, proves through the beacon state root, and
/// continues proving through the beacon block root. As a result, this proof will contain elements
/// of a `StateRootProof` under the same block root, with the addition of proving the balances field
/// within the beacon state.
/// @dev This is used to make checkpoint proofs more efficient, as a checkpoint will verify multiple balances
/// against the same balance container root.
/// @param beaconBlockRoot merkle root of the beacon block
/// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot`
function verifyBalanceContainer(
ProofVersion proofVersion,
bytes32 beaconBlockRoot,
BalanceContainerProof calldata proof
) internal view {
uint256 beaconStateTreeHeight = getBeaconStateTreeHeight(proofVersion);
require(
proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + beaconStateTreeHeight), InvalidProofLength()
);
/// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees:
/// - beaconBlockRoot
/// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT
/// -- beaconStateRoot
/// | HEIGHT: BEACON_STATE_TREE_HEIGHT
/// ---- balancesContainerRoot
uint256 index = (STATE_ROOT_INDEX << (beaconStateTreeHeight)) | BALANCE_CONTAINER_INDEX;
require(
Merkle.verifyInclusionSha256({
proof: proof.proof,
root: beaconBlockRoot,
leaf: proof.balanceContainerRoot,
index: index
}),
InvalidProof()
);
}
/// @notice Verify a merkle proof of a validator's balance against the beacon state's `balanceContainerRoot`
/// @param balanceContainerRoot the merkle root of all validators' current balances
/// @param validatorIndex the index of the validator whose balance we are proving
/// @param proof the validator's associated balance root and a merkle proof of inclusion under `balanceContainerRoot`
/// @return validatorBalanceGwei the validator's current balance (in gwei)
function verifyValidatorBalance(
bytes32 balanceContainerRoot,
uint40 validatorIndex,
BalanceProof calldata proof
) internal view returns (uint64 validatorBalanceGwei) {
/// Note: the reason we use `BALANCE_TREE_HEIGHT + 1` here is because the merklization process for
/// this container includes hashing the root of the balances tree with the length of the balances list
require(proof.proof.length == 32 * (BALANCE_TREE_HEIGHT + 1), InvalidProofLength());
/// When merkleized, beacon chain balances are combined into groups of 4 called a `balanceRoot`. The merkle
/// proof here verifies that this validator's `balanceRoot` is included in the `balanceContainerRoot`
/// - balanceContainerRoot
/// | HEIGHT: BALANCE_TREE_HEIGHT
/// -- balanceRoot
uint256 balanceIndex = uint256(validatorIndex / 4);
require(
Merkle.verifyInclusionSha256({
proof: proof.proof,
root: balanceContainerRoot,
leaf: proof.balanceRoot,
index: balanceIndex
}),
InvalidProof()
);
/// Extract the individual validator's balance from the `balanceRoot`
return getBalanceAtIndex(proof.balanceRoot, validatorIndex);
}
/**
* @notice Parses a balanceRoot to get the uint64 balance of a validator.
* @dev During merkleization of the beacon state balance tree, four uint64 values are treated as a single
* leaf in the merkle tree. We use validatorIndex % 4 to determine which of the four uint64 values to
* extract from the balanceRoot.
* @param balanceRoot is the combination of 4 validator balances being proven for
* @param validatorIndex is the index of the validator being proven for
* @return The validator's balance, in Gwei
*/
function getBalanceAtIndex(bytes32 balanceRoot, uint40 validatorIndex) internal pure returns (uint64) {
uint256 bitShiftAmount = (validatorIndex % 4) * 64;
return Endian.fromLittleEndianUint64(bytes32((uint256(balanceRoot) << bitShiftAmount)));
}
/// @notice Indices for fields in the `Validator` container:
/// 0: pubkey
/// 1: withdrawal credentials
/// 2: effective balance
/// 3: slashed?
/// 4: activation eligibility epoch
/// 5: activation epoch
/// 6: exit epoch
/// 7: withdrawable epoch
///
/// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator)
/// @dev Retrieves a validator's pubkey hash
function getPubkeyHash(
bytes32[] memory validatorFields
) internal pure returns (bytes32) {
return validatorFields[VALIDATOR_PUBKEY_INDEX];
}
/// @dev Retrieves a validator's withdrawal credentials
function getWithdrawalCredentials(
bytes32[] memory validatorFields
) internal pure returns (bytes32) {
return validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX];
}
/// @dev Retrieves a validator's effective balance (in gwei)
function getEffectiveBalanceGwei(
bytes32[] memory validatorFields
) internal pure returns (uint64) {
return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]);
}
/// @dev Retrieves a validator's activation epoch
function getActivationEpoch(
bytes32[] memory validatorFields
) internal pure returns (uint64) {
return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_ACTIVATION_EPOCH_INDEX]);
}
/// @dev Retrieves true IFF a validator is marked slashed
function isValidatorSlashed(
bytes32[] memory validatorFields
) internal pure returns (bool) {
return validatorFields[VALIDATOR_SLASHED_INDEX] != 0;
}
/// @dev Retrieves a validator's exit epoch
function getExitEpoch(
bytes32[] memory validatorFields
) internal pure returns (uint64) {
return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]);
}
/// @dev We check if the proofTimestamp is <= pectraForkTimestamp because a `proofTimestamp` at the `pectraForkTimestamp`
/// is considered to be Pre-Pectra given the EIP-4788 oracle returns the parent block.
function getBeaconStateTreeHeight(
ProofVersion proofVersion
) internal pure returns (uint256) {
return proofVersion == ProofVersion.DENEB ? DENEB_BEACON_STATE_TREE_HEIGHT : PECTRA_BEACON_STATE_TREE_HEIGHT;
}
}
````
## File: src/contracts/libraries/BytesLib.sol
````
// SPDX-License-Identifier: Unlicense
/*
* @title Solidity Bytes Arrays Utils
* @author Gonçalo Sá <goncalo.sa@consensys.net>
*
* @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity.
* The library lets you concatenate, slice and type cast bytes arrays both in memory and storage.
*/
pragma solidity >=0.8.0 <0.9.0;
library BytesLib {
error Overflow();
error OutOfBounds();
function concat(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bytes memory) {
bytes memory tempBytes;
assembly {
// Get a location of some free memory and store it in tempBytes as
// Solidity does for memory variables.
tempBytes := mload(0x40)
// Store the length of the first bytes array at the beginning of
// the memory for tempBytes.
let length := mload(_preBytes)
mstore(tempBytes, length)
// Maintain a memory counter for the current write location in the
// temp bytes array by adding the 32 bytes for the array length to
// the starting location.
let mc := add(tempBytes, 0x20)
// Stop copying when the memory counter reaches the length of the
// first bytes array.
let end := add(mc, length)
for {
// Initialize a copy counter to the start of the _preBytes data,
// 32 bytes into its memory.
let cc := add(_preBytes, 0x20)
} lt(mc, end) {
// Increase both counters by 32 bytes each iteration.
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
// Write the _preBytes data into the tempBytes memory 32 bytes
// at a time.
mstore(mc, mload(cc))
}
// Add the length of _postBytes to the current length of tempBytes
// and store it as the new length in the first 32 bytes of the
// tempBytes memory.
length := mload(_postBytes)
mstore(tempBytes, add(length, mload(tempBytes)))
// Move the memory counter back from a multiple of 0x20 to the
// actual end of the _preBytes data.
mc := end
// Stop copying when the memory counter reaches the new combined
// length of the arrays.
end := add(mc, length)
for { let cc := add(_postBytes, 0x20) } lt(mc, end) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} { mstore(mc, mload(cc)) }
// Update the free-memory pointer by padding our last write location
// to 32 bytes: add 31 bytes to the end of tempBytes to move to the
// next 32 byte block, then round down to the nearest multiple of
// 32. If the sum of the length of the two arrays is zero then add
// one before rounding down to leave a blank 32 bytes (the length block with 0).
mstore(
0x40,
and(
add(add(end, iszero(add(length, mload(_preBytes)))), 31),
not(31) // Round down to the nearest 32 bytes.
)
)
}
return tempBytes;
}
function concatStorage(bytes storage _preBytes, bytes memory _postBytes) internal {
assembly {
// Read the first 32 bytes of _preBytes storage, which is the length
// of the array. (We don't need to use the offset into the slot
// because arrays use the entire slot.)
let fslot := sload(_preBytes.slot)
// Arrays of 31 bytes or less have an even value in their slot,
// while longer arrays have an odd value. The actual length is
// the slot divided by two for odd values, and the lowest order
// byte divided by two for even values.
// If the slot is even, bitwise and the slot with 255 and divide by
// two to get the length. If the slot is odd, bitwise and the slot
// with -1 and divide by two.
let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2)
let mlength := mload(_postBytes)
let newlength := add(slength, mlength)
// slength can contain both the length and contents of the array
// if length < 32 bytes so let's prepare for that
// v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage
switch add(lt(slength, 32), lt(newlength, 32))
case 2 {
// Since the new array still fits in the slot, we just need to
// update the contents of the slot.
// uint256(bytes_storage) = uint256(bytes_storage) + uint256(bytes_memory) + new_length
sstore(
_preBytes.slot,
// all the modifications to the slot are inside this
// next block
add(
// we can just add to the slot contents because the
// bytes we want to change are the LSBs
fslot,
add(
mul(
div(
// load the bytes from memory
mload(add(_postBytes, 0x20)),
// zero all bytes to the right
exp(0x100, sub(32, mlength))
),
// and now shift left the number of bytes to
// leave space for the length in the slot
exp(0x100, sub(32, newlength))
),
// increase length by the double of the memory
// bytes length
mul(mlength, 2)
)
)
)
}
case 1 {
// The stored value fits in the slot, but the combined value
// will exceed it.
// get the keccak hash to get the contents of the array
mstore(0x0, _preBytes.slot)
let sc := add(keccak256(0x0, 0x20), div(slength, 32))
// save new length
sstore(_preBytes.slot, add(mul(newlength, 2), 1))
// The contents of the _postBytes array start 32 bytes into
// the structure. Our first read should obtain the `submod`
// bytes that can fit into the unused space in the last word
// of the stored array. To get this, we read 32 bytes starting
// from `submod`, so the data we read overlaps with the array
// contents by `submod` bytes. Masking the lowest-order
// `submod` bytes allows us to add that value directly to the
// stored value.
let submod := sub(32, slength)
let mc := add(_postBytes, submod)
let end := add(_postBytes, mlength)
let mask := sub(exp(0x100, submod), 1)
sstore(
sc,
add(
and(fslot, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00),
and(mload(mc), mask)
)
)
for {
mc := add(mc, 0x20)
sc := add(sc, 1)
} lt(mc, end) {
sc := add(sc, 1)
mc := add(mc, 0x20)
} { sstore(sc, mload(mc)) }
mask := exp(0x100, sub(mc, end))
sstore(sc, mul(div(mload(mc), mask), mask))
}
default {
// get the keccak hash to get the contents of the array
mstore(0x0, _preBytes.slot)
// Start copying to the last used word of the stored array.
let sc := add(keccak256(0x0, 0x20), div(slength, 32))
// save new length
sstore(_preBytes.slot, add(mul(newlength, 2), 1))
// Copy over the first `submod` bytes of the new data as in
// case 1 above.
let slengthmod := mod(slength, 32)
// solhint-disable-next-line no-unused-vars
let mlengthmod := mod(mlength, 32)
let submod := sub(32, slengthmod)
let mc := add(_postBytes, submod)
let end := add(_postBytes, mlength)
let mask := sub(exp(0x100, submod), 1)
sstore(sc, add(sload(sc), and(mload(mc), mask)))
for {
sc := add(sc, 1)
mc := add(mc, 0x20)
} lt(mc, end) {
sc := add(sc, 1)
mc := add(mc, 0x20)
} { sstore(sc, mload(mc)) }
mask := exp(0x100, sub(mc, end))
sstore(sc, mul(div(mload(mc), mask), mask))
}
}
}
function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory) {
require(_length + 31 >= _length, Overflow());
require(_bytes.length >= _start + _length, OutOfBounds());
bytes memory tempBytes;
assembly {
switch iszero(_length)
case 0 {
// Get a location of some free memory and store it in tempBytes as
// Solidity does for memory variables.
tempBytes := mload(0x40)
// The first word of the slice result is potentially a partial
// word read from the original array. To read it, we calculate
// the length of that partial word and start copying that many
// bytes into the array. The first word we copy will start with
// data we don't care about, but the last `lengthmod` bytes will
// land at the beginning of the contents of the new array. When
// we're done copying, we overwrite the full first word with
// the actual length of the slice.
let lengthmod := and(_length, 31)
// The multiplication in the next line is necessary
// because when slicing multiples of 32 bytes (lengthmod == 0)
// the following copy loop was copying the origin's length
// and then ending prematurely not copying everything it should.
let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod)))
let end := add(mc, _length)
for {
// The multiplication in the next line has the same exact purpose
// as the one above.
let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start)
} lt(mc, end) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} { mstore(mc, mload(cc)) }
mstore(tempBytes, _length)
//update free-memory pointer
//allocating the array padded to 32 bytes like the compiler does now
mstore(0x40, and(add(mc, 31), not(31)))
}
//if we want a zero-length slice let's just return a zero-length array
default {
tempBytes := mload(0x40)
//zero out the 32 bytes slice we are about to return
//we need to do it because Solidity does not garbage collect
mstore(tempBytes, 0)
mstore(0x40, add(tempBytes, 0x20))
}
}
return tempBytes;
}
function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) {
require(_bytes.length >= _start + 20, OutOfBounds());
address tempAddress;
assembly {
tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000)
}
return tempAddress;
}
function toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) {
require(_bytes.length >= _start + 1, OutOfBounds());
uint8 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x1), _start))
}
return tempUint;
}
function toUint16(bytes memory _bytes, uint256 _start) internal pure returns (uint16) {
require(_bytes.length >= _start + 2, OutOfBounds());
uint16 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x2), _start))
}
return tempUint;
}
function toUint32(bytes memory _bytes, uint256 _start) internal pure returns (uint32) {
require(_bytes.length >= _start + 4, OutOfBounds());
uint32 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x4), _start))
}
return tempUint;
}
function toUint64(bytes memory _bytes, uint256 _start) internal pure returns (uint64) {
require(_bytes.length >= _start + 8, OutOfBounds());
uint64 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x8), _start))
}
return tempUint;
}
function toUint96(bytes memory _bytes, uint256 _start) internal pure returns (uint96) {
require(_bytes.length >= _start + 12, OutOfBounds());
uint96 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0xc), _start))
}
return tempUint;
}
function toUint128(bytes memory _bytes, uint256 _start) internal pure returns (uint128) {
require(_bytes.length >= _start + 16, OutOfBounds());
uint128 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x10), _start))
}
return tempUint;
}
function toUint256(bytes memory _bytes, uint256 _start) internal pure returns (uint256) {
require(_bytes.length >= _start + 32, OutOfBounds());
uint256 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x20), _start))
}
return tempUint;
}
function toBytes32(bytes memory _bytes, uint256 _start) internal pure returns (bytes32) {
require(_bytes.length >= _start + 32, OutOfBounds());
bytes32 tempBytes32;
assembly {
tempBytes32 := mload(add(add(_bytes, 0x20), _start))
}
return tempBytes32;
}
function equal(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bool) {
bool success = true;
assembly {
let length := mload(_preBytes)
// if lengths don't match the arrays are not equal
switch eq(length, mload(_postBytes))
case 1 {
// cb is a circuit breaker in the for loop since there's
// no said feature for inline assembly loops
// cb = 1 - don't breaker
// cb = 0 - break
let cb := 1
let mc := add(_preBytes, 0x20)
let end := add(mc, length)
for { let cc := add(_postBytes, 0x20) } // while(uint256(mc < end) + cb == 2) // the next line is the loop condition:
eq(add(lt(mc, end), cb), 2) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
// if any of these checks fails then arrays are not equal
if iszero(eq(mload(mc), mload(cc))) {
// unsuccessful:
success := 0
cb := 0
}
}
}
default {
// unsuccessful:
success := 0
}
}
return success;
}
function equalStorage(bytes storage _preBytes, bytes memory _postBytes) internal view returns (bool) {
bool success = true;
assembly {
// we know _preBytes_offset is 0
let fslot := sload(_preBytes.slot)
// Decode the length of the stored array like in concatStorage().
let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2)
let mlength := mload(_postBytes)
// if lengths don't match the arrays are not equal
switch eq(slength, mlength)
case 1 {
// slength can contain both the length and contents of the array
// if length < 32 bytes so let's prepare for that
// v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage
if iszero(iszero(slength)) {
switch lt(slength, 32)
case 1 {
// blank the last byte which is the length
fslot := mul(div(fslot, 0x100), 0x100)
if iszero(eq(fslot, mload(add(_postBytes, 0x20)))) {
// unsuccessful:
success := 0
}
}
default {
// cb is a circuit breaker in the for loop since there's
// no said feature for inline assembly loops
// cb = 1 - don't breaker
// cb = 0 - break
let cb := 1
// get the keccak hash to get the contents of the array
mstore(0x0, _preBytes.slot)
let sc := keccak256(0x0, 0x20)
let mc := add(_postBytes, 0x20)
let end := add(mc, mlength)
// the next line is the loop condition:
// while(uint256(mc < end) + cb == 2)
// solhint-disable-next-line no-empty-blocks
for {} eq(add(lt(mc, end), cb), 2) {
sc := add(sc, 1)
mc := add(mc, 0x20)
} {
if iszero(eq(sload(sc), mload(mc))) {
// unsuccessful:
success := 0
cb := 0
}
}
}
}
}
default {
// unsuccessful:
success := 0
}
}
return success;
}
}
````
## File: src/contracts/libraries/Endian.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
library Endian {
/**
* @notice Converts a little endian-formatted uint64 to a big endian-formatted uint64
* @param lenum little endian-formatted uint64 input, provided as 'bytes32' type
* @return n The big endian-formatted uint64
* @dev Note that the input is formatted as a 'bytes32' type (i.e. 256 bits), but it is immediately truncated to a uint64 (i.e. 64 bits)
* through a right-shift/shr operation.
*/
function fromLittleEndianUint64(
bytes32 lenum
) internal pure returns (uint64 n) {
// the number needs to be stored in little-endian encoding (ie in bytes 0-8)
n = uint64(uint256(lenum >> 192));
// forgefmt: disable-next-item
return (n >> 56) |
((0x00FF000000000000 & n) >> 40) |
((0x0000FF0000000000 & n) >> 24) |
((0x000000FF00000000 & n) >> 8) |
((0x00000000FF000000 & n) << 8) |
((0x0000000000FF0000 & n) << 24) |
((0x000000000000FF00 & n) << 40) |
((0x00000000000000FF & n) << 56);
}
}
````
## File: src/contracts/libraries/Merkle.sol
````
// SPDX-License-Identifier: MIT
// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol)
pragma solidity ^0.8.0;
/**
* @dev These functions deal with verification of Merkle Tree proofs.
*
* The tree and the proofs can be generated using our
* https://github.com/OpenZeppelin/merkle-tree[JavaScript library].
* You will find a quickstart guide in the readme.
*
* WARNING: You should avoid using leaf values that are 64 bytes long prior to
* hashing, or use a hash function other than keccak256 for hashing leaves.
* This is because the concatenation of a sorted pair of internal nodes in
* the merkle tree could be reinterpreted as a leaf value.
* OpenZeppelin's JavaScript library generates merkle trees that are safe
* against this attack out of the box.
*/
library Merkle {
error InvalidProofLength();
/**
* @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
* hash matches the root of the tree. The tree is built assuming `leaf` is
* the 0 indexed `index`'th leaf from the bottom left of the tree.
*
* Note this is for a Merkle tree using the keccak/sha3 hash function
*/
function verifyInclusionKeccak(
bytes memory proof,
bytes32 root,
bytes32 leaf,
uint256 index
) internal pure returns (bool) {
return processInclusionProofKeccak(proof, leaf, index) == root;
}
/**
* @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
* hash matches the root of the tree. The tree is built assuming `leaf` is
* the 0 indexed `index`'th leaf from the bottom left of the tree.
* @dev If the proof length is 0 then the leaf hash is returned.
*
* _Available since v4.4._
*
* Note this is for a Merkle tree using the keccak/sha3 hash function
*/
function processInclusionProofKeccak(
bytes memory proof,
bytes32 leaf,
uint256 index
) internal pure returns (bytes32) {
require(proof.length % 32 == 0, InvalidProofLength());
bytes32 computedHash = leaf;
for (uint256 i = 32; i <= proof.length; i += 32) {
if (index % 2 == 0) {
// if ith bit of index is 0, then computedHash is a left sibling
assembly {
mstore(0x00, computedHash)
mstore(0x20, mload(add(proof, i)))
computedHash := keccak256(0x00, 0x40)
index := div(index, 2)
}
} else {
// if ith bit of index is 1, then computedHash is a right sibling
assembly {
mstore(0x00, mload(add(proof, i)))
mstore(0x20, computedHash)
computedHash := keccak256(0x00, 0x40)
index := div(index, 2)
}
}
}
return computedHash;
}
/**
* @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
* hash matches the root of the tree. The tree is built assuming `leaf` is
* the 0 indexed `index`'th leaf from the bottom left of the tree.
*
* Note this is for a Merkle tree using the sha256 hash function
*/
function verifyInclusionSha256(
bytes memory proof,
bytes32 root,
bytes32 leaf,
uint256 index
) internal view returns (bool) {
return processInclusionProofSha256(proof, leaf, index) == root;
}
/**
* @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
* hash matches the root of the tree. The tree is built assuming `leaf` is
* the 0 indexed `index`'th leaf from the bottom left of the tree.
*
* _Available since v4.4._
*
* Note this is for a Merkle tree using the sha256 hash function
*/
function processInclusionProofSha256(
bytes memory proof,
bytes32 leaf,
uint256 index
) internal view returns (bytes32) {
require(proof.length != 0 && proof.length % 32 == 0, InvalidProofLength());
bytes32[1] memory computedHash = [leaf];
for (uint256 i = 32; i <= proof.length; i += 32) {
if (index % 2 == 0) {
// if ith bit of index is 0, then computedHash is a left sibling
assembly {
mstore(0x00, mload(computedHash))
mstore(0x20, mload(add(proof, i)))
if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) }
index := div(index, 2)
}
} else {
// if ith bit of index is 1, then computedHash is a right sibling
assembly {
mstore(0x00, mload(add(proof, i)))
mstore(0x20, mload(computedHash))
if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) }
index := div(index, 2)
}
}
}
return computedHash[0];
}
/**
* @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function
* @param leaves the leaves of the merkle tree
* @return The computed Merkle root of the tree.
* @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly.
*/
function merkleizeSha256(
bytes32[] memory leaves
) internal pure returns (bytes32) {
//there are half as many nodes in the layer above the leaves
uint256 numNodesInLayer = leaves.length / 2;
//create a layer to store the internal nodes
bytes32[] memory layer = new bytes32[](numNodesInLayer);
//fill the layer with the pairwise hashes of the leaves
for (uint256 i = 0; i < numNodesInLayer; i++) {
layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1]));
}
//the next layer above has half as many nodes
numNodesInLayer /= 2;
//while we haven't computed the root
while (numNodesInLayer != 0) {
//overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children
for (uint256 i = 0; i < numNodesInLayer; i++) {
layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1]));
}
//the next layer above has half as many nodes
numNodesInLayer /= 2;
}
//the first node in the layer is the root
return layer[0];
}
}
````
## File: src/contracts/libraries/OperatorSetLib.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
using OperatorSetLib for OperatorSet global;
/**
* @notice An operator set identified by the AVS address and an identifier
* @param avs The address of the AVS this operator set belongs to
* @param id The unique identifier for the operator set
*/
struct OperatorSet {
address avs;
uint32 id;
}
library OperatorSetLib {
function key(
OperatorSet memory os
) internal pure returns (bytes32) {
return bytes32(abi.encodePacked(os.avs, uint96(os.id)));
}
function decode(
bytes32 _key
) internal pure returns (OperatorSet memory) {
/// forgefmt: disable-next-item
return OperatorSet({
avs: address(uint160(uint256(_key) >> 96)),
id: uint32(uint256(_key) & type(uint96).max)
});
}
}
````
## File: src/contracts/libraries/SlashingLib.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin-upgrades/contracts/utils/math/SafeCastUpgradeable.sol";
/// @dev All scaling factors have `1e18` as an initial/default value. This value is represented
/// by the constant `WAD`, which is used to preserve precision with uint256 math.
///
/// When applying scaling factors, they are typically multiplied/divided by `WAD`, allowing this
/// constant to act as a "1" in mathematical formulae.
uint64 constant WAD = 1e18;
/*
* There are 2 types of shares:
* 1. deposit shares
* - These can be converted to an amount of tokens given a strategy
* - by calling `sharesToUnderlying` on the strategy address (they're already tokens
* in the case of EigenPods)
* - These live in the storage of the EigenPodManager and individual StrategyManager strategies
* 2. withdrawable shares
* - For a staker, this is the amount of shares that they can withdraw
* - For an operator, the shares delegated to them are equal to the sum of their stakers'
* withdrawable shares
*
* Along with a slashing factor, the DepositScalingFactor is used to convert between the two share types.
*/
struct DepositScalingFactor {
uint256 _scalingFactor;
}
using SlashingLib for DepositScalingFactor global;
library SlashingLib {
using Math for uint256;
using SlashingLib for uint256;
using SafeCastUpgradeable for uint256;
// WAD MATH
function mulWad(uint256 x, uint256 y) internal pure returns (uint256) {
return x.mulDiv(y, WAD);
}
function divWad(uint256 x, uint256 y) internal pure returns (uint256) {
return x.mulDiv(WAD, y);
}
/**
* @notice Used explicitly for calculating slashed magnitude, we want to ensure even in the
* situation where an operator is slashed several times and precision has been lost over time,
* an incoming slashing request isn't rounded down to 0 and an operator is able to avoid slashing penalties.
*/
function mulWadRoundUp(uint256 x, uint256 y) internal pure returns (uint256) {
return x.mulDiv(y, WAD, Math.Rounding.Up);
}
// GETTERS
function scalingFactor(
DepositScalingFactor memory dsf
) internal pure returns (uint256) {
return dsf._scalingFactor == 0 ? WAD : dsf._scalingFactor;
}
function scaleForQueueWithdrawal(
DepositScalingFactor memory dsf,
uint256 depositSharesToWithdraw
) internal pure returns (uint256) {
return depositSharesToWithdraw.mulWad(dsf.scalingFactor());
}
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,
uint64 prevMaxMagnitude,
uint64 newMaxMagnitude
) internal pure returns (uint256) {
return scaledShares.mulWad(prevMaxMagnitude - newMaxMagnitude);
}
function update(
DepositScalingFactor storage dsf,
uint256 prevDepositShares,
uint256 addedShares,
uint256 slashingFactor
) internal {
if (prevDepositShares == 0) {
// If this is the staker's first deposit or they are delegating to an operator,
// the slashing factor is inverted and applied to the existing DSF. This has the
// effect of "forgiving" prior slashing for any subsequent deposits.
dsf._scalingFactor = dsf.scalingFactor().divWad(slashingFactor);
return;
}
/**
* Base Equations:
* (1) newShares = currentShares + addedShares
* (2) newDepositShares = prevDepositShares + addedShares
* (3) newShares = newDepositShares * newDepositScalingFactor * slashingFactor
*
* Plugging (1) into (3):
* (4) newDepositShares * newDepositScalingFactor * slashingFactor = currentShares + addedShares
*
* Solving for newDepositScalingFactor
* (5) newDepositScalingFactor = (currentShares + addedShares) / (newDepositShares * slashingFactor)
*
* Plugging in (2) into (5):
* (7) newDepositScalingFactor = (currentShares + addedShares) / ((prevDepositShares + addedShares) * slashingFactor)
* Note that magnitudes must be divided by WAD for precision. Thus,
*
* (8) newDepositScalingFactor = WAD * (currentShares + addedShares) / ((prevDepositShares + addedShares) * slashingFactor / WAD)
* (9) newDepositScalingFactor = (currentShares + addedShares) * WAD / (prevDepositShares + addedShares) * WAD / slashingFactor
*/
// Step 1: Calculate Numerator
uint256 currentShares = dsf.calcWithdrawable(prevDepositShares, slashingFactor);
// Step 2: Compute currentShares + addedShares
uint256 newShares = currentShares + addedShares;
// Step 3: Calculate newDepositScalingFactor
/// forgefmt: disable-next-item
uint256 newDepositScalingFactor = newShares
.divWad(prevDepositShares + addedShares)
.divWad(slashingFactor);
dsf._scalingFactor = newDepositScalingFactor;
}
/// @dev Reset the staker's DSF for a strategy by setting it to 0. This is the same
/// as setting it to WAD (see the `scalingFactor` getter above).
///
/// A DSF is reset when a staker reduces their deposit shares to 0, either by queueing
/// a withdrawal, or undelegating from their operator. This ensures that subsequent
/// delegations/deposits do not use a stale DSF (e.g. from a prior operator).
function reset(
DepositScalingFactor storage dsf
) internal {
dsf._scalingFactor = 0;
}
// CONVERSION
function calcWithdrawable(
DepositScalingFactor memory dsf,
uint256 depositShares,
uint256 slashingFactor
) internal pure returns (uint256) {
/// forgefmt: disable-next-item
return depositShares
.mulWad(dsf.scalingFactor())
.mulWad(slashingFactor);
}
function calcDepositShares(
DepositScalingFactor memory dsf,
uint256 withdrawableShares,
uint256 slashingFactor
) internal pure returns (uint256) {
/// forgefmt: disable-next-item
return withdrawableShares
.divWad(dsf.scalingFactor())
.divWad(slashingFactor);
}
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);
}
}
````
## File: src/contracts/libraries/Snapshots.sol
````
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin-upgrades/contracts/utils/math/MathUpgradeable.sol";
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.
* Some additional functions have also been added for convenience.
* @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` 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._
*/
library Snapshots {
struct DefaultWadHistory {
Snapshot[] _snapshots;
}
struct DefaultZeroHistory {
Snapshot[] _snapshots;
}
struct Snapshot {
uint32 _key;
uint224 _value;
}
error InvalidSnapshotOrdering();
/**
* @dev Pushes a (`key`, `value`) pair into a DefaultWadHistory so that it is stored as the snapshot.
*/
function push(DefaultWadHistory storage self, uint32 key, uint64 value) internal {
_insert(self._snapshots, key, value);
}
/**
* @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) {
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);
}
/**
* @dev Returns the value in the most recent snapshot, or WAD if there are no snapshots.
*/
function latest(
DefaultWadHistory storage self
) internal view returns (uint64) {
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));
}
/**
* @dev Returns the number of snapshots.
*/
function length(
DefaultWadHistory storage self
) internal view returns (uint256) {
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, uint224 value) private {
uint256 pos = self.length;
if (pos > 0) {
// Validate that inserted keys are always >= the previous key
Snapshot memory last = _unsafeAccess(self, pos - 1);
require(last._key <= key, InvalidSnapshotOrdering());
// Update existing snapshot if `key` matches
if (last._key == key) {
_unsafeAccess(self, pos - 1)._value = value;
return;
}
}
// `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;
}
/**
* @dev Return the index of the last (most recent) snapshot with key lower or equal than the search key, or `high` if there is none.
* `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`.
*
* WARNING: `high` should not be greater than the array's length.
*/
function _upperBinaryLookup(
Snapshot[] storage self,
uint32 key,
uint256 low,
uint256 high
) private view returns (uint256) {
while (low < high) {
uint256 mid = MathUpgradeable.average(low, high);
if (_unsafeAccess(self, mid)._key > key) {
high = mid;
} else {
low = mid + 1;
}
}
return high;
}
/**
* @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds.
*/
function _unsafeAccess(Snapshot[] storage self, uint256 pos) private pure returns (Snapshot storage result) {
assembly {
mstore(0, self.slot)
result.slot := add(keccak256(0, 0x20), pos)
}
}
}
````
## File: src/contracts/mixins/PermissionControllerMixin.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import "../interfaces/IPermissionController.sol";
abstract contract PermissionControllerMixin {
/// @dev Thrown when the caller is not allowed to call a function on behalf of an account.
error InvalidPermissions();
/// @notice Pointer to the permission controller contract.
IPermissionController public immutable permissionController;
constructor(
IPermissionController _permissionController
) {
permissionController = _permissionController;
}
/// @notice Checks if the caller (msg.sender) can call on behalf of an account.
modifier checkCanCall(
address account
) {
require(_checkCanCall(account), InvalidPermissions());
_;
}
/**
* @notice Checks if the caller is allowed to call a function on behalf of an account.
* @param account the account to check
* @dev `msg.sender` is the caller to check that can call the function on behalf of `account`.
* @dev Returns a bool, instead of reverting
*/
function _checkCanCall(
address account
) internal returns (bool) {
return permissionController.canCall(account, msg.sender, address(this), msg.sig);
}
}
````
## File: src/contracts/mixins/SemVerMixin.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import "../interfaces/ISemVerMixin.sol";
import "@openzeppelin-upgrades/contracts/utils/ShortStringsUpgradeable.sol";
/// @title SemVerMixin
/// @notice A mixin contract that provides semantic versioning functionality.
/// @dev Follows SemVer 2.0.0 specification (https://semver.org/).
abstract contract SemVerMixin is ISemVerMixin {
using ShortStringsUpgradeable for *;
/// @notice The semantic version string for this contract, stored as a ShortString for gas efficiency.
/// @dev Follows SemVer 2.0.0 specification (https://semver.org/). Prefixed with 'v' (e.g., "v1.2.3").
ShortString internal immutable _VERSION;
/// @notice Initializes the contract with a semantic version string.
/// @param _version The SemVer-formatted version string (e.g., "v1.2.3")
/// @dev Version should follow SemVer 2.0.0 format with 'v' prefix: vMAJOR.MINOR.PATCH
constructor(
string memory _version
) {
_VERSION = _version.toShortString();
}
/// @inheritdoc ISemVerMixin
function version() public view virtual returns (string memory) {
return _VERSION.toString();
}
/// @notice Returns the major version of the contract.
/// @dev Supports single digit major versions (e.g., "v1" for version "v1.2.3")
/// @return The major version string (e.g., "v1" for version "v1.2.3")
function _majorVersion() internal view returns (string memory) {
bytes memory v = bytes(_VERSION.toString());
return string(bytes.concat(v[0], v[1]));
}
}
````
## File: src/contracts/mixins/SignatureUtilsMixin.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import "@openzeppelin-upgrades/contracts/utils/ShortStringsUpgradeable.sol";
import "@openzeppelin-upgrades/contracts/utils/cryptography/SignatureCheckerUpgradeable.sol";
import "../interfaces/ISignatureUtilsMixin.sol";
import "./SemVerMixin.sol";
/// @dev The EIP-712 domain type hash used for computing the domain separator
/// See https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator
bytes32 constant EIP712_DOMAIN_TYPEHASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
/// @title SignatureUtilsMixin
/// @notice A mixin contract that provides utilities for validating signatures according to EIP-712 and EIP-1271 standards.
/// @dev The domain name is hardcoded to "EigenLayer". This contract implements signature validation functionality that can be
/// inherited by other contracts. The domain separator uses the major version (e.g., "v1") to maintain EIP-712
/// signature compatibility across minor and patch version updates.
abstract contract SignatureUtilsMixin is ISignatureUtilsMixin, SemVerMixin {
using SignatureCheckerUpgradeable for address;
/// @notice Initializes the contract with a semantic version string.
/// @param _version The SemVer-formatted version string (e.g., "v1.1.1") to use for this contract's domain separator.
/// @dev Version should follow SemVer 2.0.0 format with 'v' prefix: vMAJOR.MINOR.PATCH.
/// Only the major version component is used in the domain separator to maintain signature compatibility
/// across minor and patch version updates.
constructor(
string memory _version
) SemVerMixin(_version) {}
/// EXTERNAL FUNCTIONS ///
/// @inheritdoc ISignatureUtilsMixin
function domainSeparator() public view virtual returns (bytes32) {
// forgefmt: disable-next-item
return
keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(bytes("EigenLayer")),
keccak256(bytes(_majorVersion())),
block.chainid,
address(this)
)
);
}
/// INTERNAL HELPERS ///
/// @notice Creates a digest that can be signed using EIP-712.
/// @dev Prepends the EIP-712 prefix ("\x19\x01") and domain separator to the input hash.
/// This follows the EIP-712 specification for creating structured data hashes.
/// See https://eips.ethereum.org/EIPS/eip-712#specification.
/// @param hash The hash of the typed data to be signed.
/// @return The complete digest that should be signed according to EIP-712.
function _calculateSignableDigest(
bytes32 hash
) internal view returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", domainSeparator(), hash));
}
/// @notice Validates a signature against a signer and digest, with an expiry timestamp.
/// @dev Reverts if the signature is invalid or expired. Uses EIP-1271 for smart contract signers.
/// For EOA signers, validates ECDSA signatures directly.
/// For contract signers, calls isValidSignature according to EIP-1271.
/// See https://eips.ethereum.org/EIPS/eip-1271#specification.
/// @param signer The address that should have signed the digest.
/// @param signableDigest The digest that was signed, created via _calculateSignableDigest.
/// @param signature The signature bytes to validate.
/// @param expiry The timestamp after which the signature is no longer valid.
function _checkIsValidSignatureNow(
address signer,
bytes32 signableDigest,
bytes memory signature,
uint256 expiry
) internal view {
// First, check if the signature has expired by comparing the expiry timestamp
// against the current block timestamp.
require(expiry >= block.timestamp, SignatureExpired());
// Next, verify that the signature is valid for the given signer and digest.
// For EOA signers, this performs standard ECDSA signature verification.
// For contract signers, this calls the EIP-1271 isValidSignature method.
require(signer.isValidSignatureNow(signableDigest, signature), InvalidSignature());
}
}
````
## File: src/contracts/permissions/Pausable.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IPausable.sol";
/**
* @title Adds pausability to a contract, with pausing & unpausing controlled by the `pauser` and `unpauser` of a PauserRegistry contract.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice Contracts that inherit from this contract may define their own `pause` and `unpause` (and/or related) functions.
* These functions should be permissioned as "onlyPauser" which defers to a `PauserRegistry` for determining access control.
* @dev Pausability is implemented using a uint256, which allows up to 256 different single bit-flags; each bit can potentially pause different functionality.
* Inspiration for this was taken from the NearBridge design here https://etherscan.io/address/0x3FEFc5A4B1c02f21cBc8D3613643ba0635b9a873#code.
* For the `pause` and `unpause` functions we've implemented, if you pause, you can only flip (any number of) switches to on/1 (aka "paused"), and if you unpause,
* you can only flip (any number of) switches to off/0 (aka "paused").
* If you want a pauseXYZ function that just flips a single bit / "pausing flag", it will:
* 1) 'bit-wise and' (aka `&`) a flag with the current paused state (as a uint256)
* 2) update the paused state to this new value
* @dev We note as well that we have chosen to identify flags by their *bit index* as opposed to their numerical value, so, e.g. defining `DEPOSITS_PAUSED = 3`
* indicates specifically that if the *third bit* of `_paused` is flipped -- i.e. it is a '1' -- then deposits should be paused
*/
abstract contract Pausable is IPausable {
/// Constants
uint256 internal constant _UNPAUSE_ALL = 0;
uint256 internal constant _PAUSE_ALL = type(uint256).max;
/// @notice Address of the `PauserRegistry` contract that this contract defers to for determining access control (for pausing).
IPauserRegistry public immutable pauserRegistry;
/// Storage
/// @dev Do not remove, deprecated storage.
IPauserRegistry private __deprecated_pauserRegistry;
/// @dev Returns a bitmap representing the paused status of the contract.
uint256 private _paused;
/// Modifiers
/// @dev Thrown if the caller is not a valid pauser according to the pauser registry.
modifier onlyPauser() {
require(pauserRegistry.isPauser(msg.sender), OnlyPauser());
_;
}
/// @dev Thrown if the caller is not a valid unpauser according to the pauser registry.
modifier onlyUnpauser() {
require(msg.sender == pauserRegistry.unpauser(), OnlyUnpauser());
_;
}
/// @dev Thrown if the contract is paused, i.e. if any of the bits in `_paused` is flipped to 1.
modifier whenNotPaused() {
require(_paused == 0, CurrentlyPaused());
_;
}
/// @dev Thrown if the `indexed`th bit of `_paused` is 1, i.e. if the `index`th pause switch is flipped.
modifier onlyWhenNotPaused(
uint8 index
) {
require(!paused(index), CurrentlyPaused());
_;
}
/// Construction
constructor(
IPauserRegistry _pauserRegistry
) {
require(address(_pauserRegistry) != address(0), InputAddressZero());
pauserRegistry = _pauserRegistry;
}
/// @inheritdoc IPausable
function pause(
uint256 newPausedStatus
) external onlyPauser {
uint256 currentPausedStatus = _paused;
// verify that the `newPausedStatus` does not *unflip* any bits (i.e. doesn't unpause anything, all 1 bits remain)
require((currentPausedStatus & newPausedStatus) == currentPausedStatus, InvalidNewPausedStatus());
_setPausedStatus(newPausedStatus);
}
/// @inheritdoc IPausable
function pauseAll() external onlyPauser {
_setPausedStatus(_PAUSE_ALL);
}
/// @inheritdoc IPausable
function unpause(
uint256 newPausedStatus
) external onlyUnpauser {
uint256 currentPausedStatus = _paused;
// verify that the `newPausedStatus` does not *flip* any bits (i.e. doesn't pause anything, all 0 bits remain)
require(((~currentPausedStatus) & (~newPausedStatus)) == (~currentPausedStatus), InvalidNewPausedStatus());
_paused = newPausedStatus;
emit Unpaused(msg.sender, newPausedStatus);
}
/// @inheritdoc IPausable
function paused() public view virtual returns (uint256) {
return _paused;
}
/// @inheritdoc IPausable
function paused(
uint8 index
) public view virtual returns (bool) {
uint256 mask = 1 << index;
return ((_paused & mask) == mask);
}
/// @dev Internal helper for setting the paused status, and emitting the corresponding event.
function _setPausedStatus(
uint256 pausedStatus
) internal {
_paused = pausedStatus;
emit Paused(msg.sender, pausedStatus);
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[48] private __gap;
}
````
## File: src/contracts/permissions/PauserRegistry.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IPauserRegistry.sol";
/**
* @title Defines pauser & unpauser roles + modifiers to be used elsewhere.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
contract PauserRegistry is IPauserRegistry {
/// @notice Mapping of addresses to whether they hold the pauser role.
mapping(address => bool) public isPauser;
/// @notice Unique address that holds the unpauser role. Capable of changing *both* the pauser and unpauser addresses.
address public unpauser;
modifier onlyUnpauser() {
require(msg.sender == unpauser, OnlyUnpauser());
_;
}
constructor(address[] memory _pausers, address _unpauser) {
for (uint256 i = 0; i < _pausers.length; i++) {
_setIsPauser(_pausers[i], true);
}
_setUnpauser(_unpauser);
}
/// @notice Sets new pauser - only callable by unpauser, as the unpauser is expected to be kept more secure, e.g. being a multisig with a higher threshold
/// @param newPauser Address to be added/removed as pauser
/// @param canPause Whether the address should be added or removed as pauser
function setIsPauser(address newPauser, bool canPause) external onlyUnpauser {
_setIsPauser(newPauser, canPause);
}
/// @notice Sets new unpauser - only callable by unpauser, as the unpauser is expected to be kept more secure, e.g. being a multisig with a higher threshold
function setUnpauser(
address newUnpauser
) external onlyUnpauser {
_setUnpauser(newUnpauser);
}
function _setIsPauser(address pauser, bool canPause) internal {
require(pauser != address(0), InputAddressZero());
isPauser[pauser] = canPause;
emit PauserStatusChanged(pauser, canPause);
}
function _setUnpauser(
address newUnpauser
) internal {
require(newUnpauser != address(0), InputAddressZero());
emit UnpauserChanged(unpauser, newUnpauser);
unpauser = newUnpauser;
}
}
````
## File: src/contracts/permissions/PermissionController.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "../mixins/SemVerMixin.sol";
import "./PermissionControllerStorage.sol";
contract PermissionController is Initializable, SemVerMixin, PermissionControllerStorage {
using EnumerableSet for *;
modifier onlyAdmin(
address account
) {
require(isAdmin(account, msg.sender), NotAdmin());
_;
}
/**
*
* INITIALIZING FUNCTIONS
*
*/
constructor(
string memory _version
) SemVerMixin(_version) {
_disableInitializers();
}
/**
*
* EXTERNAL FUNCTIONS
*
*/
/// @inheritdoc IPermissionController
function addPendingAdmin(address account, address admin) external onlyAdmin(account) {
AccountPermissions storage permissions = _permissions[account];
// Revert if the admin is already set
require(!permissions.admins.contains(admin), AdminAlreadySet());
// Add the admin to the account's pending admins
// If the admin is already pending, the add will fail
require(permissions.pendingAdmins.add(admin), AdminAlreadyPending());
emit PendingAdminAdded(account, admin);
}
/// @inheritdoc IPermissionController
function removePendingAdmin(address account, address admin) external onlyAdmin(account) {
EnumerableSet.AddressSet storage pendingAdmins = _permissions[account].pendingAdmins;
// Remove the admin from the account's pending admins
// Revert if the admin is not pending
require(pendingAdmins.remove(admin), AdminNotPending());
emit PendingAdminRemoved(account, admin);
}
/// @inheritdoc IPermissionController
function acceptAdmin(
address account
) external {
AccountPermissions storage permissions = _permissions[account];
// Remove the admin from the pending list
// Revert if the admin is not pending
require(permissions.pendingAdmins.remove(msg.sender), AdminNotPending());
// Add the admin to the account's admins
// Not wrapped in a require since it must be the case the admin is not one
permissions.admins.add(msg.sender);
emit AdminSet(account, msg.sender);
}
/// @inheritdoc IPermissionController
function removeAdmin(address account, address admin) external onlyAdmin(account) {
EnumerableSet.AddressSet storage admins = _permissions[account].admins;
require(admins.length() > 1, CannotHaveZeroAdmins());
// Remove the admin from the account's admins
// If the admin is not set, the remove will fail
require(admins.remove(admin), AdminNotSet());
emit AdminRemoved(account, admin);
}
/// @inheritdoc IPermissionController
function setAppointee(
address account,
address appointee,
address target,
bytes4 selector
) external onlyAdmin(account) {
AccountPermissions storage permissions = _permissions[account];
bytes32 targetSelector = _encodeTargetSelector(target, selector);
require(!permissions.appointeePermissions[appointee].contains(targetSelector), AppointeeAlreadySet());
// Add the appointee to the account's permissions
permissions.appointeePermissions[appointee].add(targetSelector);
permissions.permissionAppointees[targetSelector].add(appointee);
emit AppointeeSet(account, appointee, target, selector);
}
/// @inheritdoc IPermissionController
function removeAppointee(
address account,
address appointee,
address target,
bytes4 selector
) external onlyAdmin(account) {
AccountPermissions storage permissions = _permissions[account];
bytes32 targetSelector = _encodeTargetSelector(target, selector);
require(permissions.appointeePermissions[appointee].contains(targetSelector), AppointeeNotSet());
// Remove the appointee from the account's permissions
permissions.appointeePermissions[appointee].remove(targetSelector);
permissions.permissionAppointees[targetSelector].remove(appointee);
emit AppointeeRemoved(account, appointee, target, selector);
}
/**
*
* INTERNAL FUNCTIONS
*
*/
/// @dev Encodes the target and selector into a single bytes32 values
/// @dev Encoded Format: [160 bits target][32 bits selector][64 bits padding],
function _encodeTargetSelector(address target, bytes4 selector) internal pure returns (bytes32) {
// Reserve 96 bits for the target
uint256 shiftedTarget = uint256(uint160(target)) << 96;
// Reserve 32 bits for the selector
uint256 shiftedSelector = uint256(uint32(selector)) << 64;
// Combine the target and selector
return bytes32(shiftedTarget | shiftedSelector);
}
/// @dev Decodes the target and selector from a single bytes32 value
/// @dev Encoded Format: [160 bits target][32 bits selector][64 bits padding],
function _decodeTargetSelector(
bytes32 targetSelector
) internal pure returns (address, bytes4) {
// The target is in the upper 160 bits of the targetSelector
address target = address(uint160(uint256(targetSelector) >> 96));
// The selector is in the lower 32 bits after the padding is removed
bytes4 selector = bytes4(uint32(uint256(targetSelector) >> 64));
return (target, selector);
}
/**
*
* VIEW FUNCTIONS
*
*/
/// @inheritdoc IPermissionController
function isAdmin(address account, address caller) public view returns (bool) {
if (_permissions[account].admins.length() == 0) {
// If the account does not have an admin, the caller must be the account
return account == caller;
} else {
// If the account has an admin, the caller must be an admin
return _permissions[account].admins.contains(caller);
}
}
/// @inheritdoc IPermissionController
function isPendingAdmin(address account, address pendingAdmin) external view returns (bool) {
return _permissions[account].pendingAdmins.contains(pendingAdmin);
}
/// @inheritdoc IPermissionController
function getAdmins(
address account
) external view returns (address[] memory) {
if (_permissions[account].admins.length() == 0) {
address[] memory admin = new address[](1);
admin[0] = account;
return admin;
} else {
return _permissions[account].admins.values();
}
}
/// @inheritdoc IPermissionController
function getPendingAdmins(
address account
) external view returns (address[] memory) {
return _permissions[account].pendingAdmins.values();
}
/// @inheritdoc IPermissionController
function canCall(address account, address caller, address target, bytes4 selector) external view returns (bool) {
return isAdmin(account, caller)
|| _permissions[account].appointeePermissions[caller].contains(_encodeTargetSelector(target, selector));
}
/// @inheritdoc IPermissionController
function getAppointeePermissions(
address account,
address appointee
) external view returns (address[] memory, bytes4[] memory) {
EnumerableSet.Bytes32Set storage appointeePermissions = _permissions[account].appointeePermissions[appointee];
uint256 length = appointeePermissions.length();
address[] memory targets = new address[](length);
bytes4[] memory selectors = new bytes4[](length);
for (uint256 i = 0; i < length; ++i) {
(targets[i], selectors[i]) = _decodeTargetSelector(appointeePermissions.at(i));
}
return (targets, selectors);
}
/// @inheritdoc IPermissionController
function getAppointees(address account, address target, bytes4 selector) external view returns (address[] memory) {
bytes32 targetSelector = _encodeTargetSelector(target, selector);
return _permissions[account].permissionAppointees[targetSelector].values();
}
}
````
## File: src/contracts/permissions/PermissionControllerStorage.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "../interfaces/IPermissionController.sol";
abstract contract PermissionControllerStorage is IPermissionController {
using EnumerableSet for EnumerableSet.Bytes32Set;
using EnumerableSet for EnumerableSet.AddressSet;
struct AccountPermissions {
/// @notice The pending admins of the account
EnumerableSet.AddressSet pendingAdmins;
/// @notice The admins of the account
EnumerableSet.AddressSet admins;
/// @notice Mapping from an appointee to the list of encoded target & selectors
mapping(address appointee => EnumerableSet.Bytes32Set) appointeePermissions;
/// @notice Mapping from encoded target & selector to the list of appointees
mapping(bytes32 targetSelector => EnumerableSet.AddressSet) permissionAppointees;
}
/// @notice Mapping from an account to its permission
mapping(address account => AccountPermissions) internal _permissions;
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[49] private __gap;
}
````
## File: src/contracts/pods/EigenPod.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../libraries/BeaconChainProofs.sol";
import "../libraries/BytesLib.sol";
import "../mixins/SemVerMixin.sol";
import "../interfaces/IETHPOSDeposit.sol";
import "../interfaces/IEigenPodManager.sol";
import "../interfaces/IPausable.sol";
import "./EigenPodPausingConstants.sol";
import "./EigenPodStorage.sol";
/**
* @title The implementation contract used for restaking beacon chain ETH on EigenLayer
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs
* @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose
* to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts
*/
contract EigenPod is
Initializable,
ReentrancyGuardUpgradeable,
EigenPodPausingConstants,
EigenPodStorage,
SemVerMixin
{
using BytesLib for bytes;
using SafeERC20 for IERC20;
using BeaconChainProofs for *;
/**
*
* CONSTANTS / IMMUTABLES
*
*/
/// @notice The beacon chain stores balances in Gwei, rather than wei. This value is used to convert between the two
uint256 internal constant GWEI_TO_WEI = 1e9;
/// @notice The address of the EIP-4788 beacon block root oracle
/// (See https://eips.ethereum.org/EIPS/eip-4788)
address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02;
/// @notice The length of the EIP-4788 beacon block root ring buffer
uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191;
/// @notice The beacon chain deposit contract
IETHPOSDeposit public immutable ethPOS;
/// @notice The single EigenPodManager for EigenLayer
IEigenPodManager public immutable eigenPodManager;
/// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp
uint64 public immutable GENESIS_TIME;
/**
*
* MODIFIERS
*
*/
/// @notice Callable only by the EigenPodManager
modifier onlyEigenPodManager() {
require(msg.sender == address(eigenPodManager), OnlyEigenPodManager());
_;
}
/// @notice Callable only by the pod's owner
modifier onlyEigenPodOwner() {
require(msg.sender == podOwner, OnlyEigenPodOwner());
_;
}
/// @notice Callable only by the pod's owner or proof submitter
modifier onlyOwnerOrProofSubmitter() {
require(msg.sender == podOwner || msg.sender == proofSubmitter, OnlyEigenPodOwnerOrProofSubmitter());
_;
}
/**
* @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction
* is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies).
* Modifier throws if the `indexed`th bit of `_paused` in the EigenPodManager is 1, i.e. if the `index`th pause switch is flipped.
*/
modifier onlyWhenNotPaused(
uint8 index
) {
require(!IPausable(address(eigenPodManager)).paused(index), CurrentlyPaused());
_;
}
/**
*
* CONSTRUCTOR / INIT
*
*/
constructor(
IETHPOSDeposit _ethPOS,
IEigenPodManager _eigenPodManager,
uint64 _GENESIS_TIME,
string memory _version
) SemVerMixin(_version) {
ethPOS = _ethPOS;
eigenPodManager = _eigenPodManager;
GENESIS_TIME = _GENESIS_TIME;
_disableInitializers();
}
/// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager.
function initialize(
address _podOwner
) external initializer {
require(_podOwner != address(0), InputAddressZero());
podOwner = _podOwner;
}
/**
*
* EXTERNAL METHODS
*
*/
/// @notice payable fallback function that receives ether deposited to the eigenpods contract
receive() external payable {
emit NonBeaconChainETHReceived(msg.value);
}
/**
* @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed
* by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total
* change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`.
* @dev Once finalized, the pod owner is awarded shares corresponding to:
* - the total change in their ACTIVE validator balances
* - any ETH in the pod not already awarded shares
* @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If
* this is the case, the pod owner MUST complete the existing checkpoint before starting a new one.
* @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner
* to prevent accidentally starting a checkpoint that will not increase their shares
*/
function startCheckpoint(
bool revertIfNoBalance
) external onlyOwnerOrProofSubmitter onlyWhenNotPaused(PAUSED_START_CHECKPOINT) {
_startCheckpoint(revertIfNoBalance);
}
/**
* @dev Progress the current checkpoint towards completion by submitting one or more validator
* checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint.
* For each validator proven, the current checkpoint's `proofsRemaining` decreases.
* @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized.
* (see `_updateCheckpoint` for more details)
* @dev This method can only be called when there is a currently-active checkpoint.
* @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot`
* @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot`
*/
function verifyCheckpointProofs(
BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof,
BeaconChainProofs.BalanceProof[] calldata proofs
) external onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS) {
uint64 checkpointTimestamp = currentCheckpointTimestamp;
require(checkpointTimestamp != 0, NoActiveCheckpoint());
Checkpoint memory checkpoint = _currentCheckpoint;
// Verify `balanceContainerProof` against `beaconBlockRoot`
BeaconChainProofs.verifyBalanceContainer({
proofVersion: _getProofVersion(checkpointTimestamp),
beaconBlockRoot: checkpoint.beaconBlockRoot,
proof: balanceContainerProof
});
// Process each checkpoint proof submitted
uint64 exitedBalancesGwei;
for (uint256 i = 0; i < proofs.length; i++) {
BeaconChainProofs.BalanceProof calldata proof = proofs[i];
ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash];
// Validator must be in the ACTIVE state to be provable during a checkpoint.
// Validators become ACTIVE when initially proven via verifyWithdrawalCredentials
// Validators become WITHDRAWN when a checkpoint proof shows they have 0 balance
if (validatorInfo.status != VALIDATOR_STATUS.ACTIVE) {
continue;
}
// Ensure we aren't proving a validator twice for the same checkpoint. This will fail if:
// - validator submitted twice during this checkpoint
// - validator withdrawal credentials verified after checkpoint starts, then submitted
// as a checkpoint proof
if (validatorInfo.lastCheckpointedAt >= checkpointTimestamp) {
continue;
}
// Process a checkpoint proof for a validator and update its balance.
//
// If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`.
// The assumption is that if this is the case, any withdrawn ETH was already in
// the pod when `startCheckpoint` was originally called.
(uint64 prevBalanceGwei, int64 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({
validatorInfo: validatorInfo,
checkpointTimestamp: checkpointTimestamp,
balanceContainerRoot: balanceContainerProof.balanceContainerRoot,
proof: proof
});
checkpoint.proofsRemaining--;
checkpoint.prevBeaconBalanceGwei += prevBalanceGwei;
checkpoint.balanceDeltasGwei += balanceDeltaGwei;
exitedBalancesGwei += exitedBalanceGwei;
// Record the updated validator in state
_validatorPubkeyHashToInfo[proof.pubkeyHash] = validatorInfo;
emit ValidatorCheckpointed(checkpointTimestamp, uint40(validatorInfo.validatorIndex));
}
// Update the checkpoint and the total amount attributed to exited validators
checkpointBalanceExitedGwei[checkpointTimestamp] += exitedBalancesGwei;
_updateCheckpoint(checkpoint);
}
/**
* @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award
* shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and
* future checkpoint proofs will need to include them.
* @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`.
* @dev Validators proven via this method MUST NOT have an exit epoch set already.
* @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds
* to the parent beacon block root against which the proof is verified.
* @param stateRootProof proves a beacon state root against a beacon block root
* @param validatorIndices a list of validator indices being proven
* @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root
* @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for
* details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator
*/
function verifyWithdrawalCredentials(
uint64 beaconTimestamp,
BeaconChainProofs.StateRootProof calldata stateRootProof,
uint40[] calldata validatorIndices,
bytes[] calldata validatorFieldsProofs,
bytes32[][] calldata validatorFields
) external onlyOwnerOrProofSubmitter onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) {
require(
(validatorIndices.length == validatorFieldsProofs.length)
&& (validatorFieldsProofs.length == validatorFields.length),
InputArrayLengthMismatch()
);
// Calling this method using a `beaconTimestamp` <= `currentCheckpointTimestamp` would allow
// a newly-verified validator to be submitted to `verifyCheckpointProofs`, making progress
// on an existing checkpoint.
require(beaconTimestamp > currentCheckpointTimestamp, BeaconTimestampTooFarInPast());
// Verify passed-in `beaconStateRoot` against the beacon block root
// forgefmt: disable-next-item
BeaconChainProofs.verifyStateRoot({
beaconBlockRoot: getParentBlockRoot(beaconTimestamp),
proof: stateRootProof
});
uint256 totalAmountToBeRestakedWei;
for (uint256 i = 0; i < validatorIndices.length; i++) {
// forgefmt: disable-next-item
totalAmountToBeRestakedWei += _verifyWithdrawalCredentials(
beaconTimestamp,
stateRootProof.beaconStateRoot,
validatorIndices[i],
validatorFieldsProofs[i],
validatorFields[i]
);
}
// Update the EigenPodManager on this pod's new balance
eigenPodManager.recordBeaconChainETHBalanceUpdate({
podOwner: podOwner,
prevRestakedBalanceWei: 0, // only used for checkpoint balance updates
balanceDeltaWei: int256(totalAmountToBeRestakedWei)
});
}
/**
* @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful
* staleness proof allows the caller to start a checkpoint.
*
* @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed!
* (See `_startCheckpoint` for details)
*
* @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon
* chain. This is intended to make it easier to external watchers to keep a pod's balance up to date.
*
* @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event
* and the validator's final exit back to the execution layer. During this time, the validator's balance may or
* may not drop further due to a correlation penalty. This method allows proof of a slashed validator
* to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator
* has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven
* "stale" via this method.
* See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info.
*
* @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds
* to the parent beacon block root against which the proof is verified.
* @param stateRootProof proves a beacon state root against a beacon block root
* @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against
* the beacon state root. See the consensus specs for more details:
* https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator
*
* @dev Staleness conditions:
* - Validator's last checkpoint is older than `beaconTimestamp`
* - Validator MUST be in `ACTIVE` status in the pod
* - Validator MUST be slashed on the beacon chain
*/
function verifyStaleBalance(
uint64 beaconTimestamp,
BeaconChainProofs.StateRootProof calldata stateRootProof,
BeaconChainProofs.ValidatorProof calldata proof
) external onlyWhenNotPaused(PAUSED_START_CHECKPOINT) onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) {
bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash();
ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey];
// Validator must be eligible for a staleness proof. Generally, this condition
// ensures that the staleness proof is newer than the last time we got an update
// on this validator.
//
// Note: It is possible for `validatorInfo.lastCheckpointedAt` to be 0 if
// a validator's withdrawal credentials are verified when no checkpoint has
// ever been completed in this pod. Technically, this would mean that `beaconTimestamp`
// can be any valid EIP-4788 timestamp - because any nonzero value satisfies the
// require below.
//
// However, in practice, if the only update we've seen from a validator is their
// `verifyWithdrawalCredentials` proof, any valid `verifyStaleBalance` proof is
// necessarily newer. This is because when a validator is initially slashed, their
// exit epoch is set. And because `verifyWithdrawalCredentials` rejects validators
// that have initiated exits, we know that if we're seeing a proof where the validator
// is slashed that it MUST be newer than the `verifyWithdrawalCredentials` proof
// (regardless of the relationship between `beaconTimestamp` and `lastCheckpointedAt`).
require(beaconTimestamp > validatorInfo.lastCheckpointedAt, BeaconTimestampTooFarInPast());
// Validator must be checkpoint-able
require(validatorInfo.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod());
// Validator must be slashed on the beacon chain
require(proof.validatorFields.isValidatorSlashed(), ValidatorNotSlashedOnBeaconChain());
// Verify passed-in `beaconStateRoot` against the beacon block root
// forgefmt: disable-next-item
BeaconChainProofs.verifyStateRoot({
beaconBlockRoot: getParentBlockRoot(beaconTimestamp),
proof: stateRootProof
});
// Verify Validator container proof against `beaconStateRoot`
BeaconChainProofs.verifyValidatorFields({
proofVersion: _getProofVersion(beaconTimestamp),
beaconStateRoot: stateRootProof.beaconStateRoot,
validatorFields: proof.validatorFields,
validatorFieldsProof: proof.proof,
validatorIndex: uint40(validatorInfo.validatorIndex)
});
// Validator verified to be stale - start a checkpoint
_startCheckpoint(false);
}
/// @notice called by owner of a pod to remove any ERC20s deposited in the pod
function recoverTokens(
IERC20[] memory tokenList,
uint256[] memory amountsToWithdraw,
address recipient
) external onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) {
require(tokenList.length == amountsToWithdraw.length, InputArrayLengthMismatch());
for (uint256 i = 0; i < tokenList.length; i++) {
tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]);
}
}
/// @notice Allows the owner of a pod to update the proof submitter, a permissioned
/// address that can call `startCheckpoint` and `verifyWithdrawalCredentials`.
/// @dev Note that EITHER the podOwner OR proofSubmitter can access these methods,
/// so it's fine to set your proofSubmitter to 0 if you want the podOwner to be the
/// only address that can call these methods.
/// @param newProofSubmitter The new proof submitter address. If set to 0, only the
/// pod owner will be able to call `startCheckpoint` and `verifyWithdrawalCredentials`
function setProofSubmitter(
address newProofSubmitter
) external onlyEigenPodOwner {
emit ProofSubmitterUpdated(proofSubmitter, newProofSubmitter);
proofSubmitter = newProofSubmitter;
}
/// @notice Called by EigenPodManager when the owner wants to create another ETH validator.
/// @dev This function only supports staking to a 0x01 validator. For compounding validators, please interact directly with the deposit contract.
function stake(
bytes calldata pubkey,
bytes calldata signature,
bytes32 depositDataRoot
) external payable onlyEigenPodManager {
// stake on ethpos
require(msg.value == 32 ether, MsgValueNot32ETH());
ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot);
emit EigenPodStaked(pubkey);
}
/**
* @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address
* @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain.
* @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `restakedExecutionLayerGwei` exceeds the
* `amountWei` input (when converted to GWEI).
* @dev Reverts if `amountWei` is not a whole Gwei amount
*/
function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager {
uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI);
amountWei = amountGwei * GWEI_TO_WEI;
require(amountGwei <= restakedExecutionLayerGwei, InsufficientWithdrawableBalance());
restakedExecutionLayerGwei -= amountGwei;
emit RestakedBeaconChainETHWithdrawn(recipient, amountWei);
// transfer ETH from pod to `recipient` directly
Address.sendValue(payable(recipient), amountWei);
}
/**
*
* INTERNAL FUNCTIONS
*
*/
/**
* @notice internal function that proves an individual validator's withdrawal credentials
* @param validatorIndex is the index of the validator being proven
* @param validatorFieldsProof is the bytes that prove the ETH validator's withdrawal credentials against a beacon chain state root
* @param validatorFields are the fields of the "Validator Container", refer to consensus specs
*/
function _verifyWithdrawalCredentials(
uint64 beaconTimestamp,
bytes32 beaconStateRoot,
uint40 validatorIndex,
bytes calldata validatorFieldsProof,
bytes32[] calldata validatorFields
) internal returns (uint256) {
bytes32 pubkeyHash = validatorFields.getPubkeyHash();
ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash];
// Withdrawal credential proofs should only be processed for "INACTIVE" validators
require(validatorInfo.status == VALIDATOR_STATUS.INACTIVE, CredentialsAlreadyVerified());
// Validator should be active on the beacon chain, or in the process of activating.
// This implies the validator has reached the minimum effective balance required
// to become active on the beacon chain.
//
// This check is important because the Pectra upgrade will move any validators that
// do NOT have an activation epoch to a "pending deposit queue," temporarily resetting
// their current and effective balances to 0. This balance can be restored if a deposit
// is made to bring the validator's balance above the minimum activation balance.
// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/fork.md#upgrading-the-state)
//
// In the context of EigenLayer slashing, this temporary reset would allow pod shares
// to temporarily decrease, then be restored later. This would effectively prevent these
// shares from being slashable on EigenLayer for a short period of time.
require(
validatorFields.getActivationEpoch() != BeaconChainProofs.FAR_FUTURE_EPOCH, ValidatorInactiveOnBeaconChain()
);
// Validator should not already be in the process of exiting. This is an important property
// this method needs to enforce to ensure a validator cannot be already-exited by the time
// its withdrawal credentials are verified.
//
// Note that when a validator initiates an exit, two values are set:
// - exit_epoch
// - withdrawable_epoch
//
// The latter of these two values describes an epoch after which the validator's ETH MIGHT
// have been exited to the EigenPod, depending on the state of the beacon chain withdrawal
// queue.
//
// Requiring that a validator has not initiated exit by the time the EigenPod sees their
// withdrawal credentials guarantees that the validator has not fully exited at this point.
//
// This is because:
// - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest
// slot available in the EIP-4788 oracle, which keeps the last 8192 slots.
// - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to
// 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots).
//
// (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit)
require(validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, ValidatorIsExitingBeaconChain());
// Ensure the validator's withdrawal credentials are pointed at this pod
require(
validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials())
|| validatorFields.getWithdrawalCredentials() == bytes32(_podCompoundingWithdrawalCredentials()),
WithdrawalCredentialsNotForEigenPod()
);
// Get the validator's effective balance. Note that this method uses effective balance, while
// `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's
// less accurate, but is good enough for verifying withdrawal credentials.
uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei();
// Verify passed-in validatorFields against verified beaconStateRoot:
BeaconChainProofs.verifyValidatorFields({
proofVersion: _getProofVersion(beaconTimestamp),
beaconStateRoot: beaconStateRoot,
validatorFields: validatorFields,
validatorFieldsProof: validatorFieldsProof,
validatorIndex: validatorIndex
});
// Account for validator in future checkpoints. Note that if this pod has never started a
// checkpoint before, `lastCheckpointedAt` will be zero here. This is fine because the main
// purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not
// eligible to progress already-existing checkpoints - however in this case, no checkpoints exist.
activeValidatorCount++;
uint64 lastCheckpointedAt =
currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp;
// Proofs complete - create the validator in state
_validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({
validatorIndex: validatorIndex,
restakedBalanceGwei: restakedBalanceGwei,
lastCheckpointedAt: lastCheckpointedAt,
status: VALIDATOR_STATUS.ACTIVE
});
// Add the validator's balance to the checkpoint's previous beacon balance
// Note that even if this checkpoint is not active, the next one will include
// the validator's restaked balance during the checkpoint process
_currentCheckpoint.prevBeaconBalanceGwei += restakedBalanceGwei;
emit ValidatorRestaked(validatorIndex);
emit ValidatorBalanceUpdated(validatorIndex, lastCheckpointedAt, restakedBalanceGwei);
return restakedBalanceGwei * GWEI_TO_WEI;
}
function _verifyCheckpointProof(
ValidatorInfo memory validatorInfo,
uint64 checkpointTimestamp,
bytes32 balanceContainerRoot,
BeaconChainProofs.BalanceProof calldata proof
) internal returns (uint64 prevBalanceGwei, int64 balanceDeltaGwei, uint64 exitedBalanceGwei) {
uint40 validatorIndex = uint40(validatorInfo.validatorIndex);
// Verify validator balance against `balanceContainerRoot`
prevBalanceGwei = validatorInfo.restakedBalanceGwei;
uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({
balanceContainerRoot: balanceContainerRoot,
validatorIndex: validatorIndex,
proof: proof
});
// Calculate change in the validator's balance since the last proof
if (newBalanceGwei != prevBalanceGwei) {
balanceDeltaGwei = int64(newBalanceGwei) - int64(prevBalanceGwei);
emit ValidatorBalanceUpdated(validatorIndex, checkpointTimestamp, newBalanceGwei);
}
validatorInfo.restakedBalanceGwei = newBalanceGwei;
validatorInfo.lastCheckpointedAt = checkpointTimestamp;
// If the validator's new balance is 0, mark them withdrawn
if (newBalanceGwei == 0) {
activeValidatorCount--;
validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN;
// If we reach this point, `balanceDeltaGwei` should always be negative,
// so this should be a safe conversion
exitedBalanceGwei = uint64(-balanceDeltaGwei);
emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex);
}
return (prevBalanceGwei, balanceDeltaGwei, exitedBalanceGwei);
}
/**
* @dev Initiate a checkpoint proof by snapshotting both the pod's ETH balance and the
* current block's parent block root. After providing a checkpoint proof for each of the
* pod's ACTIVE validators, the pod's ETH balance is awarded shares and can be withdrawn.
* @dev ACTIVE validators are validators with verified withdrawal credentials (See
* `verifyWithdrawalCredentials` for details)
* @dev If the pod does not have any ACTIVE validators, the checkpoint is automatically
* finalized.
* @dev Once started, a checkpoint MUST be completed! It is not possible to start a
* checkpoint if the existing one is incomplete.
* @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is
* true, this method will revert
*/
function _startCheckpoint(
bool revertIfNoBalance
) internal {
require(currentCheckpointTimestamp == 0, CheckpointAlreadyActive());
// Prevent a checkpoint being completable twice in the same block. This prevents an edge case
// where the second checkpoint would not be completable.
//
// This is because the validators checkpointed in the first checkpoint would have a `lastCheckpointedAt`
// value equal to the second checkpoint, causing their proofs to get skipped in `verifyCheckpointProofs`
require(lastCheckpointTimestamp != uint64(block.timestamp), CannotCheckpointTwiceInSingleBlock());
// Snapshot pod balance at the start of the checkpoint, subtracting pod balance that has
// previously been credited with shares. Once the checkpoint is finalized, `podBalanceGwei`
// will be added to the total validator balance delta and credited as shares.
//
// Note: On finalization, `podBalanceGwei` is added to `restakedExecutionLayerGwei`
// to denote that it has been credited with shares. Because this value is denominated in gwei,
// `podBalanceGwei` is also converted to a gwei amount here. This means that any sub-gwei amounts
// sent to the pod are not credited with shares and are therefore not withdrawable.
// This can be addressed by topping up a pod's balance to a value divisible by 1 gwei.
uint64 podBalanceGwei = uint64(address(this).balance / GWEI_TO_WEI) - restakedExecutionLayerGwei;
// If the caller doesn't want a "0 balance" checkpoint, revert
if (revertIfNoBalance && podBalanceGwei == 0) {
revert NoBalanceToCheckpoint();
}
// Create checkpoint using the previous block's root for proofs, and the current
// `activeValidatorCount` as the number of checkpoint proofs needed to finalize
// the checkpoint.
Checkpoint memory checkpoint = Checkpoint({
beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)),
proofsRemaining: uint24(activeValidatorCount),
podBalanceGwei: podBalanceGwei,
balanceDeltasGwei: 0,
prevBeaconBalanceGwei: 0
});
// Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint
// is automatically finalized.
currentCheckpointTimestamp = uint64(block.timestamp);
_updateCheckpoint(checkpoint);
emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot, checkpoint.proofsRemaining);
}
/**
* @dev Finish progress on a checkpoint and store it in state.
* @dev If the checkpoint has no proofs remaining, it is finalized:
* - a share delta is calculated and sent to the `EigenPodManager`
* - the checkpointed `podBalanceGwei` is added to `restakedExecutionLayerGwei`
* - `lastCheckpointTimestamp` is updated
* - `_currentCheckpoint` and `currentCheckpointTimestamp` are deleted
*/
function _updateCheckpoint(
Checkpoint memory checkpoint
) internal {
if (checkpoint.proofsRemaining != 0) {
_currentCheckpoint = checkpoint;
return;
}
// Calculate the previous total restaked balance and change in restaked balance
// Note: due to how these values are calculated, a negative `balanceDeltaGwei`
// should NEVER be greater in magnitude than `prevRestakedBalanceGwei`
uint64 prevRestakedBalanceGwei = restakedExecutionLayerGwei + checkpoint.prevBeaconBalanceGwei;
int64 balanceDeltaGwei = int64(checkpoint.podBalanceGwei) + checkpoint.balanceDeltasGwei;
// And native ETH when the checkpoint was started is now considered restaked.
// Add it to `restakedExecutionLayerGwei`, which allows it to be withdrawn via
// the `DelegationManager` withdrawal queue.
restakedExecutionLayerGwei += checkpoint.podBalanceGwei;
// Finalize the checkpoint by resetting `currentCheckpointTimestamp`.
// Note: `_currentCheckpoint` is not deleted, as it is overwritten
// when a new checkpoint is started
lastCheckpointTimestamp = currentCheckpointTimestamp;
delete currentCheckpointTimestamp;
// Convert shares and delta to wei
uint256 prevRestakedBalanceWei = prevRestakedBalanceGwei * GWEI_TO_WEI;
int256 balanceDeltaWei = balanceDeltaGwei * int256(GWEI_TO_WEI);
// Update pod owner's shares
emit CheckpointFinalized(lastCheckpointTimestamp, balanceDeltaWei);
eigenPodManager.recordBeaconChainETHBalanceUpdate({
podOwner: podOwner,
prevRestakedBalanceWei: prevRestakedBalanceWei,
balanceDeltaWei: balanceDeltaWei
});
}
function _podWithdrawalCredentials() internal view returns (bytes memory) {
return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this));
}
function _podCompoundingWithdrawalCredentials() internal view returns (bytes memory) {
return abi.encodePacked(bytes1(uint8(2)), bytes11(0), address(this));
}
///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec
function _calculateValidatorPubkeyHash(
bytes memory validatorPubkey
) internal pure returns (bytes32) {
require(validatorPubkey.length == 48, InvalidPubKeyLength());
return sha256(abi.encodePacked(validatorPubkey, bytes16(0)));
}
/**
*
* VIEW FUNCTIONS
*
*/
/// @inheritdoc IEigenPod
function withdrawableRestakedExecutionLayerGwei() external view returns (uint64) {
return restakedExecutionLayerGwei;
}
/// @notice Returns the validatorInfo for a given validatorPubkeyHash
function validatorPubkeyHashToInfo(
bytes32 validatorPubkeyHash
) external view returns (ValidatorInfo memory) {
return _validatorPubkeyHashToInfo[validatorPubkeyHash];
}
/// @notice Returns the validatorInfo for a given validatorPubkey
function validatorPubkeyToInfo(
bytes calldata validatorPubkey
) external view returns (ValidatorInfo memory) {
return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)];
}
function validatorStatus(
bytes32 pubkeyHash
) external view returns (VALIDATOR_STATUS) {
return _validatorPubkeyHashToInfo[pubkeyHash].status;
}
/// @notice Returns the validator status for a given validatorPubkey
function validatorStatus(
bytes calldata validatorPubkey
) external view returns (VALIDATOR_STATUS) {
bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey);
return _validatorPubkeyHashToInfo[validatorPubkeyHash].status;
}
/// @notice Returns the currently-active checkpoint
function currentCheckpoint() public view returns (Checkpoint memory) {
return _currentCheckpoint;
}
/// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp`
/// @param timestamp of the block for which the parent block root will be returned. MUST correspond
/// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method
/// will revert.
function getParentBlockRoot(
uint64 timestamp
) public view returns (bytes32) {
require(block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, TimestampOutOfRange());
(bool success, bytes memory result) = BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp));
require(success && result.length > 0, InvalidEIP4788Response());
return abi.decode(result, (bytes32));
}
/// @notice Returns the PROOF_TYPE depending on the `proofTimestamp` in relation to the fork timestamp.
function _getProofVersion(
uint64 proofTimestamp
) internal view returns (BeaconChainProofs.ProofVersion) {
/// Get the timestamp of the Pectra fork, read from the `EigenPodManager`
/// This returns the timestamp of the first non-missed slot at or after the Pectra hard fork
uint64 forkTimestamp = eigenPodManager.pectraForkTimestamp();
require(forkTimestamp != 0, ForkTimestampZero());
/// We check if the proofTimestamp is <= pectraForkTimestamp because a `proofTimestamp` at the `pectraForkTimestamp`
/// is considered to be Pre-Pectra given the EIP-4788 oracle returns the parent block.
return proofTimestamp <= forkTimestamp
? BeaconChainProofs.ProofVersion.DENEB
: BeaconChainProofs.ProofVersion.PECTRA;
}
}
````
## File: src/contracts/pods/EigenPodManager.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/utils/Create2.sol";
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol";
import "../libraries/SlashingLib.sol";
import "../mixins/SemVerMixin.sol";
import "../permissions/Pausable.sol";
import "./EigenPodPausingConstants.sol";
import "./EigenPodManagerStorage.sol";
/**
* @title The contract used for creating and managing EigenPods
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice The main functionalities are:
* - creating EigenPods
* - staking for new validators on EigenPods
* - keeping track of the restaked balances of all EigenPod owners
* - withdrawing eth when withdrawals are completed
*/
contract EigenPodManager is
Initializable,
OwnableUpgradeable,
Pausable,
EigenPodPausingConstants,
EigenPodManagerStorage,
ReentrancyGuardUpgradeable,
SemVerMixin
{
using SlashingLib for *;
using Math for *;
modifier onlyEigenPod(
address podOwner
) {
require(address(ownerToPod[podOwner]) == msg.sender, OnlyEigenPod());
_;
}
modifier onlyDelegationManager() {
require(msg.sender == address(delegationManager), OnlyDelegationManager());
_;
}
modifier onlyProofTimestampSetter() {
require(msg.sender == proofTimestampSetter, OnlyProofTimestampSetter());
_;
}
constructor(
IETHPOSDeposit _ethPOS,
IBeacon _eigenPodBeacon,
IDelegationManager _delegationManager,
IPauserRegistry _pauserRegistry,
string memory _version
)
EigenPodManagerStorage(_ethPOS, _eigenPodBeacon, _delegationManager)
Pausable(_pauserRegistry)
SemVerMixin(_version)
{
_disableInitializers();
}
function initialize(address initialOwner, uint256 _initPausedStatus) external initializer {
_transferOwnership(initialOwner);
_setPausedStatus(_initPausedStatus);
}
/// @inheritdoc IEigenPodManager
function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) nonReentrant returns (address) {
require(!hasPod(msg.sender), EigenPodAlreadyExists());
// deploy a pod if the sender doesn't have one already
IEigenPod pod = _deployPod();
return address(pod);
}
/// @inheritdoc IEigenPodManager
function stake(
bytes calldata pubkey,
bytes calldata signature,
bytes32 depositDataRoot
) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) nonReentrant {
IEigenPod pod = ownerToPod[msg.sender];
if (address(pod) == address(0)) {
//deploy a pod if the sender doesn't have one already
pod = _deployPod();
}
pod.stake{value: msg.value}(pubkey, signature, depositDataRoot);
}
/// @inheritdoc IEigenPodManager
function recordBeaconChainETHBalanceUpdate(
address podOwner,
uint256 prevRestakedBalanceWei,
int256 balanceDeltaWei
) external onlyEigenPod(podOwner) nonReentrant {
require(podOwner != address(0), InputAddressZero());
require(balanceDeltaWei % int256(GWEI_TO_WEI) == 0, SharesNotMultipleOfGwei());
// Negative shares only exist in certain cases where, prior to the slashing release, negative balance
// deltas were reported after a pod owner queued a withdrawal for all their shares.
//
// The new system treats negative balance deltas differently, decreasing the pod owner's slashing factor
// proportional to the decrease. This check was added to ensure the new system does not need to handle
// negative shares - instead, stakers will need to go complete any existing withdrawals before their pod
// can process a balance update.
int256 currentDepositShares = podOwnerDepositShares[podOwner];
require(currentDepositShares >= 0, LegacyWithdrawalsNotCompleted());
// Shares are only added to the pod owner's balance when `balanceDeltaWei` > 0. When a pod reports
// a negative balance delta, the pod owner's beacon chain slashing factor is decreased, devaluing
// their shares. If the delta is zero, then no action needs to be taken.
if (balanceDeltaWei > 0) {
(uint256 prevDepositShares, uint256 addedShares) = _addShares(podOwner, uint256(balanceDeltaWei));
// Update operator shares
delegationManager.increaseDelegatedShares({
staker: podOwner,
strategy: beaconChainETHStrategy,
prevDepositShares: prevDepositShares,
addedShares: addedShares
});
} else if (balanceDeltaWei < 0) {
uint64 beaconChainSlashingFactorDecrease = _reduceSlashingFactor({
podOwner: podOwner,
prevRestakedBalanceWei: prevRestakedBalanceWei,
balanceDecreasedWei: uint256(-balanceDeltaWei)
});
// Update operator shares
delegationManager.decreaseDelegatedShares({
staker: podOwner,
curDepositShares: uint256(currentDepositShares),
beaconChainSlashingFactorDecrease: beaconChainSlashingFactorDecrease
});
}
}
/**
* @notice Used by the DelegationManager to remove a pod owner's deposit shares when they enter the withdrawal queue.
* Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero.
* @dev This function reverts if it would result in `podOwnerDepositShares[podOwner]` being less than zero, i.e. it is forbidden for this function to
* result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive
* shares from the operator to whom the staker is delegated.
* @dev The delegation manager validates that the podOwner is not address(0)
* @return updatedShares the staker's deposit shares after decrement
*/
function removeDepositShares(
address staker,
IStrategy strategy,
uint256 depositSharesToRemove
) external onlyDelegationManager nonReentrant returns (uint256) {
require(strategy == beaconChainETHStrategy, InvalidStrategy());
int256 updatedShares = podOwnerDepositShares[staker] - int256(depositSharesToRemove);
require(updatedShares >= 0, SharesNegative());
podOwnerDepositShares[staker] = updatedShares;
emit NewTotalShares(staker, updatedShares);
return uint256(updatedShares);
}
/**
* @notice Increases the `podOwner`'s shares by `shares`, paying off negative shares if needed.
* Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue
* @return existingDepositShares the pod owner's shares prior to any additions. Returns 0 if negative
* @return addedShares the number of shares added to the staker's balance above 0. This means that if,
* after shares are added, the staker's balance is non-positive, this will return 0.
*/
function addShares(
address staker,
IStrategy strategy,
uint256 shares
) external onlyDelegationManager nonReentrant returns (uint256, uint256) {
require(strategy == beaconChainETHStrategy, InvalidStrategy());
return _addShares(staker, shares);
}
/**
* @notice Used by the DelegationManager to complete a withdrawal, sending tokens to the pod owner
* @dev Prioritizes decreasing the podOwner's share deficit, if they have one
* @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why
* we do not need to update the podOwnerDepositShares if `currentpodOwnerDepositShares` is positive
*/
function withdrawSharesAsTokens(
address staker,
IStrategy strategy,
IERC20,
uint256 shares
) external onlyDelegationManager nonReentrant {
require(strategy == beaconChainETHStrategy, InvalidStrategy());
require(staker != address(0), InputAddressZero());
require(int256(shares) > 0, SharesNegative());
int256 currentDepositShares = podOwnerDepositShares[staker];
uint256 sharesToWithdraw = shares;
// Negative shares only exist in certain cases where, prior to the slashing release, negative balance
// deltas were reported after a pod owner queued a withdrawal for all their shares.
//
// The new system treats negative balance deltas differently, decreasing the pod owner's slashing factor
// proportional to the decrease. This legacy codepath handles completion of withdrawals queued before
// the slashing release.
if (currentDepositShares < 0) {
uint256 currentDepositShareDeficit = uint256(-currentDepositShares);
uint256 depositSharesToAdd;
if (shares > currentDepositShareDeficit) {
// Get rid of the whole deficit and withdraw any remaining shares
depositSharesToAdd = currentDepositShareDeficit;
sharesToWithdraw = shares - currentDepositShareDeficit;
} else {
// Get rid of as much deficit as possible and don't withdraw any shares
depositSharesToAdd = shares;
sharesToWithdraw = 0;
}
int256 updatedShares = currentDepositShares + int256(depositSharesToAdd);
podOwnerDepositShares[staker] = updatedShares;
emit PodSharesUpdated(staker, int256(depositSharesToAdd));
emit NewTotalShares(staker, updatedShares);
}
// Withdraw ETH from EigenPod
if (sharesToWithdraw > 0) {
ownerToPod[staker].withdrawRestakedBeaconChainETH(staker, sharesToWithdraw);
}
}
/// @inheritdoc IShareManager
function increaseBurnableShares(IStrategy, uint256 addedSharesToBurn) external onlyDelegationManager nonReentrant {
burnableETHShares += addedSharesToBurn;
emit BurnableETHSharesIncreased(addedSharesToBurn);
}
/// @notice Sets the address that can set proof timestamps
function setProofTimestampSetter(
address newProofTimestampSetter
) external onlyOwner {
proofTimestampSetter = newProofTimestampSetter;
emit ProofTimestampSetterSet(newProofTimestampSetter);
}
/// @notice Sets the pectra fork timestamp
function setPectraForkTimestamp(
uint64 timestamp
) external onlyProofTimestampSetter {
pectraForkTimestamp = timestamp;
emit PectraForkTimestampSet(timestamp);
}
// INTERNAL FUNCTIONS
function _deployPod() internal returns (IEigenPod) {
++numPods;
// create the pod
IEigenPod pod = IEigenPod(
Create2.deploy(
0,
bytes32(uint256(uint160(msg.sender))),
// set the beacon address to the eigenPodBeacon and initialize it
abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))
)
);
pod.initialize(msg.sender);
// store the pod in the mapping
ownerToPod[msg.sender] = pod;
emit PodDeployed(address(pod), msg.sender);
return pod;
}
/// @dev Adds the shares to the staker's balance, returning their current/added shares
/// NOTE: if the staker ends with a non-positive balance, this returns (0, 0)
/// @return prevDepositShares the shares the staker had before any were added
/// @return addedShares the shares added to the staker's balance
function _addShares(address staker, uint256 shares) internal returns (uint256, uint256) {
require(staker != address(0), InputAddressZero());
require(int256(shares) >= 0, SharesNegative());
int256 sharesToAdd = int256(shares);
int256 prevDepositShares = podOwnerDepositShares[staker];
int256 updatedDepositShares = prevDepositShares + sharesToAdd;
podOwnerDepositShares[staker] = updatedDepositShares;
emit PodSharesUpdated(staker, sharesToAdd);
emit NewTotalShares(staker, updatedDepositShares);
// If we haven't added enough shares to go positive, return (0, 0)
if (updatedDepositShares <= 0) {
return (0, 0);
}
// If we have gone from negative to positive shares, return (0, positive delta)
else if (prevDepositShares < 0) {
return (0, uint256(updatedDepositShares));
}
// Else, return true previous shares and added shares
else {
return (uint256(prevDepositShares), shares);
}
}
/// @dev Calculates the proportion a pod owner's restaked balance has decreased, and
/// reduces their beacon slashing factor accordingly.
/// Note: `balanceDecreasedWei` is assumed to be less than `prevRestakedBalanceWei`
function _reduceSlashingFactor(
address podOwner,
uint256 prevRestakedBalanceWei,
uint256 balanceDecreasedWei
) internal returns (uint64) {
uint256 newRestakedBalanceWei = prevRestakedBalanceWei - balanceDecreasedWei;
uint64 prevBeaconSlashingFactor = beaconChainSlashingFactor(podOwner);
// newBeaconSlashingFactor is less than prevBeaconSlashingFactor because
// newRestakedBalanceWei < prevRestakedBalanceWei
uint64 newBeaconSlashingFactor =
uint64(prevBeaconSlashingFactor.mulDiv(newRestakedBalanceWei, prevRestakedBalanceWei));
uint64 beaconChainSlashingFactorDecrease = prevBeaconSlashingFactor - newBeaconSlashingFactor;
_beaconChainSlashingFactor[podOwner] =
BeaconChainSlashingFactor({slashingFactor: newBeaconSlashingFactor, isSet: true});
emit BeaconChainSlashingFactorDecreased(podOwner, prevBeaconSlashingFactor, newBeaconSlashingFactor);
return beaconChainSlashingFactorDecrease;
}
// VIEW FUNCTIONS
/// @inheritdoc IEigenPodManager
function getPod(
address podOwner
) public view returns (IEigenPod) {
IEigenPod pod = ownerToPod[podOwner];
// if pod does not exist already, calculate what its address *will be* once it is deployed
if (address(pod) == address(0)) {
pod = IEigenPod(
Create2.computeAddress(
bytes32(uint256(uint160(podOwner))), //salt
keccak256(abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))) //bytecode
)
);
}
return pod;
}
/// @inheritdoc IEigenPodManager
function hasPod(
address podOwner
) public view returns (bool) {
return address(ownerToPod[podOwner]) != address(0);
}
/// @notice Returns the current shares of `user` in `strategy`
/// @dev strategy must be beaconChainETHStrategy
/// @dev returns 0 if the user has negative shares
function stakerDepositShares(address user, IStrategy strategy) public view returns (uint256 depositShares) {
require(strategy == beaconChainETHStrategy, InvalidStrategy());
return podOwnerDepositShares[user] < 0 ? 0 : uint256(podOwnerDepositShares[user]);
}
/// @inheritdoc IEigenPodManager
function beaconChainSlashingFactor(
address podOwner
) public view returns (uint64) {
BeaconChainSlashingFactor memory bsf = _beaconChainSlashingFactor[podOwner];
return bsf.isSet ? bsf.slashingFactor : WAD;
}
}
````
## File: src/contracts/pods/EigenPodManagerStorage.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/proxy/beacon/IBeacon.sol";
import "../interfaces/IStrategy.sol";
import "../interfaces/IEigenPodManager.sol";
import "../interfaces/IStrategyManager.sol";
import "../interfaces/IDelegationManager.sol";
import "../interfaces/IETHPOSDeposit.sol";
import "../interfaces/IEigenPod.sol";
abstract contract EigenPodManagerStorage is IEigenPodManager {
/**
*
* CONSTANTS / IMMUTABLES
*
*/
/// @notice The ETH2 Deposit Contract
IETHPOSDeposit public immutable ethPOS;
/// @notice Beacon proxy to which the EigenPods point
IBeacon public immutable eigenPodBeacon;
/// @notice EigenLayer's DelegationManager contract
IDelegationManager public immutable delegationManager;
/**
* @notice Stored code of type(BeaconProxy).creationCode
* @dev Maintained as a constant to solve an edge case - changes to OpenZeppelin's BeaconProxy code should not cause
* addresses of EigenPods that are pre-computed with Create2 to change, even upon upgrading this contract, changing compiler version, etc.
*/
bytes internal constant beaconProxyBytecode =
hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564";
// @notice Internal constant used in calculations, since the beacon chain stores balances in Gwei rather than wei
uint256 internal constant GWEI_TO_WEI = 1e9;
/// @notice Canonical, virtual beacon chain ETH strategy
IStrategy public constant beaconChainETHStrategy = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);
/**
*
* STATE VARIABLES
*
*/
/// @notice [DEPRECATED] Previously used to query beacon block roots. We now use eip-4788 directly
address internal __deprecated_beaconChainOracle;
/// @notice Pod owner to deployed EigenPod address
mapping(address podOwner => IEigenPod) public ownerToPod;
// BEGIN STORAGE VARIABLES ADDED AFTER FIRST TESTNET DEPLOYMENT -- DO NOT SUGGEST REORDERING TO CONVENTIONAL ORDER
/// @notice The number of EigenPods that have been deployed
uint256 public numPods;
/// @notice [DEPRECATED] Was initially used to limit growth early on but there is no longer
/// a maximum number of EigenPods that can be deployed.
uint256 private __deprecated_maxPods;
// BEGIN STORAGE VARIABLES ADDED AFTER MAINNET DEPLOYMENT -- DO NOT SUGGEST REORDERING TO CONVENTIONAL ORDER
/**
* @notice mapping from pod owner to the deposit shares they have in the virtual beacon chain ETH strategy
*
* @dev When an EigenPod registers a balance increase, deposit shares are increased. When registering a balance
* decrease, however, deposit shares are NOT decreased. Instead, the pod owner's beacon chain slashing factor
* is decreased proportional to the balance decrease. This impacts the number of shares that will be withdrawn
* when the deposit shares are queued for withdrawal in the DelegationManager.
*
* Note that prior to the slashing release, deposit shares were decreased when balance decreases occurred.
* In certain cases, a combination of queueing a withdrawal plus registering a balance decrease could result
* in a staker having negative deposit shares in this mapping. This negative value would be corrected when the
* staker completes a withdrawal (as tokens or as shares).
*
* With the slashing release, negative shares are no longer possible. However, a staker can still have negative
* shares if they met the conditions for them before the slashing release. If this is the case, that staker
* should complete any outstanding queued withdrawal in the DelegationManager ("as shares"). This will correct
* the negative share count and allow the staker to continue using their pod as normal.
*/
mapping(address podOwner => int256 shares) public podOwnerDepositShares;
uint64 internal __deprecated_denebForkTimestamp;
/// @notice Returns the slashing factor applied to the `staker` for the `beaconChainETHStrategy`
/// Note: this value starts at 1 WAD (1e18) for all stakers, and is updated when a staker's pod registers
/// a balance decrease.
mapping(address staker => BeaconChainSlashingFactor) internal _beaconChainSlashingFactor;
/// @notice Returns the amount of `shares` that have been slashed on EigenLayer but not burned yet.
uint256 public burnableETHShares;
/// @notice The address that can set proof timestamps
address public proofTimestampSetter;
/// @notice The timestamp of the Pectra proof
uint64 public pectraForkTimestamp;
constructor(IETHPOSDeposit _ethPOS, IBeacon _eigenPodBeacon, IDelegationManager _delegationManager) {
ethPOS = _ethPOS;
eigenPodBeacon = _eigenPodBeacon;
delegationManager = _delegationManager;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[41] private __gap;
}
````
## File: src/contracts/pods/EigenPodPausingConstants.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
/**
* @title Constants shared between 'EigenPod' and 'EigenPodManager' contracts.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
abstract contract EigenPodPausingConstants {
/// @notice Index for flag that pauses creation of new EigenPods when set. See EigenPodManager code for details.
uint8 internal constant PAUSED_NEW_EIGENPODS = 0;
/**
* @notice Index for flag that pauses all withdrawal-of-restaked ETH related functionality `
* function *of the EigenPodManager* when set. See EigenPodManager code for details.
*/
uint8 internal constant PAUSED_WITHDRAW_RESTAKED_ETH = 1;
/// @notice Index for flag that pauses the deposit related functions *of the EigenPods* when set. see EigenPod code for details.
uint8 internal constant PAUSED_EIGENPODS_VERIFY_CREDENTIALS = 2;
// Deprecated
// uint8 internal constant PAUSED_EIGENPODS_VERIFY_BALANCE_UPDATE = 3;
// Deprecated
// uint8 internal constant PAUSED_EIGENPODS_VERIFY_WITHDRAWAL = 4;
/// @notice Pausability for EigenPod's "accidental transfer" withdrawal methods
uint8 internal constant PAUSED_NON_PROOF_WITHDRAWALS = 5;
uint8 internal constant PAUSED_START_CHECKPOINT = 6;
/// @notice Index for flag that pauses the `verifyCheckpointProofs` function *of the EigenPods* when set. see EigenPod code for details.
uint8 internal constant PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS = 7;
uint8 internal constant PAUSED_VERIFY_STALE_BALANCE = 8;
}
````
## File: src/contracts/pods/EigenPodStorage.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IEigenPod.sol";
abstract contract EigenPodStorage is IEigenPod {
/// @notice The owner of this EigenPod
address public podOwner;
/// @notice DEPRECATED: previously used to track the time when restaking was activated
uint64 internal __deprecated_mostRecentWithdrawalTimestamp;
/// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from the Beacon Chain but not from EigenLayer),
uint64 internal restakedExecutionLayerGwei;
/// @notice DEPRECATED: previously used to track whether a pod had activated restaking
bool internal __deprecated_hasRestaked;
/// @notice DEPRECATED: previously tracked withdrawals proven per validator
mapping(bytes32 => mapping(uint64 => bool)) internal __deprecated_provenWithdrawal;
/// @notice This is a mapping that tracks a validator's information by their pubkey hash
mapping(bytes32 => ValidatorInfo) internal _validatorPubkeyHashToInfo;
/// @notice DEPRECATED: previously used to track ETH sent to the fallback function
uint256 internal __deprecated_nonBeaconChainETHBalanceWei;
/// @notice DEPRECATED: previously used to track claimed partial withdrawals
uint64 __deprecated_sumOfPartialWithdrawalsClaimedGwei;
/// @notice Number of validators with proven withdrawal credentials, who do not have proven full withdrawals
uint256 public activeValidatorCount;
/// @notice The timestamp of the last checkpoint finalized
uint64 public lastCheckpointTimestamp;
/// @notice The timestamp of the currently-active checkpoint. Will be 0 if there is not active checkpoint
uint64 public currentCheckpointTimestamp;
/// @notice For each checkpoint, the total balance attributed to exited validators, in gwei
///
/// NOTE that the values added to this mapping are NOT guaranteed to capture the entirety of a validator's
/// exit - rather, they capture the total change in a validator's balance when a checkpoint shows their
/// balance change from nonzero to zero. While a change from nonzero to zero DOES guarantee that a validator
/// has been fully exited, it is possible that the magnitude of this change does not capture what is
/// typically thought of as a "full exit."
///
/// For example:
/// 1. Consider a validator was last checkpointed at 32 ETH before exiting. Once the exit has been processed,
/// it is expected that the validator's exited balance is calculated to be `32 ETH`.
/// 2. However, before `startCheckpoint` is called, a deposit is made to the validator for 1 ETH. The beacon
/// chain will automatically withdraw this ETH, but not until the withdrawal sweep passes over the validator
/// again. Until this occurs, the validator's current balance (used for checkpointing) is 1 ETH.
/// 3. If `startCheckpoint` is called at this point, the balance delta calculated for this validator will be
/// `-31 ETH`, and because the validator has a nonzero balance, it is not marked WITHDRAWN.
/// 4. After the exit is processed by the beacon chain, a subsequent `startCheckpoint` and checkpoint proof
/// will calculate a balance delta of `-1 ETH` and attribute a 1 ETH exit to the validator.
///
/// If this edge case impacts your usecase, it should be possible to mitigate this by monitoring for deposits
/// to your exited validators, and waiting to call `startCheckpoint` until those deposits have been automatically
/// exited.
///
/// Additional edge cases this mapping does not cover:
/// - If a validator is slashed, their balance exited will reflect their original balance rather than the slashed amount
/// - The final partial withdrawal for an exited validator will be likely be included in this mapping.
/// i.e. if a validator was last checkpointed at 32.1 ETH before exiting, the next checkpoint will calculate their
/// "exited" amount to be 32.1 ETH rather than 32 ETH.
mapping(uint64 => uint64) public checkpointBalanceExitedGwei;
/// @notice The current checkpoint, if there is one active
Checkpoint internal _currentCheckpoint;
/// @notice An address with permissions to call `startCheckpoint` and `verifyWithdrawalCredentials`, set
/// by the podOwner. This role exists to allow a podOwner to designate a hot wallet that can call
/// these methods, allowing the podOwner to remain a cold wallet that is only used to manage funds.
/// @dev If this address is NOT set, only the podOwner can call `startCheckpoint` and `verifyWithdrawalCredentials`
address public proofSubmitter;
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[35] private __gap;
}
````
## File: src/contracts/strategies/EigenStrategy.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
// NOTE: Mainnet uses the OpenZeppelin v4.9.0 contracts, but this imports the 4.7.1 version. This will be changed after an upgrade.
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../interfaces/IStrategyManager.sol";
import "../strategies/StrategyBase.sol";
import "../interfaces/IEigen.sol";
/**
* @title Eigen Strategy implementation of `IStrategy` interface, designed to be inherited from by more complex strategies.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @dev Note that this EigenStrategy contract is designed to be compatible with both bEIGEN and EIGEN tokens. It functions exactly the same
* as the `StrategyBase` contract if bEIGEN were the underlying token, but also allows for depositing and withdrawing EIGEN tokens. This is
* achieved by unwrapping EIGEN into bEIGEN upon deposit, and wrapping bEIGEN into EIGEN upon withdrawal. Deposits and withdrawals with bEIGEN
* does not perform and wrapping or unwrapping.
* @notice This contract functions similarly to an ERC4626 vault, only without issuing a token.
* To mitigate against the common "inflation attack" vector, we have chosen to use the 'virtual shares' mitigation route,
* similar to [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol).
* We acknowledge that this mitigation has the known downside of the virtual shares causing some losses to users, which are pronounced
* particularly in the case of the share exchange rate changing significantly, either positively or negatively.
* For a fairly thorough discussion of this issue and our chosen mitigation strategy, we recommend reading through
* [this thread](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3706) on the OpenZeppelin repo.
* We specifically use a share offset of `SHARES_OFFSET` and a balance offset of `BALANCE_OFFSET`.
*/
contract EigenStrategy is StrategyBase {
using SafeERC20 for IERC20;
/**
* @notice EIGEN can be deposited into this strategy, where it is unwrapped into bEIGEN and staked in
* this strategy contract. EIGEN can also be withdrawn by withdrawing bEIGEN from this strategy, and
* then wrapping it back into EIGEN.
*/
IEigen public EIGEN;
/// @notice Since this contract is designed to be initializable, the constructor simply sets `strategyManager`, the only immutable variable.
constructor(
IStrategyManager _strategyManager,
IPauserRegistry _pauserRegistry,
string memory _version
) StrategyBase(_strategyManager, _pauserRegistry, _version) {}
function initialize(IEigen _EIGEN, IERC20 _bEIGEN) public virtual initializer {
EIGEN = _EIGEN;
_initializeStrategyBase(_bEIGEN);
}
/**
* @notice This function hook is called in EigenStrategy.deposit() and is overridden here to
* allow for depositing of either EIGEN or bEIGEN tokens. If token is bEIGEN aka the underlyingToken,
* then the contract functions exactly the same as the StrategyBase contract and the deposit is calculated into shares.
* If token is EIGEN, then the EIGEN is first 1-1 unwrapped into bEIGEN and the deposit shares are calculated as normal.
* @param token token to be deposited, can be either EIGEN or bEIGEN. If EIGEN, then is unwrapped into bEIGEN
* @param amount deposit amount
*/
function _beforeDeposit(IERC20 token, uint256 amount) internal virtual override {
require(token == underlyingToken || token == EIGEN, OnlyUnderlyingToken());
if (token == EIGEN) {
// unwrap EIGEN into bEIGEN assuming a 1-1 unwrapping amount
// the strategy will then hold `amount` of bEIGEN
EIGEN.unwrap(amount);
}
}
/**
* @notice This function hook is called in EigenStrategy.withdraw() before withdrawn shares are calculated and is
* overridden here to allow for withdrawing shares either into EIGEN or bEIGEN tokens. If wrapping bEIGEN into EIGEN is needed,
* it is performed in _afterWithdrawal(). This hook just checks the token parameter is either EIGEN or bEIGEN.
* @param token token to be withdrawn, can be either EIGEN or bEIGEN. If EIGEN, then bEIGEN is wrapped into EIGEN
*/
function _beforeWithdrawal(
address, /*recipient*/
IERC20 token,
uint256 /*amountShares*/
) internal virtual override {
require(token == underlyingToken || token == EIGEN, OnlyUnderlyingToken());
}
/**
* @notice This function hook is called in EigenStrategy.withdraw() after withdrawn shares are calculated and is
* overridden here to allow for withdrawing shares either into EIGEN or bEIGEN tokens. If token is bEIGEN aka the underlyingToken,
* then the contract functions exactly the same as the StrategyBase contract and transfers out bEIGEN to the recipient.
* If token is EIGEN, then bEIGEN is first 1-1 wrapped into EIGEN and the strategy transfers out the EIGEN to the recipient.
* @param recipient recipient of the withdrawal
* @param token token to be withdrawn, can be either EIGEN or bEIGEN. If EIGEN, then bEIGEN is wrapped into EIGEN
* @param amountToSend amount of tokens to transfer
*/
function _afterWithdrawal(address recipient, IERC20 token, uint256 amountToSend) internal virtual override {
if (token == EIGEN) {
// wrap bEIGEN into EIGEN assuming a 1-1 wrapping amount
// the strategy will then hold `amountToSend` of EIGEN
underlyingToken.approve(address(token), amountToSend);
EIGEN.wrap(amountToSend);
}
// Whether the withdrawal specified EIGEN or bEIGEN, the strategy
// holds the correct balance and can transfer to the recipient here
token.safeTransfer(recipient, amountToSend);
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[49] private __gap;
}
````
## File: src/contracts/strategies/StrategyBase.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IStrategyManager.sol";
import "../permissions/Pausable.sol";
import "../mixins/SemVerMixin.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
/**
* @title Base implementation of `IStrategy` interface, designed to be inherited from by more complex strategies.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice Simple, basic, "do-nothing" Strategy that holds a single underlying token and returns it on withdrawals.
* Implements minimal versions of the IStrategy functions, this contract is designed to be inherited by
* more complex strategies, which can then override its functions as necessary.
* @dev Note that some functions have their mutability restricted; developers inheriting from this contract cannot broaden
* the mutability without modifying this contract itself.
* @dev This contract is expressly *not* intended for use with 'fee-on-transfer'-type tokens.
* Setting the `underlyingToken` to be a fee-on-transfer token may result in improper accounting.
* @notice This contract functions similarly to an ERC4626 vault, only without issuing a token.
* To mitigate against the common "inflation attack" vector, we have chosen to use the 'virtual shares' mitigation route,
* similar to [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol).
* We acknowledge that this mitigation has the known downside of the virtual shares causing some losses to users, which are pronounced
* particularly in the case of the share exchange rate changing significantly, either positively or negatively.
* For a fairly thorough discussion of this issue and our chosen mitigation strategy, we recommend reading through
* [this thread](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3706) on the OpenZeppelin repo.
* We specifically use a share offset of `SHARES_OFFSET` and a balance offset of `BALANCE_OFFSET`.
*/
contract StrategyBase is Initializable, Pausable, IStrategy, SemVerMixin {
using SafeERC20 for IERC20;
uint8 internal constant PAUSED_DEPOSITS = 0;
uint8 internal constant PAUSED_WITHDRAWALS = 1;
/**
* @notice virtual shares used as part of the mitigation of the common 'share inflation' attack vector.
* Constant value chosen to reasonably reduce attempted share inflation by the first depositor, while still
* incurring reasonably small losses to depositors
*/
uint256 internal constant SHARES_OFFSET = 1e3;
/**
* @notice virtual balance used as part of the mitigation of the common 'share inflation' attack vector
* Constant value chosen to reasonably reduce attempted share inflation by the first depositor, while still
* incurring reasonably small losses to depositors
*/
uint256 internal constant BALANCE_OFFSET = 1e3;
/**
* @notice The maximum total shares for a given strategy
* @dev This constant prevents overflow in offchain services for rewards
*/
uint256 internal constant MAX_TOTAL_SHARES = 1e38 - 1;
/// @notice EigenLayer's StrategyManager contract
IStrategyManager public immutable strategyManager;
/// @notice The underlying token for shares in this Strategy
IERC20 public underlyingToken;
/// @notice The total number of extant shares in this Strategy
uint256 public totalShares;
/// @notice Simply checks that the `msg.sender` is the `strategyManager`, which is an address stored immutably at construction.
modifier onlyStrategyManager() {
require(msg.sender == address(strategyManager), OnlyStrategyManager());
_;
}
/// @notice Since this contract is designed to be initializable, the constructor simply sets `strategyManager`, the only immutable variable.
constructor(
IStrategyManager _strategyManager,
IPauserRegistry _pauserRegistry,
string memory _version
) Pausable(_pauserRegistry) SemVerMixin(_version) {
strategyManager = _strategyManager;
_disableInitializers();
}
function initialize(
IERC20 _underlyingToken
) public virtual initializer {
_initializeStrategyBase(_underlyingToken);
}
/// @notice Sets the `underlyingToken` and `pauserRegistry` for the strategy.
function _initializeStrategyBase(
IERC20 _underlyingToken
) internal onlyInitializing {
underlyingToken = _underlyingToken;
_setPausedStatus(_UNPAUSE_ALL);
emit StrategyTokenSet(underlyingToken, IERC20Metadata(address(_underlyingToken)).decimals());
}
/**
* @notice Used to deposit tokens into this Strategy
* @param token is the ERC20 token being deposited
* @param amount is the amount of token being deposited
* @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's
* `depositIntoStrategy` function, and individual share balances are recorded in the strategyManager as well.
* @dev Note that the assumption is made that `amount` of `token` has already been transferred directly to this contract
* (as performed in the StrategyManager's deposit functions). In particular, setting the `underlyingToken` of this contract
* to be a fee-on-transfer token will break the assumption that the amount this contract *received* of the token is equal to
* the amount that was input when the transfer was performed (i.e. the amount transferred 'out' of the depositor's balance).
* @dev Note that any validation of `token` is done inside `_beforeDeposit`. This can be overridden if needed.
* @return newShares is the number of new shares issued at the current exchange ratio.
*/
function deposit(
IERC20 token,
uint256 amount
) external virtual override onlyWhenNotPaused(PAUSED_DEPOSITS) onlyStrategyManager returns (uint256 newShares) {
// call hook to allow for any pre-deposit logic
_beforeDeposit(token, amount);
// copy `totalShares` value to memory, prior to any change
uint256 priorTotalShares = totalShares;
/**
* @notice calculation of newShares *mirrors* `underlyingToShares(amount)`, but is different since the balance of `underlyingToken`
* has already been increased due to the `strategyManager` transferring tokens to this strategy prior to calling this function
*/
// account for virtual shares and balance
uint256 virtualShareAmount = priorTotalShares + SHARES_OFFSET;
uint256 virtualTokenBalance = _tokenBalance() + BALANCE_OFFSET;
// calculate the prior virtual balance to account for the tokens that were already transferred to this contract
uint256 virtualPriorTokenBalance = virtualTokenBalance - amount;
newShares = (amount * virtualShareAmount) / virtualPriorTokenBalance;
// extra check for correctness / against edge case where share rate can be massively inflated as a 'griefing' sort of attack
require(newShares != 0, NewSharesZero());
// update total share amount to account for deposit
totalShares = (priorTotalShares + newShares);
require(totalShares <= MAX_TOTAL_SHARES, TotalSharesExceedsMax());
// emit exchange rate
_emitExchangeRate(virtualTokenBalance, totalShares + SHARES_OFFSET);
return newShares;
}
/**
* @notice Used to withdraw tokens from this Strategy, to the `recipient`'s address
* @param recipient is the address to receive the withdrawn funds
* @param token is the ERC20 token being transferred out
* @param amountShares is the amount of shares being withdrawn
* @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's
* other functions, and individual share balances are recorded in the strategyManager as well.
* @dev Note that any validation of `token` is done inside `_beforeWithdrawal`. This can be overridden if needed.
*/
function withdraw(
address recipient,
IERC20 token,
uint256 amountShares
) external virtual override onlyWhenNotPaused(PAUSED_WITHDRAWALS) onlyStrategyManager {
// call hook to allow for any pre-withdrawal logic
_beforeWithdrawal(recipient, token, amountShares);
// copy `totalShares` value to memory, prior to any change
uint256 priorTotalShares = totalShares;
require(amountShares <= priorTotalShares, WithdrawalAmountExceedsTotalDeposits());
/**
* @notice calculation of amountToSend *mirrors* `sharesToUnderlying(amountShares)`, but is different since the `totalShares` has already
* been decremented. Specifically, notice how we use `priorTotalShares` here instead of `totalShares`.
*/
// account for virtual shares and balance
uint256 virtualPriorTotalShares = priorTotalShares + SHARES_OFFSET;
uint256 virtualTokenBalance = _tokenBalance() + BALANCE_OFFSET;
// calculate ratio based on virtual shares and balance, being careful to multiply before dividing
uint256 amountToSend = (virtualTokenBalance * amountShares) / virtualPriorTotalShares;
// Decrease the `totalShares` value to reflect the withdrawal
totalShares = priorTotalShares - amountShares;
// emit exchange rate
_emitExchangeRate(virtualTokenBalance - amountToSend, totalShares + SHARES_OFFSET);
_afterWithdrawal(recipient, token, amountToSend);
}
/**
* @notice Called in the external `deposit` function, before any logic is executed. Expected to be overridden if strategies want such logic.
* @param token The token being deposited
*/
function _beforeDeposit(
IERC20 token,
uint256 // amount
) internal virtual {
require(token == underlyingToken, OnlyUnderlyingToken());
}
/**
* @notice Called in the external `withdraw` function, before any logic is executed. Expected to be overridden if strategies want such logic.
* @param token The token being withdrawn
*/
function _beforeWithdrawal(
address, // recipient
IERC20 token,
uint256 // amountShares
) internal virtual {
require(token == underlyingToken, OnlyUnderlyingToken());
}
/**
* @notice Transfers tokens to the recipient after a withdrawal is processed
* @dev Called in the external `withdraw` function after all logic is executed
* @param recipient The destination of the tokens
* @param token The ERC20 being transferred
* @param amountToSend The amount of `token` to transfer
*/
function _afterWithdrawal(address recipient, IERC20 token, uint256 amountToSend) internal virtual {
token.safeTransfer(recipient, amountToSend);
}
/// @inheritdoc IStrategy
function explanation() external pure virtual override returns (string memory) {
return "Base Strategy implementation to inherit from for more complex implementations";
}
/// @inheritdoc IStrategy
function sharesToUnderlyingView(
uint256 amountShares
) public view virtual override returns (uint256) {
// account for virtual shares and balance
uint256 virtualTotalShares = totalShares + SHARES_OFFSET;
uint256 virtualTokenBalance = _tokenBalance() + BALANCE_OFFSET;
// calculate ratio based on virtual shares and balance, being careful to multiply before dividing
return (virtualTokenBalance * amountShares) / virtualTotalShares;
}
/// @inheritdoc IStrategy
function sharesToUnderlying(
uint256 amountShares
) public view virtual override returns (uint256) {
return sharesToUnderlyingView(amountShares);
}
/// @inheritdoc IStrategy
function underlyingToSharesView(
uint256 amountUnderlying
) public view virtual returns (uint256) {
// account for virtual shares and balance
uint256 virtualTotalShares = totalShares + SHARES_OFFSET;
uint256 virtualTokenBalance = _tokenBalance() + BALANCE_OFFSET;
// calculate ratio based on virtual shares and balance, being careful to multiply before dividing
return (amountUnderlying * virtualTotalShares) / virtualTokenBalance;
}
/// @inheritdoc IStrategy
function underlyingToShares(
uint256 amountUnderlying
) external view virtual returns (uint256) {
return underlyingToSharesView(amountUnderlying);
}
/// @inheritdoc IStrategy
function userUnderlyingView(
address user
) external view virtual returns (uint256) {
return sharesToUnderlyingView(shares(user));
}
/// @inheritdoc IStrategy
function userUnderlying(
address user
) external virtual returns (uint256) {
return sharesToUnderlying(shares(user));
}
/// @inheritdoc IStrategy
function shares(
address user
) public view virtual returns (uint256) {
return strategyManager.stakerDepositShares(user, IStrategy(address(this)));
}
/// @notice Internal function used to fetch this contract's current balance of `underlyingToken`.
// slither-disable-next-line dead-code
function _tokenBalance() internal view virtual returns (uint256) {
return underlyingToken.balanceOf(address(this));
}
/// @notice Internal function used to emit the exchange rate of the strategy in wad (18 decimals)
/// @dev Tokens that do not have 18 decimals must have offchain services scale the exchange rate down to proper magnitude
function _emitExchangeRate(uint256 virtualTokenBalance, uint256 virtualTotalShares) internal {
// Emit asset over shares ratio.
emit ExchangeRateEmitted((1e18 * virtualTokenBalance) / virtualTotalShares);
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[48] private __gap;
}
````
## File: src/contracts/strategies/StrategyBaseTVLLimits.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "./StrategyBase.sol";
/**
* @title A Strategy implementation inheriting from `StrategyBase` that limits the total amount of deposits it will accept.
* @dev Note that this implementation still converts between any amount of shares or underlying tokens in its view functions;
* these functions purposefully do not take the TVL limit into account.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
contract StrategyBaseTVLLimits is StrategyBase {
/// The maximum deposit (in underlyingToken) that this strategy will accept per deposit
uint256 public maxPerDeposit;
/// The maximum deposits (in underlyingToken) that this strategy will hold
uint256 public maxTotalDeposits;
/// @notice Emitted when `maxPerDeposit` value is updated from `previousValue` to `newValue`
event MaxPerDepositUpdated(uint256 previousValue, uint256 newValue);
/// @notice Emitted when `maxTotalDeposits` value is updated from `previousValue` to `newValue`
event MaxTotalDepositsUpdated(uint256 previousValue, uint256 newValue);
// solhint-disable-next-line no-empty-blocks
constructor(
IStrategyManager _strategyManager,
IPauserRegistry _pauserRegistry,
string memory _version
) StrategyBase(_strategyManager, _pauserRegistry, _version) {}
function initialize(
uint256 _maxPerDeposit,
uint256 _maxTotalDeposits,
IERC20 _underlyingToken
) public virtual initializer {
_setTVLLimits(_maxPerDeposit, _maxTotalDeposits);
_initializeStrategyBase(_underlyingToken);
}
/**
* @notice Sets the maximum deposits (in underlyingToken) that this strategy will hold and accept per deposit
* @param newMaxTotalDeposits The new maximum deposits
* @dev Callable only by the unpauser of this contract
* @dev We note that there is a potential race condition between a call to this function that lowers either or both of these limits and call(s)
* to `deposit`, that may result in some calls to `deposit` reverting.
*/
function setTVLLimits(uint256 newMaxPerDeposit, uint256 newMaxTotalDeposits) external onlyUnpauser {
_setTVLLimits(newMaxPerDeposit, newMaxTotalDeposits);
}
/// @notice Simple getter function that returns the current values of `maxPerDeposit` and `maxTotalDeposits`.
function getTVLLimits() external view returns (uint256, uint256) {
return (maxPerDeposit, maxTotalDeposits);
}
/// @notice Internal setter for TVL limits
function _setTVLLimits(uint256 newMaxPerDeposit, uint256 newMaxTotalDeposits) internal {
emit MaxPerDepositUpdated(maxPerDeposit, newMaxPerDeposit);
emit MaxTotalDepositsUpdated(maxTotalDeposits, newMaxTotalDeposits);
require(newMaxPerDeposit <= newMaxTotalDeposits, MaxPerDepositExceedsMax());
maxPerDeposit = newMaxPerDeposit;
maxTotalDeposits = newMaxTotalDeposits;
}
/**
* @notice Called in the external `deposit` function, before any logic is executed. Makes sure that deposits don't exceed configured maximum.
* @dev Unused token param is the token being deposited. This is already checked in the `deposit` function.
* @dev Note that the `maxTotalDeposits` is purely checked against the current `_tokenBalance()`, since by this point in the deposit flow, the
* tokens should have already been transferred to this Strategy by the StrategyManager
* @dev We note as well that this makes it possible for various race conditions to occur:
* a) multiple simultaneous calls to `deposit` may result in some of these calls reverting due to `maxTotalDeposits` being reached.
* b) transferring funds directly to this Strategy (although not generally in someone's economic self interest) in order to reach `maxTotalDeposits`
* is a route by which someone can cause calls to `deposit` to revert.
* c) increases in the token balance of this contract through other effects – including token rebasing – may cause similar issues to (a) and (b).
* @param amount The amount of `token` being deposited
*/
function _beforeDeposit(IERC20 token, uint256 amount) internal virtual override {
require(amount <= maxPerDeposit, MaxPerDepositExceedsMax());
require(_tokenBalance() <= maxTotalDeposits, BalanceExceedsMaxTotalDeposits());
super._beforeDeposit(token, amount);
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[48] private __gap;
}
````
## File: src/contracts/strategies/StrategyFactory.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
import "../mixins/SemVerMixin.sol";
import "./StrategyFactoryStorage.sol";
import "./StrategyBase.sol";
import "../permissions/Pausable.sol";
/**
* @title Factory contract for deploying BeaconProxies of a Strategy contract implementation for arbitrary ERC20 tokens
* and automatically adding them to the StrategyWhitelist in EigenLayer.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @dev This may not be compatible with non-standard ERC20 tokens. Caution is warranted.
*/
contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable, SemVerMixin {
uint8 internal constant PAUSED_NEW_STRATEGIES = 0;
/// @notice EigenLayer's StrategyManager contract
IStrategyManager public immutable strategyManager;
/// @notice Since this contract is designed to be initializable, the constructor simply sets the immutable variables.
constructor(
IStrategyManager _strategyManager,
IPauserRegistry _pauserRegistry,
string memory _version
) Pausable(_pauserRegistry) SemVerMixin(_version) {
strategyManager = _strategyManager;
_disableInitializers();
}
function initialize(
address _initialOwner,
uint256 _initialPausedStatus,
IBeacon _strategyBeacon
) public virtual initializer {
_transferOwnership(_initialOwner);
_setPausedStatus(_initialPausedStatus);
_setStrategyBeacon(_strategyBeacon);
}
/**
* @notice Deploy a new StrategyBase contract for the ERC20 token, using a beacon proxy
* @dev A strategy contract must not yet exist for the token.
* @dev Immense caution is warranted for non-standard ERC20 tokens, particularly "reentrant" tokens
* like those that conform to ERC777.
*/
function deployNewStrategy(
IERC20 token
) external onlyWhenNotPaused(PAUSED_NEW_STRATEGIES) returns (IStrategy newStrategy) {
require(!isBlacklisted[token], BlacklistedToken());
require(deployedStrategies[token] == IStrategy(address(0)), StrategyAlreadyExists());
IStrategy strategy = IStrategy(
address(
new BeaconProxy(
address(strategyBeacon), abi.encodeWithSelector(StrategyBase.initialize.selector, token)
)
)
);
_setStrategyForToken(token, strategy);
IStrategy[] memory strategiesToWhitelist = new IStrategy[](1);
strategiesToWhitelist[0] = strategy;
strategyManager.addStrategiesToDepositWhitelist(strategiesToWhitelist);
return strategy;
}
/**
* @notice Owner-only function to prevent strategies from being created for given tokens.
* @param tokens An array of token addresses to blacklist.
*/
function blacklistTokens(
IERC20[] calldata tokens
) external onlyOwner {
IStrategy[] memory strategiesToRemove = new IStrategy[](tokens.length);
uint256 removeIdx = 0;
for (uint256 i; i < tokens.length; ++i) {
require(!isBlacklisted[tokens[i]], AlreadyBlacklisted());
isBlacklisted[tokens[i]] = true;
emit TokenBlacklisted(tokens[i]);
// If someone has already deployed a strategy for this token, add it
// to the list of strategies to remove from the StrategyManager whitelist
IStrategy deployed = deployedStrategies[tokens[i]];
if (deployed != IStrategy(address(0))) {
strategiesToRemove[removeIdx] = deployed;
removeIdx++;
}
}
// Manually adjust length to remove unused entries
// New length == removeIdx
assembly {
mstore(strategiesToRemove, removeIdx)
}
if (strategiesToRemove.length > 0) {
strategyManager.removeStrategiesFromDepositWhitelist(strategiesToRemove);
}
}
/**
* @notice Owner-only function to pass through a call to `StrategyManager.addStrategiesToDepositWhitelist`
*/
function whitelistStrategies(
IStrategy[] calldata strategiesToWhitelist
) external onlyOwner {
strategyManager.addStrategiesToDepositWhitelist(strategiesToWhitelist);
}
/**
* @notice Owner-only function to pass through a call to `StrategyManager.removeStrategiesFromDepositWhitelist`
*/
function removeStrategiesFromWhitelist(
IStrategy[] calldata strategiesToRemoveFromWhitelist
) external onlyOwner {
strategyManager.removeStrategiesFromDepositWhitelist(strategiesToRemoveFromWhitelist);
}
function _setStrategyForToken(IERC20 token, IStrategy strategy) internal {
deployedStrategies[token] = strategy;
emit StrategySetForToken(token, strategy);
}
function _setStrategyBeacon(
IBeacon _strategyBeacon
) internal {
emit StrategyBeaconModified(strategyBeacon, _strategyBeacon);
strategyBeacon = _strategyBeacon;
}
}
````
## File: src/contracts/strategies/StrategyFactoryStorage.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IStrategyFactory.sol";
/**
* @title Storage for the StrategyFactory contract.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
abstract contract StrategyFactoryStorage is IStrategyFactory {
/// @notice Upgradeable beacon which new Strategies deployed by this contract point to
IBeacon public strategyBeacon;
/// @notice Mapping token => Strategy contract for the token
/// The strategies in this mapping are deployed by the StrategyFactory.
/// The factory can only deploy a single strategy per token address
/// These strategies MIGHT not be whitelisted in the StrategyManager,
/// though deployNewStrategy does whitelist by default.
/// These strategies MIGHT not be the only strategy for the underlying token
/// as additional strategies can be whitelisted by the owner of the factory.
mapping(IERC20 => IStrategy) public deployedStrategies;
/// @notice Mapping token => Whether or not a strategy can be deployed for the token
mapping(IERC20 => bool) public isBlacklisted;
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[48] private __gap;
}
````
## File: src/contracts/token/BackingEigen.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin-upgrades/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
contract BackingEigen is OwnableUpgradeable, ERC20VotesUpgradeable {
/// CONSTANTS & IMMUTABLES
/// @notice the address of the wrapped Eigen token EIGEN
IERC20 public immutable EIGEN;
/// STORAGE
/// @notice the timestamp after which transfer restrictions are disabled
uint256 public transferRestrictionsDisabledAfter;
/// @notice mapping of addresses that are allowed to transfer tokens to any address
mapping(address => bool) public allowedFrom;
/// @notice mapping of addresses that are allowed to receive tokens from any address
mapping(address => bool) public allowedTo;
// @notice whether or not an address is allowed to mint new bEIGEN tokens
mapping(address => bool) public isMinter;
/// @notice event emitted when the allowedFrom status of an address is set
event SetAllowedFrom(address indexed from, bool isAllowedFrom);
/// @notice event emitted when the allowedTo status of an address is set
event SetAllowedTo(address indexed to, bool isAllowedTo);
/// @notice event emitted when the transfer restrictions are disabled
event TransferRestrictionsDisabled();
/// @notice event emitted when the EIGEN token is backed
event Backed();
// @notice event emitted when the `isMinter` mapping is modified
event IsMinterModified(address indexed minterAddress, bool newStatus);
constructor(
IERC20 _EIGEN
) {
EIGEN = _EIGEN;
_disableInitializers();
}
// @notice Allows the contract owner to modify an entry in the `isMinter` mapping.
function setIsMinter(address minterAddress, bool newStatus) external onlyOwner {
emit IsMinterModified(minterAddress, newStatus);
isMinter[minterAddress] = newStatus;
}
/**
* @notice Allows any privileged address to mint `amount` new tokens to the address `to`.
* @dev Callable only by an address that has `isMinter` set to true.
*/
function mint(address to, uint256 amount) external {
require(isMinter[msg.sender], "BackingEigen.mint: caller is not a minter");
_mint(to, amount);
}
/**
* @dev Destroys `amount` tokens from the caller.
*
* See {ERC20-_burn}.
*/
function burn(
uint256 amount
) public virtual {
_burn(_msgSender(), amount);
}
/**
* @notice An initializer function that sets initial values for the contract's state variables.
*/
function initialize(
address initialOwner
) public initializer {
__Ownable_init();
__ERC20_init("Backing Eigen", "bEIGEN");
_transferOwnership(initialOwner);
__ERC20Permit_init("bEIGEN");
// set transfer restrictions to be disabled at type(uint256).max to be set down later
transferRestrictionsDisabledAfter = type(uint256).max;
// the EIGEN contract should be allowed to transfer tokens to any address for unwrapping
// likewise, anyone should be able to transfer bEIGEN to EIGEN for wrapping
_setAllowedFrom(address(EIGEN), true);
_setAllowedTo(address(EIGEN), true);
// Mint the entire supply of EIGEN - this is a one-time event that
// ensures bEIGEN fully backs EIGEN.
_mint(address(EIGEN), 1_673_646_668_284_660_000_000_000_000);
emit Backed();
}
/// EXTERNAL FUNCTIONS
/**
* @notice This function allows the owner to set the allowedFrom status of an address
* @param from the address whose allowedFrom status is being set
* @param isAllowedFrom the new allowedFrom status
*/
function setAllowedFrom(address from, bool isAllowedFrom) external onlyOwner {
_setAllowedFrom(from, isAllowedFrom);
}
/**
* @notice This function allows the owner to set the allowedTo status of an address
* @param to the address whose allowedTo status is being set
* @param isAllowedTo the new allowedTo status
*/
function setAllowedTo(address to, bool isAllowedTo) external onlyOwner {
_setAllowedTo(to, isAllowedTo);
}
/**
* @notice Allows the owner to disable transfer restrictions
*/
function disableTransferRestrictions() external onlyOwner {
require(
transferRestrictionsDisabledAfter == type(uint256).max,
"BackingEigen.disableTransferRestrictions: transfer restrictions are already disabled"
);
transferRestrictionsDisabledAfter = 0;
emit TransferRestrictionsDisabled();
}
/// VIEW FUNCTIONS
/**
* @dev Clock used for flagging checkpoints. Has been overridden to implement timestamp based
* checkpoints (and voting).
*/
function clock() public view override returns (uint48) {
return SafeCastUpgradeable.toUint48(block.timestamp);
}
/**
* @dev Machine-readable description of the clock as specified in EIP-6372.
* Has been overridden to inform callers that this contract uses timestamps instead of block numbers, to match `clock()`
*/
// solhint-disable-next-line func-name-mixedcase
function CLOCK_MODE() public pure override returns (string memory) {
return "mode=timestamp";
}
/// INTERNAL FUNCTIONS
function _setAllowedFrom(address from, bool isAllowedFrom) internal {
allowedFrom[from] = isAllowedFrom;
emit SetAllowedFrom(from, isAllowedFrom);
}
function _setAllowedTo(address to, bool isAllowedTo) internal {
allowedTo[to] = isAllowedTo;
emit SetAllowedTo(to, isAllowedTo);
}
/**
* @notice Overrides the beforeTokenTransfer function to enforce transfer restrictions
* @param from the address tokens are being transferred from
* @param to the address tokens are being transferred to
* @param amount the amount of tokens being transferred
*/
function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
// if transfer restrictions are enabled
if (block.timestamp <= transferRestrictionsDisabledAfter) {
// if both from and to are not whitelisted
require(
allowedFrom[from] || allowedTo[to] || from == address(0),
"BackingEigen._beforeTokenTransfer: from or to must be whitelisted"
);
}
super._beforeTokenTransfer(from, to, amount);
}
}
````
## File: src/contracts/token/Eigen.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin-upgrades/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
contract Eigen is OwnableUpgradeable, ERC20VotesUpgradeable {
/// CONSTANTS & IMMUTABLES
/// @notice the address of the backing Eigen token bEIGEN
IERC20 public immutable bEIGEN;
/// STORAGE
/// @notice mapping of minter addresses to the timestamp after which they are allowed to mint
mapping(address => uint256) public mintAllowedAfter;
/// @notice mapping of minter addresses to the amount of tokens they are allowed to mint
mapping(address => uint256) public mintingAllowance;
/// @notice the timestamp after which transfer restrictions are disabled
uint256 public transferRestrictionsDisabledAfter;
/// @notice mapping of addresses that are allowed to transfer tokens to any address
mapping(address => bool) public allowedFrom;
/// @notice mapping of addresses that are allowed to receive tokens from any address
mapping(address => bool) public allowedTo;
/// @notice event emitted when the allowedFrom status of an address is set
event SetAllowedFrom(address indexed from, bool isAllowedFrom);
/// @notice event emitted when the allowedTo status of an address is set
event SetAllowedTo(address indexed to, bool isAllowedTo);
/// @notice event emitted when a minter mints
event Mint(address indexed minter, uint256 amount);
/// @notice event emitted when the transfer restrictions disabled
event TransferRestrictionsDisabled();
constructor(
IERC20 _bEIGEN
) {
bEIGEN = _bEIGEN;
_disableInitializers();
}
/**
* @notice An initializer function that sets initial values for the contract's state variables.
* @param minters the addresses that are allowed to mint
* @param mintingAllowances the amount of tokens that each minter is allowed to mint
*/
function initialize(
address initialOwner,
address[] memory minters,
uint256[] memory mintingAllowances,
uint256[] memory mintAllowedAfters
) public initializer {
__Ownable_init();
__ERC20_init("Eigen", "EIGEN");
_transferOwnership(initialOwner);
__ERC20Permit_init("EIGEN");
require(
minters.length == mintingAllowances.length,
"Eigen.initialize: minters and mintingAllowances must be the same length"
);
require(
minters.length == mintAllowedAfters.length,
"Eigen.initialize: minters and mintAllowedAfters must be the same length"
);
// set minting allowances for each minter
for (uint256 i = 0; i < minters.length; i++) {
mintingAllowance[minters[i]] = mintingAllowances[i];
mintAllowedAfter[minters[i]] = mintAllowedAfters[i];
// allow each minter to transfer tokens
allowedFrom[minters[i]] = true;
emit SetAllowedFrom(minters[i], true);
}
// set transfer restrictions to be disabled at type(uint256).max to be set down later
transferRestrictionsDisabledAfter = type(uint256).max;
}
/**
* @notice This function allows the owner to set the allowedFrom status of an address
* @param from the address whose allowedFrom status is being set
* @param isAllowedFrom the new allowedFrom status
*/
function setAllowedFrom(address from, bool isAllowedFrom) external onlyOwner {
allowedFrom[from] = isAllowedFrom;
emit SetAllowedFrom(from, isAllowedFrom);
}
/**
* @notice This function allows the owner to set the allowedTo status of an address
* @param to the address whose allowedTo status is being set
* @param isAllowedTo the new allowedTo status
*/
function setAllowedTo(address to, bool isAllowedTo) external onlyOwner {
allowedTo[to] = isAllowedTo;
emit SetAllowedTo(to, isAllowedTo);
}
/**
* @notice Allows the owner to disable transfer restrictions
*/
function disableTransferRestrictions() external onlyOwner {
require(
transferRestrictionsDisabledAfter == type(uint256).max,
"Eigen.disableTransferRestrictions: transfer restrictions are already disabled"
);
transferRestrictionsDisabledAfter = 0;
emit TransferRestrictionsDisabled();
}
/**
* @notice This function allows minter to mint tokens
*/
function mint() external {
require(mintingAllowance[msg.sender] > 0, "Eigen.mint: msg.sender has no minting allowance");
require(block.timestamp > mintAllowedAfter[msg.sender], "Eigen.mint: msg.sender is not allowed to mint yet");
uint256 amount = mintingAllowance[msg.sender];
mintingAllowance[msg.sender] = 0;
_mint(msg.sender, amount);
emit Mint(msg.sender, amount);
}
/**
* @notice This function allows bEIGEN holders to wrap their tokens into Eigen
*/
function wrap(
uint256 amount
) external {
require(bEIGEN.transferFrom(msg.sender, address(this), amount), "Eigen.wrap: bEIGEN transfer failed");
_mint(msg.sender, amount);
}
/**
* @notice This function allows Eigen holders to unwrap their tokens into bEIGEN
*/
function unwrap(
uint256 amount
) external {
_burn(msg.sender, amount);
require(bEIGEN.transfer(msg.sender, amount), "Eigen.unwrap: bEIGEN transfer failed");
}
/**
* @notice Allows the sender to transfer tokens to multiple addresses in a single transaction
*/
function multisend(address[] calldata receivers, uint256[] calldata amounts) public {
require(receivers.length == amounts.length, "Eigen.multisend: receivers and amounts must be the same length");
for (uint256 i = 0; i < receivers.length; i++) {
_transfer(msg.sender, receivers[i], amounts[i]);
}
}
/**
* @notice Overrides the beforeTokenTransfer function to enforce transfer restrictions
* @param from the address tokens are being transferred from
* @param to the address tokens are being transferred to
* @param amount the amount of tokens being transferred
*/
function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
// if transfer restrictions are enabled
if (block.timestamp <= transferRestrictionsDisabledAfter) {
// if both from and to are not whitelisted
require(
from == address(0) || to == address(0) || allowedFrom[from] || allowedTo[to],
"Eigen._beforeTokenTransfer: from or to must be whitelisted"
);
}
super._beforeTokenTransfer(from, to, amount);
}
/**
* @notice Overridden to return the total bEIGEN supply instead.
* @dev The issued supply of EIGEN should match the bEIGEN balance of this contract,
* less any bEIGEN tokens that were sent directly to the contract (rather than being wrapped)
*/
function totalSupply() public view override returns (uint256) {
return bEIGEN.totalSupply();
}
/**
* @dev Clock used for flagging checkpoints. Has been overridden to implement timestamp based
* checkpoints (and voting).
*/
function clock() public view override returns (uint48) {
return SafeCastUpgradeable.toUint48(block.timestamp);
}
/**
* @dev Machine-readable description of the clock as specified in EIP-6372.
* Has been overridden to inform callers that this contract uses timestamps instead of block numbers, to match `clock()`
*/
// solhint-disable-next-line func-name-mixedcase
function CLOCK_MODE() public pure override returns (string memory) {
return "mode=timestamp";
}
}
````
## File: src/test/harnesses/AllocationManagerHarness.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../../contracts/core/AllocationManager.sol";
contract AllocationManagerHarness is AllocationManager {
using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque;
constructor(
IDelegationManager _delegation,
IPauserRegistry _pauserRegistry,
IPermissionController _permissionController,
uint32 _DEALLOCATION_DELAY,
uint32 _ALLOCATION_CONFIGURATION_DELAY
)
AllocationManager(_delegation, _pauserRegistry, _permissionController, _DEALLOCATION_DELAY, _ALLOCATION_CONFIGURATION_DELAY, "v9.9.9")
{}
function deallocationQueueAtIndex(address operator, IStrategy strategy, uint index) external view returns (bytes32) {
return deallocationQueue[operator][strategy].at(index);
}
}
````
## File: src/test/harnesses/DelegationManagerHarness.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../../contracts/core/DelegationManager.sol";
import "forge-std/Test.sol";
contract DelegationManagerHarness is DelegationManager {
constructor(
IStrategyManager _strategyManager,
IEigenPodManager _eigenPodManager,
IAllocationManager _allocationManager,
IPauserRegistry _pauserRegistry,
IPermissionController _permissionController,
uint32 _MIN_WITHDRAWAL_DELAY
)
DelegationManager(
_strategyManager,
_eigenPodManager,
_allocationManager,
_pauserRegistry,
_permissionController,
_MIN_WITHDRAWAL_DELAY,
"v9.9.9"
)
{}
function getSlashingFactor(address staker, IStrategy strategy, uint64 operatorMaxMagnitude) external view returns (uint) {
return _getSlashingFactor(staker, strategy, operatorMaxMagnitude);
}
function getSlashingFactors(address staker, address operator, IStrategy[] memory strategies) external view returns (uint[] memory) {
return _getSlashingFactors(staker, operator, strategies);
}
function getSlashingFactorsAtBlock(address staker, address operator, IStrategy[] memory strategies, uint32 blockNumber)
external
view
returns (uint[] memory)
{
return _getSlashingFactorsAtBlock(staker, operator, strategies, blockNumber);
}
}
````
## File: src/test/harnesses/EigenHarness.sol
````
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
import "../../contracts/token/Eigen.sol";
contract EigenHarness is Eigen {
constructor(IERC20 _bEIGEN) Eigen(_bEIGEN) {}
/// expose internal mint function
function mint(address to, uint amount) public {
_mint(to, amount);
}
/**
* @notice This function allows the owner to set the allowedFrom status of an address
* @param from the address whose allowedFrom status is being set
* @param isAllowedFrom the new allowedFrom status
* @dev this function is callable by anoyone in the harness
*/
function setAllowedFromPermissionless(address from, bool isAllowedFrom) external {
allowedFrom[from] = isAllowedFrom;
emit SetAllowedFrom(from, isAllowedFrom);
}
function setTransferRestrictionsDisabledAfterToMax() external {
transferRestrictionsDisabledAfter = type(uint).max;
}
function transferOwnershipPermissionless(address newOwner) external {
_transferOwnership(newOwner);
}
}
````
## File: src/test/harnesses/EigenPodHarness.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../../contracts/pods/EigenPod.sol";
import "forge-std/Test.sol";
contract EigenPodHarness is EigenPod {
constructor(IETHPOSDeposit _ethPOS, IEigenPodManager _eigenPodManager, uint64 _GENESIS_TIME, string memory _version)
EigenPod(_ethPOS, _eigenPodManager, _GENESIS_TIME, _version)
{}
function getActiveValidatorCount() public view returns (uint) {
return activeValidatorCount;
}
function setActiveValidatorCount(uint _count) public {
activeValidatorCount = _count;
}
function verifyWithdrawalCredentials(
uint64 beaconTimestamp,
bytes32 beaconStateRoot,
uint40 validatorIndex,
bytes calldata validatorFieldsProof,
bytes32[] calldata validatorFields
) public returns (uint) {
return _verifyWithdrawalCredentials(beaconTimestamp, beaconStateRoot, validatorIndex, validatorFieldsProof, validatorFields);
}
function setValidatorStatus(bytes32 pkhash, VALIDATOR_STATUS status) public {
_validatorPubkeyHashToInfo[pkhash].status = status;
}
function setValidatorRestakedBalance(bytes32 pkhash, uint64 restakedBalanceGwei) public {
_validatorPubkeyHashToInfo[pkhash].restakedBalanceGwei = restakedBalanceGwei;
}
}
````
## File: src/test/harnesses/EigenPodManagerWrapper.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../../contracts/pods/EigenPodManager.sol";
///@notice This contract exposes a manual setter for podShares in order to initialize podShares as negative
contract EigenPodManagerWrapper is EigenPodManager {
constructor(
IETHPOSDeposit _ethPOS,
IBeacon _eigenPodBeacon,
IDelegationManager _delegationManager,
IPauserRegistry _pauserRegistry,
string memory _version
) EigenPodManager(_ethPOS, _eigenPodBeacon, _delegationManager, _pauserRegistry, _version) {}
function setPodOwnerShares(address owner, IEigenPod pod) external {
ownerToPod[owner] = pod;
}
function setPodOwnerShares(address owner, int shares) external {
podOwnerDepositShares[owner] = shares;
}
function setBeaconChainSlashingFactor(address podOwner, uint64 slashingFactor) external {
_beaconChainSlashingFactor[podOwner] = BeaconChainSlashingFactor({slashingFactor: slashingFactor, isSet: true});
}
}
````
## File: src/test/harnesses/PausableHarness.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../../contracts/permissions/Pausable.sol";
// wrapper around the Pausable contract that exposes the internal `_setPausedStatus` function.
contract PausableHarness is Pausable {
constructor(IPauserRegistry _pauserRegistry) Pausable(_pauserRegistry) {}
function initializePauser(uint initPausedStatus) external {
_setPausedStatus(initPausedStatus);
}
}
````
## File: src/test/integration/deprecatedInterfaces/mainnet/BeaconChainProofs.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/contracts/libraries/Merkle.sol";
import "src/contracts/libraries/Endian.sol";
// DEPRECATED BeaconChainProofs at commit hash https://github.com/Layr-Labs/eigenlayer-contracts/tree/0139d6213927c0a7812578899ddd3dda58051928
//Utility library for parsing and PHASE0 beacon chain block headers
//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization
//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader
//BeaconState Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate
library BeaconChainProofs_DeprecatedM1 {
// constants are the number of fields and the heights of the different merkle trees used in merkleizing beacon chain containers
uint internal constant NUM_BEACON_BLOCK_HEADER_FIELDS = 5;
uint internal constant BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT = 3;
uint internal constant NUM_BEACON_BLOCK_BODY_FIELDS = 11;
uint internal constant BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT = 4;
uint internal constant NUM_BEACON_STATE_FIELDS = 21;
uint internal constant BEACON_STATE_FIELD_TREE_HEIGHT = 5;
uint internal constant NUM_ETH1_DATA_FIELDS = 3;
uint internal constant ETH1_DATA_FIELD_TREE_HEIGHT = 2;
uint internal constant NUM_VALIDATOR_FIELDS = 8;
uint internal constant VALIDATOR_FIELD_TREE_HEIGHT = 3;
uint internal constant NUM_EXECUTION_PAYLOAD_HEADER_FIELDS = 15;
uint internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT = 4;
uint internal constant NUM_EXECUTION_PAYLOAD_FIELDS = 15;
uint internal constant EXECUTION_PAYLOAD_FIELD_TREE_HEIGHT = 4;
// HISTORICAL_ROOTS_LIMIT = 2**24, so tree height is 24
uint internal constant HISTORICAL_ROOTS_TREE_HEIGHT = 24;
// HISTORICAL_BATCH is root of state_roots and block_root, so number of leaves = 2^1
uint internal constant HISTORICAL_BATCH_TREE_HEIGHT = 1;
// SLOTS_PER_HISTORICAL_ROOT = 2**13, so tree height is 13
uint internal constant STATE_ROOTS_TREE_HEIGHT = 13;
uint internal constant BLOCK_ROOTS_TREE_HEIGHT = 13;
uint internal constant NUM_WITHDRAWAL_FIELDS = 4;
// tree height for hash tree of an individual withdrawal container
uint internal constant WITHDRAWAL_FIELD_TREE_HEIGHT = 2;
uint internal constant VALIDATOR_TREE_HEIGHT = 40;
//refer to the eigenlayer-cli proof library. Despite being the same dimensions as the validator tree, the balance tree is merkleized differently
uint internal constant BALANCE_TREE_HEIGHT = 38;
// MAX_WITHDRAWALS_PER_PAYLOAD = 2**4, making tree height = 4
uint internal constant WITHDRAWALS_TREE_HEIGHT = 4;
//in beacon block body
uint internal constant EXECUTION_PAYLOAD_INDEX = 9;
// in beacon block header
uint internal constant STATE_ROOT_INDEX = 3;
uint internal constant PROPOSER_INDEX_INDEX = 1;
uint internal constant SLOT_INDEX = 0;
uint internal constant BODY_ROOT_INDEX = 4;
// in beacon state
uint internal constant STATE_ROOTS_INDEX = 6;
uint internal constant BLOCK_ROOTS_INDEX = 5;
uint internal constant HISTORICAL_ROOTS_INDEX = 7;
uint internal constant ETH_1_ROOT_INDEX = 8;
uint internal constant VALIDATOR_TREE_ROOT_INDEX = 11;
uint internal constant BALANCE_INDEX = 12;
uint internal constant EXECUTION_PAYLOAD_HEADER_INDEX = 24;
uint internal constant HISTORICAL_BATCH_STATE_ROOT_INDEX = 1;
// in validator
uint internal constant VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1;
uint internal constant VALIDATOR_BALANCE_INDEX = 2;
uint internal constant VALIDATOR_SLASHED_INDEX = 3;
uint internal constant VALIDATOR_WITHDRAWABLE_EPOCH_INDEX = 7;
// in execution payload header
uint internal constant BLOCK_NUMBER_INDEX = 6;
uint internal constant WITHDRAWALS_ROOT_INDEX = 14;
//in execution payload
uint internal constant WITHDRAWALS_INDEX = 14;
// in withdrawal
uint internal constant WITHDRAWAL_VALIDATOR_INDEX_INDEX = 1;
uint internal constant WITHDRAWAL_VALIDATOR_AMOUNT_INDEX = 3;
//In historicalBatch
uint internal constant HISTORICALBATCH_STATEROOTS_INDEX = 1;
//Misc Constants
uint internal constant SLOTS_PER_EPOCH = 32;
bytes8 internal constant UINT64_MASK = 0xffffffffffffffff;
struct WithdrawalProofs {
bytes blockHeaderProof;
bytes withdrawalProof;
bytes slotProof;
bytes executionPayloadProof;
bytes blockNumberProof;
uint64 blockHeaderRootIndex;
uint64 withdrawalIndex;
bytes32 blockHeaderRoot;
bytes32 blockBodyRoot;
bytes32 slotRoot;
bytes32 blockNumberRoot;
bytes32 executionPayloadRoot;
}
struct ValidatorFieldsAndBalanceProofs {
bytes validatorFieldsProof;
bytes validatorBalanceProof;
bytes32 balanceRoot;
}
struct ValidatorFieldsProof {
bytes validatorProof;
uint40 validatorIndex;
}
/**
*
* @notice This function is parses the balanceRoot to get the uint64 balance of a validator. During merkleization of the
* beacon state balance tree, four uint64 values (making 32 bytes) are grouped together and treated as a single leaf in the merkle tree. Thus the
* validatorIndex mod 4 is used to determine which of the four uint64 values to extract from the balanceRoot.
* @param validatorIndex is the index of the validator being proven for.
* @param balanceRoot is the combination of 4 validator balances being proven for.
* @return The validator's balance, in Gwei
*/
function getBalanceFromBalanceRoot(uint40 validatorIndex, bytes32 balanceRoot) internal pure returns (uint64) {
uint bitShiftAmount = (validatorIndex % 4) * 64;
bytes32 validatorBalanceLittleEndian = bytes32((uint(balanceRoot) << bitShiftAmount));
uint64 validatorBalance = Endian.fromLittleEndianUint64(validatorBalanceLittleEndian);
return validatorBalance;
}
/**
* @notice This function verifies merkle proofs of the fields of a certain validator against a beacon chain state root
* @param validatorIndex the index of the proven validator
* @param beaconStateRoot is the beacon chain state root to be proven against.
* @param proof is the data used in proving the validator's fields
* @param validatorFields the claimed fields of the validator
*/
function verifyValidatorFields(uint40 validatorIndex, bytes32 beaconStateRoot, bytes calldata proof, bytes32[] calldata validatorFields)
internal
view
{
require(
validatorFields.length == 2 ** VALIDATOR_FIELD_TREE_HEIGHT,
"BeaconChainProofs.verifyValidatorFields: Validator fields has incorrect length"
);
/**
* Note: the length of the validator merkle proof is BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1.
* There is an additional layer added by hashing the root with the length of the validator list
*/
require(
proof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_FIELD_TREE_HEIGHT),
"BeaconChainProofs.verifyValidatorFields: Proof has incorrect length"
);
uint index = (VALIDATOR_TREE_ROOT_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint(validatorIndex);
// merkleize the validatorFields to get the leaf to prove
bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields);
// verify the proof of the validatorRoot against the beaconStateRoot
require(
Merkle.verifyInclusionSha256(proof, beaconStateRoot, validatorRoot, index),
"BeaconChainProofs.verifyValidatorFields: Invalid merkle proof"
);
}
/**
* @notice This function verifies merkle proofs of the balance of a certain validator against a beacon chain state root
* @param validatorIndex the index of the proven validator
* @param beaconStateRoot is the beacon chain state root to be proven against.
* @param proof is the proof of the balance against the beacon chain state root
* @param balanceRoot is the serialized balance used to prove the balance of the validator (refer to `getBalanceFromBalanceRoot` above for detailed explanation)
*/
function verifyValidatorBalance(uint40 validatorIndex, bytes32 beaconStateRoot, bytes calldata proof, bytes32 balanceRoot)
internal
view
{
require(
proof.length == 32 * ((BALANCE_TREE_HEIGHT + 1) + BEACON_STATE_FIELD_TREE_HEIGHT),
"BeaconChainProofs.verifyValidatorBalance: Proof has incorrect length"
);
/**
* the beacon state's balance list is a list of uint64 values, and these are grouped together in 4s when merkleized.
* Therefore, the index of the balance of a validator is validatorIndex/4
*/
uint balanceIndex = uint(validatorIndex / 4);
balanceIndex = (BALANCE_INDEX << (BALANCE_TREE_HEIGHT + 1)) | balanceIndex;
require(
Merkle.verifyInclusionSha256(proof, beaconStateRoot, balanceRoot, balanceIndex),
"BeaconChainProofs.verifyValidatorBalance: Invalid merkle proof"
);
}
/**
* @notice This function verifies the slot and the withdrawal fields for a given withdrawal
* @param beaconStateRoot is the beacon chain state root to be proven against.
* @param proofs is the provided set of merkle proofs
* @param withdrawalFields is the serialized withdrawal container to be proven
*/
function verifyWithdrawalProofs(bytes32 beaconStateRoot, WithdrawalProofs calldata proofs, bytes32[] calldata withdrawalFields)
internal
view
{
require(
withdrawalFields.length == 2 ** WITHDRAWAL_FIELD_TREE_HEIGHT,
"BeaconChainProofs.verifyWithdrawalProofs: withdrawalFields has incorrect length"
);
require(
proofs.blockHeaderRootIndex < 2 ** BLOCK_ROOTS_TREE_HEIGHT,
"BeaconChainProofs.verifyWithdrawalProofs: blockRootIndex is too large"
);
require(
proofs.withdrawalIndex < 2 ** WITHDRAWALS_TREE_HEIGHT, "BeaconChainProofs.verifyWithdrawalProofs: withdrawalIndex is too large"
);
// verify the block header proof length
require(
proofs.blockHeaderProof.length == 32 * (BEACON_STATE_FIELD_TREE_HEIGHT + BLOCK_ROOTS_TREE_HEIGHT),
"BeaconChainProofs.verifyWithdrawalProofs: blockHeaderProof has incorrect length"
);
require(
proofs.withdrawalProof.length == 32 * (EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT + WITHDRAWALS_TREE_HEIGHT + 1),
"BeaconChainProofs.verifyWithdrawalProofs: withdrawalProof has incorrect length"
);
require(
proofs.executionPayloadProof.length == 32 * (BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT + BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT),
"BeaconChainProofs.verifyWithdrawalProofs: executionPayloadProof has incorrect length"
);
require(
proofs.slotProof.length == 32 * (BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT),
"BeaconChainProofs.verifyWithdrawalProofs: slotProof has incorrect length"
);
require(
proofs.blockNumberProof.length == 32 * (EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT),
"BeaconChainProofs.verifyWithdrawalProofs: blockNumberProof has incorrect length"
);
/**
* Computes the block_header_index relative to the beaconStateRoot. It concatenates the indexes of all the
* intermediate root indexes from the bottom of the sub trees (the block header container) to the top of the tree
*/
uint blockHeaderIndex = BLOCK_ROOTS_INDEX << (BLOCK_ROOTS_TREE_HEIGHT) | uint(proofs.blockHeaderRootIndex);
// Verify the blockHeaderRoot against the beaconStateRoot
require(
Merkle.verifyInclusionSha256(proofs.blockHeaderProof, beaconStateRoot, proofs.blockHeaderRoot, blockHeaderIndex),
"BeaconChainProofs.verifyWithdrawalProofs: Invalid block header merkle proof"
);
//Next we verify the slot against the blockHeaderRoot
require(
Merkle.verifyInclusionSha256(proofs.slotProof, proofs.blockHeaderRoot, proofs.slotRoot, SLOT_INDEX),
"BeaconChainProofs.verifyWithdrawalProofs: Invalid slot merkle proof"
);
// Next we verify the executionPayloadRoot against the blockHeaderRoot
uint executionPayloadIndex = BODY_ROOT_INDEX << (BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT) | EXECUTION_PAYLOAD_INDEX;
require(
Merkle.verifyInclusionSha256(
proofs.executionPayloadProof, proofs.blockHeaderRoot, proofs.executionPayloadRoot, executionPayloadIndex
),
"BeaconChainProofs.verifyWithdrawalProofs: Invalid executionPayload merkle proof"
);
// Next we verify the blockNumberRoot against the executionPayload root
require(
Merkle.verifyInclusionSha256(proofs.blockNumberProof, proofs.executionPayloadRoot, proofs.blockNumberRoot, BLOCK_NUMBER_INDEX),
"BeaconChainProofs.verifyWithdrawalProofs: Invalid blockNumber merkle proof"
);
/**
* Next we verify the withdrawal fields against the blockHeaderRoot:
* First we compute the withdrawal_index relative to the blockHeaderRoot by concatenating the indexes of all the
* intermediate root indexes from the bottom of the sub trees (the withdrawal container) to the top, the blockHeaderRoot.
* Then we calculate merkleize the withdrawalFields container to calculate the the withdrawalRoot.
* Finally we verify the withdrawalRoot against the executionPayloadRoot.
*/
uint withdrawalIndex = WITHDRAWALS_INDEX << (WITHDRAWALS_TREE_HEIGHT + 1) | uint(proofs.withdrawalIndex);
bytes32 withdrawalRoot = Merkle.merkleizeSha256(withdrawalFields);
require(
Merkle.verifyInclusionSha256(proofs.withdrawalProof, proofs.executionPayloadRoot, withdrawalRoot, withdrawalIndex),
"BeaconChainProofs.verifyWithdrawalProofs: Invalid withdrawal merkle proof"
);
}
}
````
## File: src/test/integration/deprecatedInterfaces/mainnet/IBeaconChainOracle.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
/**
* @notice DEPRECATED INTERFACE at commit hash https://github.com/Layr-Labs/eigenlayer-contracts/tree/0139d6213927c0a7812578899ddd3dda58051928
* @title Interface for the BeaconStateOracle contract.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
interface IBeaconChainOracle_DeprecatedM1 {
/// @notice Largest blockNumber that has been confirmed by the oracle.
function latestConfirmedOracleBlockNumber() external view returns (uint64);
/// @notice Mapping: Beacon Chain blockNumber => the Beacon Chain state root at the specified blockNumber.
/// @dev This will return `bytes32(0)` if the state root at the specified blockNumber is not yet confirmed.
function beaconStateRootAtBlockNumber(uint64 blockNumber) external view returns (bytes32);
/// @notice Mapping: address => whether or not the address is in the set of oracle signers.
function isOracleSigner(address _oracleSigner) external view returns (bool);
/// @notice Mapping: Beacon Chain blockNumber => oracle signer address => whether or not the oracle signer has voted on the state root at the blockNumber.
function hasVoted(uint64 blockNumber, address oracleSigner) external view returns (bool);
/// @notice Mapping: Beacon Chain blockNumber => state root => total number of oracle signer votes for the state root at the blockNumber.
function stateRootVotes(uint64 blockNumber, bytes32 stateRoot) external view returns (uint);
/// @notice Total number of members of the set of oracle signers.
function totalOracleSigners() external view returns (uint);
/**
* @notice Number of oracle signers that must vote for a state root in order for the state root to be confirmed.
* Adjustable by this contract's owner through use of the `setThreshold` function.
* @dev We note that there is an edge case -- when the threshold is adjusted downward, if a state root already has enough votes to meet the *new* threshold,
* the state root must still receive one additional vote from an oracle signer to be confirmed. This behavior is intended, to minimize unexpected root confirmations.
*/
function threshold() external view returns (uint);
/**
* @notice Owner-only function used to modify the value of the `threshold` variable.
* @param _threshold Desired new value for the `threshold` variable. Function will revert if this is set to zero.
*/
function setThreshold(uint _threshold) external;
/**
* @notice Owner-only function used to add a signer to the set of oracle signers.
* @param _oracleSigners Array of address to be added to the set.
* @dev Function will have no effect on the i-th input address if `_oracleSigners[i]`is already in the set of oracle signers.
*/
function addOracleSigners(address[] memory _oracleSigners) external;
/**
* @notice Owner-only function used to remove a signer from the set of oracle signers.
* @param _oracleSigners Array of address to be removed from the set.
* @dev Function will have no effect on the i-th input address if `_oracleSigners[i]`is already not in the set of oracle signers.
*/
function removeOracleSigners(address[] memory _oracleSigners) external;
/**
* @notice Called by a member of the set of oracle signers to assert that the Beacon Chain state root is `stateRoot` at `blockNumber`.
* @dev The state root will be finalized once the total number of votes *for this exact state root at this exact blockNumber* meets the `threshold` value.
* @param blockNumber The Beacon Chain blockNumber of interest.
* @param stateRoot The Beacon Chain state root that the caller asserts was the correct root, at the specified `blockNumber`.
*/
function voteForBeaconChainStateRoot(uint64 blockNumber, bytes32 stateRoot) external;
}
````
## File: src/test/integration/deprecatedInterfaces/mainnet/IDelayedWithdrawalRouter.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
/// @notice DEPRECATED INTERFACE at commit hash https://github.com/Layr-Labs/eigenlayer-contracts/tree/0139d6213927c0a7812578899ddd3dda58051928
interface IDelayedWithdrawalRouter_DeprecatedM1 {
// struct used to pack data into a single storage slot
struct DelayedWithdrawal {
uint224 amount;
uint32 blockCreated;
}
// struct used to store a single users delayedWithdrawal data
struct UserDelayedWithdrawals {
uint delayedWithdrawalsCompleted;
DelayedWithdrawal[] delayedWithdrawals;
}
/**
* @notice Creates an delayed withdrawal for `msg.value` to the `recipient`.
* @dev Only callable by the `podOwner`'s EigenPod contract.
*/
function createDelayedWithdrawal(address podOwner, address recipient) external payable;
/**
* @notice Called in order to withdraw delayed withdrawals made to the `recipient` that have passed the `withdrawalDelayBlocks` period.
* @param recipient The address to claim delayedWithdrawals for.
* @param maxNumberOfWithdrawalsToClaim Used to limit the maximum number of withdrawals to loop through claiming.
*/
function claimDelayedWithdrawals(address recipient, uint maxNumberOfWithdrawalsToClaim) external;
/**
* @notice Called in order to withdraw delayed withdrawals made to the caller that have passed the `withdrawalDelayBlocks` period.
* @param maxNumberOfWithdrawalsToClaim Used to limit the maximum number of withdrawals to loop through claiming.
*/
function claimDelayedWithdrawals(uint maxNumberOfWithdrawalsToClaim) external;
/// @notice Owner-only function for modifying the value of the `withdrawalDelayBlocks` variable.
function setWithdrawalDelayBlocks(uint newValue) external;
/// @notice Getter function for the mapping `_userWithdrawals`
function userWithdrawals(address user) external view returns (UserDelayedWithdrawals memory);
/// @notice Getter function to get all delayedWithdrawals of the `user`
function getUserDelayedWithdrawals(address user) external view returns (DelayedWithdrawal[] memory);
/// @notice Getter function to get all delayedWithdrawals that are currently claimable by the `user`
function getClaimableUserDelayedWithdrawals(address user) external view returns (DelayedWithdrawal[] memory);
/// @notice Getter function for fetching the delayedWithdrawal at the `index`th entry from the `_userWithdrawals[user].delayedWithdrawals` array
function userDelayedWithdrawalByIndex(address user, uint index) external view returns (DelayedWithdrawal memory);
/// @notice Getter function for fetching the length of the delayedWithdrawals array of a specific user
function userWithdrawalsLength(address user) external view returns (uint);
/// @notice Convenience function for checking whether or not the delayedWithdrawal at the `index`th entry from the `_userWithdrawals[user].delayedWithdrawals` array is currently claimable
function canClaimDelayedWithdrawal(address user, uint index) external view returns (bool);
/**
* @notice Delay enforced by this contract for completing any delayedWithdrawal. Measured in blocks, and adjustable by this contract's owner,
* up to a maximum of `MAX_WITHDRAWAL_DELAY_BLOCKS`. Minimum value is 0 (i.e. no delay enforced).
*/
function withdrawalDelayBlocks() external view returns (uint);
}
````
## File: src/test/integration/deprecatedInterfaces/mainnet/IDelegationManager.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/contracts/interfaces/IStrategy.sol";
import "src/contracts/interfaces/IPausable.sol";
import "src/contracts/interfaces/ISignatureUtilsMixin.sol";
/**
* @notice M2 DEPRECATED INTERFACE at commit hash https://github.com/Layr-Labs/eigenlayer-contracts/tree/426f461c59b4f0e16f8becdffd747075edcaded8
* @title Interface for delegation & withdrawal of funds in EigenLayer.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
interface IDelegationManager_DeprecatedM2 is IPausable, ISignatureUtilsMixin {
// @notice Struct used for storing information about a single operator who has registered with EigenLayer
struct OperatorDetails {
/// @notice DEPRECATED -- this field is no longer used, payments are handled in PaymentCoordinator.sol
address __deprecated_earningsReceiver;
/**
* @notice Address to verify signatures when a staker wishes to delegate to the operator, as well as controlling "forced undelegations".
* @dev Signature verification follows these rules:
* 1) If this address is left as address(0), then any staker will be free to delegate to the operator, i.e. no signature verification will be performed.
* 2) If this address is an EOA (i.e. it has no code), then we follow standard ECDSA signature verification for delegations to the operator.
* 3) If this address is a contract (i.e. it has code) then we forward a call to the contract and verify that it returns the correct EIP-1271 "magic value".
*/
address delegationApprover;
/**
* @notice A minimum delay -- measured in blocks -- enforced between:
* 1) the operator signalling their intent to register for a service, via calling `Slasher.optIntoSlashing`
* and
* 2) the operator completing registration for the service, via the service ultimately calling `Slasher.recordFirstStakeUpdate`
* @dev note that for a specific operator, this value *cannot decrease*, i.e. if the operator wishes to modify their OperatorDetails,
* then they are only allowed to either increase this value or keep it the same.
*/
uint32 stakerOptOutWindowBlocks;
}
/**
* @notice Abstract struct used in calculating an EIP712 signature for a staker to approve that they (the staker themselves) delegate to a specific operator.
* @dev Used in computing the `STAKER_DELEGATION_TYPEHASH` and as a reference in the computation of the stakerDigestHash in the `delegateToBySignature` function.
*/
struct StakerDelegation {
// the staker who is delegating
address staker;
// the operator being delegated to
address operator;
// the staker's nonce
uint nonce;
// the expiration timestamp (UTC) of the signature
uint expiry;
}
/**
* @notice Abstract struct used in calculating an EIP712 signature for an operator's delegationApprover to approve that a specific staker delegate to the operator.
* @dev Used in computing the `DELEGATION_APPROVAL_TYPEHASH` and as a reference in the computation of the approverDigestHash in the `_delegate` function.
*/
struct DelegationApproval {
// the staker who is delegating
address staker;
// the operator being delegated to
address operator;
// the operator's provided salt
bytes32 salt;
// the expiration timestamp (UTC) of the signature
uint expiry;
}
/**
* Struct type used to specify an existing queued withdrawal. Rather than storing the entire struct, only a hash is stored.
* In functions that operate on existing queued withdrawals -- e.g. completeQueuedWithdrawal`, the data is resubmitted and the hash of the submitted
* data is computed by `calculateWithdrawalRoot` and checked against the stored hash in order to confirm the integrity of the submitted data.
*/
struct Withdrawal {
// The address that originated the Withdrawal
address staker;
// The address that the staker was delegated to at the time that the Withdrawal was created
address delegatedTo;
// The address that can complete the Withdrawal + will receive funds when completing the withdrawal
address withdrawer;
// Nonce used to guarantee that otherwise identical withdrawals have unique hashes
uint nonce;
// Block number when the Withdrawal was created
uint32 startBlock;
// Array of strategies that the Withdrawal contains
IStrategy[] strategies;
// Array containing the amount of shares in each Strategy in the `strategies` array
uint[] shares;
}
struct QueuedWithdrawalParams {
// Array of strategies that the QueuedWithdrawal contains
IStrategy[] strategies;
// Array containing the amount of shares in each Strategy in the `strategies` array
uint[] shares;
// The address of the withdrawer
address withdrawer;
}
/**
* @notice Registers the caller as an operator in EigenLayer.
* @param registeringOperatorDetails is the `OperatorDetails` for the operator.
* @param metadataURI is a URI for the operator's metadata, i.e. a link providing more details on the operator.
*
* @dev Once an operator is registered, they cannot 'deregister' as an operator, and they will forever be considered "delegated to themself".
* @dev This function will revert if the caller is already delegated to an operator.
* @dev Note that the `metadataURI` is *never stored * and is only emitted in the `OperatorMetadataURIUpdated` event
*/
function registerAsOperator(OperatorDetails calldata registeringOperatorDetails, string calldata metadataURI) external;
/**
* @notice Updates an operator's stored `OperatorDetails`.
* @param newOperatorDetails is the updated `OperatorDetails` for the operator, to replace their current OperatorDetails`.
*
* @dev The caller must have previously registered as an operator in EigenLayer.
*/
function modifyOperatorDetails(OperatorDetails calldata newOperatorDetails) external;
/**
* @notice Called by an operator to emit an `OperatorMetadataURIUpdated` event indicating the information has updated.
* @param metadataURI The URI for metadata associated with an operator
* @dev Note that the `metadataURI` is *never stored * and is only emitted in the `OperatorMetadataURIUpdated` event
*/
function updateOperatorMetadataURI(string calldata metadataURI) external;
/**
* @notice Caller delegates their stake to an operator.
* @param operator The account (`msg.sender`) is delegating its assets to for use in serving applications built on EigenLayer.
* @param approverSignatureAndExpiry Verifies the operator approves of this delegation
* @param approverSalt A unique single use value tied to an individual signature.
* @dev The approverSignatureAndExpiry is used in the event that:
* 1) the operator's `delegationApprover` address is set to a non-zero value.
* AND
* 2) neither the operator nor their `delegationApprover` is the `msg.sender`, since in the event that the operator
* or their delegationApprover is the `msg.sender`, then approval is assumed.
* @dev In the event that `approverSignatureAndExpiry` is not checked, its content is ignored entirely; it's recommended to use an empty input
* in this case to save on complexity + gas costs
*/
function delegateTo(address operator, SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt) external;
/**
* @notice Caller delegates a staker's stake to an operator with valid signatures from both parties.
* @param staker The account delegating stake to an `operator` account
* @param operator The account (`staker`) is delegating its assets to for use in serving applications built on EigenLayer.
* @param stakerSignatureAndExpiry Signed data from the staker authorizing delegating stake to an operator
* @param approverSignatureAndExpiry is a parameter that will be used for verifying that the operator approves of this delegation action in the event that:
* @param approverSalt Is a salt used to help guarantee signature uniqueness. Each salt can only be used once by a given approver.
*
* @dev If `staker` is an EOA, then `stakerSignature` is verified to be a valid ECDSA stakerSignature from `staker`, indicating their intention for this action.
* @dev If `staker` is a contract, then `stakerSignature` will be checked according to EIP-1271.
* @dev the operator's `delegationApprover` address is set to a non-zero value.
* @dev neither the operator nor their `delegationApprover` is the `msg.sender`, since in the event that the operator or their delegationApprover
* is the `msg.sender`, then approval is assumed.
* @dev This function will revert if the current `block.timestamp` is equal to or exceeds the expiry
* @dev In the case that `approverSignatureAndExpiry` is not checked, its content is ignored entirely; it's recommended to use an empty input
* in this case to save on complexity + gas costs
*/
function delegateToBySignature(
address staker,
address operator,
SignatureWithExpiry memory stakerSignatureAndExpiry,
SignatureWithExpiry memory approverSignatureAndExpiry,
bytes32 approverSalt
) external;
/**
* @notice Undelegates the staker from the operator who they are delegated to. Puts the staker into the "undelegation limbo" mode of the EigenPodManager
* and queues a withdrawal of all of the staker's shares in the StrategyManager (to the staker), if necessary.
* @param staker The account to be undelegated.
* @return withdrawalRoot The root of the newly queued withdrawal, if a withdrawal was queued. Otherwise just bytes32(0).
*
* @dev Reverts if the `staker` is also an operator, since operators are not allowed to undelegate from themselves.
* @dev Reverts if the caller is not the staker, nor the operator who the staker is delegated to, nor the operator's specified "delegationApprover"
* @dev Reverts if the `staker` is already undelegated.
*/
function undelegate(address staker) external returns (bytes32[] memory withdrawalRoot);
/**
* Allows a staker to withdraw some shares. Withdrawn shares/strategies are immediately removed
* from the staker. If the staker is delegated, withdrawn shares/strategies are also removed from
* their operator.
*
* All withdrawn shares/strategies are placed in a queue and can be fully withdrawn after a delay.
*/
function queueWithdrawals(QueuedWithdrawalParams[] calldata queuedWithdrawalParams) external returns (bytes32[] memory);
/**
* @notice Used to complete the specified `withdrawal`. The caller must match `withdrawal.withdrawer`
* @param withdrawal The Withdrawal to complete.
* @param tokens Array in which the i-th entry specifies the `token` input to the 'withdraw' function of the i-th Strategy in the `withdrawal.strategies` array.
* This input can be provided with zero length if `receiveAsTokens` is set to 'false' (since in that case, this input will be unused)
* @param middlewareTimesIndex is the index in the operator that the staker who triggered the withdrawal was delegated to's middleware times array
* @param receiveAsTokens If true, the shares specified in the withdrawal will be withdrawn from the specified strategies themselves
* and sent to the caller, through calls to `withdrawal.strategies[i].withdraw`. If false, then the shares in the specified strategies
* will simply be transferred to the caller directly.
* @dev middlewareTimesIndex is unused, but will be used in the Slasher eventually
* @dev beaconChainETHStrategy shares are non-transferrable, so if `receiveAsTokens = false` and `withdrawal.withdrawer != withdrawal.staker`, note that
* any beaconChainETHStrategy shares in the `withdrawal` will be _returned to the staker_, rather than transferred to the withdrawer, unlike shares in
* any other strategies, which will be transferred to the withdrawer.
*/
function completeQueuedWithdrawal(
Withdrawal calldata withdrawal,
IERC20[] calldata tokens,
uint middlewareTimesIndex,
bool receiveAsTokens
) external;
/**
* @notice Array-ified version of `completeQueuedWithdrawal`.
* Used to complete the specified `withdrawals`. The function caller must match `withdrawals[...].withdrawer`
* @param withdrawals The Withdrawals to complete.
* @param tokens Array of tokens for each Withdrawal. See `completeQueuedWithdrawal` for the usage of a single array.
* @param middlewareTimesIndexes One index to reference per Withdrawal. See `completeQueuedWithdrawal` for the usage of a single index.
* @param receiveAsTokens Whether or not to complete each withdrawal as tokens. See `completeQueuedWithdrawal` for the usage of a single boolean.
* @dev See `completeQueuedWithdrawal` for relevant dev tags
*/
function completeQueuedWithdrawals(
Withdrawal[] calldata withdrawals,
IERC20[][] calldata tokens,
uint[] calldata middlewareTimesIndexes,
bool[] calldata receiveAsTokens
) external;
/**
* @notice Increases a staker's delegated share balance in a strategy.
* @param staker The address to increase the delegated shares for their operator.
* @param strategy The strategy in which to increase the delegated shares.
* @param shares The number of shares to increase.
*
* @dev *If the staker is actively delegated*, then increases the `staker`'s delegated shares in `strategy` by `shares`. Otherwise does nothing.
* @dev Callable only by the StrategyManager or EigenPodManager.
*/
function increaseDelegatedShares(address staker, IStrategy strategy, uint shares) external;
/**
* @notice Decreases a staker's delegated share balance in a strategy.
* @param staker The address to increase the delegated shares for their operator.
* @param strategy The strategy in which to decrease the delegated shares.
* @param shares The number of shares to decrease.
*
* @dev *If the staker is actively delegated*, then decreases the `staker`'s delegated shares in `strategy` by `shares`. Otherwise does nothing.
* @dev Callable only by the StrategyManager or EigenPodManager.
*/
function decreaseDelegatedShares(address staker, IStrategy strategy, uint shares) external;
/**
* @notice Owner-only function for modifying the value of the `minWithdrawalDelayBlocks` variable.
* @param newMinWithdrawalDelayBlocks new value of `minWithdrawalDelayBlocks`.
*/
function setMinWithdrawalDelayBlocks(uint newMinWithdrawalDelayBlocks) external;
/**
* @notice Called by owner to set the minimum withdrawal delay blocks for each passed in strategy
* Note that the min number of blocks to complete a withdrawal of a strategy is
* MAX(minWithdrawalDelayBlocks, strategyWithdrawalDelayBlocks[strategy])
* @param strategies The strategies to set the minimum withdrawal delay blocks for
* @param withdrawalDelayBlocks The minimum withdrawal delay blocks to set for each strategy
*/
function setStrategyWithdrawalDelayBlocks(IStrategy[] calldata strategies, uint[] calldata withdrawalDelayBlocks) external;
/**
* @notice returns the address of the operator that `staker` is delegated to.
* @notice Mapping: staker => operator whom the staker is currently delegated to.
* @dev Note that returning address(0) indicates that the staker is not actively delegated to any operator.
*/
function delegatedTo(address staker) external view returns (address);
/**
* @notice Returns the OperatorDetails struct associated with an `operator`.
*/
function operatorDetails(address operator) external view returns (OperatorDetails memory);
/**
* @notice Returns the delegationApprover account for an operator
*/
function delegationApprover(address operator) external view returns (address);
/**
* @notice Returns the stakerOptOutWindowBlocks for an operator
*/
function stakerOptOutWindowBlocks(address operator) external view returns (uint);
/**
* @notice Given array of strategies, returns array of shares for the operator
*/
function getOperatorShares(address operator, IStrategy[] memory strategies) external view returns (uint[] memory);
/**
* @notice Given a list of strategies, return the minimum number of blocks that must pass to withdraw
* from all the inputted strategies. Return value is >= minWithdrawalDelayBlocks as this is the global min withdrawal delay.
* @param strategies The strategies to check withdrawal delays for
*/
function getWithdrawalDelay(IStrategy[] calldata strategies) external view returns (uint);
/**
* @notice returns the total number of shares in `strategy` that are delegated to `operator`.
* @notice Mapping: operator => strategy => total number of shares in the strategy delegated to the operator.
* @dev By design, the following invariant should hold for each Strategy:
* (operator's shares in delegation manager) = sum (shares above zero of all stakers delegated to operator)
* = sum (delegateable shares of all stakers delegated to the operator)
*/
function operatorShares(address operator, IStrategy strategy) external view returns (uint);
/**
* @notice Returns the number of actively-delegatable shares a staker has across all strategies.
* @dev Returns two empty arrays in the case that the Staker has no actively-delegateable shares.
*/
function getDelegatableShares(address staker) external view returns (IStrategy[] memory, uint[] memory);
/**
* @notice Returns 'true' if `staker` *is* actively delegated, and 'false' otherwise.
*/
function isDelegated(address staker) external view returns (bool);
/**
* @notice Returns true is an operator has previously registered for delegation.
*/
function isOperator(address operator) external view returns (bool);
/// @notice Mapping: staker => number of signed delegation nonces (used in `delegateToBySignature`) from the staker that the contract has already checked
function stakerNonce(address staker) external view returns (uint);
/**
* @notice Mapping: delegationApprover => 32-byte salt => whether or not the salt has already been used by the delegationApprover.
* @dev Salts are used in the `delegateTo` and `delegateToBySignature` functions. Note that these functions only process the delegationApprover's
* signature + the provided salt if the operator being delegated to has specified a nonzero address as their `delegationApprover`.
*/
function delegationApproverSaltIsSpent(address _delegationApprover, bytes32 salt) external view returns (bool);
/**
* @notice Minimum delay enforced by this contract for completing queued withdrawals. Measured in blocks, and adjustable by this contract's owner,
* up to a maximum of `MAX_WITHDRAWAL_DELAY_BLOCKS`. Minimum value is 0 (i.e. no delay enforced).
* Note that strategies each have a separate withdrawal delay, which can be greater than this value. So the minimum number of blocks that must pass
* to withdraw a strategy is MAX(minWithdrawalDelayBlocks, strategyWithdrawalDelayBlocks[strategy])
*/
function minWithdrawalDelayBlocks() external view returns (uint);
/**
* @notice Minimum delay enforced by this contract per Strategy for completing queued withdrawals. Measured in blocks, and adjustable by this contract's owner,
* up to a maximum of `MAX_WITHDRAWAL_DELAY_BLOCKS`. Minimum value is 0 (i.e. no delay enforced).
*/
function strategyWithdrawalDelayBlocks(IStrategy strategy) external view returns (uint);
/// @notice return address of the beaconChainETHStrategy
function beaconChainETHStrategy() external view returns (IStrategy);
/**
* @notice Calculates the digestHash for a `staker` to sign to delegate to an `operator`
* @param staker The signing staker
* @param operator The operator who is being delegated to
* @param expiry The desired expiry time of the staker's signature
*/
function calculateCurrentStakerDelegationDigestHash(address staker, address operator, uint expiry) external view returns (bytes32);
/**
* @notice Calculates the digest hash to be signed and used in the `delegateToBySignature` function
* @param staker The signing staker
* @param _stakerNonce The nonce of the staker. In practice we use the staker's current nonce, stored at `stakerNonce[staker]`
* @param operator The operator who is being delegated to
* @param expiry The desired expiry time of the staker's signature
*/
function calculateStakerDelegationDigestHash(address staker, uint _stakerNonce, address operator, uint expiry)
external
view
returns (bytes32);
/**
* @notice Calculates the digest hash to be signed by the operator's delegationApprove and used in the `delegateTo` and `delegateToBySignature` functions.
* @param staker The account delegating their stake
* @param operator The account receiving delegated stake
* @param _delegationApprover the operator's `delegationApprover` who will be signing the delegationHash (in general)
* @param approverSalt A unique and single use value associated with the approver signature.
* @param expiry Time after which the approver's signature becomes invalid
*/
function calculateDelegationApprovalDigestHash(
address staker,
address operator,
address _delegationApprover,
bytes32 approverSalt,
uint expiry
) external view returns (bytes32);
/// @notice The EIP-712 typehash for the contract's domain
function DOMAIN_TYPEHASH() external view returns (bytes32);
/// @notice The EIP-712 typehash for the StakerDelegation struct used by the contract
function STAKER_DELEGATION_TYPEHASH() external view returns (bytes32);
/// @notice The EIP-712 typehash for the DelegationApproval struct used by the contract
function DELEGATION_APPROVAL_TYPEHASH() external view returns (bytes32);
/**
* @notice Getter function for the current EIP-712 domain separator for this contract.
*
* @dev The domain separator will change in the event of a fork that changes the ChainID.
* @dev By introducing a domain separator the DApp developers are guaranteed that there can be no signature collision.
* for more detailed information please read EIP-712.
*/
function domainSeparator() external view returns (bytes32);
/// @notice Mapping: staker => cumulative number of queued withdrawals they have ever initiated.
/// @dev This only increments (doesn't decrement), and is used to help ensure that otherwise identical withdrawals have unique hashes.
function cumulativeWithdrawalsQueued(address staker) external view returns (uint);
/// @notice Returns the keccak256 hash of `withdrawal`.
function calculateWithdrawalRoot(Withdrawal memory withdrawal) external pure returns (bytes32);
}
````
## File: src/test/integration/deprecatedInterfaces/mainnet/IEigenPod.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "./BeaconChainProofs.sol";
import "./IEigenPodManager.sol";
/**
* @notice M2 DEPRECATED INTERFACE at commit hash https://github.com/Layr-Labs/eigenlayer-contracts/tree/426f461c59b4f0e16f8becdffd747075edcaded8
* @title The implementation contract used for restaking beacon chain ETH on EigenLayer
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
interface IEigenPod_DeprecatedM2 {
/**
*
* STRUCTS / ENUMS
*
*/
enum VALIDATOR_STATUS {
INACTIVE, // doesnt exist
ACTIVE, // staked on ethpos and withdrawal credentials are pointed to the EigenPod
WITHDRAWN // withdrawn from the Beacon Chain
}
struct ValidatorInfo {
// index of the validator in the beacon chain
uint64 validatorIndex;
// amount of beacon chain ETH restaked on EigenLayer in gwei
uint64 restakedBalanceGwei;
//timestamp of the validator's most recent balance update
uint64 lastCheckpointedAt;
// status of the validator
VALIDATOR_STATUS status;
}
struct Checkpoint {
bytes32 beaconBlockRoot;
uint24 proofsRemaining;
uint64 podBalanceGwei;
int128 balanceDeltasGwei;
}
/**
*
* EXTERNAL STATE-CHANGING METHODS
*
*/
/// @notice Used to initialize the pointers to contracts crucial to the pod's functionality, in beacon proxy construction from EigenPodManager
function initialize(address owner) external;
/// @notice Called by EigenPodManager when the owner wants to create another ETH validator.
function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable;
/**
* @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address
* @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain.
* @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the
* `amountWei` input (when converted to GWEI).
* @dev Reverts if `amountWei` is not a whole Gwei amount
*/
function withdrawRestakedBeaconChainETH(address recipient, uint amount) external;
/**
* @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed
* by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total
* change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`.
* @dev Once finalized, the pod owner is awarded shares corresponding to:
* - the total change in their ACTIVE validator balances
* - any ETH in the pod not already awarded shares
* @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If
* this is the case, the pod owner MUST complete the existing checkpoint before starting a new one.
* @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner
* to prevent accidentally starting a checkpoint that will not increase their shares
*/
function startCheckpoint(bool revertIfNoBalance) external;
/**
* @dev Progress the current checkpoint towards completion by submitting one or more validator
* checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint.
* For each validator proven, the current checkpoint's `proofsRemaining` decreases.
* @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized.
* (see `_updateCheckpoint` for more details)
* @dev This method can only be called when there is a currently-active checkpoint.
* @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot`
* @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot`
*/
function verifyCheckpointProofs(
BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof,
BeaconChainProofs.BalanceProof[] calldata proofs
) external;
/**
* @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award
* shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and
* future checkpoint proofs will need to include them.
* @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`.
* @dev Validators proven via this method MUST NOT have an exit epoch set already.
* @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds
* to the parent beacon block root against which the proof is verified.
* @param stateRootProof proves a beacon state root against a beacon block root
* @param validatorIndices a list of validator indices being proven
* @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root
* @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for
* details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator
*/
function verifyWithdrawalCredentials(
uint64 beaconTimestamp,
BeaconChainProofs.StateRootProof calldata stateRootProof,
uint40[] calldata validatorIndices,
bytes[] calldata validatorFieldsProofs,
bytes32[][] calldata validatorFields
) external;
/**
* @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful
* staleness proof allows the caller to start a checkpoint.
*
* @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed!
* (See `_startCheckpoint` for details)
*
* @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon
* chain. This is intended to make it easier to external watchers to keep a pod's balance up to date.
*
* @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event
* and the validator's final exit back to the execution layer. During this time, the validator's balance may or
* may not drop further due to a correlation penalty. This method allows proof of a slashed validator
* to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator
* has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven
* "stale" via this method.
* See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info.
*
* @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds
* to the parent beacon block root against which the proof is verified.
* @param stateRootProof proves a beacon state root against a beacon block root
* @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against
* the beacon state root. See the consensus specs for more details:
* https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator
*
* @dev Staleness conditions:
* - Validator's last checkpoint is older than `beaconTimestamp`
* - Validator MUST be in `ACTIVE` status in the pod
* - Validator MUST be slashed on the beacon chain
*/
function verifyStaleBalance(
uint64 beaconTimestamp,
BeaconChainProofs.StateRootProof calldata stateRootProof,
BeaconChainProofs.ValidatorProof calldata proof
) external;
/// @notice called by owner of a pod to remove any ERC20s deposited in the pod
function recoverTokens(IERC20[] memory tokenList, uint[] memory amountsToWithdraw, address recipient) external;
/// @notice Allows the owner of a pod to update the proof submitter, a permissioned
/// address that can call `startCheckpoint` and `verifyWithdrawalCredentials`.
/// @dev Note that EITHER the podOwner OR proofSubmitter can access these methods,
/// so it's fine to set your proofSubmitter to 0 if you want the podOwner to be the
/// only address that can call these methods.
/// @param newProofSubmitter The new proof submitter address. If set to 0, only the
/// pod owner will be able to call `startCheckpoint` and `verifyWithdrawalCredentials`
function setProofSubmitter(address newProofSubmitter) external;
/**
*
* VIEW METHODS
*
*/
/// @notice An address with permissions to call `startCheckpoint` and `verifyWithdrawalCredentials`, set
/// by the podOwner. This role exists to allow a podOwner to designate a hot wallet that can call
/// these methods, allowing the podOwner to remain a cold wallet that is only used to manage funds.
/// @dev If this address is NOT set, only the podOwner can call `startCheckpoint` and `verifyWithdrawalCredentials`
function proofSubmitter() external view returns (address);
/// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from beaconchain but not EigenLayer),
function withdrawableRestakedExecutionLayerGwei() external view returns (uint64);
/// @notice The single EigenPodManager for EigenLayer
function eigenPodManager() external view returns (IEigenPodManager);
/// @notice The owner of this EigenPod
function podOwner() external view returns (address);
/// @notice Returns the validatorInfo struct for the provided pubkeyHash
function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory);
/// @notice Returns the validatorInfo struct for the provided pubkey
function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory);
/// @notice This returns the status of a given validator
function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS);
/// @notice This returns the status of a given validator pubkey
function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS);
/// @notice Number of validators with proven withdrawal credentials, who do not have proven full withdrawals
function activeValidatorCount() external view returns (uint);
/// @notice The timestamp of the last checkpoint finalized
function lastCheckpointTimestamp() external view returns (uint64);
/// @notice The timestamp of the currently-active checkpoint. Will be 0 if there is not active checkpoint
function currentCheckpointTimestamp() external view returns (uint64);
/// @notice Returns the currently-active checkpoint
function currentCheckpoint() external view returns (Checkpoint memory);
/// @notice For each checkpoint, the total balance attributed to exited validators, in gwei
///
/// NOTE that the values added to this mapping are NOT guaranteed to capture the entirety of a validator's
/// exit - rather, they capture the total change in a validator's balance when a checkpoint shows their
/// balance change from nonzero to zero. While a change from nonzero to zero DOES guarantee that a validator
/// has been fully exited, it is possible that the magnitude of this change does not capture what is
/// typically thought of as a "full exit."
///
/// For example:
/// 1. Consider a validator was last checkpointed at 32 ETH before exiting. Once the exit has been processed,
/// it is expected that the validator's exited balance is calculated to be `32 ETH`.
/// 2. However, before `startCheckpoint` is called, a deposit is made to the validator for 1 ETH. The beacon
/// chain will automatically withdraw this ETH, but not until the withdrawal sweep passes over the validator
/// again. Until this occurs, the validator's current balance (used for checkpointing) is 1 ETH.
/// 3. If `startCheckpoint` is called at this point, the balance delta calculated for this validator will be
/// `-31 ETH`, and because the validator has a nonzero balance, it is not marked WITHDRAWN.
/// 4. After the exit is processed by the beacon chain, a subsequent `startCheckpoint` and checkpoint proof
/// will calculate a balance delta of `-1 ETH` and attribute a 1 ETH exit to the validator.
///
/// If this edge case impacts your usecase, it should be possible to mitigate this by monitoring for deposits
/// to your exited validators, and waiting to call `startCheckpoint` until those deposits have been automatically
/// exited.
///
/// Additional edge cases this mapping does not cover:
/// - If a validator is slashed, their balance exited will reflect their original balance rather than the slashed amount
/// - The final partial withdrawal for an exited validator will be likely be included in this mapping.
/// i.e. if a validator was last checkpointed at 32.1 ETH before exiting, the next checkpoint will calculate their
/// "exited" amount to be 32.1 ETH rather than 32 ETH.
function checkpointBalanceExitedGwei(uint64) external view returns (uint64);
/// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp`
/// @param timestamp of the block for which the parent block root will be returned. MUST correspond
/// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method
/// will revert.
function getParentBlockRoot(uint64 timestamp) external view returns (bytes32);
}
/**
* @notice DEPRECATED INTERFACE at commit hash https://github.com/Layr-Labs/eigenlayer-contracts/tree/0139d6213927c0a7812578899ddd3dda58051928
* @title The implementation contract used for restaking beacon chain ETH on EigenLayer
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice The main functionalities are:
* - creating new ETH validators with their withdrawal credentials pointed to this contract
* - proving from beacon chain state roots that withdrawal credentials are pointed to this contract
* - proving from beacon chain state roots the balances of ETH validators with their withdrawal credentials
* pointed to this contract
* - updating aggregate balances in the EigenPodManager
* - withdrawing eth when withdrawals are initiated
* @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose
* to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts
*/
interface IEigenPod_DeprecatedM1 {
enum VALIDATOR_STATUS {
INACTIVE, // doesnt exist
ACTIVE, // staked on ethpos and withdrawal credentials are pointed to the EigenPod
OVERCOMMITTED, // proven to be overcommitted to EigenLayer
WITHDRAWN // withdrawn from the Beacon Chain
}
// this struct keeps track of PartialWithdrawalClaims
struct PartialWithdrawalClaim {
PARTIAL_WITHDRAWAL_CLAIM_STATUS status;
// block at which the PartialWithdrawalClaim was created
uint32 creationBlockNumber;
// last block (inclusive) in which the PartialWithdrawalClaim can be fraudproofed
uint32 fraudproofPeriodEndBlockNumber;
// amount of ETH -- in Gwei -- to be withdrawn until completion of this claim
uint64 partialWithdrawalAmountGwei;
}
enum PARTIAL_WITHDRAWAL_CLAIM_STATUS {
REDEEMED,
PENDING,
FAILED
}
/// @notice The amount of eth, in gwei, that is restaked per validator
function REQUIRED_BALANCE_GWEI() external view returns (uint64);
/// @notice The amount of eth, in wei, that is restaked per validator
function REQUIRED_BALANCE_WEI() external view returns (uint);
/// @notice this is a mapping of validator indices to a Validator struct containing pertinent info about the validator
function validatorStatus(uint40 validatorIndex) external view returns (VALIDATOR_STATUS);
/// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from beaconchain but not EigenLayer),
function restakedExecutionLayerGwei() external view returns (uint64);
/// @notice Used to initialize the pointers to contracts crucial to the pod's functionality, in beacon proxy construction from EigenPodManager
function initialize(address owner) external;
/// @notice Called by EigenPodManager when the owner wants to create another ETH validator.
function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable;
/**
* @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address
* @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain.
* @dev Called during withdrawal or slashing.
* @dev Note that this function is marked as non-reentrant to prevent the recipient calling back into it
*/
function withdrawRestakedBeaconChainETH(address recipient, uint amount) external;
/// @notice The single EigenPodManager for EigenLayer
function eigenPodManager() external view returns (IEigenPodManager_DeprecatedM1);
/// @notice The owner of this EigenPod
function podOwner() external view returns (address);
/// @notice an indicator of whether or not the podOwner has ever "fully restaked" by successfully calling `verifyCorrectWithdrawalCredentials`.
function hasRestaked() external view returns (bool);
/// @notice block number of the most recent withdrawal
function mostRecentWithdrawalBlockNumber() external view returns (uint64);
///@notice mapping that tracks proven partial withdrawals
function provenPartialWithdrawal(uint40 validatorIndex, uint64 slot) external view returns (bool);
/**
* @notice This function verifies that the withdrawal credentials of the podOwner are pointed to
* this contract. It also verifies the current (not effective) balance of the validator. It verifies the provided proof of the ETH validator against the beacon chain state
* root, marks the validator as 'active' in EigenLayer, and credits the restaked ETH in Eigenlayer.
* @param oracleBlockNumber is the Beacon Chain blockNumber whose state root the `proof` will be proven against.
* @param validatorIndex is the index of the validator being proven, refer to consensus specs
* @param proofs is the bytes that prove the ETH validator's balance and withdrawal credentials against a beacon chain state root
* @param validatorFields are the fields of the "Validator Container", refer to consensus specs
* for details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator
*/
function verifyWithdrawalCredentialsAndBalance(
uint64 oracleBlockNumber,
uint40 validatorIndex,
BeaconChainProofs_DeprecatedM1.ValidatorFieldsAndBalanceProofs memory proofs,
bytes32[] calldata validatorFields
) external;
/**
* @notice This function records an overcommitment of stake to EigenLayer on behalf of a certain ETH validator.
* If successful, the overcommitted balance is penalized (available for withdrawal whenever the pod's balance allows).
* The ETH validator's shares in the enshrined beaconChainETH strategy are also removed from the StrategyManager and undelegated.
* @param oracleBlockNumber The oracleBlockNumber whose state root the `proof` will be proven against.
* Must be within `VERIFY_OVERCOMMITTED_WINDOW_BLOCKS` of the current block.
* @param validatorIndex is the index of the validator being proven, refer to consensus specs
* @param proofs is the proof of the validator's balance and validatorFields in the balance tree and the balanceRoot to prove for
* @param beaconChainETHStrategyIndex is the index of the beaconChainETHStrategy for the pod owner for the callback to
* the StrategyManager in case it must be removed from the list of the podOwners strategies
* @param validatorFields are the fields of the "Validator Container", refer to consensus specs
* @dev For more details on the Beacon Chain spec, see: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator
*/
function verifyOvercommittedStake(
uint40 validatorIndex,
BeaconChainProofs_DeprecatedM1.ValidatorFieldsAndBalanceProofs calldata proofs,
bytes32[] calldata validatorFields,
uint beaconChainETHStrategyIndex,
uint64 oracleBlockNumber
) external;
/**
* @notice This function records a full withdrawal on behalf of one of the Ethereum validators for this EigenPod
* @param withdrawalProofs is the information needed to check the veracity of the block number and withdrawal being proven
* @param validatorFieldsProof is the proof of the validator's fields in the validator tree
* @param withdrawalFields are the fields of the withdrawal being proven
* @param validatorFields are the fields of the validator being proven
* @param beaconChainETHStrategyIndex is the index of the beaconChainETHStrategy for the pod owner for the callback to
* the EigenPodManager to the StrategyManager in case it must be removed from the podOwner's list of strategies
*/
function verifyAndProcessWithdrawal(
BeaconChainProofs_DeprecatedM1.WithdrawalProofs calldata withdrawalProofs,
bytes calldata validatorFieldsProof,
bytes32[] calldata validatorFields,
bytes32[] calldata withdrawalFields,
uint beaconChainETHStrategyIndex,
uint64 oracleBlockNumber
) external;
/// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false
function withdrawBeforeRestaking() external;
}
````
## File: src/test/integration/deprecatedInterfaces/mainnet/IEigenPodManager.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "./IStrategyManager.sol";
import "./IEigenPod.sol";
import "./IBeaconChainOracle.sol";
import "src/contracts/interfaces/IPausable.sol";
/**
* @notice M2 DEPRECATED INTERFACE at commit hash https://github.com/Layr-Labs/eigenlayer-contracts/tree/426f461c59b4f0e16f8becdffd747075edcaded8
* @title Interface for factory that creates and manages solo staking pods that have their withdrawal credentials pointed to EigenLayer.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
interface IEigenPodManager_DeprecatedM2 is IPausable {
/**
* @notice Creates an EigenPod for the sender.
* @dev Function will revert if the `msg.sender` already has an EigenPod.
* @dev Returns EigenPod address
*/
function createPod() external returns (address);
/**
* @notice Stakes for a new beacon chain validator on the sender's EigenPod.
* Also creates an EigenPod for the sender if they don't have one already.
* @param pubkey The 48 bytes public key of the beacon chain validator.
* @param signature The validator's signature of the deposit data.
* @param depositDataRoot The root/hash of the deposit data for the validator's deposit.
*/
function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable;
/**
* @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager
* to ensure that delegated shares are also tracked correctly
* @param podOwner is the pod owner whose balance is being updated.
* @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares
* @dev Callable only by the podOwner's EigenPod contract.
* @dev Reverts if `sharesDelta` is not a whole Gwei amount
*/
function recordBeaconChainETHBalanceUpdate(address podOwner, int sharesDelta) external;
/// @notice Returns the address of the `podOwner`'s EigenPod if it has been deployed.
function ownerToPod(address podOwner) external view returns (IEigenPod);
/// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not).
function getPod(address podOwner) external view returns (IEigenPod);
/// @notice The ETH2 Deposit Contract
function ethPOS() external view returns (IETHPOSDeposit);
/// @notice Beacon proxy to which the EigenPods point
function eigenPodBeacon() external view returns (IBeacon);
/// @notice EigenLayer's StrategyManager contract
function strategyManager() external view returns (IStrategyManager);
/// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise.
function hasPod(address podOwner) external view returns (bool);
/// @notice Returns the number of EigenPods that have been created
function numPods() external view returns (uint);
/**
* @notice Mapping from Pod owner owner to the number of shares they have in the virtual beacon chain ETH strategy.
* @dev The share amount can become negative. This is necessary to accommodate the fact that a pod owner's virtual beacon chain ETH shares can
* decrease between the pod owner queuing and completing a withdrawal.
* When the pod owner's shares would otherwise increase, this "deficit" is decreased first _instead_.
* Likewise, when a withdrawal is completed, this "deficit" is decreased and the withdrawal amount is decreased; We can think of this
* as the withdrawal "paying off the deficit".
*/
function podOwnerShares(address podOwner) external view returns (int);
/// @notice returns canonical, virtual beaconChainETH strategy
function beaconChainETHStrategy() external view returns (IStrategy);
/**
* @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue.
* Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero.
* @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to
* result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive
* shares from the operator to whom the staker is delegated.
* @dev Reverts if `shares` is not a whole Gwei amount
*/
function removeShares(address podOwner, uint shares) external;
/**
* @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible.
* Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue
* @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input
* in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero)
* @dev Reverts if `shares` is not a whole Gwei amount
*/
function addShares(address podOwner, uint shares) external returns (uint);
/**
* @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address
* @dev Prioritizes decreasing the podOwner's share deficit, if they have one
* @dev Reverts if `shares` is not a whole Gwei amount
*/
function withdrawSharesAsTokens(address podOwner, address destination, uint shares) external;
}
/**
* @notice DEPRECATED INTERFACE at commit hash https://github.com/Layr-Labs/eigenlayer-contracts/tree/0139d6213927c0a7812578899ddd3dda58051928
* @title Interface for factory that creates and manages solo staking pods that have their withdrawal credentials pointed to EigenLayer.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
interface IEigenPodManager_DeprecatedM1 is IPausable {
/**
* @notice Creates an EigenPod for the sender.
* @dev Function will revert if the `msg.sender` already has an EigenPod.
*/
function createPod() external;
/**
* @notice Stakes for a new beacon chain validator on the sender's EigenPod.
* Also creates an EigenPod for the sender if they don't have one already.
* @param pubkey The 48 bytes public key of the beacon chain validator.
* @param signature The validator's signature of the deposit data.
* @param depositDataRoot The root/hash of the deposit data for the validator's deposit.
*/
function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable;
/**
* @notice Deposits/Restakes beacon chain ETH in EigenLayer on behalf of the owner of an EigenPod.
* @param podOwner The owner of the pod whose balance must be deposited.
* @param amount The amount of ETH to 'deposit' (i.e. be credited to the podOwner).
* @dev Callable only by the podOwner's EigenPod contract.
*/
function restakeBeaconChainETH(address podOwner, uint amount) external;
/**
* @notice Removes beacon chain ETH from EigenLayer on behalf of the owner of an EigenPod, when the
* balance of a validator is lower than how much stake they have committed to EigenLayer
* @param podOwner The owner of the pod whose balance must be removed.
* @param beaconChainETHStrategyIndex is the index of the beaconChainETHStrategy for the pod owner for the callback to
* the StrategyManager in case it must be removed from the list of the podOwner's strategies
* @param amount The amount of ETH to remove.
* @dev Callable only by the podOwner's EigenPod contract.
*/
function recordOvercommittedBeaconChainETH(address podOwner, uint beaconChainETHStrategyIndex, uint amount) external;
/**
* @notice Withdraws ETH from an EigenPod. The ETH must have first been withdrawn from the beacon chain.
* @param podOwner The owner of the pod whose balance must be withdrawn.
* @param recipient The recipient of the withdrawn ETH.
* @param amount The amount of ETH to withdraw.
* @dev Callable only by the StrategyManager contract.
*/
function withdrawRestakedBeaconChainETH(address podOwner, address recipient, uint amount) external;
/**
* @notice Updates the oracle contract that provides the beacon chain state root
* @param newBeaconChainOracle is the new oracle contract being pointed to
* @dev Callable only by the owner of this contract (i.e. governance)
*/
function updateBeaconChainOracle(IBeaconChainOracle_DeprecatedM1 newBeaconChainOracle) external;
/// @notice Returns the address of the `podOwner`'s EigenPod if it has been deployed.
function ownerToPod(address podOwner) external view returns (IEigenPod_DeprecatedM1);
/// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not).
function getPod(address podOwner) external view returns (IEigenPod_DeprecatedM1);
/// @notice Oracle contract that provides updates to the beacon chain's state
function beaconChainOracle() external view returns (IBeaconChainOracle_DeprecatedM1);
/// @notice Returns the Beacon Chain state root at `blockNumber`. Reverts if the Beacon Chain state root at `blockNumber` has not yet been finalized.
function getBeaconChainStateRoot(uint64 blockNumber) external view returns (bytes32);
/// @notice EigenLayer's StrategyManager contract
function strategyManager() external view returns (IStrategyManager_DeprecatedM1);
function hasPod(address podOwner) external view returns (bool);
}
````
## File: src/test/integration/deprecatedInterfaces/mainnet/IStrategyManager.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/contracts/interfaces/IStrategy.sol";
import "src/contracts/interfaces/IDelegationManager.sol";
import "src/contracts/interfaces/IEigenPodManager.sol";
/**
* @notice M2 DEPRECATED INTERFACE at commit hash https://github.com/Layr-Labs/eigenlayer-contracts/tree/426f461c59b4f0e16f8becdffd747075edcaded8
* @title Interface for the primary entrypoint for funds into EigenLayer.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice See the `StrategyManager` contract itself for implementation details.
*/
interface IStrategyManager_DeprecatedM2 {
/**
* @notice Deposits `amount` of `token` into the specified `strategy`, with the resultant shares credited to `msg.sender`
* @param strategy is the specified strategy where deposit is to be made,
* @param token is the denomination in which the deposit is to be made,
* @param amount is the amount of token to be deposited in the strategy by the staker
* @return shares The amount of new shares in the `strategy` created as part of the action.
* @dev The `msg.sender` must have previously approved this contract to transfer at least `amount` of `token` on their behalf.
* @dev Cannot be called by an address that is 'frozen' (this function will revert if the `msg.sender` is frozen).
*
* WARNING: Depositing tokens that allow reentrancy (eg. ERC-777) into a strategy is not recommended. This can lead to attack vectors
* where the token balance and corresponding strategy shares are not in sync upon reentrancy.
*/
function depositIntoStrategy(IStrategy strategy, IERC20 token, uint amount) external returns (uint shares);
/**
* @notice Used for depositing an asset into the specified strategy with the resultant shares credited to `staker`,
* who must sign off on the action.
* Note that the assets are transferred out/from the `msg.sender`, not from the `staker`; this function is explicitly designed
* purely to help one address deposit 'for' another.
* @param strategy is the specified strategy where deposit is to be made,
* @param token is the denomination in which the deposit is to be made,
* @param amount is the amount of token to be deposited in the strategy by the staker
* @param staker the staker that the deposited assets will be credited to
* @param expiry the timestamp at which the signature expires
* @param signature is a valid signature from the `staker`. either an ECDSA signature if the `staker` is an EOA, or data to forward
* following EIP-1271 if the `staker` is a contract
* @return shares The amount of new shares in the `strategy` created as part of the action.
* @dev The `msg.sender` must have previously approved this contract to transfer at least `amount` of `token` on their behalf.
* @dev A signature is required for this function to eliminate the possibility of griefing attacks, specifically those
* targeting stakers who may be attempting to undelegate.
* @dev Cannot be called if thirdPartyTransfersForbidden is set to true for this strategy
*
* WARNING: Depositing tokens that allow reentrancy (eg. ERC-777) into a strategy is not recommended. This can lead to attack vectors
* where the token balance and corresponding strategy shares are not in sync upon reentrancy
*/
function depositIntoStrategyWithSignature(
IStrategy strategy,
IERC20 token,
uint amount,
address staker,
uint expiry,
bytes memory signature
) external returns (uint shares);
/// @notice Used by the DelegationManager to remove a Staker's shares from a particular strategy when entering the withdrawal queue
function removeShares(address staker, IStrategy strategy, uint shares) external;
/// @notice Used by the DelegationManager to award a Staker some shares that have passed through the withdrawal queue
function addShares(address staker, IERC20 token, IStrategy strategy, uint shares) external;
/// @notice Used by the DelegationManager to convert withdrawn shares to tokens and send them to a recipient
function withdrawSharesAsTokens(address recipient, IStrategy strategy, uint shares, IERC20 token) external;
/// @notice Returns the current shares of `user` in `strategy`
function stakerStrategyShares(address user, IStrategy strategy) external view returns (uint shares);
/**
* @notice Get all details on the staker's deposits and corresponding shares
* @param staker The staker of interest, whose deposits this function will fetch
* @return (staker's strategies, shares in these strategies)
*/
function getDeposits(address staker) external view returns (IStrategy[] memory, uint[] memory);
/// @notice Simple getter function that returns `stakerStrategyList[staker].length`.
function stakerStrategyListLength(address staker) external view returns (uint);
/**
* @notice Owner-only function that adds the provided Strategies to the 'whitelist' of strategies that stakers can deposit into
* @param strategiesToWhitelist Strategies that will be added to the `strategyIsWhitelistedForDeposit` mapping (if they aren't in it already)
* @param thirdPartyTransfersForbiddenValues bool values to set `thirdPartyTransfersForbidden` to for each strategy
*/
function addStrategiesToDepositWhitelist(IStrategy[] calldata strategiesToWhitelist, bool[] calldata thirdPartyTransfersForbiddenValues)
external;
/**
* @notice Owner-only function that removes the provided Strategies from the 'whitelist' of strategies that stakers can deposit into
* @param strategiesToRemoveFromWhitelist Strategies that will be removed to the `strategyIsWhitelistedForDeposit` mapping (if they are in it)
*/
function removeStrategiesFromDepositWhitelist(IStrategy[] calldata strategiesToRemoveFromWhitelist) external;
/**
* If true for a strategy, a user cannot depositIntoStrategyWithSignature into that strategy for another staker
* and also when performing DelegationManager.queueWithdrawals, a staker can only withdraw to themselves.
* Defaulted to false for all existing strategies.
* @param strategy The strategy to set `thirdPartyTransfersForbidden` value to
* @param value bool value to set `thirdPartyTransfersForbidden` to
*/
function setThirdPartyTransfersForbidden(IStrategy strategy, bool value) external;
/// @notice Returns the single, central Delegation contract of EigenLayer
function delegation() external view returns (IDelegationManager);
/// @notice Returns the EigenPodManager contract of EigenLayer
function eigenPodManager() external view returns (IEigenPodManager);
/// @notice Returns the address of the `strategyWhitelister`
function strategyWhitelister() external view returns (address);
/// @notice Returns bool for whether or not `strategy` is whitelisted for deposit
function strategyIsWhitelistedForDeposit(IStrategy strategy) external view returns (bool);
/**
* @notice Owner-only function to change the `strategyWhitelister` address.
* @param newStrategyWhitelister new address for the `strategyWhitelister`.
*/
function setStrategyWhitelister(address newStrategyWhitelister) external;
/**
* @notice Returns bool for whether or not `strategy` enables credit transfers. i.e enabling
* depositIntoStrategyWithSignature calls or queueing withdrawals to a different address than the staker.
*/
function thirdPartyTransfersForbidden(IStrategy strategy) external view returns (bool);
/**
* @notice Getter function for the current EIP-712 domain separator for this contract.
* @dev The domain separator will change in the event of a fork that changes the ChainID.
*/
function domainSeparator() external view returns (bytes32);
/// VIEW FUNCTIONS FOR PUBLIC VARIABLES, NOT IN ORIGINAL INTERFACE
function nonces(address user) external view returns (uint);
function DEPOSIT_TYPEHASH() external view returns (bytes32);
}
/**
* @notice M1 DEPRECATED INTERFACE at commit hash https://github.com/Layr-Labs/eigenlayer-contracts/tree/0139d6213927c0a7812578899ddd3dda58051928
* @title Interface for the primary entrypoint for funds into EigenLayer.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice See the `StrategyManager` contract itself for implementation details.
*/
interface IStrategyManager_DeprecatedM1 {
// packed struct for queued withdrawals; helps deal with stack-too-deep errors
struct WithdrawerAndNonce {
address withdrawer;
uint96 nonce;
}
/**
* Struct type used to specify an existing queued withdrawal. Rather than storing the entire struct, only a hash is stored.
* In functions that operate on existing queued withdrawals -- e.g. `startQueuedWithdrawalWaitingPeriod` or `completeQueuedWithdrawal`,
* the data is resubmitted and the hash of the submitted data is computed by `calculateWithdrawalRoot` and checked against the
* stored hash in order to confirm the integrity of the submitted data.
*/
struct QueuedWithdrawal {
IStrategy[] strategies;
uint[] shares;
address depositor;
WithdrawerAndNonce withdrawerAndNonce;
uint32 withdrawalStartBlock;
address delegatedAddress;
}
/**
* @notice Deposits `amount` of `token` into the specified `strategy`, with the resultant shares credited to `msg.sender`
* @param strategy is the specified strategy where deposit is to be made,
* @param token is the denomination in which the deposit is to be made,
* @param amount is the amount of token to be deposited in the strategy by the depositor
* @return shares The amount of new shares in the `strategy` created as part of the action.
* @dev The `msg.sender` must have previously approved this contract to transfer at least `amount` of `token` on their behalf.
* @dev Cannot be called by an address that is 'frozen' (this function will revert if the `msg.sender` is frozen).
*
* WARNING: Depositing tokens that allow reentrancy (eg. ERC-777) into a strategy is not recommended. This can lead to attack vectors
* where the token balance and corresponding strategy shares are not in sync upon reentrancy.
*/
function depositIntoStrategy(IStrategy strategy, IERC20 token, uint amount) external returns (uint shares);
/**
* @notice Deposits `amount` of beaconchain ETH into this contract on behalf of `staker`
* @param staker is the entity that is restaking in eigenlayer,
* @param amount is the amount of beaconchain ETH being restaked,
* @dev Only callable by EigenPodManager.
*/
function depositBeaconChainETH(address staker, uint amount) external;
/**
* @notice Records an overcommitment event on behalf of a staker. The staker's beaconChainETH shares are decremented by `amount`.
* @param overcommittedPodOwner is the pod owner to be slashed
* @param beaconChainETHStrategyIndex is the index of the beaconChainETHStrategy in case it must be removed,
* @param amount is the amount to decrement the slashedAddress's beaconChainETHStrategy shares
* @dev Only callable by EigenPodManager.
*/
function recordOvercommittedBeaconChainETH(address overcommittedPodOwner, uint beaconChainETHStrategyIndex, uint amount) external;
/**
* @notice Used for depositing an asset into the specified strategy with the resultant shares credited to `staker`,
* who must sign off on the action.
* Note that the assets are transferred out/from the `msg.sender`, not from the `staker`; this function is explicitly designed
* purely to help one address deposit 'for' another.
* @param strategy is the specified strategy where deposit is to be made,
* @param token is the denomination in which the deposit is to be made,
* @param amount is the amount of token to be deposited in the strategy by the depositor
* @param staker the staker that the deposited assets will be credited to
* @param expiry the timestamp at which the signature expires
* @param signature is a valid signature from the `staker`. either an ECDSA signature if the `staker` is an EOA, or data to forward
* following EIP-1271 if the `staker` is a contract
* @return shares The amount of new shares in the `strategy` created as part of the action.
* @dev The `msg.sender` must have previously approved this contract to transfer at least `amount` of `token` on their behalf.
* @dev A signature is required for this function to eliminate the possibility of griefing attacks, specifically those
* targeting stakers who may be attempting to undelegate.
* @dev Cannot be called on behalf of a staker that is 'frozen' (this function will revert if the `staker` is frozen).
*
* WARNING: Depositing tokens that allow reentrancy (eg. ERC-777) into a strategy is not recommended. This can lead to attack vectors
* where the token balance and corresponding strategy shares are not in sync upon reentrancy
*/
function depositIntoStrategyWithSignature(
IStrategy strategy,
IERC20 token,
uint amount,
address staker,
uint expiry,
bytes memory signature
) external returns (uint shares);
/// @notice Returns the current shares of `user` in `strategy`
function stakerDepositShares(address user, IStrategy strategy) external view returns (uint shares);
/**
* @notice Get all details on the depositor's deposits and corresponding shares
* @return (depositor's strategies, shares in these strategies)
*/
function getDeposits(address depositor) external view returns (IStrategy[] memory, uint[] memory);
/// @notice Simple getter function that returns `stakerStrategyList[staker].length`.
function stakerStrategyListLength(address staker) external view returns (uint);
/**
* @notice Called by a staker to queue a withdrawal of the given amount of `shares` from each of the respective given `strategies`.
* @dev Stakers will complete their withdrawal by calling the 'completeQueuedWithdrawal' function.
* User shares are decreased in this function, but the total number of shares in each strategy remains the same.
* The total number of shares is decremented in the 'completeQueuedWithdrawal' function instead, which is where
* the funds are actually sent to the user through use of the strategies' 'withdrawal' function. This ensures
* that the value per share reported by each strategy will remain consistent, and that the shares will continue
* to accrue gains during the enforced withdrawal waiting period.
* @param strategyIndexes is a list of the indices in `stakerStrategyList[msg.sender]` that correspond to the strategies
* for which `msg.sender` is withdrawing 100% of their shares
* @param strategies The Strategies to withdraw from
* @param shares The amount of shares to withdraw from each of the respective Strategies in the `strategies` array
* @param withdrawer The address that can complete the withdrawal and will receive any withdrawn funds or shares upon completing the withdrawal
* @param undelegateIfPossible If this param is marked as 'true' *and the withdrawal will result in `msg.sender` having no shares in any Strategy,*
* then this function will also make an internal call to `undelegate(msg.sender)` to undelegate the `msg.sender`.
* @return The 'withdrawalRoot' of the newly created Queued Withdrawal
* @dev Strategies are removed from `stakerStrategyList` by swapping the last entry with the entry to be removed, then
* popping off the last entry in `stakerStrategyList`. The simplest way to calculate the correct `strategyIndexes` to input
* is to order the strategies *for which `msg.sender` is withdrawing 100% of their shares* from highest index in
* `stakerStrategyList` to lowest index
* @dev Note that if the withdrawal includes shares in the enshrined 'beaconChainETH' strategy, then it must *only* include shares in this strategy, and
* `withdrawer` must match the caller's address. The first condition is because slashing of queued withdrawals cannot be guaranteed
* for Beacon Chain ETH (since we cannot trigger a withdrawal from the beacon chain through a smart contract) and the second condition is because shares in
* the enshrined 'beaconChainETH' strategy technically represent non-fungible positions (deposits to the Beacon Chain, each pointed at a specific EigenPod).
*/
function queueWithdrawal(
uint[] calldata strategyIndexes,
IStrategy[] calldata strategies,
uint[] calldata shares,
address withdrawer,
bool undelegateIfPossible
) external returns (bytes32);
/**
* @notice Used to complete the specified `queuedWithdrawal`. The function caller must match `queuedWithdrawal.withdrawer`
* @param queuedWithdrawal The QueuedWithdrawal to complete.
* @param tokens Array in which the i-th entry specifies the `token` input to the 'withdraw' function of the i-th Strategy in the `strategies` array
* of the `queuedWithdrawal`. This input can be provided with zero length if `receiveAsTokens` is set to 'false' (since in that case, this input will be unused)
* @param middlewareTimesIndex is the index in the operator that the staker who triggered the withdrawal was delegated to's middleware times array
* @param receiveAsTokens If true, the shares specified in the queued withdrawal will be withdrawn from the specified strategies themselves
* and sent to the caller, through calls to `queuedWithdrawal.strategies[i].withdraw`. If false, then the shares in the specified strategies
* will simply be transferred to the caller directly.
* @dev middlewareTimesIndex should be calculated off chain before calling this function by finding the first index that satisfies `slasher.canWithdraw`
*/
function completeQueuedWithdrawal(
QueuedWithdrawal calldata queuedWithdrawal,
IERC20[] calldata tokens,
uint middlewareTimesIndex,
bool receiveAsTokens
) external;
/**
* @notice Used to complete the specified `queuedWithdrawals`. The function caller must match `queuedWithdrawals[...].withdrawer`
* @param queuedWithdrawals The QueuedWithdrawals to complete.
* @param tokens Array of tokens for each QueuedWithdrawal. See `completeQueuedWithdrawal` for the usage of a single array.
* @param middlewareTimesIndexes One index to reference per QueuedWithdrawal. See `completeQueuedWithdrawal` for the usage of a single index.
* @param receiveAsTokens If true, the shares specified in the queued withdrawal will be withdrawn from the specified strategies themselves
* and sent to the caller, through calls to `queuedWithdrawal.strategies[i].withdraw`. If false, then the shares in the specified strategies
* will simply be transferred to the caller directly.
* @dev Array-ified version of `completeQueuedWithdrawal`
* @dev middlewareTimesIndex should be calculated off chain before calling this function by finding the first index that satisfies `slasher.canWithdraw`
*/
function completeQueuedWithdrawals(
QueuedWithdrawal[] calldata queuedWithdrawals,
IERC20[][] calldata tokens,
uint[] calldata middlewareTimesIndexes,
bool[] calldata receiveAsTokens
) external;
/**
* @notice Slashes the shares of a 'frozen' operator (or a staker delegated to one)
* @param slashedAddress is the frozen address that is having its shares slashed
* @param recipient is the address that will receive the slashed funds, which could e.g. be a harmed party themself,
* or a MerkleDistributor-type contract that further sub-divides the slashed funds.
* @param strategies Strategies to slash
* @param shareAmounts The amount of shares to slash in each of the provided `strategies`
* @param tokens The tokens to use as input to the `withdraw` function of each of the provided `strategies`
* @param strategyIndexes is a list of the indices in `stakerStrategyList[msg.sender]` that correspond to the strategies
* for which `msg.sender` is withdrawing 100% of their shares
* @param recipient The slashed funds are withdrawn as tokens to this address.
* @dev strategies are removed from `stakerStrategyList` by swapping the last entry with the entry to be removed, then
* popping off the last entry in `stakerStrategyList`. The simplest way to calculate the correct `strategyIndexes` to input
* is to order the strategies *for which `msg.sender` is withdrawing 100% of their shares* from highest index in
* `stakerStrategyList` to lowest index
*/
function slashShares(
address slashedAddress,
address recipient,
IStrategy[] calldata strategies,
IERC20[] calldata tokens,
uint[] calldata strategyIndexes,
uint[] calldata shareAmounts
) external;
/**
* @notice Slashes an existing queued withdrawal that was created by a 'frozen' operator (or a staker delegated to one)
* @param recipient The funds in the slashed withdrawal are withdrawn as tokens to this address.
* @param queuedWithdrawal The previously queued withdrawal to be slashed
* @param tokens Array in which the i-th entry specifies the `token` input to the 'withdraw' function of the i-th Strategy in the `strategies`
* array of the `queuedWithdrawal`.
* @param indicesToSkip Optional input parameter -- indices in the `strategies` array to skip (i.e. not call the 'withdraw' function on). This input exists
* so that, e.g., if the slashed QueuedWithdrawal contains a malicious strategy in the `strategies` array which always reverts on calls to its 'withdraw' function,
* then the malicious strategy can be skipped (with the shares in effect "burned"), while the non-malicious strategies are still called as normal.
*/
function slashQueuedWithdrawal(
address recipient,
QueuedWithdrawal calldata queuedWithdrawal,
IERC20[] calldata tokens,
uint[] calldata indicesToSkip
) external;
/// @notice Returns the keccak256 hash of `queuedWithdrawal`.
function calculateWithdrawalRoot(QueuedWithdrawal memory queuedWithdrawal) external pure returns (bytes32);
/**
* @notice Owner-only function that adds the provided Strategies to the 'whitelist' of strategies that stakers can deposit into
* @param strategiesToWhitelist Strategies that will be added to the `strategyIsWhitelistedForDeposit` mapping (if they aren't in it already)
*/
function addStrategiesToDepositWhitelist(IStrategy[] calldata strategiesToWhitelist) external;
/**
* @notice Owner-only function that removes the provided Strategies from the 'whitelist' of strategies that stakers can deposit into
* @param strategiesToRemoveFromWhitelist Strategies that will be removed to the `strategyIsWhitelistedForDeposit` mapping (if they are in it)
*/
function removeStrategiesFromDepositWhitelist(IStrategy[] calldata strategiesToRemoveFromWhitelist) external;
/// @notice Returns the single, central Delegation contract of EigenLayer
function delegation() external view returns (IDelegationManager);
/// @notice returns the enshrined, virtual 'beaconChainETH' Strategy
function beaconChainETHStrategy() external view returns (IStrategy);
/// @notice Returns the number of blocks that must pass between the time a withdrawal is queued and the time it can be completed
function withdrawalDelayBlocks() external view returns (uint);
/// VIEW FUNCTIONS FOR PUBLIC VARIABLES, NOT IN ORIGINAL INTERFACE
function withdrawalRootPending(bytes32 withdrawalRoot) external view returns (bool);
function numWithdrawalsQueued(address staker) external view returns (uint);
}
````
## File: src/test/integration/mocks/BeaconChainMock_Deneb.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/mocks/BeaconChainMock.t.sol";
/// @notice A backwards-compatible BeaconChain Mock that updates containers & proofs from Deneb to Pectra
contract BeaconChainMock_DenebForkable is BeaconChainMock {
using StdStyle for *;
using print for *;
// Denotes whether the beacon chain has been forked to Pectra
bool isPectra;
// The timestamp of the Pectra hard fork
uint64 public pectraForkTimestamp;
constructor(EigenPodManager _eigenPodManager, uint64 _genesisTime) BeaconChainMock(_eigenPodManager, _genesisTime) {
/// DENEB SPECIFIC CONSTANTS (PROOF LENGTHS, FIELD SIZES):
// see https://eth2book.info/capella/part3/containers/state/#beaconstate
BEACON_STATE_FIELDS = 32;
VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT);
BALANCE_CONTAINER_PROOF_LEN =
32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT);
MAX_EFFECTIVE_BALANCE_GWEI = 32 gwei;
MAX_EFFECTIVE_BALANCE_WEI = 32 ether;
}
function NAME() public pure override returns (string memory) {
return "BeaconChain_PectraForkable";
}
/**
*
* INTERNAL FUNCTIONS
*
*/
function _advanceEpoch() public override {
cheats.pauseTracing();
// Update effective balances for each validator
for (uint i = 0; i < validators.length; i++) {
Validator storage v = validators[i];
if (v.isDummy) continue; // don't process dummy validators
// Get current balance and trim anything over MaxEB
uint64 balanceGwei = _currentBalanceGwei(uint40(i));
if (balanceGwei > MAX_EFFECTIVE_BALANCE_GWEI) balanceGwei = MAX_EFFECTIVE_BALANCE_GWEI;
v.effectiveBalanceGwei = balanceGwei;
}
// console.log(" Updated effective balances...".dim());
// console.log(" timestamp:", block.timestamp);
// console.log(" epoch:", currentEpoch());
uint64 curEpoch = currentEpoch();
cheats.warp(_nextEpochStartTimestamp(curEpoch));
curTimestamp = uint64(block.timestamp);
// console.log(" Jumping to next epoch...".dim());
// console.log(" timestamp:", block.timestamp);
// console.log(" epoch:", currentEpoch());
// console.log(" Building beacon state trees...".dim());
// Log total number of validators and number being processed for the first time
if (validators.length > 0) {
lastIndexProcessed = validators.length - 1;
} else {
// generate an empty root if we don't have any validators
EIP_4788_ORACLE.setBlockRoot(curTimestamp, keccak256(""));
// console.log("-- no validators; added empty block root");
return;
}
// Build merkle tree for validators
bytes32 validatorsRoot = _buildMerkleTree({
leaves: _getValidatorLeaves(),
treeHeight: BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1,
tree: trees[curTimestamp].validatorTree
});
// console.log("-- validator container root", validatorsRoot);
// Build merkle tree for current balances
bytes32 balanceContainerRoot = _buildMerkleTree({
leaves: _getBalanceLeaves(),
treeHeight: BeaconChainProofs.BALANCE_TREE_HEIGHT + 1,
tree: trees[curTimestamp].balancesTree
});
// console.log("-- balances container root", balanceContainerRoot);
// Build merkle tree for BeaconState
bytes32 beaconStateRoot = _buildMerkleTree({
leaves: _getBeaconStateLeaves(validatorsRoot, balanceContainerRoot),
treeHeight: getBeaconStateTreeHeight(),
tree: trees[curTimestamp].stateTree
});
// console.log("-- beacon state root", beaconStateRoot);
// Build merkle tree for BeaconBlock
bytes32 beaconBlockRoot = _buildMerkleTree({
leaves: _getBeaconBlockLeaves(beaconStateRoot),
treeHeight: BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT,
tree: trees[curTimestamp].blockTree
});
// console.log("-- beacon block root", cheats.toString(beaconBlockRoot));
// Push new block root to oracle
EIP_4788_ORACLE.setBlockRoot(curTimestamp, beaconBlockRoot);
// Pre-generate proofs to pass to EigenPod methods
_genStateRootProof(beaconStateRoot);
_genBalanceContainerProof(balanceContainerRoot);
_genCredentialProofs();
_genBalanceProofs();
cheats.resumeTracing();
}
function _genCredentialProofs() internal override {
mapping(uint40 => ValidatorFieldsProof) storage vfProofs = validatorFieldsProofs[curTimestamp];
// Calculate credential proofs for each validator
for (uint i = 0; i < validators.length; i++) {
bytes memory proof = new bytes(VAL_FIELDS_PROOF_LEN);
bytes32[] memory validatorFields = _getValidatorFields(uint40(i));
bytes32 curNode = Merkle.merkleizeSha256(validatorFields);
// Validator fields leaf -> validator container root
uint depth = 0;
for (uint j = 0; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT; j++) {
bytes32 sibling = trees[curTimestamp].validatorTree.siblings[curNode];
// proof[j] = sibling;
assembly {
mstore(add(proof, add(32, mul(32, j))), sibling)
}
curNode = trees[curTimestamp].validatorTree.parents[curNode];
depth++;
}
// Validator container root -> beacon state root
for (uint j = depth; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + getBeaconStateTreeHeight(); j++) {
bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode];
// proof[j] = sibling;
assembly {
mstore(add(proof, add(32, mul(32, j))), sibling)
}
curNode = trees[curTimestamp].stateTree.parents[curNode];
depth++;
}
vfProofs[uint40(i)].validatorFields = validatorFields;
vfProofs[uint40(i)].validatorFieldsProof = proof;
}
}
function _genBalanceContainerProof(bytes32 balanceContainerRoot) internal override {
bytes memory proof = new bytes(BALANCE_CONTAINER_PROOF_LEN);
bytes32 curNode = balanceContainerRoot;
uint totalHeight = BALANCE_CONTAINER_PROOF_LEN / 32;
uint depth = 0;
for (uint i = 0; i < getBeaconStateTreeHeight(); i++) {
bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode];
// proof[j] = sibling;
assembly {
mstore(add(proof, add(32, mul(32, i))), sibling)
}
curNode = trees[curTimestamp].stateTree.parents[curNode];
depth++;
}
for (uint i = depth; i < totalHeight; i++) {
bytes32 sibling = trees[curTimestamp].blockTree.siblings[curNode];
// proof[j] = sibling;
assembly {
mstore(add(proof, add(32, mul(32, i))), sibling)
}
curNode = trees[curTimestamp].blockTree.parents[curNode];
depth++;
}
balanceContainerProofs[curTimestamp] =
BeaconChainProofs.BalanceContainerProof({balanceContainerRoot: balanceContainerRoot, proof: proof});
}
/// @notice Forks the beacon chain to Pectra
/// @dev Test battery should warp to the fork timestamp after calling this method
function forkToPectra(uint64 _pectraForkTimestamp) public {
// https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#beaconstate
BEACON_STATE_FIELDS = 37;
VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT);
BALANCE_CONTAINER_PROOF_LEN =
32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT);
MAX_EFFECTIVE_BALANCE_GWEI = 2048 gwei;
MAX_EFFECTIVE_BALANCE_WEI = 2048 ether;
isPectra = true;
cheats.warp(_pectraForkTimestamp);
pectraForkTimestamp = _pectraForkTimestamp;
}
function getBeaconStateTreeHeight() public view returns (uint) {
return isPectra ? BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT : BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT;
}
}
````
## File: src/test/integration/mocks/BeaconChainMock.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "src/contracts/libraries/BeaconChainProofs.sol";
import "src/contracts/libraries/Merkle.sol";
import "src/contracts/pods/EigenPodManager.sol";
import "src/test/mocks/ETHDepositMock.sol";
import "src/test/integration/mocks/EIP_4788_Oracle_Mock.t.sol";
import "src/test/utils/Logger.t.sol";
struct ValidatorFieldsProof {
bytes32[] validatorFields;
bytes validatorFieldsProof;
}
struct BalanceRootProof {
bytes32 balanceRoot;
bytes proof;
}
struct CheckpointProofs {
BeaconChainProofs.BalanceContainerProof balanceContainerProof;
BeaconChainProofs.BalanceProof[] balanceProofs;
}
struct CredentialProofs {
uint64 beaconTimestamp;
BeaconChainProofs.StateRootProof stateRootProof;
bytes[] validatorFieldsProofs;
bytes32[][] validatorFields;
}
struct StaleBalanceProofs {
uint64 beaconTimestamp;
BeaconChainProofs.StateRootProof stateRootProof;
BeaconChainProofs.ValidatorProof validatorProof;
}
/// @notice A Pectra Beacon Chain Mock Contract. For testing upgrades, use BeaconChainMock_Upgradeable
/// @notice This mock assumed the following
/**
* @notice A Semi-Compatible Pectra Beacon Chain Mock Contract. For Testing Upgrades to Pectra use BeaconChainMock_Upgradeable
* @dev This mock assumes the following:
* - Ceiling is Max EB, at which sweeps will be triggered
* - No support for consolidations or any execution layer triggerable actions (exits, partial withdrawals)
*/
contract BeaconChainMock is Logger {
using StdStyle for *;
using print for *;
struct Validator {
bool isDummy;
bool isSlashed;
bytes32 pubkeyHash;
bytes withdrawalCreds;
uint64 effectiveBalanceGwei;
uint64 activationEpoch;
uint64 exitEpoch;
}
/// @dev The type of slash to apply to a validator
enum SlashType {
Minor, // `MINOR_SLASH_AMOUNT_GWEI`
Half, // Half of the validator's balance
Full // The validator's entire balance
}
/// @dev All withdrawals are processed with index == 0
uint constant ZERO_NODES_LENGTH = 100;
// Rewards given to each validator during epoch processing
uint64 public constant CONSENSUS_REWARD_AMOUNT_GWEI = 1;
uint64 public constant MINOR_SLASH_AMOUNT_GWEI = 10;
// Max effective balance for a validator
// see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#gwei-values
uint public MAX_EFFECTIVE_BALANCE_WEI = 2048 ether;
uint64 public MAX_EFFECTIVE_BALANCE_GWEI = 2048 gwei;
/// PROOF CONSTANTS (PROOF LENGTHS, FIELD SIZES):
/// @dev Non-constant values will change with the Pectra hard fork
// see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#beaconstate
uint BEACON_STATE_FIELDS = 37;
// see https://eth2book.info/capella/part3/containers/blocks/#beaconblock
uint constant BEACON_BLOCK_FIELDS = 5;
uint immutable BLOCKROOT_PROOF_LEN = 32 * BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT;
uint VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT);
uint BALANCE_CONTAINER_PROOF_LEN =
32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT);
uint immutable BALANCE_PROOF_LEN = 32 * (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1);
uint64 genesisTime;
uint64 public nextTimestamp;
EigenPodManager eigenPodManager;
IETHPOSDeposit constant DEPOSIT_CONTRACT = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa);
EIP_4788_Oracle_Mock constant EIP_4788_ORACLE = EIP_4788_Oracle_Mock(0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02);
/**
* BeaconState containers, used for proofgen:
* https://eth2book.info/capella/part3/containers/state/#beaconstate
*/
// Validator container, references every validator created so far
Validator[] validators;
// Current balance container, keeps a balance for every validator
//
// Since balances are stored in groups of 4, it's easier to make
// this a mapping rather than an array. If it were an array, its
// length would be validators.length / 4;
mapping(uint40 => bytes32) balances;
/**
* Generated proofs for each block timestamp:
*/
// Maps block.timestamp -> calculated beacon block roots
mapping(uint64 => bytes32) beaconBlockRoots;
// Keeps track of the index of the last validator we've seen during epoch processing
uint lastIndexProcessed;
uint64 curTimestamp;
// Maps block.timestamp -> beacon state root and proof
mapping(uint64 => BeaconChainProofs.StateRootProof) stateRootProofs;
// Maps block.timestamp -> balance container root and proof
mapping(uint64 => BeaconChainProofs.BalanceContainerProof) balanceContainerProofs;
// Maps block.timestamp -> validatorIndex -> credential proof for that timestamp
mapping(uint64 => mapping(uint40 => ValidatorFieldsProof)) validatorFieldsProofs;
// Maps block.timestamp -> balanceRootIndex -> balance proof for that timestamp
mapping(uint64 => mapping(uint40 => BalanceRootProof)) balanceRootProofs;
bytes32[] zeroNodes;
constructor(EigenPodManager _eigenPodManager, uint64 _genesisTime) {
genesisTime = _genesisTime;
eigenPodManager = _eigenPodManager;
// Create mock 4788 oracle
cheats.etch(address(DEPOSIT_CONTRACT), type(ETHPOSDepositMock).runtimeCode);
cheats.etch(address(EIP_4788_ORACLE), type(EIP_4788_Oracle_Mock).runtimeCode);
// Calculate nodes of empty merkle tree
bytes32 curNode = Merkle.merkleizeSha256(new bytes32[](8));
zeroNodes = new bytes32[](ZERO_NODES_LENGTH);
zeroNodes[0] = curNode;
for (uint i = 1; i < zeroNodes.length; i++) {
zeroNodes[i] = sha256(abi.encodePacked(curNode, curNode));
curNode = zeroNodes[i];
}
}
function NAME() public pure virtual override returns (string memory) {
return "BeaconChain";
}
/**
*
* EXTERNAL METHODS
*
*/
/// @dev Creates a new validator by:
/// - Creating the validator container
/// - Setting their current/effective balance
/// - Assigning them a new, unique index
function newValidator(bytes memory withdrawalCreds) public payable returns (uint40) {
print.method("newValidator");
uint balanceWei = msg.value;
// These checks mimic the checks made in the beacon chain deposit contract
//
// We sanity-check them here because this contract sorta acts like the
// deposit contract and this ensures we only create validators that could
// exist IRL
require(balanceWei >= 1 ether, "BeaconChainMock.newValidator: deposit value too low");
require(balanceWei % 1 gwei == 0, "BeaconChainMock.newValidator: value not multiple of gwei");
uint depositAmount = balanceWei / GWEI_TO_WEI;
require(depositAmount <= type(uint64).max, "BeaconChainMock.newValidator: deposit value too high");
// Create new validator and return its unique index
return _createValidator(withdrawalCreds, uint64(depositAmount));
}
/// @dev Initiate an exit by:
/// - Updating the validator's exit epoch
/// - Decreasing current balance to 0
/// - Withdrawing the balance to the validator's withdrawal credentials
/// NOTE that the validator's effective balance is unchanged until advanceEpoch is called
/// @return exitedBalanceGwei The balance exited to the withdrawal address
///
/// This partially mimics the beacon chain's behavior, which is:
/// 1. when an exit is initiated, the validator's exit/withdrawable epochs are immediately set
/// 2. in a future slot (as part of the withdrawal queue), the validator's current balance is set to 0
/// - at the end of this slot, the eth is withdrawn to the withdrawal credentials
/// 3. when the epoch finalizes, the validator's effective balance is updated
///
/// Because this mock beacon chain doesn't implement a withdrawal queue or per-slot processing,
/// `exitValidator` combines steps 1 and 2 into this method.
///
/// TODO we may need to advance a slot here to maintain the properties we want in startCheckpoint
function exitValidator(uint40 validatorIndex) public returns (uint64 exitedBalanceGwei) {
print.method("exitValidator");
// Update validator.exitEpoch
Validator storage v = validators[validatorIndex];
require(!v.isDummy, "BeaconChainMock: attempting to exit dummy validator. We need those for proofgen >:(");
require(v.exitEpoch == BeaconChainProofs.FAR_FUTURE_EPOCH, "BeaconChainMock: validator already exited");
v.exitEpoch = currentEpoch() + 1;
// Set current balance to 0
exitedBalanceGwei = _currentBalanceGwei(validatorIndex);
_setCurrentBalance(validatorIndex, 0);
// Send current balance to pod
address destination = _toAddress(validators[validatorIndex].withdrawalCreds);
cheats.deal(destination, address(destination).balance + uint(uint(exitedBalanceGwei) * GWEI_TO_WEI));
return exitedBalanceGwei;
}
function slashValidators(uint40[] memory _validators, SlashType _slashType) public returns (uint64 slashedBalanceGwei) {
print.method("slashValidators");
for (uint i = 0; i < _validators.length; i++) {
uint40 validatorIndex = _validators[i];
Validator storage v = validators[validatorIndex];
require(!v.isDummy, "BeaconChainMock: attempting to exit dummy validator. We need those for proofgen >:(");
// Mark slashed and initiate validator exit
if (!v.isSlashed) {
v.isSlashed = true;
v.exitEpoch = currentEpoch() + 1;
}
// Calculate slash amount
uint64 slashAmountGwei;
uint64 curBalanceGwei = _currentBalanceGwei(validatorIndex);
if (_slashType == SlashType.Minor) slashAmountGwei = MINOR_SLASH_AMOUNT_GWEI;
else if (_slashType == SlashType.Half) slashAmountGwei = curBalanceGwei / 2;
else if (_slashType == SlashType.Full) slashAmountGwei = curBalanceGwei;
// Calculate slash amount
if (slashAmountGwei > curBalanceGwei) {
slashedBalanceGwei += curBalanceGwei;
curBalanceGwei = 0;
} else {
slashedBalanceGwei += slashAmountGwei;
curBalanceGwei -= slashAmountGwei;
}
// Decrease current balance (effective balance updated during epoch processing)
_setCurrentBalance(validatorIndex, curBalanceGwei);
console.log(" - Slashed validator %s by %s gwei", validatorIndex, slashAmountGwei);
}
return slashedBalanceGwei;
}
function slashValidators(uint40[] memory _validators, uint64 _slashAmountGwei) public {
print.method("slashValidatorsAmountGwei");
for (uint i = 0; i < _validators.length; i++) {
uint40 validatorIndex = _validators[i];
Validator storage v = validators[validatorIndex];
require(!v.isDummy, "BeaconChainMock: attempting to exit dummy validator. We need those for proofgen >:(");
// Mark slashed and initiate validator exit
if (!v.isSlashed) {
v.isSlashed = true;
v.exitEpoch = currentEpoch() + 1;
}
// Calculate slash amount
uint64 curBalanceGwei = _currentBalanceGwei(validatorIndex);
// Calculate slash amount
uint64 slashedAmountGwei;
if (_slashAmountGwei > curBalanceGwei) {
slashedAmountGwei = curBalanceGwei;
_slashAmountGwei -= curBalanceGwei;
curBalanceGwei = 0;
} else {
slashedAmountGwei = _slashAmountGwei;
curBalanceGwei -= _slashAmountGwei;
}
// Decrease current balance (effective balance updated during epoch processing)
_setCurrentBalance(validatorIndex, curBalanceGwei);
console.log(" - Slashed validator %s by %s gwei", validatorIndex, slashedAmountGwei);
}
}
/// @dev Move forward one epoch on the beacon chain, taking care of important epoch processing:
/// - Award ALL validators CONSENSUS_REWARD_AMOUNT
/// - Withdraw any balance over Max EB
/// - Withdraw any balance for exited validators
/// - Effective balances updated (NOTE: we do not use hysteresis!)
/// - Move time forward one epoch
/// - State root calculated and credential/balance proofs generated for all validators
/// - Send state root to 4788 oracle
///
/// Note:
/// - DOES generate consensus rewards for ALL non-exited validators
/// - DOES withdraw in excess of Max EB / if validator is exited
function advanceEpoch() public {
print.method("advanceEpoch");
_generateRewards();
_withdrawExcess();
_advanceEpoch();
}
/// @dev Like `advanceEpoch`, but does NOT generate consensus rewards for validators.
/// This amount is added to each validator's current balance before effective balances
/// are updated.
///
/// Note:
/// - does NOT generate consensus rewards
/// - DOES withdraw in excess of Max EB / if validator is exited
function advanceEpoch_NoRewards() public {
print.method("advanceEpoch_NoRewards");
_withdrawExcess();
_advanceEpoch();
}
/// @dev Like `advanceEpoch`, but explicitly does NOT withdraw if balances
/// are over Max EB. This exists to support tests that check share increases solely
/// due to beacon chain balance changes.
///
/// Note:
/// - DOES generate consensus rewards for ALL non-exited validators
/// - does NOT withdraw in excess of Max EB
/// - does NOT withdraw if validator is exited
function advanceEpoch_NoWithdraw() public {
print.method("advanceEpoch_NoWithdraw");
_generateRewards();
_advanceEpoch();
}
function advanceEpoch_NoWithdrawNoRewards() public {
print.method("advanceEpoch_NoWithdrawNoRewards");
_advanceEpoch();
}
/// @dev Iterate over all validators. If the validator is still active,
/// add CONSENSUS_REWARD_AMOUNT_GWEI to its current balance
function _generateRewards() internal {
uint totalRewarded;
for (uint i = 0; i < validators.length; i++) {
Validator storage v = validators[i];
if (v.isDummy) continue; // don't process dummy validators
// If validator has not initiated exit, add rewards to their current balance
if (v.exitEpoch == BeaconChainProofs.FAR_FUTURE_EPOCH) {
uint64 balanceGwei = _currentBalanceGwei(uint40(i));
balanceGwei += CONSENSUS_REWARD_AMOUNT_GWEI;
totalRewarded++;
_setCurrentBalance(uint40(i), balanceGwei);
}
}
console.log(" - Generated rewards for %s of %s validators.", totalRewarded, validators.length);
}
/// @dev Iterate over all validators. If the validator has > Max EB current balance
/// OR is exited, withdraw the excess to the validator's withdrawal address.
function _withdrawExcess() internal {
uint totalExcessWei;
for (uint i = 0; i < validators.length; i++) {
Validator storage v = validators[i];
if (v.isDummy) continue; // don't process dummy validators
uint balanceWei = uint(_currentBalanceGwei(uint40(i))) * GWEI_TO_WEI;
address destination = _toAddress(v.withdrawalCreds);
uint excessBalanceWei;
uint64 newBalanceGwei = uint64(balanceWei / GWEI_TO_WEI);
// If the validator has exited, withdraw any existing balance
//
// If the validator has > Max EB, withdraw anything over that
if (v.exitEpoch != BeaconChainProofs.FAR_FUTURE_EPOCH) {
if (balanceWei == 0) continue;
excessBalanceWei = balanceWei;
newBalanceGwei = 0;
} else if (balanceWei > MAX_EFFECTIVE_BALANCE_WEI) {
excessBalanceWei = balanceWei - MAX_EFFECTIVE_BALANCE_WEI;
newBalanceGwei = MAX_EFFECTIVE_BALANCE_GWEI;
}
// Send ETH to withdrawal address
cheats.deal(destination, address(destination).balance + excessBalanceWei);
totalExcessWei += excessBalanceWei;
// Update validator's current balance
_setCurrentBalance(uint40(i), newBalanceGwei);
}
if (totalExcessWei != 0) console.log("- Withdrew excess balance:", totalExcessWei.asGwei());
}
function _advanceEpoch() public virtual {
cheats.pauseTracing();
// Update effective balances for each validator
for (uint i = 0; i < validators.length; i++) {
Validator storage v = validators[i];
if (v.isDummy) continue; // don't process dummy validators
// Get current balance and trim anything over MAX EB
uint64 balanceGwei = _currentBalanceGwei(uint40(i));
if (balanceGwei > MAX_EFFECTIVE_BALANCE_GWEI) balanceGwei = MAX_EFFECTIVE_BALANCE_GWEI;
v.effectiveBalanceGwei = balanceGwei;
}
// console.log(" Updated effective balances...".dim());
// console.log(" timestamp:", block.timestamp);
// console.log(" epoch:", currentEpoch());
uint64 curEpoch = currentEpoch();
cheats.warp(_nextEpochStartTimestamp(curEpoch));
curTimestamp = uint64(block.timestamp);
// console.log(" Jumping to next epoch...".dim());
// console.log(" timestamp:", block.timestamp);
// console.log(" epoch:", currentEpoch());
// console.log(" Building beacon state trees...".dim());
// Log total number of validators and number being processed for the first time
if (validators.length > 0) {
lastIndexProcessed = validators.length - 1;
} else {
// generate an empty root if we don't have any validators
EIP_4788_ORACLE.setBlockRoot(curTimestamp, keccak256(""));
// console.log("-- no validators; added empty block root");
return;
}
// Build merkle tree for validators
bytes32 validatorsRoot = _buildMerkleTree({
leaves: _getValidatorLeaves(),
treeHeight: BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1,
tree: trees[curTimestamp].validatorTree
});
// console.log("-- validator container root", validatorsRoot);
// Build merkle tree for current balances
bytes32 balanceContainerRoot = _buildMerkleTree({
leaves: _getBalanceLeaves(),
treeHeight: BeaconChainProofs.BALANCE_TREE_HEIGHT + 1,
tree: trees[curTimestamp].balancesTree
});
// console.log("-- balances container root", balanceContainerRoot);
// Build merkle tree for BeaconState
bytes32 beaconStateRoot = _buildMerkleTree({
leaves: _getBeaconStateLeaves(validatorsRoot, balanceContainerRoot),
treeHeight: BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT,
tree: trees[curTimestamp].stateTree
});
// console.log("-- beacon state root", beaconStateRoot);
// Build merkle tree for BeaconBlock
bytes32 beaconBlockRoot = _buildMerkleTree({
leaves: _getBeaconBlockLeaves(beaconStateRoot),
treeHeight: BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT,
tree: trees[curTimestamp].blockTree
});
// console.log("-- beacon block root", cheats.toString(beaconBlockRoot));
// Push new block root to oracle
EIP_4788_ORACLE.setBlockRoot(curTimestamp, beaconBlockRoot);
// Pre-generate proofs to pass to EigenPod methods
_genStateRootProof(beaconStateRoot);
_genBalanceContainerProof(balanceContainerRoot);
_genCredentialProofs();
_genBalanceProofs();
cheats.resumeTracing();
}
/**
*
* INTERNAL FUNCTIONS
*
*/
function _createValidator(bytes memory withdrawalCreds, uint64 balanceGwei) internal returns (uint40) {
cheats.pauseTracing();
uint40 validatorIndex = uint40(validators.length);
// HACK to make balance proofs work. Every 4 validators we create
// a dummy validator with empty withdrawal credentials and a unique
// balance value. This ensures that each balanceRoot is unique, which
// allows our efficient beacon state builder to work.
//
// For more details on this hack see _buildMerkleTree
if (validatorIndex % 4 == 0) {
uint64 dummyBalanceGwei = type(uint64).max - uint64(validators.length);
bytes memory _dummyPubkey = new bytes(48);
assembly {
mstore(add(48, _dummyPubkey), validatorIndex)
}
validators.push(
Validator({
isDummy: true,
isSlashed: false,
pubkeyHash: sha256(abi.encodePacked(_dummyPubkey, bytes16(0))),
withdrawalCreds: "",
effectiveBalanceGwei: dummyBalanceGwei,
activationEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH,
exitEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH
})
);
_setCurrentBalance(validatorIndex, dummyBalanceGwei);
validatorIndex++;
}
// Use pubkey format from `EigenPod._calculateValidatorPubkeyHash`
bytes memory _pubkey = new bytes(48);
assembly {
mstore(add(48, _pubkey), validatorIndex)
}
validators.push(
Validator({
isDummy: false,
isSlashed: false,
pubkeyHash: sha256(abi.encodePacked(_pubkey, bytes16(0))),
withdrawalCreds: withdrawalCreds,
effectiveBalanceGwei: balanceGwei,
activationEpoch: currentEpoch(),
exitEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH
})
);
_setCurrentBalance(validatorIndex, balanceGwei);
cheats.resumeTracing();
return validatorIndex;
}
struct Tree {
mapping(bytes32 => bytes32) siblings;
mapping(bytes32 => bytes32) parents;
}
struct MerkleTrees {
Tree validatorTree;
Tree balancesTree;
Tree stateTree;
Tree blockTree;
}
/// Timestamp -> merkle trees constructed at that timestamp
/// Used to generate proofs
mapping(uint64 => MerkleTrees) trees;
/// @dev Builds a merkle tree using the given leaves and height
/// -- if the leaves given are not complete (i.e. the depth should have more leaves),
/// a pre-calculated zero-node is used to complete the tree.
/// -- each pair of nodes is stored in `siblings`, and their parent in `parents`.
/// These mappings are used to build proofs for any individual leaf
/// @return The root of the merkle tree
///
/// HACK: this sibling/parent method of tree construction relies on all passed-in leaves
/// being unique, so that we don't overwrite siblings/parents. This is simple for trees
/// like the validator tree, as each leaf is a validator's unique validatorFields.
/// However, for the balances tree, the leaves may not be distinct. To get around this,
/// _createValidator adds "dummy" validators every 4 validators created, with a unique
/// balance value. This ensures each balance root is unique.
function _buildMerkleTree(bytes32[] memory leaves, uint treeHeight, Tree storage tree) internal returns (bytes32) {
for (uint depth = 0; depth < treeHeight; depth++) {
uint newLength = (leaves.length + 1) / 2;
bytes32[] memory newLeaves = new bytes32[](newLength);
// Hash each pair of nodes in this level of the tree
for (uint i = 0; i < newLength; i++) {
uint leftIdx = 2 * i;
uint rightIdx = leftIdx + 1;
// Get left leaf
bytes32 leftLeaf = leaves[leftIdx];
// Calculate right leaf
bytes32 rightLeaf;
if (rightIdx < leaves.length) rightLeaf = leaves[rightIdx];
else rightLeaf = _getZeroNode(depth);
// Hash left and right
bytes32 result = sha256(abi.encodePacked(leftLeaf, rightLeaf));
newLeaves[i] = result;
// Record results, used to generate individual proofs later:
// Record left and right as siblings
tree.siblings[leftLeaf] = rightLeaf;
tree.siblings[rightLeaf] = leftLeaf;
// Record the result as the parent of left and right
tree.parents[leftLeaf] = result;
tree.parents[rightLeaf] = result;
}
// Move up one level
leaves = newLeaves;
}
require(leaves.length == 1, "BeaconChainMock._buildMerkleTree: invalid tree somehow");
return leaves[0];
}
function _genStateRootProof(bytes32 beaconStateRoot) internal {
bytes memory proof = new bytes(BLOCKROOT_PROOF_LEN);
bytes32 curNode = beaconStateRoot;
uint depth = 0;
for (uint i = 0; i < BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; i++) {
bytes32 sibling = trees[curTimestamp].blockTree.siblings[curNode];
// proof[j] = sibling;
assembly {
mstore(add(proof, add(32, mul(32, i))), sibling)
}
curNode = trees[curTimestamp].blockTree.parents[curNode];
depth++;
}
stateRootProofs[curTimestamp] = BeaconChainProofs.StateRootProof({beaconStateRoot: beaconStateRoot, proof: proof});
}
function _genBalanceContainerProof(bytes32 balanceContainerRoot) internal virtual {
bytes memory proof = new bytes(BALANCE_CONTAINER_PROOF_LEN);
bytes32 curNode = balanceContainerRoot;
uint totalHeight = BALANCE_CONTAINER_PROOF_LEN / 32;
uint depth = 0;
for (uint i = 0; i < BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; i++) {
bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode];
// proof[j] = sibling;
assembly {
mstore(add(proof, add(32, mul(32, i))), sibling)
}
curNode = trees[curTimestamp].stateTree.parents[curNode];
depth++;
}
for (uint i = depth; i < totalHeight; i++) {
bytes32 sibling = trees[curTimestamp].blockTree.siblings[curNode];
// proof[j] = sibling;
assembly {
mstore(add(proof, add(32, mul(32, i))), sibling)
}
curNode = trees[curTimestamp].blockTree.parents[curNode];
depth++;
}
balanceContainerProofs[curTimestamp] =
BeaconChainProofs.BalanceContainerProof({balanceContainerRoot: balanceContainerRoot, proof: proof});
}
function _genCredentialProofs() internal virtual {
mapping(uint40 => ValidatorFieldsProof) storage vfProofs = validatorFieldsProofs[curTimestamp];
// Calculate credential proofs for each validator
for (uint i = 0; i < validators.length; i++) {
bytes memory proof = new bytes(VAL_FIELDS_PROOF_LEN);
bytes32[] memory validatorFields = _getValidatorFields(uint40(i));
bytes32 curNode = Merkle.merkleizeSha256(validatorFields);
// Validator fields leaf -> validator container root
uint depth = 0;
for (uint j = 0; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT; j++) {
bytes32 sibling = trees[curTimestamp].validatorTree.siblings[curNode];
// proof[j] = sibling;
assembly {
mstore(add(proof, add(32, mul(32, j))), sibling)
}
curNode = trees[curTimestamp].validatorTree.parents[curNode];
depth++;
}
// Validator container root -> beacon state root
for (uint j = depth; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; j++) {
bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode];
// proof[j] = sibling;
assembly {
mstore(add(proof, add(32, mul(32, j))), sibling)
}
curNode = trees[curTimestamp].stateTree.parents[curNode];
depth++;
}
vfProofs[uint40(i)].validatorFields = validatorFields;
vfProofs[uint40(i)].validatorFieldsProof = proof;
}
}
function _genBalanceProofs() internal {
mapping(uint40 => BalanceRootProof) storage brProofs = balanceRootProofs[curTimestamp];
// Calculate current balance proofs for each balance root
uint numBalanceRoots = _numBalanceRoots();
for (uint i = 0; i < numBalanceRoots; i++) {
bytes memory proof = new bytes(BALANCE_PROOF_LEN);
bytes32 balanceRoot = balances[uint40(i)];
bytes32 curNode = balanceRoot;
// Balance root leaf -> balances container root
uint depth = 0;
for (uint j = 0; j < 1 + BeaconChainProofs.BALANCE_TREE_HEIGHT; j++) {
bytes32 sibling = trees[curTimestamp].balancesTree.siblings[curNode];
// proof[j] = sibling;
assembly {
mstore(add(proof, add(32, mul(32, j))), sibling)
}
curNode = trees[curTimestamp].balancesTree.parents[curNode];
depth++;
}
brProofs[uint40(i)].balanceRoot = balanceRoot;
brProofs[uint40(i)].proof = proof;
}
}
function _getValidatorLeaves() internal view returns (bytes32[] memory) {
bytes32[] memory leaves = new bytes32[](validators.length);
// Place each validator's validatorFields into tree
for (uint i = 0; i < validators.length; i++) {
leaves[i] = Merkle.merkleizeSha256(_getValidatorFields(uint40(i)));
}
return leaves;
}
function _getBalanceLeaves() internal view returns (bytes32[] memory) {
// Place each validator's current balance into tree
bytes32[] memory leaves = new bytes32[](_numBalanceRoots());
for (uint i = 0; i < leaves.length; i++) {
leaves[i] = balances[uint40(i)];
}
return leaves;
}
function _numBalanceRoots() internal view returns (uint) {
// Each balance leaf is shared by 4 validators. This uses div_ceil
// to calculate the number of balance leaves
return (validators.length == 0) ? 0 : ((validators.length - 1) / 4) + 1;
}
function _getBeaconStateLeaves(bytes32 validatorsRoot, bytes32 balancesRoot) internal view returns (bytes32[] memory) {
bytes32[] memory leaves = new bytes32[](BEACON_STATE_FIELDS);
// Pre-populate leaves with dummy values so sibling/parent tracking is correct
for (uint i = 0; i < leaves.length; i++) {
leaves[i] = bytes32(i + 1);
}
// Place validatorsRoot and balancesRoot into tree
leaves[BeaconChainProofs.VALIDATOR_CONTAINER_INDEX] = validatorsRoot;
leaves[BeaconChainProofs.BALANCE_CONTAINER_INDEX] = balancesRoot;
return leaves;
}
function _getBeaconBlockLeaves(bytes32 beaconStateRoot) internal pure returns (bytes32[] memory) {
bytes32[] memory leaves = new bytes32[](BEACON_BLOCK_FIELDS);
// Pre-populate leaves with dummy values so sibling/parent tracking is correct
for (uint i = 0; i < leaves.length; i++) {
leaves[i] = bytes32(i + 1);
}
// Place beaconStateRoot into tree
leaves[BeaconChainProofs.STATE_ROOT_INDEX] = beaconStateRoot;
return leaves;
}
function _currentBalanceGwei(uint40 validatorIndex) internal view returns (uint64) {
return currentBalance(validatorIndex);
}
function currentEpoch() public view returns (uint64) {
require(block.timestamp >= genesisTime, "BeaconChain.currentEpoch: current time is before genesis time");
return uint64((block.timestamp - genesisTime) / BeaconChainProofs.SECONDS_PER_EPOCH);
}
/// @dev Returns the validator's exit epoch
function exitEpoch(uint40 validatorIndex) public view returns (uint64) {
return validators[validatorIndex].exitEpoch;
}
function totalEffectiveBalanceWei(uint40[] memory validatorIndices) public view returns (uint) {
uint total;
for (uint i = 0; i < validatorIndices.length; i++) {
total += uint(validators[validatorIndices[i]].effectiveBalanceGwei * GWEI_TO_WEI);
}
return total;
}
/// @dev Returns the validator's effective balance, in gwei
function effectiveBalance(uint40 validatorIndex) public view returns (uint64) {
return validators[validatorIndex].effectiveBalanceGwei;
}
/// @dev Returns the validator's current balance, in gwei
function currentBalance(uint40 validatorIndex) public view returns (uint64) {
return BeaconChainProofs.getBalanceAtIndex(getBalanceRoot(validatorIndex), validatorIndex);
}
function getBalanceRoot(uint40 validatorIndex) public view returns (bytes32) {
return balances[validatorIndex / 4];
}
function _getBalanceRootIndex(uint40 validatorIndex) internal pure returns (uint40) {
return validatorIndex / 4;
}
function _getValidatorFields(uint40 validatorIndex) internal view returns (bytes32[] memory) {
bytes32[] memory vFields = new bytes32[](8);
Validator memory v = validators[validatorIndex];
vFields[BeaconChainProofs.VALIDATOR_PUBKEY_INDEX] = v.pubkeyHash;
vFields[BeaconChainProofs.VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX] = bytes32(v.withdrawalCreds);
vFields[BeaconChainProofs.VALIDATOR_BALANCE_INDEX] = _toLittleEndianUint64(v.effectiveBalanceGwei);
vFields[BeaconChainProofs.VALIDATOR_SLASHED_INDEX] = bytes32(abi.encode(v.isSlashed));
vFields[BeaconChainProofs.VALIDATOR_ACTIVATION_EPOCH_INDEX] = _toLittleEndianUint64(v.activationEpoch);
vFields[BeaconChainProofs.VALIDATOR_EXIT_EPOCH_INDEX] = _toLittleEndianUint64(v.exitEpoch);
return vFields;
}
/// @dev Update the validator's current balance
function _setCurrentBalance(uint40 validatorIndex, uint64 newBalanceGwei) internal {
bytes32 balanceRoot = balances[validatorIndex / 4];
balanceRoot = _calcBalanceRoot(balanceRoot, validatorIndex, newBalanceGwei);
balances[validatorIndex / 4] = balanceRoot;
}
/// From EigenPod.sol
function _nextEpochStartTimestamp(uint64 epoch) internal view returns (uint64) {
return genesisTime + ((1 + epoch) * BeaconChainProofs.SECONDS_PER_EPOCH);
}
function _calcValProofIndex(uint40 validatorIndex) internal pure returns (uint) {
return (BeaconChainProofs.VALIDATOR_CONTAINER_INDEX << (BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1)) | uint(validatorIndex);
}
function _calcBalanceProofIndex(uint40 balanceRootIndex) internal pure returns (uint) {
return (BeaconChainProofs.BALANCE_CONTAINER_INDEX << (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1)) | uint(balanceRootIndex);
}
function _getZeroNode(uint depth) internal view returns (bytes32) {
require(depth < ZERO_NODES_LENGTH, "_getZeroNode: invalid depth");
return zeroNodes[depth];
}
/// @dev Opposite of Endian.fromLittleEndianUint64
function _toLittleEndianUint64(uint64 num) internal pure returns (bytes32) {
uint lenum;
// Rearrange the bytes from big-endian to little-endian format
lenum |= uint((num & 0xFF) << 56);
lenum |= uint((num & 0xFF00) << 40);
lenum |= uint((num & 0xFF0000) << 24);
lenum |= uint((num & 0xFF000000) << 8);
lenum |= uint((num & 0xFF00000000) >> 8);
lenum |= uint((num & 0xFF0000000000) >> 24);
lenum |= uint((num & 0xFF000000000000) >> 40);
lenum |= uint((num & 0xFF00000000000000) >> 56);
// Shift the little-endian bytes to the end of the bytes32 value
return bytes32(lenum << 192);
}
/// @dev Opposite of BeaconChainProofs.getBalanceAtIndex, calculates a new balance
/// root by updating the balance at validatorIndex
/// @return The new, updated balance root
function _calcBalanceRoot(bytes32 balanceRoot, uint40 validatorIndex, uint64 newBalanceGwei) internal pure returns (bytes32) {
// Clear out old balance
uint bitShiftAmount = 256 - (64 * ((validatorIndex % 4) + 1));
uint mask = ~(uint(0xFFFFFFFFFFFFFFFF) << bitShiftAmount);
uint clearedRoot = uint(balanceRoot) & mask;
// Convert validator balance to little endian and shift to correct position
uint leBalance = uint(_toLittleEndianUint64(newBalanceGwei));
uint shiftedBalance = leBalance >> (192 - bitShiftAmount);
return bytes32(clearedRoot | shiftedBalance);
}
/// @dev Helper to convert 32-byte withdrawal credentials to an address
function _toAddress(bytes memory withdrawalCreds) internal pure returns (address a) {
bytes32 creds = bytes32(withdrawalCreds);
uint160 mask = type(uint160).max;
assembly {
a := and(creds, mask)
}
}
/**
*
* VIEW METHODS
*
*/
function getCredentialProofs(uint40[] memory _validators) public view returns (CredentialProofs memory) {
// If we have not advanced an epoch since a validator was created, no proofs have been
// generated for that validator. We check this here and revert early so we don't return
// empty proofs.
for (uint i = 0; i < _validators.length; i++) {
require(
_validators[i] <= lastIndexProcessed,
"BeaconChain.getCredentialProofs: validator has not been included in beacon chain state (DID YOU CALL advanceEpoch YET?)"
);
}
CredentialProofs memory proofs = CredentialProofs({
beaconTimestamp: curTimestamp,
stateRootProof: stateRootProofs[curTimestamp],
validatorFieldsProofs: new bytes[](_validators.length),
validatorFields: new bytes32[][](_validators.length)
});
// Get proofs for each validator
for (uint i = 0; i < _validators.length; i++) {
ValidatorFieldsProof memory proof = validatorFieldsProofs[curTimestamp][_validators[i]];
proofs.validatorFieldsProofs[i] = proof.validatorFieldsProof;
proofs.validatorFields[i] = proof.validatorFields;
}
return proofs;
}
function getCheckpointProofs(uint40[] memory _validators, uint64 timestamp) public view returns (CheckpointProofs memory) {
// If we have not advanced an epoch since a validator was created, no proofs have been
// generated for that validator. We check this here and revert early so we don't return
// empty proofs.
for (uint i = 0; i < _validators.length; i++) {
require(
_validators[i] <= lastIndexProcessed,
"BeaconChain.getCredentialProofs: no checkpoint proof found (did you call advanceEpoch yet?)"
);
}
CheckpointProofs memory proofs = CheckpointProofs({
balanceContainerProof: balanceContainerProofs[timestamp],
balanceProofs: new BeaconChainProofs.BalanceProof[](_validators.length)
});
// Get proofs for each validator
for (uint i = 0; i < _validators.length; i++) {
uint40 validatorIndex = _validators[i];
uint40 balanceRootIndex = _getBalanceRootIndex(validatorIndex);
BalanceRootProof memory proof = balanceRootProofs[timestamp][balanceRootIndex];
proofs.balanceProofs[i] = BeaconChainProofs.BalanceProof({
pubkeyHash: validators[validatorIndex].pubkeyHash,
balanceRoot: proof.balanceRoot,
proof: proof.proof
});
}
return proofs;
}
function getStaleBalanceProofs(uint40 validatorIndex) public view returns (StaleBalanceProofs memory) {
ValidatorFieldsProof memory vfProof = validatorFieldsProofs[curTimestamp][validatorIndex];
return StaleBalanceProofs({
beaconTimestamp: curTimestamp,
stateRootProof: stateRootProofs[curTimestamp],
validatorProof: BeaconChainProofs.ValidatorProof({validatorFields: vfProof.validatorFields, proof: vfProof.validatorFieldsProof})
});
}
function balanceOfGwei(uint40 validatorIndex) public view returns (uint64) {
return validators[validatorIndex].effectiveBalanceGwei;
}
function pubkeyHash(uint40 validatorIndex) public view returns (bytes32) {
return validators[validatorIndex].pubkeyHash;
}
function pubkey(uint40 validatorIndex) public pure returns (bytes memory) {
bytes memory _pubkey = new bytes(48);
assembly {
mstore(add(48, _pubkey), validatorIndex)
}
return _pubkey;
}
function getPubkeyHashes(uint40[] memory _validators) public view returns (bytes32[] memory) {
bytes32[] memory pubkeyHashes = new bytes32[](_validators.length);
for (uint i = 0; i < _validators.length; i++) {
pubkeyHashes[i] = validators[_validators[i]].pubkeyHash;
}
return pubkeyHashes;
}
function isActive(uint40 validatorIndex) public view returns (bool) {
return validators[validatorIndex].exitEpoch == BeaconChainProofs.FAR_FUTURE_EPOCH;
}
}
````
## File: src/test/integration/mocks/EIP_4788_Oracle_Mock.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
contract EIP_4788_Oracle_Mock {
mapping(uint => bytes32) blockRoots;
uint constant HISTORY_BUFFER_LENGTH = 8191;
fallback() external {
require(msg.data.length == 32, "4788OracleMock.fallback: malformed msg.data");
uint timestamp = abi.decode(msg.data, (uint));
require(timestamp != 0, "4788OracleMock.fallback: timestamp is 0");
bytes32 blockRoot = blockRoots[timestamp];
require(blockRoot != 0, "4788OracleMock.fallback: no block root found. DID YOU USE CHEATS.WARP?");
assembly {
mstore(0, blockRoot)
return(0, 32)
}
}
function timestampToBlockRoot(uint timestamp) public view returns (bytes32) {
return blockRoots[uint64(timestamp)];
}
function setBlockRoot(uint64 timestamp, bytes32 blockRoot) public {
blockRoots[timestamp] = blockRoot;
}
}
````
## File: src/test/integration/tests/eigenpod/FullySlashed_EigenPod.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
contract Integration_FullySlashedEigenpod_Base is IntegrationCheckUtils {
using ArrayLib for *;
User staker;
IStrategy[] strategies;
uint[] initTokenBalances;
uint[] initDepositShares;
uint64 slashedGwei;
uint40[] validators;
function _init() internal virtual override {
_configAssetTypes(HOLDS_ETH);
(staker, strategies, initTokenBalances) = _newRandomStaker();
cheats.assume(initTokenBalances[0] >= 64 ether);
// Deposit staker
uint[] memory shares = _calculateExpectedShares(strategies, initTokenBalances);
staker.depositIntoEigenlayer(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, shares);
initDepositShares = shares;
validators = staker.getActiveValidators();
// Slash all validators fully
slashedGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Full);
beaconChain.advanceEpoch_NoRewards(); // Withdraw slashed validators to pod
}
}
contract Integration_FullySlashedEigenpod_Checkpointed is Integration_FullySlashedEigenpod_Base {
function _init() internal override {
super._init();
// Start & complete a checkpoint
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, 0);
staker.completeCheckpoint();
check_CompleteCheckpoint_FullySlashed_State(staker, validators, slashedGwei);
}
function testFuzz_fullSlash_Delegate(uint24 _rand) public rand(_rand) {
(User operator,,) = _newRandomOperator();
// Delegate to an operator - should succeed given that delegation only checks the operator's slashing factor
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
}
function testFuzz_fullSlash_Revert_Redeposit(uint24 _rand) public rand(_rand) {
// Start a new validator & verify withdrawal credentials
cheats.deal(address(staker), 32 ether);
(uint40[] memory newValidators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
// We should revert on verifyWithdrawalCredentials since the staker's slashing factor is 0
cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector);
staker.verifyWithdrawalCredentials(newValidators);
}
function testFuzz_fullSlash_registerStakerAsOperator_Revert_Redeposit(uint24 _rand) public rand(_rand) {
// Register staker as operator
staker.registerAsOperator();
// Start a new validator & verify withdrawal credentials
cheats.deal(address(staker), 32 ether);
(uint40[] memory newValidators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
// We should revert on verifyWithdrawalCredentials since the staker's slashing factor is 0
cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector);
staker.verifyWithdrawalCredentials(newValidators);
}
function testFuzz_fullSlash_registerStakerAsOperator_delegate_undelegate_completeAsShares(uint24 _rand) public rand(_rand) {
// Register staker as operator
staker.registerAsOperator();
User operator = User(payable(address(staker)));
// Initialize new staker
(User staker2, IStrategy[] memory strategies2, uint[] memory initTokenBalances2) = _newRandomStaker();
uint[] memory shares = _calculateExpectedShares(strategies2, initTokenBalances2);
staker2.depositIntoEigenlayer(strategies2, initTokenBalances2);
check_Deposit_State(staker2, strategies2, shares);
// Delegate to an operator who has now become a staker, this should succeed as slashed operator's BCSF should not affect the staker
staker2.delegateTo(operator);
check_Delegation_State(staker2, operator, strategies2, shares);
// Register as operator and undelegate - the equivalent of redelegating to yourself
Withdrawal[] memory withdrawals = staker2.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker2, operator, withdrawals, withdrawalRoots, strategies2, shares);
// Complete withdrawals as shares
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
staker2.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(staker2, operator, withdrawals[i], strategies2, shares);
}
}
}
contract Integration_FullySlashedEigenpod_NotCheckpointed is Integration_FullySlashedEigenpod_Base {
/// @dev Adding funds prior to checkpointing allows the pod to not be "bricked"
function testFuzz_proveValidator_checkpoint_queue_completeAsTokens(uint24 _rand) public rand(_rand) {
// Deal ETH to staker
uint amount = 32 ether;
cheats.deal(address(staker), amount);
uint[] memory initTokenBalances2 = new uint[](1);
initTokenBalances2[0] = amount;
// Deposit staker
uint[] memory shares = _calculateExpectedShares(strategies, initTokenBalances2);
staker.depositIntoEigenlayer(strategies, initTokenBalances2);
check_Deposit_State(staker, strategies, shares);
// Checkpoint slashed EigenPod
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, 0);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedGwei);
// Queue Full Withdrawal
uint[] memory depositShares = _getStakerDepositShares(staker, strategies);
uint[] memory withdrawableShares = _getWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, depositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(
staker, User(payable(address(0))), strategies, depositShares, withdrawableShares, withdrawals, withdrawalRoots
);
// Complete withdrawal as tokens
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
IERC20[] memory tokens = _getUnderlyingTokens(withdrawals[i].strategies);
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawableShares);
staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, User(payable(address(0))), withdrawals[i], withdrawals[i].strategies, withdrawableShares, tokens, expectedTokens
);
}
}
function testFuzz_depositMinimumAmount_checkpoint(uint24 _rand) public rand(_rand) {
// Deal ETH to staker, minimum amount to be checkpointed
uint64 podBalanceGwei = 1;
uint amountToDeal = 1 * GWEI_TO_WEI;
bool isBricked;
// Randomly deal 1 less than minimum amount to be checkpointed such that the pod is bricked
if (_randBool()) {
amountToDeal -= 1;
podBalanceGwei -= 1;
isBricked = true;
}
// Send ETH to pod
cheats.prank(address(staker));
(bool success,) = address(staker.pod()).call{value: amountToDeal}("");
require(success, "pod call failed");
// Checkpoint slashed EigenPod
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, podBalanceGwei);
staker.completeCheckpoint();
if (isBricked) {
// BCSF is asserted to be zero here
check_CompleteCheckpoint_FullySlashed_State(staker, validators, slashedGwei);
} else {
check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedGwei - podBalanceGwei);
}
}
}
````
## File: src/test/integration/tests/eigenpod/Register_Allocate_Slash_VerifyWC_.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/mocks/BeaconChainMock.t.sol";
import "src/test/integration/IntegrationChecks.t.sol";
/// @notice Testing the rounding behavior when operator magnitude is initially 1
contract Integration_Register_Allocate_Slash_VerifyWC is IntegrationCheckUtils {
using ArrayLib for *;
AVS avs;
OperatorSet operatorSet;
User operator;
IAllocationManagerTypes.AllocateParams allocateParams;
User staker;
IStrategy[] strategies;
// uint[] initTokenBalances;
uint[] initDepositShares;
uint40[] validators;
uint64 beaconBalanceGwei;
uint64 slashedGwei;
/**
* 1. Create an operatorSet and register the operator allocating all magnitude
* 2. slash operator to 1 magnitude remaining
* 3. delegate staker to operator
* 4. deposit (verify withdrawal credentials)
*/
function _init() internal override {
_configAssetTypes(HOLDS_ETH);
(staker, strategies, initDepositShares) = _newRandomStaker();
(operator,,) = _newRandomOperator();
(avs,) = _newRandomAVS();
cheats.assume(initDepositShares[0] >= 64 ether);
// 1. Create an operator set and register an operator
operatorSet = avs.createOperatorSet(strategies);
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, strategies);
// Allocate all magnitude to the operator set
allocateParams = _genAllocation_AllAvailable(operator, operatorSet, strategies);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable_NoDelegatedStake(operator, allocateParams);
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);
// 2. Fully slash operator
SlashingParams memory slashParams = _genSlashing_Custom(operator, operatorSet, WAD - 1);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// 3. Delegate to an operator
staker.delegateTo(operator);
// delegate staker without any depositShares in beaconChainETHStrategy yet
IStrategy[] memory emptyStrategies;
uint[] memory emptyTokenBalances;
check_Delegation_State(staker, operator, emptyStrategies, emptyTokenBalances);
// 4. deposit/verify withdrawal credentials
(validators, beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
}
/**
* Test sequence following _init()
* 4. slash validators on beacon chain (start/complete checkpoint)
* 5. queue withdrawal
* 6. complete withdrawal
* Given the operator has 1 magnitude remaining, the being slashed on the beacon chain
* and calculating a non-WAD BCSF, their slashing factor should be rounded down to 0. Resulting in
* the staker having 0 withdrawable shares.
*/
function test_slashBC_startCompleteCP_queue_complete(uint24 _r) public rand(_r) {
// 4. slash validators on beacon chain (start/complete checkpoint)
uint40[] memory slashedValidators = _choose(validators);
slashedGwei = beaconChain.slashValidators(slashedValidators, BeaconChainMock.SlashType.Minor);
// ensure non zero amount slashed gwei so that we can test rounding down behavior
cheats.assume(slashedGwei > 0);
beaconChain.advanceEpoch_NoWithdrawNoRewards();
// start and complete checkpoint
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
staker.completeCheckpoint();
check_CompleteCheckPoint_WithSlashing_LowMagnitude_State(staker, slashedGwei);
// 5. queue withdrawal
(, uint[] memory withdrawShares) = _randWithdrawal(strategies, initDepositShares);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, withdrawShares);
bytes32[] memory roots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State({
staker: staker,
operator: operator,
strategies: strategies,
depositShares: withdrawShares, // amount of deposit shares to withdraw
withdrawableShares: 0.toArrayU256(), // amount of withdrawable shares is 0 due to slashing factor being 0
withdrawals: withdrawals,
withdrawalRoots: roots
});
// Operator's maxMagnitude is 1 and staker was slashed to non-WAD BCSF. Therefore
// staker should have been rounded down to 0
uint slashingFactor = staker.getSlashingFactor(beaconChainETHStrategy);
assertEq(slashingFactor, 0, "slashing factor should be rounded down to 0");
// 6. complete withdrawal
// only strategy is beaconChainETHStrategy
_rollBlocksForCompleteWithdrawals(withdrawals);
staker.completeWithdrawalAsShares(withdrawals[0]);
check_Withdrawal_AsShares_State(staker, operator, withdrawals[0], strategies, 0.toArrayU256());
}
/**
* Test sequence following _init()
* 4. fully slash operator on opSet again to 0 magnitude
* 5. undelegate
* 6. redeposit (start/complete checkpoint)
* This is testing when an operator is fully slashed with 0 magnitude, the staker can still undelegate
* and "redeposit" to Eigenlayer.
*/
function test_slash_undelegate_redeposit(uint24 _r) public rand(_r) {
// 4. AVS slashes operator again to 0 magnitude and fully slashed
SlashingParams memory slashParams = _genSlashing_Full(operator, operatorSet);
slashParams.wadsToSlash[0] = WAD;
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// 5. undelegate results in 0 delegated shares removed since operator has 0 magnitude and staker is fully slashed too
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, withdrawableShares);
// 6. redeposit (start/complete checkpoint or verifyWC)
if (_randBool()) {
// Verify WC
(validators, beaconBalanceGwei,) = staker.startValidators(uint8(_randUint(3, 10)));
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
} else {
// Start/complete CP
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
staker.completeCheckpoint();
check_CompleteCheckpoint_State(staker);
}
}
/**
* Test sequence following _init()
* 4. fully slash operator on opSet again to 0 magnitude
* 5. undelegate
* 6. complete withdrawals as shares(although amount 0 from fully slashed operator)
* 7. redeposit (start/complete checkpoint)
* This is testing when an operator is fully slashed with 0 magnitude, the staker can still undelegate,
* complete withdrawals as shares(0 shares though), and redeposit to Eigenlayer.
*/
function test_slash_undelegate_completeAsShares_startCompleteCP(uint24 _r) public rand(_r) {
// 4. AVS slashes operator again to 0 magnitude and fully slashed
SlashingParams memory slashParams = _genSlashing_Full(operator, operatorSet);
slashParams.wadsToSlash[0] = WAD;
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// 5. undelegate results in 0 delegated shares removed since operator has 0 magnitude and staker is fully slashed too
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, withdrawableShares);
// 6. complete withdrawals as shares(although amount 0 from fully slashed operator)
_rollBlocksForCompleteWithdrawals(withdrawals);
staker.completeWithdrawalAsShares(withdrawals[0]);
check_Withdrawal_AsShares_State(staker, operator, withdrawals[0], strategies, 0.toArrayU256());
// 7. redeposit (start/complete checkpoint)
beaconChain.advanceEpoch();
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
staker.completeCheckpoint();
check_CompleteCheckpoint_State(staker);
}
/**
* Test sequence following _init()
* 4. fully slash operator on opSet again to 0 magnitude
* 5. undelegate
* 6. complete withdrawals as tokens(although amount 0 from fully slashed operator)
* 7. redeposit (start new validators and verifywc)
* This is testing when an operator is fully slashed with 0 magnitude, the staker can still undelegate,
* complete withdrawals as tokens(0 tokens though), and redeposit to Eigenlayer.
*/
function test_slash_undelegate_completeAsTokens_verifyWC(uint24 _r) public rand(_r) {
// 4. AVS slashes operator again to 0 magnitude and fully slashed
SlashingParams memory slashParams = _genSlashing_Full(operator, operatorSet);
slashParams.wadsToSlash[0] = WAD;
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// 5. undelegate results in 0 delegated shares removed since operator has 0 magnitude and staker is fully slashed too
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, withdrawableShares);
// 6. complete withdrawals as tokens(although amount 0 from fully slashed operator)
// This also exits validators on the beacon chain
_rollBlocksForCompleteWithdrawals(withdrawals);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[0]);
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[0], strategies, 0.toArrayU256(), tokens, 0.toArrayU256());
// 7. deposit/verify withdrawal credentials
// randomly startup 1-10 validators
(validators, beaconBalanceGwei,) = staker.startValidators(uint8(_randUint(1, 10)));
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
}
/**
* Test sequence following _init()
* 4. queue withdrawal
* 5. complete withdrawal as tokens
* This is testing a staker can queue a withdrawal and complete as tokens even
* though the operator has 1 maxMagnitude
*/
function test_queueAllShares_completeAsTokens(uint24 _r) public rand(_r) {
// 4. queue withdrawal
// ( , uint[] memory withdrawShares) = _randWithdrawal(strategies, initDepositShares);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory roots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State({
staker: staker,
operator: operator,
strategies: strategies,
depositShares: initDepositShares,
withdrawableShares: initDepositShares,
withdrawals: withdrawals,
withdrawalRoots: roots
});
// 5. complete withdrawal as tokens
// - exits validators
// - advances epoch
// - starts/completes checkpoint
_rollBlocksForCompleteWithdrawals(withdrawals);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[0]);
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[0], strategies, initDepositShares, tokens, initDepositShares);
}
}
````
## File: src/test/integration/tests/eigenpod/SlashBC_OneBCSF.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/mocks/BeaconChainMock.t.sol";
import "src/test/integration/IntegrationChecks.t.sol";
import "src/test/harnesses/EigenPodManagerWrapper.sol";
/// @notice Testing the rounding behavior when beacon chain slashing factor is initially 1
contract Integration_SlashBC_OneBCSF is IntegrationCheckUtils {
using ArrayLib for *;
AVS avs;
OperatorSet operatorSet;
User operator;
IAllocationManagerTypes.AllocateParams allocateParams;
User staker;
IStrategy[] strategies;
uint[] initDepositShares;
uint40[] validators;
uint64 beaconBalanceGwei;
uint64 slashedGwei;
/**
* Shared setup:
* 1. Update the EPM implementation to manually set the beaconChainSlashingFactor to 1
* Note: Slashing the EigenPod enough (in units of gwei) to reduce the beaconChainSlashingFactor to 1 without
* rounding down to 0 in a single slash even with increased MaxEB to 2048 is not possible and would require several
* iterations of slashing to do so. Using a harness to set the beaconChainSlashingFactor to 1 is the easiest way for this test.
* 2. create a new staker, operator, and avs
* 3. start validators and verify withdrawal credentials
*/
function _init() internal override {
// 1. etch a new implementation to set staker's beaconChainSlashingFactor to 1
EigenPodManagerWrapper eigenPodManagerWrapper =
new EigenPodManagerWrapper(DEPOSIT_CONTRACT, eigenPodBeacon, delegationManager, eigenLayerPauserReg, "v9.9.9");
address targetAddr = address(eigenPodManagerImplementation);
cheats.etch(targetAddr, address(eigenPodManagerWrapper).code);
// 2. create a new staker, operator, and avs
_configAssetTypes(HOLDS_ETH);
(staker, strategies, initDepositShares) = _newRandomStaker();
(operator,,) = _newRandomOperator();
(avs,) = _newRandomAVS();
cheats.assume(initDepositShares[0] >= 64 ether);
EigenPodManagerWrapper(address(eigenPodManager)).setBeaconChainSlashingFactor(address(staker), 1);
assertEq(eigenPodManager.beaconChainSlashingFactor(address(staker)), 1);
// 3. start validators and verify withdrawal credentials
(validators, beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
}
/// @notice Test that a staker can still verify WC, start/complete CP even if the operator has 1 magnitude remaining
function test_verifyWC_startCompleteCP(uint24 _r) public rand(_r) {
// 4. start validators and verify withdrawal credentials
(validators, beaconBalanceGwei,) = staker.startValidators(uint8(_randUint(1, 10)));
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
// 5. start/complete checkpoint
beaconChain.advanceEpoch();
staker.startCheckpoint();
beaconChain.advanceEpoch();
staker.completeCheckpoint();
check_CompleteCheckpoint_State(staker);
// 6. Assert BCSF is still 1
assertEq(eigenPodManager.beaconChainSlashingFactor(address(staker)), 1);
}
/// @notice Test that a staker is slashed to 0 BCSF from a minor slash and that they can't deposit more shares
/// from their EigenPod (either through verifyWC or start/complete CP)
function test_slashFullyBC_revert_deposit(uint24 _r) public rand(_r) {
// 4. slash validators on beacon chain (start/complete checkpoint)
uint40[] memory slashedValidators = _choose(validators);
slashedGwei = beaconChain.slashValidators(slashedValidators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.startCheckpoint();
staker.completeCheckpoint();
check_CompleteCheckpoint_WithCLSlashing_HandleRoundDown_State(staker, slashedGwei);
// BCSF should be 0 now
assertEq(eigenPodManager.beaconChainSlashingFactor(address(staker)), 0);
// 5. deposit expecting revert (randomly pick to verifyWC, start/complete CP)
if (_randBool()) {
// Verify WC
(validators,,) = staker.startValidators(uint8(_randUint(3, 10)));
beaconChain.advanceEpoch();
cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector);
staker.verifyWithdrawalCredentials(validators);
} else {
// Start/complete CP
// Ensure that not all validators were slashed so that some rewards can be generated when
// we advance epoch
cheats.assume(slashedValidators.length < validators.length);
beaconChain.advanceEpoch();
staker.startCheckpoint();
cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector);
staker.completeCheckpoint();
}
}
/**
* @notice Test that a staker with 1 maxMagnitude and 1 BCSF has slashingFactor rounded down to 0
* and further deposits are not possible
* Test sequence following _init()
* 4. Create an operator set and register an operator
* 5. slash operator to 1 magnitude remaining
* 6. delegate to operator
* 7. deposit expecting revert (randomly pick to verifyWC, start/complete CP)
*/
function test_slashAVS_delegate_revert_startCompleteCP(uint24 _r) public rand(_r) {
// 4. Create an operator set and register an operator
operatorSet = avs.createOperatorSet(strategies);
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, strategies);
// Allocate all magnitude to the operator set
allocateParams = _genAllocation_AllAvailable(operator, operatorSet, strategies);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable_NoDelegatedStake(operator, allocateParams);
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);
// 5. slash operator to 1 magnitude remaining
SlashingParams memory slashParams = _genSlashing_Custom(operator, operatorSet, WAD - 1);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// assert operator has 1 magnitude remaining
assertEq(allocationManager.getMaxMagnitude(address(operator), beaconChainETHStrategy), 1);
// 6. delegate to operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
// 7. deposit expecting revert (randomly pick to verifyWC, start/complete CP)
if (_randBool()) {
// Verify WC
(validators,,) = staker.startValidators(uint8(_randUint(1, 10)));
beaconChain.advanceEpoch();
cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector);
staker.verifyWithdrawalCredentials(validators);
} else {
// Start/complete CP
beaconChain.advanceEpoch();
staker.startCheckpoint();
cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector);
staker.completeCheckpoint();
}
}
}
````
## File: src/test/integration/tests/eigenpod/Slashed_Eigenpod_BC.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
contract Integration_SlashedEigenpod_BC is IntegrationCheckUtils {
using ArrayLib for *;
AVS avs;
OperatorSet operatorSet;
User operator;
AllocateParams allocateParams;
User staker;
IStrategy[] strategies;
uint[] initTokenBalances;
uint64 beaconBalanceGwei;
uint64 slashedGwei;
IERC20[] tokens;
uint40[] validators;
uint40[] slashedValidators;
function _init() internal virtual override {
_configAssetTypes(HOLDS_ETH);
(staker, strategies, initTokenBalances) = _newRandomStaker();
(operator,,) = _newRandomOperator();
(avs,) = _newRandomAVS();
tokens = _getUnderlyingTokens(strategies); // Should only return ETH
cheats.assume(initTokenBalances[0] >= 64 ether);
// Deposit staker
uint[] memory shares = _calculateExpectedShares(strategies, initTokenBalances);
staker.depositIntoEigenlayer(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, shares);
validators = staker.getActiveValidators();
//Slash on Beacon chain
slashedValidators = _choose(validators);
slashedGwei = beaconChain.slashValidators(slashedValidators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoWithdrawNoRewards();
// Checkpoint post slash
staker.startCheckpoint();
staker.completeCheckpoint();
check_CompleteCheckpoint_WithCLSlashing_HandleRoundDown_State(staker, slashedGwei);
}
function testFuzz_delegateSlashedStaker_dsfWad(uint24 _random) public rand(_random) {
uint[] memory initDelegatableShares = _getWithdrawableShares(staker, strategies);
uint[] memory initDepositShares = _getStakerDepositShares(staker, strategies);
// Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
// Undelegate from an operator
IDelegationManagerTypes.Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, initDelegatableShares);
// Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(staker, operator, withdrawals[i], withdrawals[i].strategies, initDelegatableShares);
}
(uint[] memory withdrawableSharesAfter, uint[] memory depositSharesAfter) =
delegationManager.getWithdrawableShares(address(staker), strategies);
assertEq(depositSharesAfter[0], initDelegatableShares[0], "Deposit shares should reset to reflect slash(es)");
assertApproxEqAbs(
withdrawableSharesAfter[0], depositSharesAfter[0], 100, "Withdrawable shares should equal deposit shares after withdrawal"
);
}
function testFuzz_delegateSlashedStaker_dsfNonWad(uint24 _random) public rand(_random) {
//Additional deposit on beacon chain so dsf is nonwad
uint amount = 32 ether * _randUint({min: 1, max: 5});
cheats.deal(address(staker), amount);
(uint40[] memory newValidators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(newValidators);
uint[] memory initDelegatableShares = _getWithdrawableShares(staker, strategies);
uint[] memory initDepositShares = _getStakerDepositShares(staker, strategies);
// Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
// Undelegate from an operator
IDelegationManagerTypes.Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, initDelegatableShares);
// Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(staker, operator, withdrawals[i], withdrawals[i].strategies, initDelegatableShares);
}
(uint[] memory withdrawableSharesAfter, uint[] memory depositSharesAfter) =
delegationManager.getWithdrawableShares(address(staker), strategies);
assertEq(depositSharesAfter[0], initDelegatableShares[0], "Deposit shares should reset to reflect slash(es)");
assertApproxEqAbs(
withdrawableSharesAfter[0], depositSharesAfter[0], 1000, "Withdrawable shares should equal deposit shares after withdrawal"
);
}
function testFuzz_delegateSlashedStaker_slashedOperator(uint24 _random) public rand(_random) {
(User staker2,,) = _newRandomStaker();
(uint40[] memory validators2,,) = staker2.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker2.verifyWithdrawalCredentials(validators2);
staker2.startCheckpoint();
staker2.completeCheckpoint();
staker2.delegateTo(operator);
//randomize additional deposit to eigenpod
if (_randBool()) {
uint amount = 32 ether * _randUint({min: 1, max: 5});
cheats.deal(address(staker), amount);
(uint40[] memory newValidators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(newValidators);
staker.startCheckpoint();
staker.completeCheckpoint();
}
// Create an operator set and register an operator.
operatorSet = avs.createOperatorSet(strategies);
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, strategies);
// Allocate to operator set
allocateParams = _genAllocation_AllAvailable(operator, operatorSet, strategies);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable(operator, allocateParams);
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);
//Slash operator before delegation
IAllocationManagerTypes.SlashingParams memory slashingParams;
uint wadToSlash = _randWadToSlash();
slashingParams = avs.slashOperator(operator, operatorSet.id, strategies, wadToSlash.toArrayU256());
assert_Snap_Allocations_Slashed(slashingParams, operatorSet, true, "operator allocations should be slashed");
uint[] memory initDepositShares = _getStakerDepositShares(staker, strategies);
// Delegate to an operator
staker.delegateTo(operator);
(uint[] memory delegatedShares,) = delegationManager.getWithdrawableShares(address(staker), strategies);
check_Delegation_State(staker, operator, strategies, initDepositShares);
// Undelegate from an operator
IDelegationManagerTypes.Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, delegatedShares);
// Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(staker, operator, withdrawals[i], withdrawals[i].strategies, delegatedShares);
}
(uint[] memory withdrawableSharesAfter, uint[] memory depositSharesAfter) =
delegationManager.getWithdrawableShares(address(staker), strategies);
assertEq(depositSharesAfter[0], delegatedShares[0], "Deposit shares should reset to reflect slash(es)");
assertApproxEqAbs(
withdrawableSharesAfter[0], depositSharesAfter[0], 1000, "Withdrawable shares should equal deposit shares after withdrawal"
);
}
function testFuzz_delegateSlashedStaker_redelegate_complete(uint24 _random) public rand(_random) {
(User operator2,,) = _newRandomOperator();
//Additional deposit on beacon chain so dsf is nonwad
uint amount = 32 ether * _randUint({min: 1, max: 5});
cheats.deal(address(staker), amount);
(uint40[] memory newValidators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(newValidators);
uint[] memory initDepositShares = _getStakerDepositShares(staker, strategies);
// Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
(uint[] memory delegatedShares,) = delegationManager.getWithdrawableShares(address(staker), strategies);
// Undelegate from an operator
IDelegationManagerTypes.Withdrawal[] memory withdrawals = staker.redelegate(operator2);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Redelegate_State(staker, operator, operator2, withdrawals, withdrawalRoots, strategies, delegatedShares);
// Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Redelegated_State(
staker, operator, operator2, withdrawals[i], withdrawals[i].strategies, delegatedShares
);
}
(uint[] memory withdrawableSharesAfter, uint[] memory depositSharesAfter) =
delegationManager.getWithdrawableShares(address(staker), strategies);
assertEq(depositSharesAfter[0], delegatedShares[0], "Deposit shares should reset to reflect slash(es)");
assertApproxEqAbs(
withdrawableSharesAfter[0], depositSharesAfter[0], 1000, "Withdrawable shares should equal deposit shares after withdrawal"
);
}
function testFuzz_delegateSlashedStaker_slashedOperator_withdrawAllShares_complete(uint24 _random) public rand(_random) {
//Additional deposit on beacon chain so dsf is nonwad
uint amount = 32 ether * _randUint({min: 1, max: 5});
cheats.deal(address(staker), amount);
(uint40[] memory newValidators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(newValidators);
uint[] memory initDepositShares = _getStakerDepositShares(staker, strategies);
// Create an operator set and register an operator.
operatorSet = avs.createOperatorSet(strategies);
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, strategies);
// Slash operator before delegation
SlashingParams memory slashingParams;
uint wadToSlash = _randWadToSlash();
slashingParams = avs.slashOperator(operator, operatorSet.id, strategies, wadToSlash.toArrayU256());
assert_Snap_Allocations_Slashed(slashingParams, operatorSet, true, "operator allocations should be slashed");
// Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
(uint[] memory delegatedShares,) = delegationManager.getWithdrawableShares(address(staker), strategies);
// Allocate to operator set
allocateParams = _genAllocation_AllAvailable(operator, operatorSet, strategies);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable(operator, allocateParams);
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);
// Withdraw all shares
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, initDepositShares, withdrawableShares, withdrawals, withdrawalRoots);
// Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_State(staker, operator, withdrawals[i], withdrawals[i].strategies, withdrawableShares);
}
(uint[] memory withdrawableSharesAfter, uint[] memory depositSharesAfter) =
delegationManager.getWithdrawableShares(address(staker), strategies);
assertEq(depositSharesAfter[0], delegatedShares[0], "Deposit shares should reset to reflect slash(es)");
assertApproxEqAbs(
withdrawableSharesAfter[0], depositSharesAfter[0], 1000, "Withdrawable shares should equal deposit shares after withdrawal"
);
}
function testFuzz_redeposit_queue_completeAsTokens(uint24 _random) public rand(_random) {
// Prove an additional validator
uint amount = 32 ether * _randUint({min: 1, max: 5});
cheats.deal(address(staker), amount);
(uint40[] memory newValidators, uint64 addedBeaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, addedBeaconBalanceGwei);
// Queue withdrawal for all tokens
uint[] memory depositShares = _getStakerDepositShares(staker, strategies);
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, depositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(
staker, User(payable(address(0))), strategies, depositShares, withdrawableShares, withdrawals, withdrawalRoots
);
// Complete withdrawal as tokens
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawableShares);
staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, withdrawableShares, tokens, expectedTokens
);
}
}
function testFuzz_redeposit_queue_completeAsShares(uint24 _random) public rand(_random) {
// Prove an additional validator
uint amount = 32 ether * _randUint({min: 1, max: 5});
cheats.deal(address(staker), amount);
(uint40[] memory newValidators, uint64 addedBeaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, addedBeaconBalanceGwei);
// Queue withdrawal for all
uint[] memory depositShares = _getStakerDepositShares(staker, strategies);
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, depositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(
staker, User(payable(address(0))), strategies, depositShares, withdrawableShares, withdrawals, withdrawalRoots
);
// Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(staker, operator, withdrawals[i], withdrawals[i].strategies, withdrawableShares);
}
}
function testFuzz_verifyAdditionalValidator_delegateSlashedStaker(uint24 _random) public rand(_random) {
// Create operatorSet
operatorSet = avs.createOperatorSet(strategies);
// Allocate to operatorSet
allocateParams = _genAllocation_AllAvailable(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_Base_IncrAlloc_State(operator, allocateParams); // No increase in allocated stake since staker not delegated
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);
// Register to opSet
operator.registerForOperatorSet(operatorSet);
check_Registration_State_PendingAllocation(operator, allocateParams);
// Slash operator - should have non-WAD magnitude
SlashingParams memory slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// Verify additional validator
cheats.deal(address(staker), 32 ether);
uint64 beaconBalanceAddedGwei = uint64(32 ether / GWEI_TO_WEI);
(uint40[] memory newValidators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, beaconBalanceAddedGwei);
// Delegate to operator
uint[] memory initDepositShares = _getStakerDepositShares(staker, strategies); // Deposit shares increased after verifying validator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
}
}
/// @notice This is not considered dual slashing since the operator is pre-slashed
contract Integration_SlashedOperator_SlashedEigenpod_Base is IntegrationCheckUtils {
using ArrayLib for *;
AVS avs;
OperatorSet operatorSet;
AllocateParams allocateParams;
User operator;
User staker;
IStrategy[] strategies;
uint[] initTokenBalances;
uint[] initDepositShares;
function _init() internal virtual override {
_configAssetTypes(HOLDS_ETH);
(staker, strategies, initTokenBalances) = _newRandomStaker();
operator = _newRandomOperator_NoAssets();
(avs,) = _newRandomAVS();
// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, initTokenBalances);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, initDepositShares);
// 2. Create an operator set and register an operator.
operatorSet = avs.createOperatorSet(strategies);
// Steps 3-4-5
// 3. Allocate to operatorSet
// 4. Register to opSet
// 5. Slash operator
allocateParams = _giveOperatorNonWadMagnitude(operator);
}
/// @dev Assumes operatorSet has been initialized with relevant strategies for staker
function _giveOperatorNonWadMagnitude(User _operator) internal returns (AllocateParams memory _allocateParams) {
// Allocate to operatorSet
_allocateParams = _genAllocation_AllAvailable(_operator, operatorSet);
_operator.modifyAllocations(_allocateParams);
check_Base_IncrAlloc_State(_operator, _allocateParams); // No increase in allocated stake since staker not delegated
_rollBlocksForCompleteAllocation(_operator, operatorSet, strategies);
// Register to opSet
_operator.registerForOperatorSet(operatorSet);
check_Registration_State_PendingAllocation(_operator, _allocateParams);
// Slash operator
SlashingParams memory slashParams = _genSlashing_Rand(_operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(_operator, _allocateParams, slashParams);
}
}
contract Integration_SlashedOperator_SlashedEigenpod is Integration_SlashedOperator_SlashedEigenpod_Base {
function _init() internal virtual override {
// 1-5
super._init();
// 6. Slash on BC
uint40[] memory validators = staker.getActiveValidators();
uint40[] memory slashedValidators = _choose(validators);
uint64 slashedGwei = beaconChain.slashValidators(slashedValidators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
staker.completeCheckpoint();
check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, slashedValidators, slashedGwei);
// 7. Delegate to operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
// 8. Verify Additional Validator
cheats.deal(address(staker), 32 ether);
uint64 beaconBalanceAddedGwei = uint64(32 ether / GWEI_TO_WEI);
(uint40[] memory newValidators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, beaconBalanceAddedGwei);
initDepositShares = _getStakerDepositShares(staker, strategies); // Deposit shares increased after verifying validator
}
function testFuzz_slashOnBC_delegate_verifyValidator_undelegate_completeAsTokens(uint24 _random) public rand(_random) {
// 9. Undelegate
uint[] memory stakerWithdrawableShares = _getWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, stakerWithdrawableShares);
// 10. Complete withdrawal as tokens
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, stakerWithdrawableShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, stakerWithdrawableShares, tokens, expectedTokens
);
}
}
function testFuzz_slashOnBC_delegate_verifyValidator_undelegate_completeAsShares(uint24 _random) public rand(_random) {
// 9. Undelegate
uint[] memory stakerWithdrawableShares = _getWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, stakerWithdrawableShares);
// 10. Complete withdrawal as shares
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, stakerWithdrawableShares
);
}
}
function testFuzz_slashOnBC_delegate_verifyValidator_redelegate_completeAsTokens(uint24 _random) public rand(_random) {
// Create new operator
User operator2 = _newRandomOperator_NoAssets();
// Randomly give the operator a non-WAD magnitude
if (_randBool()) _giveOperatorNonWadMagnitude(operator2);
// 9. Redelegate
uint[] memory stakerWithdrawableShares = _getWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.redelegate(operator2);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Redelegate_State(staker, operator, operator2, withdrawals, withdrawalRoots, strategies, stakerWithdrawableShares);
// 10. Complete withdrawal as tokens
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, stakerWithdrawableShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator2, withdrawals[i], withdrawals[i].strategies, stakerWithdrawableShares, tokens, expectedTokens
);
}
}
function testFuzz_slashOnBC_delegate_verifyValidator_redelegate_completeAsShares(uint24 _random) public rand(_random) {
// Create new operator
User operator2 = _newRandomOperator_NoAssets();
// Randomly give the operator a non-WAD magnitude
if (_randBool()) _giveOperatorNonWadMagnitude(operator2);
// 9. Redelegate
uint[] memory stakerWithdrawableShares = _getWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.redelegate(operator2);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Redelegate_State(staker, operator, operator2, withdrawals, withdrawalRoots, strategies, stakerWithdrawableShares);
// 10. Complete withdrawal as shares
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Redelegated_State(
staker, operator, operator2, withdrawals[i], withdrawals[i].strategies, stakerWithdrawableShares
);
}
}
}
contract Integration_Redelegate_SlashOperator_SlashEigenpod is Integration_SlashedOperator_SlashedEigenpod_Base {
using ArrayLib for *;
User operator2;
Withdrawal withdrawal;
uint64 slashedGwei;
function _init() internal virtual override {
// 1-5.
super._init();
// 6. Delegate staker
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
// 7. Create new operator & randomly give it a non-WAD magnitude
operator2 = _newRandomOperator_NoAssets();
if (_randBool()) _giveOperatorNonWadMagnitude(operator2);
// 8. Redelegate
uint[] memory stakerWithdrawableShares = _getWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.redelegate(operator2);
require(withdrawals.length == 1, "Expected 1 withdrawal");
withdrawal = withdrawals[0];
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Redelegate_State(staker, operator, operator2, withdrawals, withdrawalRoots, strategies, stakerWithdrawableShares);
// 9. Slash original operator
SlashingParams memory slashParams = _genSlashing_Half(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// 10. Slash on BC
uint40[] memory validators = staker.getActiveValidators();
uint40[] memory slashedValidators = _choose(validators);
slashedGwei = beaconChain.slashValidators(slashedValidators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.startCheckpoint();
staker.completeCheckpoint();
}
function testFuzz_delegate_redelegate_slashAVS_slashBC_verifyValidator_completeAsShares(uint24 _random) public rand(_random) {
// Populate withdrawal array
Withdrawal[] memory withdrawals = new Withdrawal[](1);
withdrawals[0] = withdrawal;
// 11. Verify additional validator
cheats.deal(address(staker), 32 ether);
uint64 beaconBalanceAddedGwei = uint64(32 ether / GWEI_TO_WEI);
(uint40[] memory newValidators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, beaconBalanceAddedGwei);
// 12. Complete withdrawal as shares
uint[] memory stakerWithdrawableShares = _getWithdrawableSharesAfterCompletion(staker);
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Redelegated_State(
staker, operator, operator2, withdrawals[i], withdrawals[i].strategies, stakerWithdrawableShares
);
}
}
function testFuzz_delegate_redelegate_slashAVS_slashBC_verifyValidator_completeAsTokens(uint24 _random) public rand(_random) {
// Populate withdrawal array
Withdrawal[] memory withdrawals = new Withdrawal[](1);
withdrawals[0] = withdrawal;
// 11. Verify additional validator
cheats.deal(address(staker), 32 ether);
uint64 beaconBalanceAddedGwei = uint64(32 ether / GWEI_TO_WEI);
(uint40[] memory newValidators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, beaconBalanceAddedGwei);
// 12. Complete withdrawal as tokens
_rollBlocksForCompleteWithdrawals(withdrawals);
uint[] memory stakerWithdrawableShares = _getWithdrawableSharesAfterCompletion(staker);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, stakerWithdrawableShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator2, withdrawals[i], withdrawals[i].strategies, stakerWithdrawableShares, tokens, expectedTokens
);
}
}
function testFuzz_delegate_redelegate_slashAVS_slashBC_completeAsShares_verifyValidator(uint24 _random) public rand(_random) {
// Populate withdrawal array
Withdrawal[] memory withdrawals = new Withdrawal[](1);
withdrawals[0] = withdrawal;
// 11. Complete withdrawal as shares
uint[] memory stakerWithdrawableShares = _getWithdrawableSharesAfterCompletion(staker);
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Redelegated_State(
staker, operator, operator2, withdrawals[i], withdrawals[i].strategies, stakerWithdrawableShares
);
}
// 12. Verify additional validator
cheats.deal(address(staker), 32 ether);
uint64 beaconBalanceAddedGwei = uint64(32 ether / GWEI_TO_WEI);
(uint40[] memory newValidators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, beaconBalanceAddedGwei);
}
function testFuzz_delegate_redelegate_slashAVS_slashBC_completeAsTokens_verifyValidator(uint24 _random) public rand(_random) {
// Populate withdrawal array
Withdrawal[] memory withdrawals = new Withdrawal[](1);
withdrawals[0] = withdrawal;
// 11. Complete withdrawal as tokens
_rollBlocksForCompleteWithdrawals(withdrawals);
uint[] memory stakerWithdrawableShares = _getWithdrawableSharesAfterCompletion(staker);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, stakerWithdrawableShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator2, withdrawals[i], withdrawals[i].strategies, stakerWithdrawableShares, tokens, expectedTokens
);
}
// 12. Verify additional validator
cheats.deal(address(staker), 32 ether);
uint64 beaconBalanceAddedGwei = uint64(32 ether / GWEI_TO_WEI);
(uint40[] memory newValidators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, beaconBalanceAddedGwei);
}
}
contract Integration_SlashedEigenpod_BC_HalfSlash is IntegrationCheckUtils {
using ArrayLib for *;
AVS avs;
OperatorSet operatorSet;
User operator;
AllocateParams allocateParams;
User staker;
IStrategy[] strategies;
uint[] initTokenBalances;
uint[] initDepositShares;
uint64 slashedGwei;
uint40[] slashedValidators;
function _init() internal override {
_configAssetTypes(HOLDS_ETH);
(staker, strategies, initTokenBalances) = _newRandomStaker();
(operator,,) = _newRandomOperator();
(avs,) = _newRandomAVS();
// 1. Deposit staker
uint[] memory shares = _calculateExpectedShares(strategies, initTokenBalances);
initDepositShares = shares;
staker.depositIntoEigenlayer(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, shares);
uint40[] memory validators = staker.getActiveValidators();
// 2. Slash on Beacon chain
slashedValidators = _choose(validators);
slashedGwei = beaconChain.slashValidators(slashedValidators, BeaconChainMock.SlashType.Half);
beaconChain.advanceEpoch_NoWithdrawNoRewards();
// 3. Checkpoint post slash
staker.startCheckpoint();
staker.completeCheckpoint();
check_CompleteCheckpoint_WithCLSlashing_HandleRoundDown_State(staker, slashedGwei);
}
/**
* @notice Test sets up an EigenPod which has a non-WAD BCSF. After queue withdrawing all depositShares
* which sets it to 0, they can then complete checkpoints repeatedly with 0 shares increase to increase the staker DSF each time
*/
function test_completeCP_withNoAddedShares(uint24 _rand) public rand(_rand) {
// 4. queue withdraw all depositShares having it set to 0
uint withdrawableSharesBefore = _getStakerWithdrawableShares(staker, strategies)[0];
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
// 5. advance epoch no rewards
// start/complete cp repeatedly with 0 shares increase to increase the staker DSF each time
for (uint i = 0; i < 10; i++) {
beaconChain.advanceEpoch_NoWithdrawNoRewards();
staker.startCheckpoint();
staker.completeCheckpoint();
}
// Staker deposit shares should be 0 from queue withdrawing all depositShares
// therefore the depositScalingFactor should also be reset WAD
assertEq(eigenPodManager.podOwnerDepositShares(address(staker)), 0);
assertEq(delegationManager.depositScalingFactor(address(staker), beaconChainETHStrategy), WAD);
// 6. deposit: can either verify wc or start/complete cp or complete the withdrawals as shares
_rollBlocksForCompleteWithdrawals(withdrawals);
staker.completeWithdrawalsAsShares(withdrawals);
// 7. delegateTo an operator
staker.delegateTo(operator);
// End state: staker and operator have much higher inflated withdrawable and delegated shares respectively
// The staker's withdrawable shares should be <= from withdrawable shares before (should be equal but could be less due to rounding)
uint withdrawableSharesAfter = _getStakerWithdrawableShares(staker, strategies)[0];
uint operatorShares = delegationManager.operatorShares(address(operator), strategies[0]);
assertLe(
withdrawableSharesAfter, withdrawableSharesBefore, "staker withdrawable shares should be <= from withdrawable shares before"
);
assertLe(operatorShares, withdrawableSharesBefore, "operatorShares should be <= from withdrawable shares before");
}
}
````
## File: src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
import "src/test/integration/users/User.t.sol";
contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils {
function _init() internal override {
_configAssetTypes(HOLDS_ETH);
_configUserTypes(DEFAULT);
}
function test_GasMetering() public rand(0) {
(User staker,,) = _newRandomStaker();
// Deal user 20 full stakers worth of ETH
emit log_named_string("Dealing ETH to", staker.NAME());
cheats.deal(address(staker), 32 ether * 20);
cheats.pauseGasMetering();
(uint40[] memory validators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
EigenPod pod = staker.pod();
CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators);
cheats.startPrank(address(staker));
cheats.resumeGasMetering();
uint startGas = gasleft();
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
uint endGas = gasleft();
cheats.pauseGasMetering();
uint totalGas = startGas - endGas;
emit log_named_uint("== num validators", validators.length);
emit log_named_uint("== verifyWC gas", totalGas);
// check_VerifyWC_State(staker, validators, beaconBalanceWei);
beaconChain.advanceEpoch();
// check pod balances have increased
staker.startCheckpoint();
// check_StartCheckpoint_State(staker);
CheckpointProofs memory cpProofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp());
cheats.resumeGasMetering();
startGas = gasleft();
pod.verifyCheckpointProofs({balanceContainerProof: cpProofs.balanceContainerProof, proofs: cpProofs.balanceProofs});
endGas = gasleft();
cheats.pauseGasMetering();
totalGas = startGas - endGas;
emit log_named_uint("== checkpoint gas", totalGas);
// check_CompleteCheckpoint_State(staker);
// revert();
}
/**
*
* VERIFY -> START -> COMPLETE CHECKPOINT
* (TIMING VARIANTS)
*
*/
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// => no change in shares between 1 and 3
function test_VerifyWC_StartCP_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
staker.completeCheckpoint();
check_CompleteCheckpoint_State(staker);
}
/// 1. Verify validators' withdrawal credentials
/// 2. Verify validators' withdrawal credentials again
/// => This should fail
function test_VerifyWC_VerifyWC_Fails(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
cheats.expectRevert(IEigenPodErrors.CredentialsAlreadyVerified.selector);
staker.verifyWithdrawalCredentials(validators);
}
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// 3. start a checkpoint again
/// => This should fail
function test_VerifyWC_StartCP_StartCP_Fails(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
cheats.expectRevert(IEigenPodErrors.CheckpointAlreadyActive.selector);
staker.startCheckpoint();
}
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// 4. start a checkpoint without advancing a block
/// => this should fail
function test_VerifyWC_StartCP_CompleteCP_StartCP_Fails(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
staker.completeCheckpoint();
check_CompleteCheckpoint_State(staker);
cheats.expectRevert(IEigenPodErrors.CannotCheckpointTwiceInSingleBlock.selector);
staker.startCheckpoint();
}
/// 1. Verify validators' withdrawal credentials
/// -- move forward 1 or more epochs
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// => no change in shares between 1 and 3
function test_VerifyWC_Advance_StartCP_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
// Advance epoch without generating consensus rewards
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
staker.completeCheckpoint();
check_CompleteCheckpoint_State(staker);
}
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// -- move forward 1 or more epochs
/// 3. complete a checkpoint
/// => no change in shares between 1 and 3
function test_VerifyWC_StartCP_Advance_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
// Advance epoch without generating consensus rewards
beaconChain.advanceEpoch_NoRewards();
staker.completeCheckpoint();
check_CompleteCheckpoint_State(staker);
}
/**
*
* VERIFY -> START -> COMPLETE CHECKPOINT
* (EXIT TO POD VARIANTS)
*
*/
/// -- Fully exit validators before verifying withdrawal credentials
/// 1. Verify validators' withdrawal credentials
/// => This should fail
function test_ExitValidators_VerifyWC_Fails(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators,,) = staker.startValidators();
staker.exitValidators(validators);
beaconChain.advanceEpoch_NoRewards();
cheats.expectRevert(IEigenPodErrors.ValidatorIsExitingBeaconChain.selector);
staker.verifyWithdrawalCredentials(validators);
}
/// 1. Verify validators' withdrawal credentials
/// -- fully exit validators to pod
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// => no change in shares between 1 and 3
function test_VerifyWC_ExitValidators_StartCP_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
// Fully exit one or more validators and advance epoch without generating rewards
uint40[] memory subset = _choose(validators);
uint64 exitedBalanceGwei = staker.exitValidators(subset);
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, exitedBalanceGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithExits_State(staker, subset, exitedBalanceGwei);
}
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// -- fully exit validators to pod
/// 3. complete a checkpoint
/// => no change in shares between 1 and 3
/// -- move forward an epoch
/// 4. start a checkpoint
/// 5. complete a checkpoint
/// => exited balance should be reflected in 4 and 5
function test_VerifyWC_StartCP_ExitValidators_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
// Fully exit one or more validators and advance epoch without generating rewards
uint40[] memory subset = _choose(validators);
uint64 exitedBalanceGwei = staker.exitValidators(subset);
beaconChain.advanceEpoch_NoRewards();
staker.completeCheckpoint();
check_CompleteCheckpoint_State(staker);
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, exitedBalanceGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithExits_State(staker, subset, exitedBalanceGwei);
}
/**
*
* VERIFY -> START -> COMPLETE CHECKPOINT
* (SLASH TO POD VARIANTS)
*
*/
/// -- get slashed on beacon chain
/// 1. Try to verify validators' withdrawal credentials
/// => this should fail
function test_SlashToPod_VerifyWC_Fails(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators,,) = staker.startValidators();
beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
// Advance epoch, withdrawing slashed validators to pod
beaconChain.advanceEpoch_NoRewards();
cheats.expectRevert(IEigenPodErrors.ValidatorIsExitingBeaconChain.selector);
staker.verifyWithdrawalCredentials(validators);
}
/// 1. Verify validators' withdrawal credentials
/// -- get slashed on beacon chain; exit to pod
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// => after 3, shares should decrease by slashed amount
function test_VerifyWC_SlashToPod_StartCP_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
// Advance epoch without generating rewards
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
uint64 slashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedBalanceGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedBalanceGwei);
}
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// -- get slashed on beacon chain; exit to pod
/// 3. complete a checkpoint
/// => no change in shares between 1 and 3
/// -- move forward an epoch
/// 4. start a checkpoint
/// 5. complete a checkpoint
/// => slashed balance should be reflected in 4 and 5
function test_VerifyWC_StartCP_SlashToPod_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
// Advance epoch without generating rewards
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
uint64 slashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
staker.completeCheckpoint();
check_CompleteCheckpoint_State(staker);
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedBalanceGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedBalanceGwei);
}
/// 1. Verify validators' withdrawal credentials
/// 2. slash validators
/// 3. start a checkpoint
/// 4. verify withdrawal credentials for another validator while checkpoint in progress
/// 5. complete a checkpoint
/// => Increase in shares between 1 and 4 should reflect the new validator, less the slashed amount
function test_VerifyWC_Slash_StartCP_VerifyWC_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
// Slash validators
uint64 slashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
// Start a checkpoint
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedBalanceGwei);
// Start a new validator & verify withdrawal credentials
cheats.deal(address(staker), 32 ether);
(uint40[] memory newValidators, uint64 addedBeaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, addedBeaconBalanceGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedBalanceGwei);
}
/**
*
* VERIFY -> PROVE STALE BALANCE -> COMPLETE CHECKPOINT
*
*/
/// 1. Verify validators' withdrawal credentials
/// -- get slashed on beacon chain; exit to pod
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// => after 3, shares should decrease by slashed amount
function test_VerifyWC_SlashToPod_VerifyStale_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
// Advance epoch without generating rewards
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
uint64 slashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
staker.verifyStaleBalance(validators[0]);
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedBalanceGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedBalanceGwei);
}
/// 1. Verify validators' withdrawal credentials
/// -- get slashed on beacon chain; do not exit to pod
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// => after 3, shares should decrease by slashed amount
function test_VerifyWC_SlashToCL_VerifyStale_CompleteCP_SlashAgain(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
// Advance epoch without generating rewards
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
// Slash validators but do not process exits to pod
uint64 slashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoWithdraw();
staker.verifyStaleBalance(validators[0]);
check_StartCheckpoint_WithPodBalance_State(staker, 0);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithCLSlashing_HandleRoundDown_State(staker, slashedBalanceGwei);
// Slash validators again but do not process exits to pod
uint64 secondSlashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoWithdraw();
staker.verifyStaleBalance(validators[0]);
check_StartCheckpoint_WithPodBalance_State(staker, 0);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithCLSlashing_HandleRoundDown_State(staker, secondSlashedBalanceGwei);
}
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// -- get slashed on beacon chain; exit to pod
/// 3. complete a checkpoint
/// => no change in shares between 1 and 3
/// -- move forward an epoch
/// 4. start a checkpoint
/// 5. complete a checkpoint
/// => slashed balance should be reflected in 4 and 5
function test_VerifyWC_StartCP_SlashToPod_CompleteCP_VerifyStale(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
// Advance epoch without generating rewards
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
uint64 slashedBalanceGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
staker.completeCheckpoint();
check_CompleteCheckpoint_State(staker);
staker.verifyStaleBalance(validators[0]);
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedBalanceGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedBalanceGwei);
}
/**
*
* VERIFY -> START -> COMPLETE CHECKPOINT
* (EARN ON CL VARIANTS)
*
*/
/// -- earn rewards on beacon chain (not withdrawn to pod)
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// => after 3, shares increase by rewards earned on beacon chain
function test_EarnOnBeacon_VerifyWC_StartCP_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei, uint maxEBValidators) = staker.startValidators();
// Advance epoch and generate consensus rewards, but don't withdraw to pod
beaconChain.advanceEpoch_NoWithdraw();
// Get the expected increase in beacon balance, accounting for validators with MaxEB
// The validators < MaxEB will have their rewards proven in the checkpoint
// The validators == MaxEB will have their rewards proven in verifyWC
uint64 beaconBalanceIncreaseGwei = uint64(validators.length) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI();
uint64 expectedWithdrawnGwei = uint64(maxEBValidators) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI();
uint64 verifyWCRewardsIncreaseGwei = beaconBalanceIncreaseGwei - expectedWithdrawnGwei;
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei + verifyWCRewardsIncreaseGwei);
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
staker.completeCheckpoint();
check_CompleteCheckpoint_EarnOnBeacon_State(staker, expectedWithdrawnGwei);
}
/// 1. Verify validators' withdrawal credentials
/// -- earn rewards on beacon chain (not withdrawn to pod)
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// => after 3, shares increase by rewards earned on beacon chain
function test_VerifyWC_EarnOnBeacon_StartCP_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
// Advance epoch and generate consensus rewards, but don't withdraw to pod
beaconChain.advanceEpoch_NoWithdraw();
uint64 beaconBalanceIncreaseGwei = uint64(validators.length) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI();
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
staker.completeCheckpoint();
check_CompleteCheckpoint_EarnOnBeacon_State(staker, beaconBalanceIncreaseGwei);
}
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// -- earn rewards on beacon chain (not withdrawn to pod)
/// 3. complete a checkpoint
/// => no change in shares between 1 and 3
function test_VerifyWC_StartCP_EarnOnBeacon_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
// Advance epoch and generate consensus rewards, but don't withdraw to pod
beaconChain.advanceEpoch_NoWithdraw();
staker.completeCheckpoint();
check_CompleteCheckpoint_EarnOnBeacon_State(staker, 0);
}
/**
*
* VERIFY -> START -> COMPLETE CHECKPOINT
* (EARN TO POD VARIANTS)
*
*/
/// -- earn rewards on beacon chain (withdrawn to pod)
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// => after 3, shares increase by rewards withdrawn to pod
function test_EarnToPod_VerifyWC_StartCP_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei, uint maxEBValidators) = staker.startValidators();
// Advance epoch, generating consensus rewards and withdrawing anything over MaxEB
beaconChain.advanceEpoch();
// The validators < MaxEB will have their rewards proven in the checkpoint
// The validators == MaxEB will have their rewards proven in verifyWC
uint64 beaconBalanceIncreaseGwei = uint64(validators.length) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI();
uint64 expectedWithdrawnGwei = uint64(maxEBValidators) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI();
uint64 verifyWCRewardsIncreaseGwei = beaconBalanceIncreaseGwei - expectedWithdrawnGwei;
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei + verifyWCRewardsIncreaseGwei);
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, expectedWithdrawnGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithPodBalance_State(staker, expectedWithdrawnGwei);
}
/// 1. Verify validators' withdrawal credentials
/// -- earn rewards on beacon chain (withdrawn to pod)
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// => after 3, shares increase by rewards withdrawn to pod
function test_VerifyWC_EarnToPod_StartCP_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei, uint maxEBValidators) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
// Advance epoch, generating consensus rewards and withdrawing anything over Max EB (2048 ETH)
beaconChain.advanceEpoch();
uint64 expectedWithdrawnGwei = uint64(maxEBValidators) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI();
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, expectedWithdrawnGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithPodBalance_State(staker, expectedWithdrawnGwei);
}
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// -- earn rewards on beacon chain (withdrawn to pod)
/// 3. complete a checkpoint
/// => no change in shares between 1 and 3
function test_VerifyWC_StartCP_EarnToPod_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei, uint maxEBValidators) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, 0);
// Advance epoch, generating consensus rewards and withdrawing anything over 2048 ETH
beaconChain.advanceEpoch();
uint64 expectedWithdrawnGwei = uint64(maxEBValidators) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI();
staker.completeCheckpoint();
// `pod.balance == gweiSent + remainderSent
assert_PodBalance_Eq(staker, (expectedWithdrawnGwei * GWEI_TO_WEI), "pod balance should equal expected");
check_CompleteCheckpoint_WithPodBalance_State(staker, 0);
}
/**
*
* VERIFY -> START -> COMPLETE CHECKPOINT
* (NATIVE ETH VARIANTS)
*
*/
/// -- Pod receives native ETH via fallback
/// 1. start a checkpoint
/// => checkpoint should auto-complete, awarding shares for ETH in pod
function test_NativeETH_StartCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
// Send a random amount of ETH to staker's fallback
(uint64 gweiSent,) = _sendRandomETH(address(staker.pod()));
// Move forward an epoch so we generate a state root that can be queried in startCheckpoint
beaconChain.advanceEpoch();
// should behave identically to partial withdrawals captured by the "earn to pod" variants
staker.startCheckpoint();
check_StartCheckpoint_NoValidators_State(staker, gweiSent);
}
/// -- Pod receives native ETH via fallback
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// => after 3, shares should account for native ETH
function test_NativeETH_VerifyWC_StartCP_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
// Send a random amount of ETH to staker's fallback
(uint64 gweiSent, uint remainderSent) = _sendRandomETH(address(staker.pod()));
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
// should behave identically to partial withdrawals captured by the "earn to pod" variants
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, gweiSent);
staker.completeCheckpoint();
// check that `pod.balance == restakedExecutionLayerGwei + remainderSent
assert_PodBalance_Eq(staker, (gweiSent * GWEI_TO_WEI) + remainderSent, "pod balance should equal expected");
check_CompleteCheckpoint_WithPodBalance_State(staker, gweiSent);
}
/// 1. Verify validators' withdrawal credentials
/// -- Pod receives native ETH via fallback
/// 2. start a checkpoint
/// 3. complete a checkpoint
/// => after 3, shares should account for native ETH
function test_VerifyWC_NativeETH_StartCP_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
// Send a random amount of ETH to staker's fallback
(uint64 gweiSent, uint remainderSent) = _sendRandomETH(address(staker.pod()));
// should behave identically to partial withdrawals captured by the "earn to pod" variants
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, gweiSent);
staker.completeCheckpoint();
// check that `pod.balance == restakedExecutionLayerGwei + remainderSent
assert_PodBalance_Eq(staker, (gweiSent * GWEI_TO_WEI) + remainderSent, "pod balance should equal expected");
check_CompleteCheckpoint_WithPodBalance_State(staker, gweiSent);
}
/// 1. Verify validators' withdrawal credentials
/// 2. start a checkpoint
/// -- Pod receives native ETH via fallback
/// 3. complete a checkpoint
/// => no change in shares between 1 and 3
function test_VerifyWC_StartCP_NativeETH_CompleteCP(uint24 _rand) public rand(_rand) {
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
// should behave identically to partial withdrawals captured by the "earn to pod" variants
// ... if we didn't have any partial withdrawals!
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, 0);
// Send a random amount of ETH to staker's fallback
(uint64 gweiSent, uint remainderSent) = _sendRandomETH(address(staker.pod()));
staker.completeCheckpoint();
// `pod.balance == gweiSent + remainderSent
assert_PodBalance_Eq(staker, (gweiSent * GWEI_TO_WEI) + remainderSent, "pod balance should equal expected");
check_CompleteCheckpoint_WithPodBalance_State(staker, 0);
}
}
````
## File: src/test/integration/tests/upgrade/Complete_PreSlashing_Withdrawal.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/UpgradeTest.t.sol";
contract Integration_Upgrade_Complete_PreSlashing_Withdrawal_Base is UpgradeTest {
struct TestState {
User staker;
User operator;
IStrategy[] strategies;
uint[] tokenBalances;
uint[] shares;
uint[] withdrawalShares;
uint[] expectedTokens;
Withdrawal[] withdrawals;
bool isPartial;
bool completeAsTokens;
}
function _init_(bool withOperator, bool withDelegation) internal virtual returns (TestState memory state) {
// Create staker
(state.staker, state.strategies, state.tokenBalances) = _newRandomStaker();
state.shares = _calculateExpectedShares(state.strategies, state.tokenBalances);
// Delegate staker to operator
state.operator = withOperator ? _newRandomOperator_NoAssets() : User(payable(0));
if (withDelegation) state.staker.delegateTo(state.operator);
// Deposit into EigenLayer
state.staker.depositIntoEigenlayer(state.strategies, state.tokenBalances);
// Setup withdrawal shares (full or partial)
state.isPartial = _randBool();
state.withdrawalShares = new uint[](state.shares.length);
for (uint i = 0; i < state.shares.length; i++) {
state.withdrawalShares[i] = state.isPartial ? state.shares[i] / 2 : state.shares[i];
}
state.expectedTokens = _calculateExpectedTokens(state.strategies, state.withdrawalShares);
state.completeAsTokens = _randBool();
}
function _completeWithdrawal(TestState memory state) internal {
for (uint i = 0; i < state.withdrawals.length; i++) {
if (state.completeAsTokens) {
IERC20[] memory tokens = state.staker.completeWithdrawalAsTokens(state.withdrawals[i]);
check_Withdrawal_AsTokens_State(
state.staker,
state.operator,
state.withdrawals[i],
state.strategies,
state.withdrawalShares,
tokens,
state.expectedTokens
);
} else {
state.staker.completeWithdrawalAsShares(state.withdrawals[i]);
check_Withdrawal_AsShares_State(
state.staker, state.operator, state.withdrawals[i], state.strategies, state.withdrawalShares
);
}
}
}
}
contract Integration_Upgrade_Complete_PreSlashing_Withdrawal is Integration_Upgrade_Complete_PreSlashing_Withdrawal_Base {
function testFuzz_deposit_queue_upgrade_complete(uint24 r) public rand(r) {
TestState memory state = _init_({withOperator: false, withDelegation: false});
state.withdrawals = state.staker.queueWithdrawals(state.strategies, state.withdrawalShares);
_upgradeEigenLayerContracts();
_rollBlocksForCompleteWithdrawals(state.withdrawals);
_completeWithdrawal(state);
}
function testFuzz_delegate_deposit_queue_upgrade_complete(uint24 r) public rand(r) {
TestState memory state = _init_({withOperator: true, withDelegation: true});
state.withdrawals = state.staker.queueWithdrawals(state.strategies, state.withdrawalShares);
_upgradeEigenLayerContracts();
_rollBlocksForCompleteWithdrawals(state.withdrawals);
_completeWithdrawal(state);
}
function testFuzz_upgrade_delegate_queuePartial_complete(uint24 r) public rand(r) {
TestState memory state = _init_({withOperator: true, withDelegation: false});
_upgradeEigenLayerContracts();
state.staker.delegateTo(state.operator);
state.withdrawals = state.staker.queueWithdrawals(state.strategies, state.withdrawalShares);
_rollBlocksForCompleteWithdrawals(state.withdrawals);
_completeWithdrawal(state);
}
function testFuzz_delegate_deposit_queue_completeBeforeUpgrade(uint24 r) public rand(r) {
TestState memory state = _init_({withOperator: true, withDelegation: true});
state.withdrawals = state.staker.queueWithdrawals(state.strategies, state.withdrawalShares);
_rollBlocksForCompleteWithdrawals(state.withdrawals);
// We must roll forward by the delay twice since pre-upgrade delay is half as long as post-upgrade delay.
rollForward(delegationManager.minWithdrawalDelayBlocks() + 1);
_upgradeEigenLayerContracts();
_completeWithdrawal(state);
}
}
contract Integration_Upgrade_Complete_PreSlashing_Withdrawal_Slash is Integration_Upgrade_Complete_PreSlashing_Withdrawal_Base {
using ArrayLib for *;
AVS avs;
OperatorSet operatorSet;
AllocateParams allocateParams;
function _init_(bool withOperator, bool withDelegation) internal override returns (TestState memory state) {
// Initialize state, queue a full withdrawal
state = super._init_({withOperator: withOperator, withDelegation: withDelegation});
state.withdrawals = state.staker.queueWithdrawals(state.strategies, state.withdrawalShares);
// Upgrade contracts
_upgradeEigenLayerContracts();
// Set allocation delay, register for operatorSet & allocate fully
state.operator.setAllocationDelay(1);
rollForward({blocks: ALLOCATION_CONFIGURATION_DELAY + 1});
(avs,) = _newRandomAVS();
operatorSet = avs.createOperatorSet(state.strategies);
allocateParams = _genAllocation_AllAvailable(state.operator, operatorSet);
state.operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(state.operator, operatorSet, allStrats);
state.operator.modifyAllocations(allocateParams);
check_Base_IncrAlloc_State(state.operator, allocateParams);
_rollBlocksForCompleteAllocation(state.operator, operatorSet, state.strategies);
}
function testFuzz_delegate_deposit_queue_upgrade_slashFully_revertCompleteAsShares(uint24 r) public rand(r) {
TestState memory state = _init_({withOperator: true, withDelegation: true});
// Slash operator by AVS
SlashingParams memory slashParams = _genSlashing_Full(state.operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(state.operator, allocateParams, slashParams);
// Complete withdrawals as shares
state.completeAsTokens = false;
_rollBlocksForCompleteWithdrawals(state.withdrawals);
cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector);
state.staker.completeWithdrawalsAsShares(state.withdrawals);
}
function testFuzz_delegate_deposit_queue_upgrade_slashFully_completeAsTokens(uint24 r) public rand(r) {
TestState memory state = _init_({withOperator: true, withDelegation: true});
// Slash operator by AVS
SlashingParams memory slashParams = _genSlashing_Full(state.operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(state.operator, allocateParams, slashParams);
// Complete withdrawals as tokens
state.completeAsTokens = true;
_rollBlocksForCompleteWithdrawals(state.withdrawals);
_completeWithdrawal(state);
}
function testFuzz_delegate_deposit_queue_upgrade_slash_completeAsShares(uint24 r) public rand(r) {
TestState memory state = _init_({withOperator: true, withDelegation: true});
// Slash operator by AVS
SlashingParams memory slashParams = _genSlashing_Rand(state.operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(state.operator, allocateParams, slashParams);
// Complete withdrawals as shares. Shares need to be recalculated to handle slight round down after slashing.
state.completeAsTokens = false;
_rollBlocksForCompleteWithdrawals(state.withdrawals);
_completeWithdrawal(state);
}
function testFuzz_delegate_deposit_queue_upgrade_slash_completeAsTokens(uint24 r) public rand(r) {
TestState memory state = _init_({withOperator: true, withDelegation: true});
// Slash operator by AVS
SlashingParams memory slashParams = _genSlashing_Rand(state.operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(state.operator, allocateParams, slashParams);
// Complete withdrawals as tokens
state.completeAsTokens = true;
_rollBlocksForCompleteWithdrawals(state.withdrawals);
_completeWithdrawal(state);
}
}
````
## File: src/test/integration/tests/upgrade/Delegate_Upgrade_Allocate.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/UpgradeTest.t.sol";
contract Integration_Upgrade_Deposit_Delegate_Allocate is UpgradeTest {
struct TestState {
User staker;
User operator;
AVS avs;
IStrategy[] strategies;
uint[] tokenBalances;
OperatorSet operatorSet;
AllocateParams allocateParams;
}
function _init_() internal returns (TestState memory state) {
(state.staker, state.strategies, state.tokenBalances) = _newRandomStaker();
(state.operator,,) = _newRandomOperator();
// Pre-upgrade:
// 1. Create staker and operator with assets, then deposit into EigenLayer
// 2. Delegate to operator
state.staker.depositIntoEigenlayer(state.strategies, state.tokenBalances);
state.staker.delegateTo(state.operator);
}
function _setupAllocation(TestState memory state) internal {
// 3. Set allocation delay for operator
state.operator.setAllocationDelay(1);
rollForward({blocks: ALLOCATION_CONFIGURATION_DELAY + 1});
// 4. Create an operator set and register an operator.
state.operatorSet = state.avs.createOperatorSet(state.strategies);
state.operator.registerForOperatorSet(state.operatorSet);
check_Registration_State_NoAllocation(state.operator, state.operatorSet, allStrats);
// 5. Allocate to operator set.
state.allocateParams = AllocateParams({
operatorSet: state.operatorSet,
strategies: state.strategies,
newMagnitudes: _randMagnitudes({sum: 1 ether, len: state.strategies.length})
});
state.operator.modifyAllocations(state.allocateParams);
check_IncrAlloc_State_Slashable(state.operator, state.allocateParams);
}
function testFuzz_deposit_delegate_upgrade_allocate(uint24 r) public rand(r) {
TestState memory state = _init_();
_upgradeEigenLayerContracts();
(state.avs,) = _newRandomAVS();
_setupAllocation(state);
}
}
````
## File: src/test/integration/tests/upgrade/EigenPod_Slashing_Migration.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/UpgradeTest.t.sol";
contract Integration_Upgrade_EigenPod_Slashing_Migration is UpgradeTest, EigenPodPausingConstants {
User staker;
uint40[] validators;
uint[] shares;
IStrategy[] strategies;
uint[] tokenBalances;
function _init() internal override {
_configAssetTypes(HOLDS_ETH);
_configUserTypes(DEFAULT);
/// 0. Create a staker with underlying assets
(staker, strategies, tokenBalances) = _newRandomStaker();
shares = _calculateExpectedShares(strategies, tokenBalances);
/// 1. Deposit into strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
validators = staker.getActiveValidators();
}
function _completeEigenpodMigration() internal {
// Start a checkpoint
staker.startCheckpoint();
// Pause checkpoint starting
cheats.prank(pauserMultisig);
eigenPodManager.pause(2 ** PAUSED_START_CHECKPOINT);
cheats.expectRevert("EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager");
staker.startCheckpoint();
// Complete in progress checkpoint
staker.completeCheckpoint();
// Upgrade Contracts for slashing
_upgradeEigenLayerContracts();
// Unpause EigenPodManager
cheats.prank(eigenLayerPauserReg.unpauser());
eigenPodManager.unpause(0);
}
function testFuzz_earnRewards_migrate_exit(uint24 _rand) public rand(_rand) {
// 2. Advance epoch, generating consensus rewards and withdrawing anything over 32 ETH
beaconChain.advanceEpoch();
// 3. Migrate & Upgrade
_completeEigenpodMigration();
// 4. Exit validators
// Fully exit one or more validators and advance epoch without generating rewards
uint40[] memory subset = _choose(validators);
uint64 exitedBalanceGwei = staker.exitValidators(subset);
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, exitedBalanceGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithExits_State(staker, subset, exitedBalanceGwei);
}
function testFuzz_slash_migrate(uint24 _rand) public rand(_rand) {
// 2. Slash validators
uint40[] memory subset = _choose(validators);
uint64 slashedGwei = beaconChain.slashValidators(subset, _randSlashType());
beaconChain.advanceEpoch_NoRewards();
shares[0] = shares[0] - slashedGwei * GWEI_TO_WEI; // Shares should decrease by slashed amount
// 3. Migrate & Upgrade
_completeEigenpodMigration();
// Assertions
assert_BCSF_WAD(staker, "BCSF should be WAD");
assert_ActiveValidatorCount(staker, validators.length - subset.length, "validator count should decrease");
assert_HasExpectedShares(staker, strategies, shares, "shares should decrease by slashed amount");
}
}
````
## File: src/test/integration/tests/upgrade/EigenPod.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/UpgradeTest.t.sol";
contract Integration_Upgrade_EigenPod_Base is UpgradeTest {
User staker;
IStrategy[] strategies;
uint[] tokenBalances;
uint[] shares;
function _init() internal virtual override {
_configAssetTypes(HOLDS_ETH);
_configUserTypes(DEFAULT);
/// 0. Create a staker with underlying assets
(staker, strategies, tokenBalances) = _newRandomStaker();
shares = _calculateExpectedShares(strategies, tokenBalances);
/// 1. Deposit into strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
}
}
contract Integration_Upgrade_EigenPod_SlashAfterUpgrade is Integration_Upgrade_EigenPod_Base {
uint40[] validators;
uint64 slashedGwei;
function testFuzz_deposit_upgrade_slash_completeCheckpoint(uint24 _rand) public rand(_rand) {
uint64 initBeaconBalanceGwei = uint64(tokenBalances[0] / GWEI_TO_WEI);
/// 2. Upgrade contracts
_upgradeEigenLayerContracts();
/// 3. Slash the staker partially
validators = staker.getActiveValidators();
slashedGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards(); // Withdraw slashed validators to pod
// 4. Start and complete a checkpoint
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, initBeaconBalanceGwei - slashedGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedGwei);
}
}
contract Integration_Upgrade_EigenPod_FullSlash is Integration_Upgrade_EigenPod_Base {
uint40[] validators;
uint64 slashedGwei;
function _init() internal override {
super._init();
/// 2. Fully slash the staker
validators = staker.getActiveValidators();
slashedGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Full);
beaconChain.advanceEpoch_NoRewards(); // Withdraw slashed validators to pod
/// 3. Upgrade contracts
_upgradeEigenLayerContracts();
}
function testFuzz_deposit_fullSlash_upgrade_delegate(uint24 _rand) public rand(_rand) {
/// 4. Delegate to operator
(User operator,,) = _newRandomOperator();
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
}
function testFuzz_deposit_fullSlash_upgrade_deposit_delegate(uint24 _rand) public rand(_rand) {
// 5. Start a new validator & verify withdrawal credentials
cheats.deal(address(staker), 32 ether);
tokenBalances[0] = tokenBalances[0] + 32 ether;
(uint40[] memory newValidators, uint64 addedBeaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, addedBeaconBalanceGwei);
shares = _calculateExpectedShares(strategies, tokenBalances);
// 6. Delegate to operator
(User operator,,) = _newRandomOperator();
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
}
}
contract Integration_Upgrade_EigenPod_NegativeShares is Integration_Upgrade_EigenPod_Base {
User operator;
Withdrawal withdrawal;
int[] tokenDeltas;
int[] balanceUpdateShareDelta;
function _init() internal override {
super._init();
// 3. Delegate to operator
(operator,,) = _newRandomOperator();
staker.delegateTo(operator);
/// 4. Queue a withdrawal for all shares
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
withdrawal = withdrawals[0];
/// 5. Update balance randomly (can be positive or negative)
(tokenDeltas, balanceUpdateShareDelta,) = _randBalanceUpdate(staker, strategies);
staker.updateBalances(strategies, tokenDeltas);
/// 6. Upgrade contracts
_upgradeEigenLayerContracts();
}
function testFuzz_deposit_delegate_updateBalance_upgrade_completeAsShares(uint24 _rand) public rand(_rand) {
/// 7. Complete the withdrawal as shares
Withdrawal[] memory withdrawals = new Withdrawal[](1);
withdrawals[0] = withdrawal;
_rollBlocksForCompleteWithdrawals(withdrawals);
staker.completeWithdrawalAsShares(withdrawal);
// Manually complete checks since we could still negative shares prior to the upgrade, causing a revert in the share check
(uint[] memory expectedOperatorShareDelta, int[] memory expectedStakerShareDelta) =
_getPostWithdrawalExpectedShareDeltas(balanceUpdateShareDelta[0], withdrawal);
assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending");
assert_Snap_Unchanged_TokenBalances(staker, "staker should not have any change in underlying token balances");
assert_Snap_Added_OperatorShares(
operator, withdrawal.strategies, expectedOperatorShareDelta, "operator should have received shares"
);
assert_Snap_Delta_StakerShares(staker, strategies, expectedStakerShareDelta, "staker should have received expected shares");
}
function testFuzz_deposit_delegate_updateBalance_upgrade_completeAsTokens(uint24 _rand) public rand(_rand) {
/// 7. Complete the withdrawal as tokens
Withdrawal[] memory withdrawals = new Withdrawal[](1);
withdrawals[0] = withdrawal;
_rollBlocksForCompleteWithdrawals(withdrawals);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawal);
uint[] memory expectedTokens = _getPostWithdrawalExpectedTokenDeltas(balanceUpdateShareDelta[0], withdrawal);
// Manually complete checks since we could still negative shares prior to the upgrade, causing a revert in the share check
assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending");
assert_Snap_Added_TokenBalances(staker, tokens, expectedTokens, "staker should have received expected tokens");
assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed");
// If we had a positive balance update, then the staker shares should not have changed
if (balanceUpdateShareDelta[0] > 0) {
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker shares should not have changed");
}
// Else, the staker shares should have increased by the magnitude of the negative share delta
else {
int[] memory expectedStakerShareDelta = new int[](1);
expectedStakerShareDelta[0] = -balanceUpdateShareDelta[0];
assert_Snap_Delta_StakerShares(staker, strategies, expectedStakerShareDelta, "staker should have received expected shares");
}
}
function _getPostWithdrawalExpectedShareDeltas(int _balanceUpdateShareDelta, Withdrawal memory _withdrawal)
internal
pure
returns (uint[] memory, int[] memory)
{
uint[] memory operatorShareDelta = new uint[](1);
int[] memory stakerShareDelta = new int[](1);
// The staker share delta is the withdrawal scaled shares since it can go from negative to positive
stakerShareDelta[0] = int(_withdrawal.scaledShares[0]);
if (_balanceUpdateShareDelta > 0) {
// If balanceUpdateShareDelta is positive, then the operator delta is the withdrawal scaled shares
operatorShareDelta[0] = _withdrawal.scaledShares[0];
} else {
// Operator shares never went negative, so we can just add the withdrawal scaled shares and the negative share delta
operatorShareDelta[0] = uint(int(_withdrawal.scaledShares[0]) + _balanceUpdateShareDelta);
}
return (operatorShareDelta, stakerShareDelta);
}
function _getPostWithdrawalExpectedTokenDeltas(int _balanceUpdateShareDelta, Withdrawal memory _withdrawal)
internal
pure
returns (uint[] memory)
{
uint[] memory expectedTokenDeltas = new uint[](1);
if (_balanceUpdateShareDelta > 0) {
// If we had a positive balance update, then the expected token delta is the withdrawal scaled shares
expectedTokenDeltas[0] = _withdrawal.scaledShares[0];
} else {
// If we had a negative balance update, then the expected token delta is the withdrawal scaled shares plus the negative share delta
expectedTokenDeltas[0] = uint(int(_withdrawal.scaledShares[0]) + _balanceUpdateShareDelta);
}
return expectedTokenDeltas;
}
}
````
## File: src/test/integration/tests/upgrade/Prooftra.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/UpgradeTest.t.sol";
contract Integration_Upgrade_Pectra is UpgradeTest, EigenPodPausingConstants {
function _init() internal override {
_configAssetTypes(HOLDS_ETH);
_configUserTypes(DEFAULT);
}
function test_Upgrade_VerifyWC_StartCP_CompleteCP(uint24 _rand) public rand(_rand) {
// 1. Pause, Fork, and Upgrade
_pauseForkAndUpgrade();
// 2. Set Pectra Fork Timestamp & unpause
_setTimestampAndUnpause();
// 3. Initialize Staker
(User staker,,) = _newRandomStaker();
(uint40[] memory validators, uint64 beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
// 4. Verify Withdrawal Credentials
staker.verifyWithdrawalCredentials(validators);
check_VerifyWC_State(staker, validators, beaconBalanceGwei);
// 4. Start Checkpoint
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
// 5. Complete Checkpoint
staker.completeCheckpoint();
check_CompleteCheckpoint_State(staker);
}
function test_VerifyWC_StartCP_Fork_CompleteCP(uint24 _rand) public rand(_rand) {
// Initialize state
(User staker,,) = _newRandomStaker();
(uint40[] memory validators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
// 1. Verify validators' withdrawal credentials
staker.verifyWithdrawalCredentials(validators);
// 2. Start a checkpoint
staker.startCheckpoint();
// 3. Pause, Fork, and Upgrade
_pauseForkAndUpgrade();
// 4. Set Pectra Fork Timestamp & unpause
_setTimestampAndUnpause();
// 5. Complete in progress checkpoint
staker.completeCheckpoint();
check_CompleteCheckpoint_State(staker);
}
function test_VerifyWC_Fork_EarnToPod_StartCP_CompleteCP(uint24 _rand) public rand(_rand) {
// Initialize state
(User staker,,) = _newRandomStaker();
(uint40[] memory validators,,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
// 1. Verify validators' withdrawal credentials
staker.verifyWithdrawalCredentials(validators);
// 2. Pause, Fork, and Upgrade
_pauseForkAndUpgrade();
// 3. Set timestamp and unpause
_setTimestampAndUnpause();
// 4. Advance epoch, generating consensus rewards and withdrawing anything over Max EB
// Not: Nothing is withdrawn because all validators were created with 32 ETH
beaconChain.advanceEpoch();
uint64 expectedEarnedGwei = uint64(validators.length) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI();
// 5. Start a checkpoint
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
// 6. Complete in progress checkpoint
staker.completeCheckpoint();
check_CompleteCheckpoint_EarnOnBeacon_State(staker, expectedEarnedGwei);
}
function _pauseForkAndUpgrade() internal {
// 1. Pause starting checkpoint, completing, and credential proofs
cheats.prank(pauserMultisig);
eigenPodManager.pause(
2 ** PAUSED_START_CHECKPOINT | 2 ** PAUSED_EIGENPODS_VERIFY_CREDENTIALS | 2 ** PAUSED_VERIFY_STALE_BALANCE
| 2 ** PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS
);
// 2. Fork to Pectra
uint64 pectraForkTimestamp = uint64(block.timestamp) + 12;
BeaconChainMock_DenebForkable(address(beaconChain)).forkToPectra(pectraForkTimestamp);
// 3. Upgrade EigenPodManager & EigenPod
_upgradeEigenLayerContracts();
// 4. Set proof timestamp setter to operations multisig
cheats.prank(eigenPodManager.owner());
eigenPodManager.setProofTimestampSetter(address(operationsMultisig));
}
function _setTimestampAndUnpause() internal {
// 1. Set Timestamp
cheats.startPrank(eigenPodManager.proofTimestampSetter());
eigenPodManager.setPectraForkTimestamp(BeaconChainMock_DenebForkable(address(beaconChain)).pectraForkTimestamp());
cheats.stopPrank();
// 2. Randomly warp to just after the fork timestamp
// If we do not warp, proofs will be against deneb state
if (_randBool()) {
// If we warp, proofs will be against electra state
cheats.warp(block.timestamp + 1);
}
// 3. Unpause
cheats.prank(eigenLayerPauserReg.unpauser());
eigenPodManager.unpause(0);
}
}
````
## File: src/test/integration/tests/upgrade/README.md
````markdown
## Upgrade Tests
This folder contains specific tests we want to conduct to determine upgrade compatibility. Tests in this folder are only run if the `forktest` profile is selected, e.g:
`env FOUNDRY_PROFILE=forktest forge t --mc Upgrade`
#### How to Write
Upgrade tests inherit from the `UpgradeTest` mixin, which imports everything you need for a standard integration test. A standard integration test automatically upgrades all contracts to the latest repo contracts, even when forking from mainnet.
In contrast, the `UpgradeTest` mixin ensures that (when a test begins) the contracts we're working with are _not upgraded_. This allows the test writer to perform some initial setup actions before calling `_upgradeEigenLayerContracts()`, after which we can check that the upgrade correctly handles pre-upgrade state.
#### Example
Say we want to check that withdrawals initiated before the slashing upgrade are completable after the slashing upgrade. This example test shows how:
```solidity
contract Integration_Upgrade_Complete_PreSlashing_Withdrawal is UpgradeTest {
function testFuzz_deposit_queue_upgrade_completeAsShares(uint24 _random) public rand(_random) {
/// Pre-upgrade:
/// 1. Create staker with some assets
/// 2. Staker deposits into EigenLayer
/// 3. Staker queues a withdrawal
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
User operator = User(payable(0));
staker.depositIntoEigenlayer(strategies, tokenBalances);
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
IDelegationManagerTypes.Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
/// Upgrade to slashing contracts
_upgradeEigenLayerContracts();
// Complete pre-slashing withdrawals as shares
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_State(staker, operator, withdrawals[i], strategies, shares);
}
}
}
```
**Note** how the initial staker actions are NOT followed by `check_X_State` methods. This is because, before calling `_upgradeEigenLayerContracts`, the test is being run on old contracts. Adding invariant checks to old state transitions is not the point of this test and would add a lot of maintenance overhead.
````
## File: src/test/integration/tests/ALM_Multi.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
contract Integration_ALM_Multi is IntegrationCheckUtils {
using StdStyle for *;
enum Action {
REGISTER,
DEREGISTER,
INCR_ALLOC,
INCR_ALLOC_FULL,
DECR_ALLOC,
DECR_ALLOC_FULL
}
enum State {
NONE,
REGISTERED,
ALLOCATED,
FULLY_ALLOCATED,
REG_ALLOC,
REG_FULLY_ALLOC
}
AVS avs;
OperatorSet operatorSet;
IStrategy[] strategies;
/// iteration idx -> list of operators in each state
mapping(uint => mapping(State => User[])) operators;
/// operator -> list of strategies they have delegated assets in
mapping(User => IStrategy[]) allocatedStrats;
/// Last modifyAllocations params made by the operator
mapping(User => AllocateParams) lastModifyParams;
uint constant NUM_UNIQUE_ASSETS = 3;
uint constant NUM_OPERATORS = 5;
uint constant NUM_ITERATIONS = 20;
function _init() internal virtual override {
_configAssetAmounts(NUM_UNIQUE_ASSETS);
(avs,) = _newRandomAVS();
operatorSet = avs.createOperatorSet(allStrats);
for (uint i = 0; i < NUM_OPERATORS; i++) {
(User staker, IStrategy[] memory _strategies, uint[] memory initTokenBalances) = _newRandomStaker();
User operator = _newRandomOperator_NoAssets();
// 1. Deposit into strategies
staker.depositIntoEigenlayer(_strategies, initTokenBalances);
uint[] memory initDepositShares = _calculateExpectedShares(_strategies, initTokenBalances);
check_Deposit_State(staker, _strategies, initDepositShares);
// 2. Delegate to operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, _strategies, initDepositShares);
allocatedStrats[operator] = _strategies;
// Add operator to NONE state for the 0th iteration
operators[0][State.NONE].push(operator);
}
}
/// Reduce fuzz runs because this test is thiccc:
///
/// forge-config: default.fuzz.runs = 10
/// forge-config: forktest.fuzz.runs = 3
function test_Multi(uint24 _r) public rand(_r) {
// Do 20 iterations
for (uint i = 1; i <= NUM_ITERATIONS; i++) {
console.log("%s: %d", "iter".green().italic(), i - 1);
_dispatchNone(i);
_dispatchRegistered(i);
_dispatchAllocated(i);
_dispatchFullyAllocated(i);
_dispatchRegAlloc(i);
_dispatchRegFullyAlloc(i);
// Ensure all pending actions are completed for the next iteration
_rollForward_DeallocationDelay();
}
}
/// @dev NONE operators can:
/// [REGISTER, INCR_ALLOC, INCR_ALLOC_FULL]
function _dispatchNone(uint iter) internal {
// Fetch all NONE operators from previous iteration
User[] memory _operators = operators[iter - 1][State.NONE];
Action[3] memory actions = [Action.REGISTER, Action.INCR_ALLOC, Action.INCR_ALLOC_FULL];
if (_operators.length == 0) return;
console.log("%s: %d operators", "_dispatchNone".green(), _operators.length);
for (uint i = 0; i < _operators.length; i++) {
// Get operator and allocated strategies
User operator = _operators[i];
IStrategy[] memory _strats = allocatedStrats[operator];
// Get action
uint aI = _randUint(0, actions.length - 1);
Action action = actions[aI];
// Process action
if (action == Action.REGISTER) {
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, allStrats);
operators[iter][State.REGISTERED].push(operator);
} else if (action == Action.INCR_ALLOC) {
AllocateParams memory params = _genAllocation_HalfAvailable(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_IncrAlloc_State_NotSlashable(operator, params);
lastModifyParams[operator] = params;
operators[iter][State.ALLOCATED].push(operator);
} else if (action == Action.INCR_ALLOC_FULL) {
AllocateParams memory params = _genAllocation_AllAvailable(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_IncrAlloc_State_NotSlashable(operator, params);
lastModifyParams[operator] = params;
operators[iter][State.FULLY_ALLOCATED].push(operator);
}
}
}
/// @dev REGISTERED operators can:
/// [DEREGISTER, INCR_ALLOC, INCR_ALLOC_FULL]
function _dispatchRegistered(uint iter) internal {
// Fetch all REGISTERED operators from previous iteration
User[] memory _operators = operators[iter - 1][State.REGISTERED];
Action[3] memory actions = [Action.DEREGISTER, Action.INCR_ALLOC, Action.INCR_ALLOC_FULL];
if (_operators.length == 0) return;
console.log("%s: %d operators", "_dispatchRegistered".green(), _operators.length);
for (uint i = 0; i < _operators.length; i++) {
// Get operator
User operator = _operators[i];
IStrategy[] memory _strats = allocatedStrats[operator];
// Get action
uint aI = _randUint(0, actions.length - 1);
Action action = actions[aI];
// Process action
if (action == Action.DEREGISTER) {
operator.deregisterFromOperatorSet(operatorSet);
check_Deregistration_State_NoAllocation(operator, operatorSet);
operators[iter][State.NONE].push(operator);
} else if (action == Action.INCR_ALLOC) {
AllocateParams memory params = _genAllocation_HalfAvailable(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_IncrAlloc_State_Slashable(operator, params);
lastModifyParams[operator] = params;
operators[iter][State.REG_ALLOC].push(operator);
} else if (action == Action.INCR_ALLOC_FULL) {
AllocateParams memory params = _genAllocation_AllAvailable(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_IncrAlloc_State_Slashable(operator, params);
// check_FullyAllocated_State(operator, operatorSet, params.strategies); TODO
lastModifyParams[operator] = params;
operators[iter][State.REG_FULLY_ALLOC].push(operator);
}
}
}
/// @dev ALLOCATED operators can:
/// [REGISTER, INCR_ALLOC, INCR_ALLOC_FULL, DECR_ALLOC, DECR_ALLOC_FULL]
function _dispatchAllocated(uint iter) internal {
// Fetch all ALLOCATED operators from previous iteration
User[] memory _operators = operators[iter - 1][State.ALLOCATED];
Action[5] memory actions = [Action.REGISTER, Action.INCR_ALLOC, Action.INCR_ALLOC_FULL, Action.DECR_ALLOC, Action.DECR_ALLOC_FULL];
if (_operators.length == 0) return;
console.log("%s: %d operators", "_dispatchAllocated".green(), _operators.length);
for (uint i = 0; i < _operators.length; i++) {
// Get operator
User operator = _operators[i];
IStrategy[] memory _strats = allocatedStrats[operator];
// Get action
uint aI = _randUint(0, actions.length - 1);
Action action = actions[aI];
// Process action
if (action == Action.REGISTER) {
operator.registerForOperatorSet(operatorSet);
check_Registration_State_ActiveAllocation(operator, lastModifyParams[operator]);
operators[iter][State.REG_ALLOC].push(operator);
} else if (action == Action.INCR_ALLOC) {
AllocateParams memory params = _genAllocation_HalfAvailable(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_IncrAlloc_State_NotSlashable(operator, params);
lastModifyParams[operator] = params;
operators[iter][State.ALLOCATED].push(operator);
} else if (action == Action.INCR_ALLOC_FULL) {
AllocateParams memory params = _genAllocation_AllAvailable(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_IncrAlloc_State_NotSlashable(operator, params);
// check_FullyAllocated_State(operator); TODO
lastModifyParams[operator] = params;
operators[iter][State.FULLY_ALLOCATED].push(operator);
} else if (action == Action.DECR_ALLOC) {
AllocateParams memory params = _genDeallocation_HalfRemaining(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_DecrAlloc_State_NotSlashable(operator, params);
lastModifyParams[operator] = params;
operators[iter][State.ALLOCATED].push(operator);
} else if (action == Action.DECR_ALLOC_FULL) {
AllocateParams memory params = _genDeallocation_Full(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_DecrAlloc_State_NotSlashable(operator, params);
// check_FullyDeallocated_State(operator); TODO
lastModifyParams[operator] = params;
operators[iter][State.NONE].push(operator);
}
}
}
/// @dev FULLY_ALLOCATED operators can:
/// [REGISTER, DECR_ALLOC, DECR_ALLOC_FULL]
function _dispatchFullyAllocated(uint iter) internal {
// Fetch all FULLY_ALLOCATED operators from previous iteration
User[] memory _operators = operators[iter - 1][State.FULLY_ALLOCATED];
Action[3] memory actions = [Action.REGISTER, Action.DECR_ALLOC, Action.DECR_ALLOC_FULL];
if (_operators.length == 0) return;
console.log("%s: %d operators", "_dispatchFullyAllocated".green(), _operators.length);
for (uint i = 0; i < _operators.length; i++) {
// Get operator
User operator = _operators[i];
IStrategy[] memory _strats = allocatedStrats[operator];
// Get action
uint aI = _randUint(0, actions.length - 1);
Action action = actions[aI];
// Process action
if (action == Action.REGISTER) {
operator.registerForOperatorSet(operatorSet);
check_Registration_State_ActiveAllocation(operator, lastModifyParams[operator]);
operators[iter][State.REG_FULLY_ALLOC].push(operator);
} else if (action == Action.DECR_ALLOC) {
AllocateParams memory params = _genDeallocation_HalfRemaining(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_DecrAlloc_State_NotSlashable(operator, params);
lastModifyParams[operator] = params;
operators[iter][State.ALLOCATED].push(operator);
} else if (action == Action.DECR_ALLOC_FULL) {
AllocateParams memory params = _genDeallocation_Full(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_DecrAlloc_State_NotSlashable(operator, params);
// check_FullyDeallocated_State(operator); TODO
lastModifyParams[operator] = params;
operators[iter][State.NONE].push(operator);
}
}
}
/// @dev REG_ALLOC operators can:
/// [DEREGISTER, INCR_ALLOC, INCR_ALLOC_FULL, DECR_ALLOC, DECR_ALLOC_FULL]
function _dispatchRegAlloc(uint iter) internal {
// Fetch all REG_ALLOC operators from previous iteration
User[] memory _operators = operators[iter - 1][State.REG_ALLOC];
Action[5] memory actions = [Action.DEREGISTER, Action.INCR_ALLOC, Action.INCR_ALLOC_FULL, Action.DECR_ALLOC, Action.DECR_ALLOC_FULL];
if (_operators.length == 0) return;
console.log("%s: %d operators", "_dispatchRegAlloc".green(), _operators.length);
for (uint i = 0; i < _operators.length; i++) {
// Get operator
User operator = _operators[i];
IStrategy[] memory _strats = allocatedStrats[operator];
// Get action
uint aI = _randUint(0, actions.length - 1);
Action action = actions[aI];
// Process action
if (action == Action.DEREGISTER) {
operator.deregisterFromOperatorSet(operatorSet);
check_Deregistration_State_ActiveAllocation(operator, operatorSet);
operators[iter][State.ALLOCATED].push(operator);
} else if (action == Action.INCR_ALLOC) {
AllocateParams memory params = _genAllocation_HalfAvailable(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_IncrAlloc_State_Slashable(operator, params);
lastModifyParams[operator] = params;
operators[iter][State.REG_ALLOC].push(operator);
} else if (action == Action.INCR_ALLOC_FULL) {
AllocateParams memory params = _genAllocation_AllAvailable(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_IncrAlloc_State_Slashable(operator, params);
// check_FullyAllocated_State(operator); TODO
lastModifyParams[operator] = params;
operators[iter][State.REG_FULLY_ALLOC].push(operator);
} else if (action == Action.DECR_ALLOC) {
AllocateParams memory params = _genDeallocation_HalfRemaining(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_DecrAlloc_State_Slashable(operator, params);
lastModifyParams[operator] = params;
operators[iter][State.REG_ALLOC].push(operator);
} else if (action == Action.DECR_ALLOC_FULL) {
AllocateParams memory params = _genDeallocation_Full(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_DecrAlloc_State_Slashable(operator, params);
// check_FullyDeallocated_State(operator); TODO
lastModifyParams[operator] = params;
operators[iter][State.REGISTERED].push(operator);
}
}
}
/// @dev REG_FULLY_ALLOC operators can:
/// [DEREGISTER, DECR_ALLOC, DECR_ALLOC_FULL]
function _dispatchRegFullyAlloc(uint iter) internal {
// Fetch all REG_FULLY_ALLOC operators from previous iteration
User[] memory _operators = operators[iter - 1][State.REG_FULLY_ALLOC];
Action[3] memory actions = [Action.DEREGISTER, Action.DECR_ALLOC, Action.DECR_ALLOC_FULL];
if (_operators.length == 0) return;
console.log("%s: %d operators", "_dispatchRegFullyAlloc".green(), _operators.length);
for (uint i = 0; i < _operators.length; i++) {
// Get operator
User operator = _operators[i];
IStrategy[] memory _strats = allocatedStrats[operator];
// Get action
uint aI = _randUint(0, actions.length - 1);
Action action = actions[aI];
// Process action
if (action == Action.DEREGISTER) {
operator.deregisterFromOperatorSet(operatorSet);
check_Deregistration_State_ActiveAllocation(operator, operatorSet);
operators[iter][State.FULLY_ALLOCATED].push(operator);
} else if (action == Action.DECR_ALLOC) {
AllocateParams memory params = _genDeallocation_HalfRemaining(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_DecrAlloc_State_Slashable(operator, params);
lastModifyParams[operator] = params;
operators[iter][State.REG_ALLOC].push(operator);
} else if (action == Action.DECR_ALLOC_FULL) {
AllocateParams memory params = _genDeallocation_Full(operator, operatorSet, _strats);
operator.modifyAllocations(params);
check_DecrAlloc_State_Slashable(operator, params);
// check_FullyDeallocated_State(operator); TODO
lastModifyParams[operator] = params;
operators[iter][State.REGISTERED].push(operator);
}
}
}
}
````
## File: src/test/integration/tests/ALM_RegisterAndModify.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
contract Integration_ALMBase is IntegrationCheckUtils {
AVS avs;
OperatorSet operatorSet;
User operator;
AllocateParams allocateParams;
User staker;
IStrategy[] strategies;
uint[] initTokenBalances;
uint[] initDepositShares;
/// Shared setup:
///
/// 1. Generate staker with deposited assets, operator, and AVS
/// 2. Deposit asssets and delegate to operator
/// 3. AVS creates an operator set containing the strategies held by the staker
function _init() internal virtual override {
(staker, strategies, initTokenBalances) = _newRandomStaker();
operator = _newRandomOperator_NoAssets();
(avs,) = _newRandomAVS();
// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, initTokenBalances);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, initDepositShares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
// 3. Create an operator set containing the strategies held by the staker
operatorSet = avs.createOperatorSet(strategies);
}
}
contract Integration_InitRegistered is Integration_ALMBase {
/// @dev Integration test variants that start with the operator being registered
/// for the operator set
function _init() internal virtual override {
super._init();
// Register for operator set before allocating to any strategies
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, allStrats);
}
function testFuzz_allocate_deallocate_deregister(uint24 _r) public rand(_r) {
// 1. Allocate to the operator set
allocateParams = _genAllocation_Rand(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable(operator, allocateParams);
// 2. Roll to the allocation's effect block
_rollForward_AllocationDelay(operator);
// 3. Deallocate fully from the operator set
AllocateParams memory deallocateParams = _genDeallocation_Full(operator, operatorSet);
operator.modifyAllocations(deallocateParams);
check_DecrAlloc_State_Slashable(operator, deallocateParams);
// 4. Deregister from operator set
operator.deregisterFromOperatorSet(operatorSet);
check_Deregistration_State_PendingAllocation(operator, operatorSet);
// 5. Check the operator is fully deallocated after the deallocation delay
_rollForward_DeallocationDelay();
check_FullyDeallocated_State(operator, allocateParams, deallocateParams);
}
function testFuzz_allocate_deallocate_waitDeallocate_deregister(uint24 _r) public rand(_r) {
// 1. Allocate to the operator set
allocateParams = _genAllocation_Rand(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable(operator, allocateParams);
// 2. Roll to the allocation's effect block
_rollForward_AllocationDelay(operator);
// 3. Deallocate fully from the operator set
AllocateParams memory deallocateParams = _genDeallocation_Full(operator, operatorSet);
operator.modifyAllocations(deallocateParams);
check_DecrAlloc_State_Slashable(operator, deallocateParams);
// 4. Check the operator is fully deallocated after the deallocation delay
_rollForward_DeallocationDelay();
check_FullyDeallocated_State(operator, allocateParams, deallocateParams);
// 5. Deregister from operator set
operator.deregisterFromOperatorSet(operatorSet);
check_Deregistration_State_NoAllocation(operator, operatorSet);
}
function testFuzz_deregister_waitDeregister_allocate_deallocate(uint24 _r) public rand(_r) {
// 1. Deregister from operator set
operator.deregisterFromOperatorSet(operatorSet);
check_Deregistration_State_NoAllocation(operator, operatorSet);
// 2. Wait for deallocation delay. Operator is no longer slashable
_rollForward_DeallocationDelay();
// 3. Allocate to operator set
allocateParams = _genAllocation_Rand(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_NotSlashable(operator, allocateParams);
// 3. Wait for allocation delay
_rollForward_AllocationDelay(operator);
// 4. Deallocate operator from operator set
AllocateParams memory deallocateParams = _genDeallocation_Full(operator, operatorSet);
operator.modifyAllocations(deallocateParams);
check_DecrAlloc_State_NotSlashable(operator, deallocateParams);
check_FullyDeallocated_State(operator, allocateParams, deallocateParams);
}
function testFuzz_deregister_allocate_waitAllocate_deallocate(uint24 _r) public rand(_r) {
// 1. Deregister from operator set
operator.deregisterFromOperatorSet(operatorSet);
check_Deregistration_State_NoAllocation(operator, operatorSet);
// 2. Before deregistration is complete, allocate to operator set
// The operator should be slashable after the allocation delay
allocateParams = _genAllocation_Rand(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable(operator, allocateParams);
// 3. Wait for allocation delay. Operator remains slashable
_rollForward_AllocationDelay(operator);
// 4. Deallocate operator from operator set
AllocateParams memory deallocateParams = _genDeallocation_Full(operator, operatorSet);
operator.modifyAllocations(deallocateParams);
check_DecrAlloc_State_Slashable(operator, deallocateParams);
// 5. Check the operator is fully deallocated after the deallocation delay
_rollForward_DeallocationDelay();
check_FullyDeallocated_State(operator, allocateParams, deallocateParams);
}
function testFuzz_deregister_allocate_waitDeregister_deallocate(uint24 _r) public rand(_r) {
// 1. Deregister from operator set
operator.deregisterFromOperatorSet(operatorSet);
check_Deregistration_State_NoAllocation(operator, operatorSet);
// 2. Before deregistration is complete, allocate to operator set
// The operator should be slashable after the allocation delay
allocateParams = _genAllocation_Rand(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable(operator, allocateParams);
// 3. Wait for deallocation delay so operator is no longer slashable
_rollForward_DeallocationDelay();
// 4. Instant-deallocate operator from operator set
AllocateParams memory deallocateParams = _genDeallocation_Full(operator, operatorSet);
operator.modifyAllocations(deallocateParams);
check_DecrAlloc_State_NotSlashable(operator, deallocateParams);
check_FullyDeallocated_State(operator, allocateParams, deallocateParams);
}
}
contract Integration_InitAllocated is Integration_ALMBase {
/// @dev Integration test variants that start with the operator being allocated
/// for the operator set
function _init() internal virtual override {
super._init();
// Allocate fully to operator set
allocateParams = _genAllocation_AllAvailable(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_NotSlashable(operator, allocateParams);
}
function testFuzz_register_deallocate_deregister(uint24 _r) public rand(_r) {
// 1. Register for the operator set
operator.registerForOperatorSet(operatorSet);
check_Registration_State_PendingAllocation(operator, allocateParams);
// 2. Roll to the allocation's effect block
_rollForward_AllocationDelay(operator);
// 3. Deallocate fully from the operator set
AllocateParams memory deallocateParams = _genDeallocation_Full(operator, operatorSet);
operator.modifyAllocations(deallocateParams);
check_DecrAlloc_State_Slashable(operator, deallocateParams);
// 4. Deregister from operator set
operator.deregisterFromOperatorSet(operatorSet);
check_Deregistration_State_PendingAllocation(operator, operatorSet);
// 5. Roll forward to the deallocation's effect block and check the operator is fully deallocated
_rollForward_DeallocationDelay();
check_FullyDeallocated_State(operator, allocateParams, deallocateParams);
}
function testFuzz_waitAllocation_register_deallocate(uint24 _r) public rand(_r) {
_rollForward_AllocationDelay(operator);
// 1. Register for the operator set. The allocation immediately becomes slashable
operator.registerForOperatorSet(operatorSet);
check_Registration_State_ActiveAllocation(operator, allocateParams);
// 2. Deallocate fully from the operator set
AllocateParams memory deallocateParams = _genDeallocation_Full(operator, operatorSet);
operator.modifyAllocations(deallocateParams);
check_DecrAlloc_State_Slashable(operator, deallocateParams);
// 3. Roll forward to the deallocation's effect block and check the operator is fully deallocated
_rollForward_DeallocationDelay();
check_FullyDeallocated_State(operator, allocateParams, deallocateParams);
}
}
````
## File: src/test/integration/tests/Delegate_Deposit_Queue_Complete.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
import "src/test/integration/users/User.t.sol";
contract Integration_Delegate_Deposit_Queue_Complete is IntegrationCheckUtils {
function testFuzz_delegate_deposit_queue_completeAsShares(uint24 _random) public rand(_random) {
// Create a staker and an operator with a nonzero balance and corresponding strategies
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
// 1. Delegate to operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, new uint[](strategies.length)); // Initial shares are zero
// 2. Deposit into strategy
staker.depositIntoEigenlayer(strategies, tokenBalances);
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
// Check that the deposit increased operator shares the staker is delegated to
check_Deposit_State(staker, strategies, shares);
assert_Snap_Added_OperatorShares(operator, strategies, shares, "operator should have received shares");
// 3. Queue Withdrawal
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, shares, withdrawableShares, withdrawals, withdrawalRoots);
// 4. Complete Queued Withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_State(staker, operator, withdrawals[i], strategies, shares);
}
}
function testFuzz_delegate_deposit_queue_completeAsTokens(uint24 _random) public rand(_random) {
// Create a staker and an operator with a nonzero balance and corresponding strategies
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
// 1. Delegate to operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, new uint[](strategies.length)); // Initial shares are zero
// 2. Deposit into strategy
staker.depositIntoEigenlayer(strategies, tokenBalances);
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
uint[] memory expectedTokens = _calculateExpectedTokens(strategies, shares);
// Check that the deposit increased operator shares the staker is delegated to
check_Deposit_State(staker, strategies, shares);
assert_Snap_Added_OperatorShares(operator, strategies, shares, "operator should have received shares");
// 3. Queue Withdrawal
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, shares, withdrawableShares, withdrawals, withdrawalRoots);
// 4. Complete Queued Withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], strategies, shares, tokens, expectedTokens);
}
}
}
````
## File: src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Queue_Redeposit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
import "src/test/integration/users/User.t.sol";
import {console} from "forge-std/console.sol";
contract Integration_Deposit_Delegate_Allocate_Slash_Queue_Redeposit is IntegrationCheckUtils {
AVS avs;
OperatorSet operatorSet;
User operator;
AllocateParams allocateParams;
User staker;
IStrategy[] strategies;
IERC20[] tokens;
uint[] initTokenBalances;
uint[] initDepositShares;
uint[] numTokensRemaining;
function _init() internal override {
_configUserTypes(DEFAULT);
(staker, strategies, initTokenBalances) = _newRandomStaker();
(operator,,) = _newRandomOperator();
(avs,) = _newRandomAVS();
tokens = _getUnderlyingTokens(strategies);
uint[] memory tokensToDeposit = new uint[](initTokenBalances.length);
numTokensRemaining = new uint[](initTokenBalances.length);
uint eth_to_deal;
for (uint i = 0; i < initTokenBalances.length; i++) {
if (strategies[i] == BEACONCHAIN_ETH_STRAT) {
tokensToDeposit[i] = initTokenBalances[i];
//user.depositIntoEigenlayer uses all ETH balance for a pod, so we deal back staker's
//starting ETH to replicate partial deposit state
eth_to_deal = initTokenBalances[i];
numTokensRemaining[i] = initTokenBalances[i];
continue;
}
tokensToDeposit[i] = initTokenBalances[i] / 2;
numTokensRemaining[i] = initTokenBalances[i] - tokensToDeposit[i];
}
// 1. Deposit Into Strategies
initDepositShares = _calculateExpectedShares(strategies, tokensToDeposit);
staker.depositIntoEigenlayer(strategies, tokensToDeposit);
//dealing back ETH
cheats.deal(address(staker), eth_to_deal);
check_Deposit_State_PartialDeposit(staker, strategies, initDepositShares, numTokensRemaining);
// 2. Delegate to operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
// Create operator set and register operator
operatorSet = avs.createOperatorSet(strategies);
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, allStrats);
// 3. Allocate to operator set
allocateParams = _genAllocation_AllAvailable(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable(operator, allocateParams);
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);
}
function testFuzz_fullSlash_undelegate_complete_redeposit(uint24 _random) public rand(_random) {
// 4. Fully slash operator
SlashingParams memory slashParams = _genSlashing_Full(operator, operatorSet);
avs.slashOperator(slashParams);
check_FullySlashed_State(operator, allocateParams, slashParams);
// 5. Undelegate from an operator
uint[] memory shares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory roots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, roots, strategies, shares);
// 6. Complete withdrawal. Staker should receive 0 shares/tokens after a full slash
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], strategies, new uint[](strategies.length), tokens, new uint[](strategies.length)
);
}
// 7. Redeposit
uint[] memory depositShares = _calculateExpectedShares(strategies, numTokensRemaining);
staker.depositIntoEigenlayer(strategies, numTokensRemaining);
check_Deposit_State(staker, strategies, depositShares);
}
function testFuzz_undelegate_fullSlash_complete_redeposit(uint24 _random) public rand(_random) {
// 4. Undelegate from an operator
uint[] memory shares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory roots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, roots, strategies, shares);
// 5. Fully slash operator
SlashingParams memory slashParams = _genSlashing_Full(operator, operatorSet);
avs.slashOperator(slashParams);
check_FullySlashed_State(operator, allocateParams, slashParams);
// 6. Complete withdrawal. Staker should receive 0 shares/tokens after a full slash
uint[] memory expectedShares = new uint[](strategies.length);
uint[] memory expectedTokens = new uint[](strategies.length);
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], strategies, expectedShares, tokens, expectedTokens);
}
// 7. Redeposit
uint[] memory depositShares = _calculateExpectedShares(strategies, numTokensRemaining);
staker.depositIntoEigenlayer(strategies, numTokensRemaining);
check_Deposit_State(staker, strategies, depositShares);
}
function testFuzz_depositFull_fullSlash_undelegate_completeAsShares(uint24 _random) public rand(_random) {
uint[] memory depositShares = _calculateExpectedShares(strategies, numTokensRemaining);
staker.depositIntoEigenlayer(strategies, numTokensRemaining);
check_Deposit_State(staker, strategies, depositShares);
// 4. Fully slash random proper subset of operators strategies
SlashingParams memory slashParams = _genSlashing_Full(operator, operatorSet);
avs.slashOperator(slashParams);
check_FullySlashed_State(operator, allocateParams, slashParams);
// add deposit shares to initDepositShares
for (uint i = 0; i < depositShares.length; i++) {
initDepositShares[i] += depositShares[i];
}
// 5. Undelegate from an operator
uint[] memory shares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory roots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, roots, strategies, shares);
// 6. Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedShares = _calculateExpectedShares(withdrawals[i]);
tokens = staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(staker, operator, withdrawals[i], withdrawals[i].strategies, expectedShares);
}
}
function testFuzz_deposit_delegate_allocate_partialSlash_redeposit_queue_complete(uint24 r) public rand(r) {
// Partially slash operator
SlashingParams memory slashParams = _genSlashing_Half(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// Redeposit remaining tokens
uint[] memory depositShares = _calculateExpectedShares(strategies, numTokensRemaining);
staker.depositIntoEigenlayer(strategies, numTokensRemaining);
check_Deposit_State(staker, strategies, depositShares);
for (uint i = 0; i < depositShares.length; i++) {
initDepositShares[i] += depositShares[i];
}
// Queue withdrawal
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, initDepositShares, withdrawableShares, withdrawals, withdrawalRoots);
// Complete withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
uint[] memory expectedShares = _calculateExpectedShares(withdrawals[i]);
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, expectedShares);
tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, expectedShares, tokens, expectedTokens
);
}
}
function testFuzz_deposit_delegate_undelegate_partialSlash_complete(uint24 r) public rand(r) {
// Undelegate from operator
uint[] memory shares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory roots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, roots, strategies, shares);
// Partially slash operator
SlashingParams memory slashParams = _genSlashing_Half(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// Complete withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
uint[] memory expectedShares = _calculateExpectedShares(withdrawals[i]);
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, expectedShares);
tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, expectedShares, tokens, expectedTokens
);
}
}
function testFuzz_deposit_delegate_deallocate_partialSlash_queue_complete(uint24 r) public rand(r) {
// Deallocate from operator set
AllocateParams memory deallocateParams = _genDeallocation_Full(operator, operatorSet);
operator.modifyAllocations(deallocateParams);
check_DecrAlloc_State_Slashable(operator, deallocateParams);
// Partially slash operator
SlashingParams memory slashParams = _genSlashing_Half(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// Queue withdrawal
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, initDepositShares, withdrawableShares, withdrawals, withdrawalRoots);
// Complete withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
uint[] memory expectedShares = _calculateExpectedShares(withdrawals[i]);
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, expectedShares);
tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, expectedShares, tokens, expectedTokens
);
}
}
function testFuzz_deposit_delegate_deregister_partialSlash_queue_complete(uint24 r) public rand(r) {
// Deregister operator from operator set
operator.deregisterFromOperatorSet(operatorSet);
check_Deregistration_State_ActiveAllocation(operator, operatorSet);
// Partially slash operator
SlashingParams memory slashParams = _genSlashing_Half(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// Queue withdrawal
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, initDepositShares, withdrawableShares, withdrawals, withdrawalRoots);
// Complete withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
uint[] memory expectedShares = _calculateExpectedShares(withdrawals[i]);
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, expectedShares);
tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, expectedShares, tokens, expectedTokens
);
}
}
function testFuzz_delegate_zeroShares_partialSlash_deposit_undelegate_complete(uint24 r) public rand(r) {
// Create a new staker with 0 shares
(User zeroSharesStaker, uint[] memory tokensToDeposit) = _newStaker(strategies);
// Delegate to operator (with 0 shares)
zeroSharesStaker.delegateTo(operator);
// Partially slash operator
SlashingParams memory slashParams = _genSlashing_Half(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// Deposit tokens
uint[] memory depositShares = _calculateExpectedShares(strategies, tokensToDeposit);
zeroSharesStaker.depositIntoEigenlayer(strategies, tokensToDeposit);
check_Deposit_State(zeroSharesStaker, strategies, depositShares);
// Undelegate
uint[] memory shares = _getStakerWithdrawableShares(zeroSharesStaker, strategies);
Withdrawal[] memory withdrawals = zeroSharesStaker.undelegate();
bytes32[] memory roots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(zeroSharesStaker, operator, withdrawals, roots, strategies, shares);
// Complete withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
uint[] memory expectedShares = _calculateExpectedShares(withdrawals[i]);
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, expectedShares);
tokens = zeroSharesStaker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
zeroSharesStaker, operator, withdrawals[i], withdrawals[i].strategies, expectedShares, tokens, expectedTokens
);
}
}
function testFuzz_deposit_delegate_allocate_partialSlash_deallocate(uint24 r) public rand(r) {
// Partially slash operator
SlashingParams memory slashParams = _genSlashing_Half(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// Deallocate from operator set
AllocateParams memory deallocateParams = _genDeallocation_Full(operator, operatorSet);
operator.modifyAllocations(deallocateParams);
check_DecrAlloc_State_Slashable(operator, deallocateParams);
}
function testFuzz_fullSlash_undelegate_redeposit_complete(uint24 _random) public rand(_random) {
initDepositShares = _getStakerDepositShares(staker, strategies);
// 4. Fully slash operator
SlashingParams memory slashParams = _genSlashing_Full(operator, operatorSet);
avs.slashOperator(slashParams);
check_FullySlashed_State(operator, allocateParams, slashParams);
// 5. Undelegate from an operator
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, withdrawableShares);
// 6. Redeposit
uint[] memory shares = _calculateExpectedShares(strategies, numTokensRemaining);
staker.depositIntoEigenlayer(strategies, numTokensRemaining);
check_Deposit_State(staker, strategies, shares);
// 7. Complete withdrawal. Staker should receive 0 shares/tokens after a full slash
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedShares = _calculateExpectedShares(withdrawals[i]);
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(staker, operator, withdrawals[i], withdrawals[i].strategies, expectedShares);
}
// Final state checks
assert_HasExpectedShares(staker, strategies, shares, "staker should have expected shares after redeposit");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
function testFuzz_fullSlash_redelegate_redeposit_complete(uint24 _random) public rand(_random) {
(User operator2,,) = _newRandomOperator();
initDepositShares = _getStakerDepositShares(staker, strategies);
// 4. Fully slash operator
SlashingParams memory slashParams = _genSlashing_Full(operator, operatorSet);
avs.slashOperator(slashParams);
check_FullySlashed_State(operator, allocateParams, slashParams);
// 5. Undelegate from an operator
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.redelegate(operator2);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Redelegate_State(staker, operator, operator2, withdrawals, withdrawalRoots, strategies, withdrawableShares);
// 6. Redeposit
uint[] memory shares = _calculateExpectedShares(strategies, numTokensRemaining);
staker.depositIntoEigenlayer(strategies, numTokensRemaining);
check_Deposit_State(staker, strategies, shares);
// 7. Complete withdrawal. Staker should receive 0 shares/tokens after a full slash
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedShares = _calculateExpectedShares(withdrawals[i]);
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(staker, operator, withdrawals[i], withdrawals[i].strategies, expectedShares);
}
// Final state checks
assert_HasExpectedShares(staker, strategies, shares, "staker should have expected shares after redeposit");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
}
````
## File: src/test/integration/tests/Deposit_Delegate_Queue_Complete.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
import "src/test/integration/users/User.t.sol";
contract Integration_Deposit_Delegate_Queue_Complete is IntegrationCheckUtils {
/**
*
* FULL WITHDRAWALS
*
*/
// TODO: fix test
/// Generates a random staker and operator. The staker:
/// 1. deposits all assets into strategies
/// 2. delegates to an operator
/// 3. queues a withdrawal for a ALL shares
/// 4. completes the queued withdrawal as tokens
function testFuzz_deposit_delegate_queue_completeAsTokens(uint24 _random) public rand(_random) {
/// 0. Create an operator and a staker with:
// - some nonzero underlying token balances
// - corresponding to a random subset of valid strategies (StrategyManager and/or EigenPodManager)
//
// ... check that the staker has no delegatable shares and isn't currently delegated
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
// 3. Queue Withdrawals
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, shares, withdrawableShares, withdrawals, withdrawalRoots);
// 4. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].scaledShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], strategies, shares, tokens, expectedTokens);
}
// Check final state:
assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator");
assert_HasNoDelegatableShares(staker, "staker should have withdrawn all shares");
assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "staker should once again have original token balances");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
/// Generates a random staker and operator. The staker:
/// 1. deposits all assets into strategies
/// 2. delegates to an operator
/// 3. queues a withdrawal for a ALL shares
/// 4. completes the queued withdrawal as shares
function testFuzz_deposit_delegate_queue_completeAsShares(uint24 _random) public rand(_random) {
/// 0. Create an operator and a staker with:
// - some nonzero underlying token balances
// - corresponding to a random subset of valid strategies (StrategyManager and/or EigenPodManager)
//
// ... check that the staker has no delegatable shares and isn't currently delegated
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
// 3. Queue Withdrawals
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, shares, withdrawableShares, withdrawals, withdrawalRoots);
// 4. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_State(staker, operator, withdrawals[i], strategies, shares);
}
// Check final state:
assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator");
assert_HasExpectedShares(staker, strategies, shares, "staker should have all original shares");
assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker not have any underlying tokens");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
/**
*
* RANDOM WITHDRAWALS
*
*/
/// Generates a random staker and operator. The staker:
/// 1. deposits all assets into strategies
/// 2. delegates to an operator
/// 3. queues a withdrawal for a random subset of shares
/// 4. completes the queued withdrawal as tokens
function testFuzz_deposit_delegate_queueRand_completeAsTokens(uint24 _random) public rand(_random) {
/// 0. Create an operator and a staker with:
// - some nonzero underlying token balances
// - corresponding to a random subset of valid strategies (StrategyManager and/or EigenPodManager)
//
// ... check that the staker has no delegatable shares and isn't currently delegated
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
// 3. Queue Withdrawals
// Randomly select one or more assets to withdraw
(IStrategy[] memory withdrawStrats, uint[] memory withdrawShares) = _randWithdrawal(strategies, shares);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(withdrawStrats, withdrawShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, withdrawStrats, withdrawShares, withdrawShares, withdrawals, withdrawalRoots);
// 4. Complete withdrawals
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].scaledShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], withdrawStrats, withdrawShares, tokens, expectedTokens);
}
// Check final state:
assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
/// Generates a random staker and operator. The staker:
/// 1. deposits all assets into strategies
/// 2. delegates to an operator
/// 3. queues a withdrawal for a random subset of shares
/// 4. completes the queued withdrawal as shares
function testFuzz_deposit_delegate_queueRand_completeAsShares(uint24 _random) public rand(_random) {
/// 0. Create an operator and a staker with:
// - some nonzero underlying token balances
// - corresponding to a random subset of valid strategies (StrategyManager and/or EigenPodManager)
//
// ... check that the staker has no delegatable shares and isn't currently delegated
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
// 3. Queue Withdrawals
// Randomly select one or more assets to withdraw
(IStrategy[] memory withdrawStrats, uint[] memory withdrawShares) = _randWithdrawal(strategies, shares);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(withdrawStrats, withdrawShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, withdrawStrats, withdrawShares, withdrawShares, withdrawals, withdrawalRoots);
// 4. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_State(staker, operator, withdrawals[i], withdrawStrats, withdrawShares);
}
// Check final state:
assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator");
assert_HasExpectedShares(staker, strategies, shares, "staker should have all original shares");
assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker not have any underlying tokens");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
/**
*
* UNHAPPY PATH TESTS
*
*/
/// Generates a random staker and operator. The staker:
/// 1. deposits all assets into strategies
/// --- registers as an operator
/// 2. delegates to an operator
///
/// ... we check that the final step fails
function testFuzz_deposit_delegate_revert_alreadyDelegated(uint24 _random) public rand(_random) {
_configAssetTypes(NO_ASSETS | HOLDS_LST | HOLDS_ETH | HOLDS_ALL);
/// 0. Create a staker and operator
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Register staker as an operator
staker.registerAsOperator();
assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated");
// 3. Attempt to delegate to an operator
// This should fail as the staker is already delegated to themselves.
cheats.expectRevert();
staker.delegateTo(operator);
}
}
````
## File: src/test/integration/tests/Deposit_Delegate_Redelegate_Complete.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/users/User.t.sol";
import "src/test/integration/IntegrationChecks.t.sol";
contract Integration_Deposit_Delegate_Redelegate_Complete is IntegrationCheckUtils {
using ArrayLib for *;
/// Randomly generates a user with different held assets. Then:
/// 1. deposit into strategy
/// 2. delegate to an operator
/// 3. undelegates from the operator
/// 4. complete queued withdrawal as shares
/// 5. delegate to a new operator
/// 5. queueWithdrawal
/// 7. complete their queued withdrawal as tokens
function testFuzz_deposit_delegate_reDelegate_completeAsTokens(uint24 _random) public rand(_random) {
/// 0. Create an operator and a staker with:
// - some nonzero underlying token balances
// - corresponding to a random number of strategies
//
// ... check that the staker has no deleagatable shares and isn't delegated
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator1,,) = _newRandomOperator();
(User operator2,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
//delegatable shares equals deposit shares here because no bc slashing
uint[] memory delegatableShares = shares;
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
/// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator1);
check_Delegation_State(staker, operator1, strategies, shares);
// 3. Undelegate from an operator
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator1, withdrawals, withdrawalRoots, strategies, delegatableShares);
// 4. Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(
staker, operator1, withdrawals[i], withdrawals[i].strategies, withdrawals[i].scaledShares
);
}
// 5. Delegate to a new operator
staker.delegateTo(operator2);
check_Delegation_State(staker, operator2, strategies, shares);
assertNotEq(address(operator1), delegationManager.delegatedTo(address(staker)), "staker should not be delegated to operator1");
// 6. Queue Withdrawal
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
withdrawals = staker.queueWithdrawals(strategies, shares);
withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator2, strategies, shares, withdrawableShares, withdrawals, withdrawalRoots);
// 7. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
// Complete withdrawals
for (uint i = 0; i < withdrawals.length; i++) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].scaledShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator2, withdrawals[i], withdrawals[i].strategies, withdrawals[i].scaledShares, tokens, expectedTokens
);
}
}
function testFuzz_deposit_delegate_reDelegate_completeAsShares(uint24 _random) public rand(_random) {
/// 0. Create an operator and a staker with:
// - some nonzero underlying token balances
// - corresponding to a random number of strategies
//
// ... check that the staker has no deleagatable shares and isn't delegated
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator1,,) = _newRandomOperator();
(User operator2,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
//delegatable shares equals deposit shares here because no bc slashing
uint[] memory delegatableShares = shares;
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
/// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator1);
check_Delegation_State(staker, operator1, strategies, shares);
// 3. Undelegate from an operator
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator1, withdrawals, withdrawalRoots, strategies, delegatableShares);
// 4. Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(
staker, operator1, withdrawals[i], withdrawals[i].strategies, withdrawals[i].scaledShares
);
}
// 5. Delegate to a new operator
staker.delegateTo(operator2);
check_Delegation_State(staker, operator2, strategies, shares);
assertNotEq(address(operator1), delegationManager.delegatedTo(address(staker)), "staker should not be delegated to operator1");
// 6. Queue Withdrawal
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
withdrawals = staker.queueWithdrawals(strategies, shares);
withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator2, strategies, shares, withdrawableShares, withdrawals, withdrawalRoots);
// 7. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
// Complete all but last withdrawal as tokens
for (uint i = 0; i < withdrawals.length - 1; i++) {
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
uint[] memory expectedTokens = _calculateExpectedTokens(strategies, shares);
check_Withdrawal_AsTokens_State(staker, staker, withdrawals[i], strategies, shares, tokens, expectedTokens);
}
// Complete last withdrawal as shares
IERC20[] memory finalWithdrawaltokens = staker.completeWithdrawalAsTokens(withdrawals[withdrawals.length - 1]);
uint[] memory finalExpectedTokens = _calculateExpectedTokens(strategies, shares);
check_Withdrawal_AsTokens_State(
staker, operator2, withdrawals[withdrawals.length - 1], strategies, shares, finalWithdrawaltokens, finalExpectedTokens
);
}
function testFuzz_deposit_delegate_reDelegate_depositAfterRedelegate(uint24 _random) public rand(_random) {
_configAssetTypes(HOLDS_LST); // not holding ETH since we can only deposit 32 ETH multiples
/// 0. Create an operator and a staker with:
// - some nonzero underlying token balances
// - corresponding to a random number of strategies
//
// ... check that the staker has no deleagatable shares and isn't delegated
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator1,,) = _newRandomOperator();
(User operator2,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
{
// Divide shares by 2 in new array to do deposits after redelegate
uint[] memory numTokensToDeposit = new uint[](tokenBalances.length);
uint[] memory numTokensRemaining = new uint[](tokenBalances.length);
for (uint i = 0; i < shares.length; i++) {
numTokensToDeposit[i] = tokenBalances[i] / 2;
numTokensRemaining[i] = tokenBalances[i] - numTokensToDeposit[i];
}
uint[] memory halfShares = _calculateExpectedShares(strategies, numTokensToDeposit);
//delegatable shares equals deposit shares here because no bc slashing
uint[] memory delegatableShares = halfShares;
/// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, numTokensToDeposit);
check_Deposit_State_PartialDeposit(staker, strategies, halfShares, numTokensRemaining);
// 2. Delegate to an operator
staker.delegateTo(operator1);
check_Delegation_State(staker, operator1, strategies, halfShares);
// 3. Undelegate from an operator
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator1, withdrawals, withdrawalRoots, strategies, delegatableShares);
// 4. Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(
staker, operator1, withdrawals[i], withdrawals[i].strategies, withdrawals[i].scaledShares
);
}
// 5. Delegate to a new operator
staker.delegateTo(operator2);
check_Delegation_State(staker, operator2, strategies, halfShares);
assertNotEq(address(operator1), delegationManager.delegatedTo(address(staker)), "staker should not be delegated to operator1");
// 6. Deposit into Strategies
uint[] memory sharesAdded = _calculateExpectedShares(strategies, numTokensRemaining);
staker.depositIntoEigenlayer(strategies, numTokensRemaining);
tokenBalances = _calculateExpectedTokens(strategies, shares);
check_Deposit_State(staker, strategies, sharesAdded);
}
{
// 7. Queue Withdrawal
shares = _calculateExpectedShares(strategies, tokenBalances);
Withdrawal[] memory newWithdrawals = staker.queueWithdrawals(strategies, shares);
bytes32[] memory newWithdrawalRoots = _getWithdrawalHashes(newWithdrawals);
check_QueuedWithdrawal_State(staker, operator2, strategies, shares, shares, newWithdrawals, newWithdrawalRoots);
// 8. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(newWithdrawals);
// Complete withdrawals
for (uint i = 0; i < newWithdrawals.length; i++) {
uint[] memory expectedTokens = _calculateExpectedTokens(newWithdrawals[i].strategies, newWithdrawals[i].scaledShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(newWithdrawals[i]);
check_Withdrawal_AsTokens_State(staker, operator2, newWithdrawals[i], strategies, shares, tokens, expectedTokens);
}
}
}
function testFuzz_deposit_delegate_reDelegate_depositBeforeRedelegate(uint24 _random) public rand(_random) {
_configAssetTypes(HOLDS_LST); // not holding ETH since we can only deposit 32 ETH multiples
/// 0. Create an operator and a staker with:
// - some nonzero underlying token balances
// - corresponding to a random number of strategies
//
// ... check that the staker has no deleagatable shares and isn't delegated
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator1,,) = _newRandomOperator();
(User operator2,,) = _newRandomOperator();
uint[] memory totalShares = new uint[](strategies.length);
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
{
// Divide shares by 2 in new array to do deposits after redelegate
uint[] memory numTokensToDeposit = new uint[](tokenBalances.length);
uint[] memory numTokensRemaining = new uint[](tokenBalances.length);
for (uint i = 0; i < strategies.length; i++) {
numTokensToDeposit[i] = tokenBalances[i] / 2;
numTokensRemaining[i] = tokenBalances[i] - numTokensToDeposit[i];
}
{
uint[] memory sharesFromFirstDeposit = _calculateExpectedShares(strategies, numTokensToDeposit);
//delegatable shares equals deposit shares here because no bc slashing
uint[] memory delegatableShares = sharesFromFirstDeposit;
/// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, numTokensToDeposit);
check_Deposit_State_PartialDeposit(staker, strategies, sharesFromFirstDeposit, numTokensRemaining);
// 2. Delegate to an operator
staker.delegateTo(operator1);
check_Delegation_State(staker, operator1, strategies, sharesFromFirstDeposit);
// 3. Undelegate from an operator
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator1, withdrawals, withdrawalRoots, strategies, delegatableShares);
// 4. Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(
staker, operator1, withdrawals[i], withdrawals[i].strategies, withdrawals[i].scaledShares
);
}
// 5. Deposit into Strategies
uint[] memory sharesFromSecondDeposit = _calculateExpectedShares(strategies, numTokensRemaining);
for (uint i = 0; i < strategies.length; i++) {
totalShares[i] = sharesFromFirstDeposit[i] + sharesFromSecondDeposit[i];
}
staker.depositIntoEigenlayer(strategies, numTokensRemaining);
tokenBalances = _calculateExpectedTokens(strategies, totalShares);
check_Deposit_State(staker, strategies, sharesFromSecondDeposit);
}
// 6. Delegate to a new operator
staker.delegateTo(operator2);
check_Delegation_State(staker, operator2, strategies, totalShares);
assertNotEq(address(operator1), delegationManager.delegatedTo(address(staker)), "staker should not be delegated to operator1");
}
{
// 7. Queue Withdrawal
totalShares = _calculateExpectedShares(strategies, tokenBalances);
Withdrawal[] memory newWithdrawals = staker.queueWithdrawals(strategies, totalShares);
bytes32[] memory newWithdrawalRoots = _getWithdrawalHashes(newWithdrawals);
check_QueuedWithdrawal_State(staker, operator2, strategies, totalShares, totalShares, newWithdrawals, newWithdrawalRoots);
// 8. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(newWithdrawals);
// Complete withdrawals
for (uint i = 0; i < newWithdrawals.length; i++) {
uint[] memory expectedTokens = _calculateExpectedTokens(newWithdrawals[i].strategies, newWithdrawals[i].scaledShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(newWithdrawals[i]);
check_Withdrawal_AsTokens_State(staker, operator2, newWithdrawals[i], strategies, totalShares, tokens, expectedTokens);
}
}
}
function testFuzz_deposit_delegate_undelegate_withdrawAsTokens_reDelegate_completeAsTokens(uint24 _random) public rand(_random) {
/// 0. Create operators and a staker
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator1,,) = _newRandomOperator();
(User operator2,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
//delegatable shares equals deposit shares here because no bc slashing
uint[] memory delegatableShares = shares;
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
/// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
uint[] memory withdrawnTokenBalances = _calculateExpectedTokens(strategies, shares);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator1);
check_Delegation_State(staker, operator1, strategies, shares);
// 3. Undelegate from an operator
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator1, withdrawals, withdrawalRoots, strategies, delegatableShares);
// 4. Complete withdrawal as tokens
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].scaledShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator1, withdrawals[i], withdrawals[i].strategies, withdrawals[i].scaledShares, tokens, expectedTokens
);
}
//5. Deposit into Strategies
staker.depositIntoEigenlayer(strategies, withdrawnTokenBalances);
shares = _calculateExpectedShares(strategies, withdrawnTokenBalances);
check_Deposit_State(staker, strategies, shares);
// 6. Delegate to a new operator
staker.delegateTo(operator2);
check_Delegation_State(staker, operator2, strategies, shares);
assertNotEq(address(operator1), delegationManager.delegatedTo(address(staker)), "staker should not be delegated to operator1");
{
// 7. Queue Withdrawal
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
withdrawals = staker.queueWithdrawals(strategies, shares);
withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator2, strategies, shares, withdrawableShares, withdrawals, withdrawalRoots);
}
// 8. Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
// Complete withdrawals as tokens
for (uint i = 0; i < withdrawals.length; i++) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].scaledShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(staker, operator2, withdrawals[i], strategies, shares, tokens, expectedTokens);
}
}
function testFuzz_deposit_delegate_undelegate_withdrawAsTokens_reDelegate_completeAsShares(uint24 _random) public rand(_random) {
/// 0. Create operators and a staker
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator1,,) = _newRandomOperator();
(User operator2,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
//delegatable shares equals deposit shares here because no bc slashing
uint[] memory delegatableShares = shares;
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
/// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
uint[] memory withdrawnTokenBalances = _calculateExpectedTokens(strategies, shares);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator1);
check_Delegation_State(staker, operator1, strategies, shares);
// 3. Undelegate from an operator
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator1, withdrawals, withdrawalRoots, strategies, delegatableShares);
// 4. Complete withdrawal as Tokens
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].scaledShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator1, withdrawals[i], withdrawals[i].strategies, withdrawals[i].scaledShares, tokens, expectedTokens
);
}
// 5. Deposit into Strategies
shares = _calculateExpectedShares(strategies, withdrawnTokenBalances);
staker.depositIntoEigenlayer(strategies, withdrawnTokenBalances);
check_Deposit_State(staker, strategies, shares);
// 6. Delegate to a new operator
staker.delegateTo(operator2);
check_Delegation_State(staker, operator2, strategies, shares);
assertNotEq(address(operator1), delegationManager.delegatedTo(address(staker)), "staker should not be delegated to operator1");
// 7. Queue Withdrawal
shares = _calculateExpectedShares(strategies, withdrawnTokenBalances);
withdrawals = staker.queueWithdrawals(strategies, shares);
withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator2, strategies, shares, shares, withdrawals, withdrawalRoots);
// 8. Complete withdrawal as shares
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
// Complete withdrawals as shares
for (uint i = 0; i < withdrawals.length; i++) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_State(staker, operator2, withdrawals[i], strategies, shares);
}
}
}
````
## File: src/test/integration/tests/Deposit_Delegate_Undelegate_Complete.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/users/User.t.sol";
import "src/test/integration/IntegrationChecks.t.sol";
contract Integration_Deposit_Delegate_Undelegate_Complete is IntegrationCheckUtils {
/// Randomly generates a user with different held assets. Then:
/// 1. deposit into strategy
/// 2. delegate to an operator
/// 3. undelegates from the operator
/// 4. complete their queued withdrawal as tokens
function testFuzz_deposit_undelegate_completeAsTokens(uint24 _random) public rand(_random) {
/// 0. Create an operator and a staker with:
// - some nonzero underlying token balances
// - corresponding to a random number of strategies
//
// ... check that the staker has no deleagatable shares and isn't delegated
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
//delegatable shares equals deposit shares here because no bc slashing
uint[] memory delegatableShares = shares;
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
/// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
// 3. Undelegate from an operator
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, delegatableShares);
// 4. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
// Complete withdrawal
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].scaledShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, withdrawals[i].scaledShares, tokens, expectedTokens
);
}
// Check Final State
assert_HasNoDelegatableShares(staker, "staker should have withdrawn all shares");
assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "staker should once again have original token balances");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
/// Randomly generates a user with different held assets. Then:
/// 1. deposit into strategy
/// 2. delegate to an operator
/// 3. undelegates from the operator
/// 4. complete their queued withdrawal as shares
function testFuzz_deposit_undelegate_completeAsShares(uint24 _random) public rand(_random) {
/// 0. Create an operator and a staker with:
// - some nonzero underlying token balances
// - corresponding to a random number of strategies
//
// ... check that the staker has no deleagatable shares and isn't delegated
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
//delegatable shares equals deposit shares here because no bc slashing
uint[] memory delegatableShares = shares;
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
/// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
// 3. Undelegate from an operator
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, delegatableShares);
// 4. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, withdrawals[i].scaledShares
);
}
// Check final state:
assert_HasExpectedShares(staker, strategies, shares, "staker should have all original shares");
assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker not have any underlying tokens");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
function testFuzz_deposit_delegate_forceUndelegate_completeAsTokens(uint24 _random) public rand(_random) {
/// 0. Create an operator and a staker with:
// - some nonzero underlying token balances
// - corresponding to a random number of strategies
//
// ... check that the staker has no deleagatable shares and isn't delegated
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
//delegatable shares equals deposit shares here because no bc slashing
uint[] memory delegatableShares = shares;
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
/// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
// 3. Force undelegate
Withdrawal[] memory withdrawals = operator.forceUndelegate(staker);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, delegatableShares);
// 4. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].scaledShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, withdrawals[i].scaledShares, tokens, expectedTokens
);
}
// Check Final State
assert_HasNoDelegatableShares(staker, "staker should have withdrawn all shares");
assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "staker should once again have original token balances");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
function testFuzz_deposit_delegate_forceUndelegate_completeAsShares(uint24 _random) public rand(_random) {
/// 0. Create an operator and a staker with:
// - some nonzero underlying token balances
// - corresponding to a random number of strategies
//
// ... check that the staker has no deleagatable shares and isn't delegated
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
//delegatable shares equals deposit shares here because no bc slashing
uint[] memory delegatableShares = shares;
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
/// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
// 3. Force undelegate
Withdrawal[] memory withdrawals = operator.forceUndelegate(staker);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, delegatableShares);
// 4. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_Undelegated_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, withdrawals[i].scaledShares
);
}
// Check final state:
assert_HasExpectedShares(staker, strategies, shares, "staker should have all original shares");
assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker not have any underlying tokens");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
function testFuzz_deposit_delegate_undelegate_completeAsTokens_Max_Strategies(uint24 _random) public rand(_random) {
_configAssetTypes(HOLDS_MAX);
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
if (forkType == LOCAL) assertEq(strategies.length, 33, "sanity");
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
//delegatable shares equals deposit shares here because no bc slashing
uint[] memory delegatableShares = shares;
assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
/// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
// 3. Undelegate from an operator
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, delegatableShares);
// 4. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
// Complete withdrawal
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].scaledShares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, withdrawals[i].scaledShares, tokens, expectedTokens
);
}
// Check Final State
assert_HasNoDelegatableShares(staker, "staker should have withdrawn all shares");
assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "staker should once again have original token tokenBalances");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
}
````
## File: src/test/integration/tests/Deposit_Delegate_UpdateBalance.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
import "src/test/integration/users/User.t.sol";
contract Integration_Deposit_Delegate_UpdateBalance is IntegrationCheckUtils {
// TODO: fix for slashing
/// Generates a random stake and operator. The staker:
/// 1. deposits all assets into strategies
/// 2. delegates to an operator
/// 3. queues a withdrawal for a ALL shares
/// 4. updates their balance randomly
/// 5. completes the queued withdrawal as tokens
// function testFuzz_deposit_delegate_updateBalance_completeAsTokens(uint24 _random) public {
// _configRand({
// _randomSeed: _random,
// _assetTypes: HOLDS_ETH, // HOLDS_LST | HOLDS_ETH | HOLDS_ALL,
// _userTypes: DEFAULT //| ALT_METHODS
// });
// /// 0. Create an operator and staker with some underlying assets
// (
// User staker,
// IStrategy[] memory strategies,
// uint[] memory tokenBalances
// ) = _newRandomStaker();
// (User operator, ,) = _newRandomOperator();
// // Upgrade contracts if forkType is not local
// _upgradeEigenLayerContracts();
// uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
// assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing");
// assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
// /// 1. Deposit into strategies
// staker.depositIntoEigenlayer(strategies, tokenBalances);
// check_Deposit_State(staker, strategies, shares);
// /// 2. Delegate to an operator
// staker.delegateTo(operator);
// check_Delegation_State(staker, operator, strategies, shares);
// /// 3. Queue withdrawals for ALL shares
// Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
// bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
// check_QueuedWithdrawal_State(staker, operator, strategies, shares, withdrawals, withdrawalRoots);
// // Generate a random balance update:
// // - For LSTs, the tokenDelta is positive tokens minted to the staker
// // - For ETH, the tokenDelta is a positive or negative change in beacon chain balance
// (
// int[] memory tokenDeltas,
// int[] memory stakerShareDeltas,
// int[] memory operatorShareDeltas
// ) = _randBalanceUpdate(staker, strategies);
// // 4. Update LST balance by depositing, and beacon balance by submitting a proof
// staker.updateBalances(strategies, tokenDeltas);
// assert_Snap_Delta_StakerShares(staker, strategies, stakerShareDeltas, "staker should have applied deltas correctly");
// assert_Snap_Delta_OperatorShares(operator, strategies, operatorShareDeltas, "operator should have applied deltas correctly");
// console.log("withdrawble: ", staker.pod().withdrawableRestakedExecutionLayerGwei());
// // Fast forward to when we can complete the withdrawal
// _rollBlocksForCompleteWithdrawals(withdrawals);
// // 5. Complete queued withdrawals as tokens
// staker.completeWithdrawalsAsTokens(withdrawals);
// assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator");
// assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
// assert_Snap_Unchanged_TokenBalances(operator, "operator token balances should not have changed");
// assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed");
// }
}
````
## File: src/test/integration/tests/Deposit_Queue_Complete.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/users/User.t.sol";
import "src/test/integration/IntegrationChecks.t.sol";
contract Integration_Deposit_QueueWithdrawal_Complete is IntegrationCheckUtils {
/// Randomly generates a user with different held assets. Then:
/// 1. deposit into strategy
/// 2. queueWithdrawal
/// 3. completeQueuedWithdrawal"
function testFuzz_deposit_queueWithdrawal_completeAsTokens(uint24 _random) public rand(_random) {
// Create a staker with a nonzero balance and corresponding strategies
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
// 1. Deposit into strategy
staker.depositIntoEigenlayer(strategies, tokenBalances);
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// Ensure staker is not delegated to anyone post deposit
assertFalse(delegationManager.isDelegated(address(staker)), "Staker should not be delegated after deposit");
// 2. Queue Withdrawal
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
// 3. Complete Queued Withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
uint[] memory expectedTokens = _calculateExpectedTokens(strategies, shares);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(staker, User(payable(0)), withdrawals[i], strategies, shares, tokens, expectedTokens);
}
// Ensure staker is still not delegated to anyone post withdrawal completion
assertFalse(delegationManager.isDelegated(address(staker)), "Staker should still not be delegated after withdrawal");
}
function testFuzz_deposit_queueWithdrawal_completeAsShares(uint24 _random) public rand(_random) {
// Create a staker with a nonzero balance and corresponding strategies
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
// 1. Deposit into strategy
staker.depositIntoEigenlayer(strategies, tokenBalances);
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// Ensure staker is not delegated to anyone post deposit
assertFalse(delegationManager.isDelegated(address(staker)), "Staker should not be delegated after deposit");
// 2. Queue Withdrawal
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
// 3. Complete Queued Withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_State(staker, User(payable(0)), withdrawals[i], strategies, shares);
}
// Ensure staker is still not delegated to anyone post withdrawal completion
assertFalse(delegationManager.isDelegated(address(staker)), "Staker should still not be delegated after withdrawal");
}
}
````
## File: src/test/integration/tests/Deposit_Register_QueueWithdrawal_Complete.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/users/User.t.sol";
import "src/test/integration/IntegrationChecks.t.sol";
contract Integration_Deposit_Register_QueueWithdrawal_Complete is IntegrationCheckUtils {
function testFuzz_deposit_registerOperator_queueWithdrawal_completeAsShares(uint24 _random) public rand(_random) {
// Create a staker with a nonzero balance and corresponding strategies
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
// 1. Staker deposits into strategy
staker.depositIntoEigenlayer(strategies, tokenBalances);
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Staker registers as an operator
staker.registerAsOperator();
assertTrue(delegationManager.isOperator(address(staker)), "Staker should be registered as an operator");
// 3. Queue Withdrawal
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, staker, strategies, shares, withdrawableShares, withdrawals, withdrawalRoots);
// 4. Complete Queued Withdrawal as Shares
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_State(staker, staker, withdrawals[i], strategies, shares);
}
}
function testFuzz_deposit_registerOperator_queueWithdrawal_completeAsTokens(uint24 _random) public rand(_random) {
// Create a staker with a nonzero balance and corresponding strategies
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
// 1. Staker deposits into strategy
staker.depositIntoEigenlayer(strategies, tokenBalances);
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Staker registers as an operator
staker.registerAsOperator();
assertTrue(delegationManager.isOperator(address(staker)), "Staker should be registered as an operator");
// 3. Queue Withdrawal
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, staker, strategies, shares, withdrawableShares, withdrawals, withdrawalRoots);
// 4. Complete Queued Withdrawal as Tokens
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
uint[] memory expectedTokens = _calculateExpectedTokens(strategies, shares);
check_Withdrawal_AsTokens_State(staker, staker, withdrawals[i], strategies, shares, tokens, expectedTokens);
}
}
}
````
## File: src/test/integration/tests/DualSlashing.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
/// @notice Tests where we slash native eth on the Beacon Chain and by an OperatorSet
contract Integration_DualSlashing_Base is IntegrationCheckUtils {
using ArrayLib for *;
AVS avs;
OperatorSet operatorSet;
User operator;
AllocateParams allocateParams;
User staker;
uint64 beaconBalanceGwei;
uint40[] validators;
IStrategy[] strategies;
uint[] initTokenBalances;
uint[] initDepositShares;
function _init() internal virtual override {
_configAssetTypes(HOLDS_ETH);
// Create staker, operator, and avs
(staker, strategies, initTokenBalances) = _newRandomStaker();
(operator,,) = _newRandomOperator();
(avs,) = _newRandomAVS();
// 1. Deposit into strategies
(validators, beaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(validators);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, initDepositShares);
// 2. Delegate staker to operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
// 3. Create an operator set containing the strategies held by the staker
operatorSet = avs.createOperatorSet(strategies);
// 4. Register operator to AVS
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, allStrats);
// 5. Allocate to the operator set
allocateParams = _genAllocation_AllAvailable(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable(operator, allocateParams);
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);
}
}
contract Integration_DualSlashing_BeaconChainFirst is Integration_DualSlashing_Base {
function testFuzz_bcSlash_checkpoint_avsSlash(uint24 _random) public rand(_random) {
// 6. Slash staker on BC
uint64 slashedAmountGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
// 7. Checkpoint
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedAmountGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(staker, validators, slashedAmountGwei);
// 8. Slash operator by AVS
SlashingParams memory slashingParams;
{
slashingParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashingParams);
assert_Snap_Allocations_Slashed(slashingParams, operatorSet, true, "operator allocations should be slashed");
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker deposit shares should be unchanged after slashing");
assert_Snap_StakerWithdrawableShares_AfterBCSlash_AVSSlash(
staker, allocateParams, slashingParams, "staker withdrawable shares should be slashed"
);
}
}
}
contract Integration_DualSlashing_AVSFirst is Integration_DualSlashing_Base {
using ArrayLib for *;
SlashingParams slashingParams;
function _init() internal virtual override {
super._init();
// 6. Slash operator by AVS
slashingParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashingParams);
check_Base_Slashing_State(operator, allocateParams, slashingParams);
assert_Snap_Allocations_Slashed(slashingParams, operatorSet, true, "operator allocations should be slashed");
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker deposit shares should be unchanged after slashing");
assert_Snap_StakerWithdrawableShares_AfterSlash(
staker, allocateParams, slashingParams, "staker withdrawable shares should be slashed"
);
}
/// @dev Validates behavior of "restaking", ie. that the funds can be slashed twice
function testFuzz_avsSlash_bcSlash_checkpoint(uint24 _random) public rand(_random) {
// 7. Slash Staker on BC
uint64 slashedAmountGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
// 8. Checkpoint
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedAmountGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_AfterAVSSlash_BCSlash(
staker, validators, initDepositShares[0], slashedAmountGwei, allocateParams, slashingParams
);
}
/// @notice Because the validator is proven prior to the BC slash, the system applies the new balance
/// to the BC and AVS slash combined
function testFuzz_avsSlash_verifyValidator_bcSlash_checkpoint(uint24 _random) public rand(_random) {
// 7. Verify Validator
cheats.deal(address(staker), 32 ether);
(uint40[] memory newValidators, uint64 addedBeaconBalanceGwei,) = staker.startValidators();
uint beaconSharesAdded = uint(addedBeaconBalanceGwei * GWEI_TO_WEI);
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, addedBeaconBalanceGwei);
assert_Snap_Added_Staker_WithdrawableShares_AtLeast(
staker,
BEACONCHAIN_ETH_STRAT.toArray(),
beaconSharesAdded.toArrayU256(),
"staker withdrawable shares should increase by the added beacon balance"
);
// 8. Slash first validators on BC
uint64 slashedAmountGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
// 9. Checkpoint
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedAmountGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_AfterAVSSlash_ValidatorProven_BCSlash(
staker, validators, initDepositShares[0], beaconSharesAdded, allocateParams, slashingParams
);
}
/// @dev Same as above, but validator is proven after BC slash (this ordering doesn't matter to EL)
function testFuzz_avsSlash_bcSlash_verifyValidator_checkpoint(uint24 _random) public rand(_random) {
// 7. Slash Staker on BC
uint64 slashedAmountGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
// 8. Verify Validator
cheats.deal(address(staker), 32 ether);
(uint40[] memory newValidators, uint64 addedBeaconBalanceGwei,) = staker.startValidators();
uint beaconSharesAdded = uint(addedBeaconBalanceGwei * GWEI_TO_WEI);
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, addedBeaconBalanceGwei);
assert_Snap_Added_Staker_WithdrawableShares_AtLeast(
staker,
BEACONCHAIN_ETH_STRAT.toArray(),
beaconSharesAdded.toArrayU256(),
"staker withdrawable shares should increase by the added beacon balance"
);
// 9. Checkpoint
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedAmountGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_AfterAVSSlash_ValidatorProven_BCSlash(
staker, validators, initDepositShares[0], beaconSharesAdded, allocateParams, slashingParams
);
}
/// @notice The validator proven should not be affected by the BC or AVS slashes
function testFuzz_avsSlash_bcSlash_checkpoint_verifyValidator(uint24 _rand) public rand(_rand) {
// 7. Slash Staker on BC
uint64 slashedAmountGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
// 8. Checkpoint
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedAmountGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_AfterAVSSlash_BCSlash(
staker, validators, initDepositShares[0], slashedAmountGwei, allocateParams, slashingParams
);
// 9. Verify Validator
cheats.deal(address(staker), 32 ether);
(uint40[] memory newValidators, uint64 addedBeaconBalanceGwei,) = staker.startValidators();
uint beaconSharesAdded = uint(addedBeaconBalanceGwei * GWEI_TO_WEI);
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, addedBeaconBalanceGwei);
assert_Snap_Added_Staker_WithdrawableShares_AtLeast(
staker,
BEACONCHAIN_ETH_STRAT.toArray(),
beaconSharesAdded.toArrayU256(),
"staker withdrawable shares should increase by the added beacon balance"
);
}
/// @notice The balance increase results in the pods not processing the beacon slash as a slash, given
/// that the checkpoint had a positive delta
function testFuzz_avsSlash_bcSlash_balanceIncrease_checkpoint(uint24 _rand) public rand(_rand) {
// 7. Slash Staker on BC
uint64 slashedAmountGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
// 8. Send 32 ETH to pod, some random amount of ETH, greater than the amount slashed
uint ethToDeposit = 32 ether;
cheats.deal(address(staker), ethToDeposit);
cheats.prank(address(staker));
(bool success,) = address(staker.pod()).call{value: ethToDeposit}("");
require(success, "pod call failed");
uint64 ethDepositedGwei = uint64(ethToDeposit / GWEI_TO_WEI);
// 9. Checkpoint
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedAmountGwei + ethDepositedGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_AfterAVSSlash_ETHDeposit_BCSlash(staker, validators, slashedAmountGwei, ethDepositedGwei);
}
/// @notice The balance increase occurs after the slashings are processed, so it should be unaffected by the slashings
function testFuzz_avsSlash_bcSlash_checkpoint_balanceIncrease(uint24 _rand) public rand(_rand) {
// 7. Slash Staker on BC
uint64 slashedAmountGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch_NoRewards();
// 8. Checkpoint
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedAmountGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_AfterAVSSlash_BCSlash(
staker, validators, initDepositShares[0], slashedAmountGwei, allocateParams, slashingParams
);
// 9. Send 32 ETH to pod, some random amount of ETH, greater than the amount slashed
uint ethToDeposit = 32 ether;
cheats.deal(address(staker), ethToDeposit);
cheats.prank(address(staker));
(bool success,) = address(staker.pod()).call{value: ethToDeposit}("");
require(success, "pod call failed");
uint64 ethDepositedGwei = uint64(ethToDeposit / GWEI_TO_WEI);
// 10. Checkpoint. This should immediately complete as there are no more active validators
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
check_StartCheckpoint_NoValidators_State(staker, ethDepositedGwei);
}
}
contract Integration_DualSlashing_FullSlashes is Integration_DualSlashing_Base {
using ArrayLib for *;
using SlashingLib for *;
using Math for uint;
SlashingParams slashingParams;
uint64 slashedAmountGwei;
IERC20[] tokens;
function _init() internal virtual override {
super._init();
tokens = _getUnderlyingTokens(strategies);
// Either Fully Slash on AVS or BC, ordering does not matter
if (_randBool()) {
// 6. Slash operator by AVS fully
slashingParams = _genSlashing_Full(operator, operatorSet);
avs.slashOperator(slashingParams);
check_Base_Slashing_State(operator, allocateParams, slashingParams);
assert_Snap_Allocations_Slashed(slashingParams, operatorSet, true, "operator allocations should be slashed");
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker deposit shares should be unchanged after slashing");
assert_Snap_StakerWithdrawableShares_AfterSlash(
staker, allocateParams, slashingParams, "staker withdrawable shares should be slashed"
);
/// 7. Fully slash on BC
slashedAmountGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Full);
beaconChain.advanceEpoch_NoRewards();
} else {
// 6. Slash operator by BC fully
slashedAmountGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Full);
beaconChain.advanceEpoch_NoRewards();
// 7. Slash operator by AVS fully
slashingParams = _genSlashing_Full(operator, operatorSet);
avs.slashOperator(slashingParams);
check_Base_Slashing_State(operator, allocateParams, slashingParams);
assert_Snap_Allocations_Slashed(slashingParams, operatorSet, true, "operator allocations should be slashed");
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker deposit shares should be unchanged after slashing");
assert_Snap_StakerWithdrawableShares_AfterSlash(
staker, allocateParams, slashingParams, "staker withdrawable shares should be slashed"
);
}
}
function testFuzz_fullDualSlash_undelegate_verifyValidator_checkpoint_exitEverything(uint24 _random) public rand(_random) {
// 8. Undelegate staker, so we don't revert when verifying a validator
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, withdrawalRoots, strategies, uint(0).toArrayU256());
// 9. Verify validator
cheats.deal(address(staker), 32 ether);
(uint40[] memory newValidators, uint64 addedBeaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, addedBeaconBalanceGwei);
uint withdrawableShares = _getWithdrawableShares(staker, strategies)[0];
// 10. Checkpoint
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithSlashing_Exits_State_Base(staker, validators);
// Withdrawable shares should decrease by a factor of the BCSF
uint sharesRemoved = withdrawableShares.mulWad(WAD - _getBeaconChainSlashingFactor(staker));
assert_Snap_Removed_Staker_WithdrawableShares_AtLeast(
staker, strategies, sharesRemoved.toArrayU256(), "should have decreased withdrawable shares correctly"
);
// 11. Exit remaining validators & checkpoint
staker.exitValidators(newValidators);
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, addedBeaconBalanceGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_WithPodBalance_State(staker, addedBeaconBalanceGwei);
// 12. Complete first set of withdrawals
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, uint(0).toArrayU256(), tokens, uint(0).toArrayU256()
);
}
// 13. Queue withdrawal for all remaining shares
uint[] memory depositShares = _getStakerDepositShares(staker, strategies);
uint[] memory withdrawableShares2 = _getWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals2 = staker.queueWithdrawals(strategies, depositShares);
bytes32[] memory withdrawalRoots2 = _getWithdrawalHashes(withdrawals2);
check_QueuedWithdrawal_State(staker, operator, strategies, depositShares, withdrawableShares2, withdrawals2, withdrawalRoots2);
// 14. Complete second set of withdrawals
_rollBlocksForCompleteWithdrawals(withdrawals2);
uint[] memory expectedTokens = _calculateExpectedTokens(strategies, withdrawableShares2);
for (uint i = 0; i < withdrawals2.length; ++i) {
staker.completeWithdrawalAsTokens(withdrawals2[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals2[i], withdrawals2[i].strategies, withdrawableShares2, tokens, expectedTokens
);
}
// Sanity check that balance locked in pod and depositShares are 0
assertEq(0, _getStakerDepositShares(staker, strategies)[0], "deposit shares should be 0");
assertEq(address(staker.pod()).balance, depositShares[0] - expectedTokens[0], "staker withdrew more than expected");
}
function testFuzz_fullDualSlash_redeposit_revertCheckpoint(uint24 _random) public rand(_random) {
// 8. Deposit ETH into pod, doesn't matter how large it is, we'll still revert
uint ethToDeposit = 1000 ether;
cheats.deal(address(staker), ethToDeposit);
cheats.prank(address(staker));
(bool success,) = address(staker.pod()).call{value: ethToDeposit}("");
require(success, "pod call failed");
uint64 ethDepositedGwei = uint64(ethToDeposit / GWEI_TO_WEI);
// 9. Checkpoint. This should revert as the slashing factor is 0
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, ethDepositedGwei);
cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector);
staker.completeCheckpoint();
}
function testFuzz_fullDualSlash_checkpoint(uint24 _random) public rand(_random) {
// 8. Checkpoint
staker.startCheckpoint();
check_StartCheckpoint_WithPodBalance_State(staker, beaconBalanceGwei - slashedAmountGwei);
staker.completeCheckpoint();
check_CompleteCheckpoint_FullDualSlashes(staker, validators, allocateParams, slashingParams);
}
}
````
## File: src/test/integration/tests/FullySlashed_Operator.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
contract Integration_FullySlashed_Operator is IntegrationCheckUtils {
AVS avs;
User staker;
User operator;
OperatorSet operatorSet;
AllocateParams allocateParams;
SlashingParams slashParams;
IStrategy[] strategies;
IERC20[] tokens;
uint[] initTokenBalances;
uint[] initDepositShares;
function _init() internal virtual override {
_configAssetTypes(HOLDS_LST);
(staker, strategies, initTokenBalances) = _newRandomStaker();
operator = _newRandomOperator_NoAssets();
(avs,) = _newRandomAVS();
operatorSet = avs.createOperatorSet(strategies);
tokens = _getUnderlyingTokens(strategies);
// 1) Register operator for operator set.
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, strategies);
// 2) Operator allocates to operator set.
allocateParams = _genAllocation_AllAvailable(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_Base_IncrAlloc_State(operator, allocateParams);
// 3) Roll forward to complete allocation.
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);
// 4) Operator is full slashed.
slashParams = _genSlashing_Full(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
}
function testFuzz_register_allocate_fullSlash_deposit_delegate(uint24 r) public rand(r) {
// 5) Staker deposits into strategies.
staker.depositIntoEigenlayer(strategies, initTokenBalances);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, initDepositShares);
// 6) Staker delegates to operator who is fully slashed, should fail.
vm.expectRevert(IDelegationManagerErrors.FullySlashed.selector);
staker.delegateTo(operator);
}
function testFuzz_register_allocate_fullSlash_delegate_deposit(uint24 r) public rand(r) {
// 5) Staker delegates to operator who is fully slashed.
staker.delegateTo(operator);
// NOTE: We didn't use check_Delegation_State as it leads to division by zero.
assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should be delegated to operator");
// 6) Staker deposits into strategies, should fail.
vm.expectRevert(IDelegationManagerErrors.FullySlashed.selector);
staker.depositIntoEigenlayer(strategies, initTokenBalances);
}
function testFuzz_register_allocate_fullSlash_delegate_redelegate_deposit(uint24 r) public rand(r) {
// 5) Staker delegates to operator who is fully slashed
staker.delegateTo(operator);
User newOperator = _newRandomOperator_NoAssets();
newOperator.registerForOperatorSet(operatorSet);
// 6) Staker redelegates to new operator.
Withdrawal[] memory withdrawals = staker.redelegate(newOperator);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_Redelegate_State(staker, operator, newOperator, withdrawals, withdrawalRoots, strategies, new uint[](strategies.length));
for (uint i = 0; i < withdrawals.length; i++) {
for (uint j = 0; j < withdrawals[i].strategies.length; j++) {
assertEq(withdrawals[i].scaledShares[j], 0, "sanity: scaled shares should be zero");
}
}
// 7) Staker deposits into strategies.
staker.depositIntoEigenlayer(strategies, initTokenBalances);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, initDepositShares);
}
}
````
## File: src/test/integration/tests/HighDSF_Multiple_Deposits.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
/// @notice Testing the rounding behavior when the DSF is high and there are multiple deposits
contract Integration_HighDSF_Multiple_Deposits is IntegrationCheckUtils {
using ArrayLib for *;
AVS avs;
OperatorSet operatorSet;
User operator;
AllocateParams allocateParams;
SlashingParams slashParams;
User staker;
IStrategy[] strategies;
IERC20[] tokens; // underlying token for each strategy
uint[] initTokenBalances;
uint[] initDepositShares;
/**
* Shared setup:
* 1. create a new staker, operator, and avs
* 2. create an operator set and register an operator, allocate all magnitude to the operator set
* 3. slash operator to 1 magnitude remaining
* 4. delegate to operator
*/
function _init() internal override {
// 1. create a new staker, operator, and avs
_configAssetTypes(HOLDS_LST);
(staker, strategies, initTokenBalances) = _newRandomStaker();
(operator,,) = _newRandomOperator();
(avs,) = _newRandomAVS();
// 2. Create an operator set and register an operator, allocate all magnitude to the operator set
operatorSet = avs.createOperatorSet(strategies);
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, strategies);
allocateParams = _genAllocation_AllAvailable(operator, operatorSet, strategies);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable_NoDelegatedStake(operator, allocateParams);
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);
// 3. slash operator to 1 magnitude remaining
slashParams = _genSlashing_Custom(operator, operatorSet, WAD - 1);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// 4. delegate to operator
staker.delegateTo(operator);
uint slashingFactor = staker.getSlashingFactor(strategies[0]);
assertEq(slashingFactor, 1, "slashing factor should be 1");
}
/// @notice Test setup with a staker with slashingFactor of 1 (maxMagnitude = 1)
/// with repeat deposits to increase the DSF. Limiting number of fuzzed runs to speed up tests since this
/// for loops several times.
/// forge-config: default.fuzz.runs = 10
function test_multiple_deposits(uint24 _r) public rand(_r) {
// deposit initial assets into strategies
staker.depositIntoEigenlayer(strategies, initTokenBalances);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, initDepositShares);
// Repeat the deposit 50 times
// Gas intensive so we pause gas metering for this loop
cheats.pauseGasMetering();
for (uint i = 0; i < 50; i++) {
_dealAmounts(staker, strategies, initTokenBalances);
staker.depositIntoEigenlayer(strategies, initTokenBalances);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, initDepositShares);
}
cheats.resumeGasMetering();
// Check that the DSF is still bounded without overflow
for (uint i = 0; i < strategies.length; i++) {
assertGe(delegationManager.depositScalingFactor(address(staker), strategies[i]), WAD, "DSF should be >= WAD");
// theoretical upper bound on DSF is 1e74
assertLt(delegationManager.depositScalingFactor(address(staker), strategies[i]), 1e74, "DSF should be < 1e74");
}
}
}
````
## File: src/test/integration/tests/Slashed_Eigenpod_AVS.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
contract Integration_SlashedEigenpod_AVS_Base is IntegrationCheckUtils {
using ArrayLib for *;
using SlashingLib for *;
using Math for uint;
AVS avs;
OperatorSet operatorSet;
User operator;
AllocateParams allocateParams;
SlashingParams slashParams;
User staker;
IStrategy[] strategies;
uint[] initTokenBalances;
uint[] initDepositShares;
function _init() internal virtual override {
_configAssetTypes(HOLDS_ETH);
(staker, strategies, initTokenBalances) = _newRandomStaker();
(operator,,) = _newRandomOperator();
(avs,) = _newRandomAVS();
cheats.assume(initTokenBalances[0] >= 64 ether);
// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, initTokenBalances);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, initDepositShares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
// 3. Create an operator set and register an operator.
operatorSet = avs.createOperatorSet(strategies);
// 4. Register for operator set
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, allStrats);
// 5. Allocate to operator set
allocateParams = _genAllocation_AllAvailable(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable(operator, allocateParams);
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);
}
}
contract Integration_SlashedEigenpod_AVS_Checkpoint is Integration_SlashedEigenpod_AVS_Base {
function _init() internal override {
super._init();
// 6. Slash
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
beaconChain.advanceEpoch_NoRewards();
}
/// @dev Asserts that the DSF isn't updated after a slash & checkpoint with 0 balance
function testFuzz_deposit_delegate_allocate_slash_checkpointZeroBalance(uint24 _rand) public rand(_rand) {
// 7. Start & complete checkpoint
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
staker.completeCheckpoint();
check_CompleteCheckpoint_ZeroBalanceDelta_State(staker);
}
}
contract Integration_SlashedEigenpod_AVS_Withdraw is Integration_SlashedEigenpod_AVS_Base {
using Math for uint;
using SlashingLib for uint;
uint[] withdrawableSharesAfterSlash;
function _init() internal override {
super._init();
// Slash or queue a withdrawal in a random order
if (_randBool()) {
// Slash -> Queue Withdrawal
// 7. Slash
slashParams = _genSlashing_Half(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// 8. Queue Withdrawal for all shares.
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
withdrawableSharesAfterSlash = _calcWithdrawable(staker, strategies, initDepositShares);
check_QueuedWithdrawal_State(
staker, operator, strategies, initDepositShares, withdrawableSharesAfterSlash, withdrawals, withdrawalRoots
);
} else {
// Queue Withdrawal -> Slash
// 7. Queue Withdrawal for all shares
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
withdrawableSharesAfterSlash = _calcWithdrawable(staker, strategies, initDepositShares);
check_QueuedWithdrawal_State(
staker, operator, strategies, initDepositShares, withdrawableSharesAfterSlash, withdrawals, withdrawalRoots
);
// 8. Slash
slashParams = _genSlashing_Half(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
}
withdrawableSharesAfterSlash = _calcWithdrawable(staker, strategies, initDepositShares);
beaconChain.advanceEpoch_NoRewards();
}
/// @dev Asserts that the DSF isn't updated after a slash/queue and a checkpoint with 0 balance.
/// @dev The staker should subsequently not be able to inflate their withdrawable shares
function testFuzz_deposit_delegate_allocate_slashAndQueue_checkpoint_redeposit(uint24 _rand) public rand(_rand) {
// 9. Start & complete checkpoint.
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
staker.completeCheckpoint();
check_CompleteCheckpoint_ZeroBalanceDelta_State(staker);
// 10. Redeposit
cheats.deal(address(staker), 32 ether);
(uint40[] memory newValidators, uint64 addedBeaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, addedBeaconBalanceGwei);
}
/// @dev Asserts that the staker cannot inflate withdrawable shares after redepositing
function testFuzz_deposit_delegate_allocate_slashAndQueue_completeAsTokens_redeposit(uint24 _rand) public rand(_rand) {
Withdrawal[] memory withdrawals = _getQueuedWithdrawals(staker);
_rollBlocksForCompleteWithdrawals(withdrawals);
// 9. Complete withdrawal as tokens
for (uint i = 0; i < withdrawals.length; ++i) {
IERC20[] memory tokens = _getUnderlyingTokens(withdrawals[i].strategies);
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawableSharesAfterSlash);
staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], strategies, withdrawals[i].scaledShares, tokens, expectedTokens
);
}
// 10. Redeposit
cheats.deal(address(staker), 32 ether);
(uint40[] memory newValidators, uint64 addedBeaconBalanceGwei,) = staker.startValidators();
beaconChain.advanceEpoch_NoRewards();
staker.verifyWithdrawalCredentials(newValidators);
check_VerifyWC_State(staker, newValidators, addedBeaconBalanceGwei);
}
/// @dev Asserts that the staker cannot inflate withdrawable shares after checkpointing & completing as shares
function testFuzz_deposit_delegate_allocate_slashAndQueue_checkPoint_completeAsShares(uint24 _rand) public rand(_rand) {
Withdrawal[] memory withdrawals = _getQueuedWithdrawals(staker);
_rollBlocksForCompleteWithdrawals(withdrawals);
uint[] memory withdrawableShares = _calcWithdrawable(staker, strategies, initDepositShares);
// 9. Start & complete checkpoint, since the next step does not.
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
staker.completeCheckpoint();
check_CompleteCheckpoint_ZeroBalanceDelta_State(staker);
// 10. Complete withdrawal as shares. Deposit scaling factor is doubled because operator was slashed by half.
staker.completeWithdrawalAsShares(withdrawals[0]);
check_Withdrawal_AsShares_State(staker, operator, withdrawals[0], strategies, withdrawableShares);
}
}
````
## File: src/test/integration/tests/SlashingWithdrawals.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
contract Integration_ALMSlashBase is IntegrationCheckUtils {
AVS avs;
OperatorSet operatorSet;
User operator;
AllocateParams allocateParams;
SlashingParams slashParams;
User staker;
IStrategy[] strategies;
IERC20[] tokens; // underlying token for each strategy
uint[] initTokenBalances;
uint[] initDepositShares;
/// Shared setup:
///
/// 1. Generate staker, operator, and AVS
/// 2. Staker deposits and delegates to operator
/// 3. AVS creates an operator set containing the strategies held by the staker
/// 4. Operator allocates to operator set
/// 5. Operator registers for operator set
/// NOTE: Steps 4 and 5 are done in random order, as these should not have an outcome on the test
function _init() internal virtual override {
(staker, strategies, initTokenBalances) = _newRandomStaker();
operator = _newRandomOperator_NoAssets();
(avs,) = _newRandomAVS();
tokens = _getUnderlyingTokens(strategies);
// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, initTokenBalances);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, initDepositShares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
// 3. Create an operator set and register an operator.
operatorSet = avs.createOperatorSet(strategies);
// randomly choose between:
// register -> allocate / allocate -> register
if (_randBool()) {
// register -> allocate
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, allStrats);
allocateParams = _genAllocation_AllAvailable(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_Slashable(operator, allocateParams);
} else {
// allocate -> register
allocateParams = _genAllocation_AllAvailable(operator, operatorSet);
operator.modifyAllocations(allocateParams);
check_IncrAlloc_State_NotSlashable(operator, allocateParams);
operator.registerForOperatorSet(operatorSet);
check_Registration_State_PendingAllocation(operator, allocateParams);
}
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);
}
}
contract Integration_SlashThenWithdraw is Integration_ALMSlashBase {
User stakerB;
uint[] initTokenBalancesB;
uint[] initTokenSharesB;
User operatorB;
OperatorSet operatorSetB;
AllocateParams allocateParamsB;
SlashingParams slashParamsB;
/// Init: Registered operator gets slashed one or more times for a random magnitude
/// - Second operator (for redelegation) may or may not be slashed
///
/// Tests: Operator is slashed one or more times, then staker withdraws in different ways
function _init() internal override {
super._init();
/// Create second operator set with the same strategies as the first
/// Create a second operator. Register and allocate to operatorSetB
/// Also create a second staker with the same assets as the first,
/// delegated to operatorB. This is to give operatorB initial assets that can
/// be checked via invariants.
{
// Create operatorB
operatorB = _newRandomOperator_NoAssets();
// Create stakerB, deposit, and delegate to operatorB
(stakerB, initTokenBalancesB) = _newStaker(strategies);
stakerB.depositIntoEigenlayer(strategies, initTokenBalancesB);
initTokenSharesB = _calculateExpectedShares(strategies, initTokenBalancesB);
check_Deposit_State(stakerB, strategies, initTokenSharesB);
stakerB.delegateTo(operatorB);
check_Delegation_State(stakerB, operatorB, strategies, initTokenSharesB);
// Create operatorSetB
operatorSetB = avs.createOperatorSet(strategies);
// Register and allocate fully to operatorSetB
operatorB.registerForOperatorSet(operatorSetB);
check_Registration_State_NoAllocation(operatorB, operatorSetB, allStrats);
allocateParamsB = _genAllocation_AllAvailable(operatorB, operatorSetB);
operatorB.modifyAllocations(allocateParamsB);
check_IncrAlloc_State_Slashable(operatorB, allocateParamsB);
_rollBlocksForCompleteAllocation(operatorB, operatorSetB, strategies);
}
/// Slash first operator one or more times
/// Each slash is for 1 to 99%
{
uint numSlashes = _randUint(1, 10);
for (uint i = 0; i < numSlashes; i++) {
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// TODO - staker variant?
// assert_Snap_Unchanged_Staker_DepositShares(staker, "staker deposit shares should be unchanged after slashing");
// assert_Snap_StakerWithdrawableShares_AfterSlash(staker, allocateParams, slashParams, "staker deposit shares should be slashed");
}
}
/// Optionally do a single slash for the second operator
/// This is to test redelegation where the new operator has already been slashed
{
bool slashSecondOperator = _randBool();
if (slashSecondOperator) {
slashParamsB = _genSlashing_Half(operatorB, operatorSetB);
avs.slashOperator(slashParamsB);
check_Base_Slashing_State(operatorB, allocateParamsB, slashParamsB);
}
}
}
function testFuzz_undelegate_completeAsTokens(uint24 _r) public rand(_r) {
/// Undelegate from operatorA
uint[] memory shares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory roots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, roots, strategies, shares);
_rollBlocksForCompleteWithdrawals(withdrawals);
/// Complete withdrawal as tokens
uint[] memory expectedTokens = _calculateExpectedTokens(strategies, shares);
staker.completeWithdrawalsAsTokens(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], strategies, shares, tokens, expectedTokens);
}
}
function testFuzz_redelegate_completeAsTokens(uint24 _r) public rand(_r) {
/// Redelegate to operatorB
uint[] memory shares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.redelegate(operatorB);
bytes32[] memory roots = _getWithdrawalHashes(withdrawals);
check_Redelegate_State(staker, operator, operatorB, withdrawals, roots, strategies, shares);
_rollBlocksForCompleteWithdrawals(withdrawals);
/// Complete withdrawal as tokens
uint[] memory expectedTokens = _calculateExpectedTokens(strategies, shares);
staker.completeWithdrawalsAsTokens(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], strategies, shares, tokens, expectedTokens);
}
}
function testFuzz_queueFull_completeAsTokens(uint24 _r) public rand(_r) {
// Queue a withdrawal for all shares
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, initDepositShares, withdrawableShares, withdrawals, withdrawalRoots);
_rollBlocksForCompleteWithdrawals(withdrawals);
// Complete withdrawal as tokens
for (uint i = 0; i < withdrawals.length; i++) {
uint[] memory expectedShares = _calculateExpectedShares(withdrawals[i]);
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, expectedShares);
staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(
staker, operator, withdrawals[i], withdrawals[i].strategies, expectedShares, tokens, expectedTokens
);
}
}
function testFuzz_undelegate_completeAsShares(uint24 _r) public rand(_r) {
// Undelegate from operatorA
uint[] memory shares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.undelegate();
bytes32[] memory roots = _getWithdrawalHashes(withdrawals);
check_Undelegate_State(staker, operator, withdrawals, roots, strategies, shares);
_rollBlocksForCompleteWithdrawals(withdrawals);
// Complete withdrawal as shares
staker.completeWithdrawalsAsShares(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
check_Withdrawal_AsShares_Undelegated_State(staker, operator, withdrawals[i], strategies, shares);
}
}
function testFuzz_redelegate_completeAsShares(uint24 _r) public rand(_r) {
// Redelegate to operatorB
uint[] memory shares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.redelegate(operatorB);
bytes32[] memory roots = _getWithdrawalHashes(withdrawals);
check_Redelegate_State(staker, operator, operatorB, withdrawals, roots, strategies, shares);
_rollBlocksForCompleteWithdrawals(withdrawals);
// Complete withdrawal as shares
staker.completeWithdrawalsAsShares(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
check_Withdrawal_AsShares_Redelegated_State(staker, operator, operatorB, withdrawals[i], strategies, shares);
}
}
function testFuzz_queueFull_completeAsShares(uint24 _r) public rand(_r) {
// Queue a withdrawal for all shares
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, initDepositShares, withdrawableShares, withdrawals, withdrawalRoots);
_rollBlocksForCompleteWithdrawals(withdrawals);
// Complete withdrawal as shares
staker.completeWithdrawalsAsShares(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
check_Withdrawal_AsShares_State(staker, operator, withdrawals[i], strategies, withdrawableShares);
}
}
}
contract Integration_QueueWithdrawalThenSlash is Integration_ALMSlashBase {
function testFuzz_queue_slash_completeAsTokens(uint24 _r) public rand(_r) {
// 4. Queue withdrawal
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, initDepositShares, withdrawableShares, withdrawals, withdrawalRoots);
// 5. Slash operator
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// TODO - staker variants?
// 6. Complete withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedShares = _calculateExpectedShares(withdrawals[i]);
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, expectedShares);
staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], strategies, expectedShares, tokens, expectedTokens);
}
// Check Final State
// check_FullyWithdrawn_State(staker, ..., ); TODO
assert_HasNoDelegatableShares(staker, "staker should have withdrawn all shares");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
function testFuzz_queue_slash_completeAsShares(uint24 _r) public rand(_r) {
// 4. Queue withdrawal
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, initDepositShares, withdrawableShares, withdrawals, withdrawalRoots);
// 5. Slash operator
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// TODO - staker variants?
// 4. Complete withdrawal
// Fast forward to when we can complete the withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedShares = _calculateExpectedShares(withdrawals[i]);
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_State(staker, operator, withdrawals[i], strategies, expectedShares);
}
// Check final state:
assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker not have any underlying tokens");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
}
contract Integration_DeallocateThenSlash is Integration_ALMSlashBase {
function testFuzz_deallocate_slash_queue_completeAsTokens(uint24 _r) public rand(_r) {
// 4. Deallocate all.
AllocateParams memory deallocateParams = _genDeallocation_Full(operator, operatorSet);
operator.modifyAllocations(deallocateParams);
check_DecrAlloc_State_Slashable(operator, deallocateParams);
_rollForward_DeallocationDelay();
// 5. Slash operator
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, deallocateParams, slashParams);
// TODO - staker variants?
// 6. Queue withdrawals
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
check_QueuedWithdrawal_State(staker, operator, strategies, initDepositShares, withdrawableShares, withdrawals, withdrawalRoots);
// 7. Complete withdrawal
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; ++i) {
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].scaledShares);
staker.completeWithdrawalAsTokens(withdrawals[i]);
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], strategies, initDepositShares, tokens, expectedTokens);
}
// Check Final State
assert_HasUnderlyingTokenBalances(staker, allocateParams.strategies, initTokenBalances, "staker should have withdrawn all shares");
assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending");
}
function testFuzz_deregister_slash(uint24 _r) public rand(_r) {
// 4. Deregister.
operator.deregisterFromOperatorSet(operatorSet);
check_Deregistration_State_PendingAllocation(operator, operatorSet);
// 5. Slash operator
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
// TODO - staker variants?
}
}
````
## File: src/test/integration/tests/Timing.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/tests/SlashingWithdrawals.t.sol";
/**
* @notice These tests check for specific withdrawal correctness around timing
* @dev These tests assume the following:
* - The staker has a positive balance in all given strategies
* - The staker has no pending withdrawals
* - The staker is delegated to the operator
*/
contract Integration_WithdrawalTiming is Integration_ALMSlashBase {
///////////////////////////////
/// WITHDRAWAL TIMING TESTS ///
///////////////////////////////
/**
* @notice Test that a slash works correctly just before a _partial_ withdrawal is completed
*/
function testFuzz_queuePartialWithdrawal_slashBeforeWithdrawalDelay_completeAsTokens(uint24 _r) public rand(_r) {
uint[] memory depositSharesToWithdraw = new uint[](initDepositShares.length);
/// 0. Calculate partial withdrawal amounts
for (uint i = 0; i < initDepositShares.length; ++i) {
// Note: 2 is specifically chosen as the minimum divisor to ensure that the withdrawal is partial but 10 as
// the maximum divisor is more arbitrary
depositSharesToWithdraw[i] = initDepositShares[i] / _randUint(2, 10);
}
/// 1. Queue withdrawal
uint[] memory withdrawableShares = _calcWithdrawable(staker, strategies, depositSharesToWithdraw);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, depositSharesToWithdraw);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
// Validate correctly queued withdrawals
check_QueuedWithdrawal_State(
staker, operator, strategies, depositSharesToWithdraw, withdrawableShares, withdrawals, withdrawalRoots
);
/// 2. Move time forward to _just before_ withdrawal block
// Expected behavior: Withdrawals are still pending and cannot be completed, but slashes can still be performed
_rollBlocksForCompleteWithdrawals(withdrawals);
vm.roll(block.number - 1);
// Verify that the withdrawals are _still_ slashable
for (uint i = 0; i < withdrawals.length; ++i) {
uint32 slashableUntil = withdrawals[i].startBlock + delegationManager.minWithdrawalDelayBlocks();
assert(uint32(block.number) <= slashableUntil);
}
/// 3. Slash operator
SlashingParams memory slashingParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(operator, operatorSet.id, slashingParams.strategies, slashingParams.wadsToSlash);
// Verify that the slash was performed correctly
check_Base_Slashing_State(operator, allocateParams, slashingParams);
/// 4. Move time forward to withdrawal block
vm.roll(block.number + 1);
// Verify that the withdrawals are _no longer_ slashable
for (uint i = 0; i < withdrawals.length; ++i) {
uint32 slashableUntil = withdrawals[i].startBlock + delegationManager.minWithdrawalDelayBlocks();
assert(uint32(block.number) > slashableUntil);
}
/// 5. Complete withdrawals
// Note: expectedTokens must be recalculated because the withdrawable shares have changed due to the slash
withdrawableShares = _calcWithdrawable(staker, strategies, depositSharesToWithdraw);
uint[] memory expectedTokens = _calculateExpectedTokens(strategies, withdrawableShares);
staker.completeWithdrawalsAsTokens(withdrawals);
// Verify that the withdrawals were completed correctly
for (uint i = 0; i < withdrawals.length; ++i) {
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], strategies, withdrawableShares, tokens, expectedTokens);
}
/// 6. Check final state
// Assert that all strategies have some shares remaining
(IStrategy[] memory strats,) = delegationManager.getDepositedShares(address(staker));
assertEq(strats.length, strategies.length, "all strategies should have some shares remaining");
}
/**
* @notice Test that a slash works correctly just before a _total_ withdrawal is completed
*/
function testFuzz_queueTotalWithdrawal_slashBeforeWithdrawalDelay_completeAsTokens(uint24 _r) public rand(_r) {
/// 1. Queue withdrawal
uint[] memory withdrawableShares = _calcWithdrawable(staker, strategies, initDepositShares);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
// Validate correctly queued withdrawals
check_QueuedWithdrawal_State(staker, operator, strategies, initDepositShares, withdrawableShares, withdrawals, withdrawalRoots);
/// 2. Move time forward to _just before_ withdrawal block
// Expected behavior: Withdrawals are still pending and cannot be completed, but slashes can still be performed
_rollBlocksForCompleteWithdrawals(withdrawals);
vm.roll(block.number - 1);
// Verify that the withdrawals are _still_ slashable
for (uint i = 0; i < withdrawals.length; ++i) {
uint32 slashableUntil = withdrawals[i].startBlock + delegationManager.minWithdrawalDelayBlocks();
assert(uint32(block.number) <= slashableUntil);
}
/// 3. Slash operator
SlashingParams memory slashingParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(operator, operatorSet.id, slashingParams.strategies, slashingParams.wadsToSlash);
// Verify that the slash was performed correctly
check_Base_Slashing_State(operator, allocateParams, slashingParams);
/// 4. Move time forward to withdrawal block
vm.roll(block.number + 1);
// Verify that the withdrawals are _no longer_ slashable
for (uint i = 0; i < withdrawals.length; ++i) {
uint32 slashableUntil = withdrawals[i].startBlock + delegationManager.minWithdrawalDelayBlocks();
assert(uint32(block.number) > slashableUntil);
}
/// 5. Complete withdrawals
// Note: expectedTokens must be recalculated because the withdrawable shares have changed due to the slash
withdrawableShares = _calcWithdrawable(staker, strategies, initDepositShares);
uint[] memory expectedTokens = _calculateExpectedTokens(strategies, withdrawableShares);
staker.completeWithdrawalsAsTokens(withdrawals);
// Verify that the withdrawals were completed correctly
for (uint i = 0; i < withdrawals.length; ++i) {
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], strategies, withdrawableShares, tokens, expectedTokens);
}
/// 6. Check final state
// Assert that all strategies have some shares remaining
(IStrategy[] memory strats,) = delegationManager.getDepositedShares(address(staker));
assertEq(strats.length, 0, "all strategies should have no shares remaining");
}
/**
* @notice Test that a staker can still complete a partial withdrawal even after a slash has been performed
*/
function testFuzz_queuePartialWithdrawal_slashAfterWithdrawalDelay_completeAsTokens(uint24 _r) public rand(_r) {
uint[] memory depositSharesToWithdraw = new uint[](initDepositShares.length);
/// 0. Calculate partial withdrawal amounts
for (uint i = 0; i < initDepositShares.length; ++i) {
// Note: 2 is specifically chosen as the minimum divisor to ensure that the withdrawal is partial
// but 10 as the maximum divisor is more arbitrary
depositSharesToWithdraw[i] = initDepositShares[i] / _randUint(2, 10);
}
/// 1. Queue withdrawal
uint[] memory withdrawableShares = _calcWithdrawable(staker, strategies, depositSharesToWithdraw);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, depositSharesToWithdraw);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
// Validate correctly queued withdrawals
check_QueuedWithdrawal_State(
staker, operator, strategies, depositSharesToWithdraw, withdrawableShares, withdrawals, withdrawalRoots
);
/// 2. Move time forward to _just after_ withdrawal block
// Expected behavior: Withdrawals are no longer slashable, so slashes no longer affect the staker
_rollBlocksForCompleteWithdrawals(withdrawals);
// Verify that the withdrawals are _no longer_ slashable
for (uint i = 0; i < withdrawals.length; ++i) {
uint32 slashableUntil = withdrawals[i].startBlock + delegationManager.minWithdrawalDelayBlocks();
assert(uint32(block.number) > slashableUntil);
}
/// 3. Slash operator
SlashingParams memory slashingParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(operator, operatorSet.id, slashingParams.strategies, slashingParams.wadsToSlash);
// Verify that the slash was performed correctly
check_Base_Slashing_State(operator, allocateParams, slashingParams);
/// 4. Complete withdrawals
// Note: expectedTokens must be recalculated because the withdrawable shares have changed due to the slash
uint[] memory expectedTokens = _calculateExpectedTokens(strategies, withdrawableShares);
staker.completeWithdrawalsAsTokens(withdrawals);
// Verify that the withdrawals were completed correctly
for (uint i = 0; i < withdrawals.length; ++i) {
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], strategies, withdrawableShares, tokens, expectedTokens);
}
/// 5. Check final state
// Assert that all strategies have some shares remaining
(IStrategy[] memory strats,) = delegationManager.getDepositedShares(address(staker));
assertEq(strats.length, strategies.length, "all strategies should have some shares remaining");
}
/**
* @notice Test that a staker is unaffected by a slash after the withdrawal delay has passed
*/
function testFuzz_queueTotalWithdrawal_slashAfterWithdrawalDelay_completeAsTokens(uint24 _r) public rand(_r) {
/// 1. Queue withdrawal
uint[] memory withdrawableShares = _calcWithdrawable(staker, strategies, initDepositShares);
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, initDepositShares);
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
// Validate correctly queued withdrawals
check_QueuedWithdrawal_State(staker, operator, strategies, initDepositShares, withdrawableShares, withdrawals, withdrawalRoots);
/// 2. Move time forward to withdrawal block
// Expected behavior: Withdrawals are no longer slashable, so slashes no longer affect the staker
_rollBlocksForCompleteWithdrawals(withdrawals);
// Verify that the withdrawals are _no longer_ slashable
for (uint i = 0; i < withdrawals.length; ++i) {
uint32 slashableUntil = withdrawals[i].startBlock + delegationManager.minWithdrawalDelayBlocks();
assert(uint32(block.number) > slashableUntil);
}
/// 3. Slash operator
SlashingParams memory slashingParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(operator, operatorSet.id, slashingParams.strategies, slashingParams.wadsToSlash);
// Verify that the slash was performed correctly
check_Base_Slashing_State(operator, allocateParams, slashingParams);
/// 4. Complete withdrawals
staker.completeWithdrawalsAsTokens(withdrawals);
// Verify that the withdrawals were completed correctly
for (uint i = 0; i < withdrawals.length; ++i) {
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], strategies, withdrawableShares, tokens, initTokenBalances);
}
/// 5. Check final state
// Assert that all strategies have some shares remaining
(IStrategy[] memory strats,) = delegationManager.getDepositedShares(address(staker));
assertEq(strats.length, 0, "all strategies should have no shares remaining");
}
}
/**
* @notice These tests check for specific deallocation correctness around timing
* @dev These tests assume the following:
* - The operator is registered and allocated to the operator set
*/
contract Integration_OperatorDeallocationTiming is Integration_ALMSlashBase {
//////////////////////////////////////////
/// OPERATOR DEALLOCATION TIMING TESTS ///
//////////////////////////////////////////
function testFuzz_deallocateFully_slashBeforeDelay(uint24 _r) public rand(_r) {
/// 1. Deallocate
AllocateParams memory deallocateParams = _genDeallocation_Full(operator, operatorSet);
operator.modifyAllocations(deallocateParams);
// Validate the deallocation
check_DecrAlloc_State_Slashable(operator, deallocateParams);
/// 2. Move time forward to _just before_ deallocation delay
// Expected behavior: Deallocation delay is not yet passed, so slashes can still be performed
_rollForward_DeallocationDelay();
rollBackward(1);
// Verify that the operator is _still_ slashable
check_IsSlashable_State(operator, operatorSet, strategies);
/// 3. Slash operator
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
// Verify that the slash was performed correctly
check_Base_Slashing_State(operator, allocateParams, slashParams);
}
function testFuzz_deallocateFully_slashAfterDelay(uint24 _r) public rand(_r) {
/// 1. Deallocate
AllocateParams memory deallocateParams = _genDeallocation_Full(operator, operatorSet);
operator.modifyAllocations(deallocateParams);
// Validate the deallocation
check_DecrAlloc_State_Slashable(operator, deallocateParams);
/// 2. Move time forward to deallocation delay
_rollForward_DeallocationDelay();
// Verify that the operator is fully deallocated.
// Note: even though the operator is technically still slashable, a slash is effectively useless as the
// operator is no longer allocated.
check_FullyDeallocated_State(operator, allocateParams, deallocateParams);
/// 3. Slash operator
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
// Verify that the slash has no impact on the operator
// Note: emptySlashParams remains uninitialized, as the slash _should not_ impact the operator.
SlashingParams memory emptySlashParams;
check_Base_Slashing_State(operator, allocateParams, emptySlashParams);
}
}
contract Integration_OperatorDeregistrationTiming is Integration_ALMSlashBase {
////////////////////////////////////////////
/// OPERATOR DEREGISTRATION TIMING TESTS ///
////////////////////////////////////////////
function testFuzz_deregister_slashBeforeDelay(uint24 _r) public rand(_r) {
/// 1. Deregister
operator.deregisterFromOperatorSet(operatorSet);
// Validate the deregistration
check_Deregistration_State_PendingAllocation(operator, operatorSet);
/// 2. Move time forward to _just before_ deregistration delay
// Expected behavior: Deregistration delay is not yet passed, so slashes can still be performed
_rollForward_DeallocationDelay();
rollBackward(1);
// Verify that the operator is _still_ slashable
check_IsSlashable_State(operator, operatorSet, strategies);
/// 3. Slash operator
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams);
}
function testFuzz_deregister_slashAfterDelay(uint24 _r) public rand(_r) {
/// 1. Deregister
operator.deregisterFromOperatorSet(operatorSet);
// Validate the deregistration
check_Deregistration_State_PendingAllocation(operator, operatorSet);
/// 2. Move time forward to deregistration delay
_rollForward_DeallocationDelay();
// Verify that the operator is _no longer_ slashable
check_NotSlashable_State(operator, operatorSet);
/// 3. Slash operator
// Note: Unlike the deallocation case, the operator is no longer registered, so a slash will revert entirely.
slashParams = _genSlashing_Rand(operator, operatorSet);
vm.expectRevert(IAllocationManagerErrors.OperatorNotSlashable.selector);
avs.slashOperator(slashParams);
}
}
/**
* @notice These tests check for specific allocation correctness around timing
* @dev These tests inherit from IntegrationCheckUtils instead of Integration_ALMSlashBase because they require a
* different initialization -- specifically, the allocation must be performed within the tests. As such, there are no
* assumptions and many state variables are declared below.
*/
contract Integration_OperatorAllocationTiming is IntegrationCheckUtils {
AVS avs;
User operator;
OperatorSet operatorSet;
AllocateParams allocateParams;
SlashingParams slashParams;
User staker;
IStrategy[] strategies;
IERC20[] tokens;
uint[] initTokenBalances;
uint[] initDepositShares;
////////////////////////////////////////
/// OPERATOR ALLOCATION TIMING TESTS ///
////////////////////////////////////////
function _init() internal virtual override {
/// 0. Instantiate relevant objects
_configAssetTypes(HOLDS_LST);
(staker, strategies, initTokenBalances) = _newRandomStaker();
operator = _newRandomOperator_NoAssets();
(avs,) = _newRandomAVS();
tokens = _getUnderlyingTokens(strategies);
/// 1. Deposit into strategies
staker.depositIntoEigenlayer(strategies, initTokenBalances);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
// Validate the deposits
check_Deposit_State(staker, strategies, initDepositShares);
/// 2. Delegate to an operator
staker.delegateTo(operator);
// Validate the delegation
check_Delegation_State(staker, operator, strategies, initDepositShares);
/// 3. Create an operator set and register an operator.
operatorSet = avs.createOperatorSet(strategies);
// Validate that the operator set was correctly created
assertTrue(allocationManager.isOperatorSet(operatorSet));
}
function testFuzz_register_allocate_slashBeforeDelay(uint24 _r) public rand(_r) {
/// 1. Create and register operator
operator.registerForOperatorSet(operatorSet);
// Validate registration
check_Registration_State_NoAllocation(operator, operatorSet, allStrats);
/// 2. Allocate
allocateParams = _genAllocation_AllAvailable(operator, operatorSet);
operator.modifyAllocations(allocateParams);
// Validate the allocation
check_IncrAlloc_State_Slashable(operator, allocateParams);
/// 3. Move time forward to _just before_ allocation delay
_rollForward_AllocationDelay(operator);
rollBackward(1); // make sure that allocation delay is not yet passed
/// 4. Slash operator
// Note: This slash does not revert as the operator, even though it is not allocated, is
// still registered. However, since there is no allocation, the slash has no material effect.
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
// Verify that the slash has no impact on the operator
// Note: emptySlashParams remains uninitialized, as the slash _should not_ impact the operator.
SlashingParams memory emptySlashParams;
check_Base_Slashing_State(operator, allocateParams, emptySlashParams);
}
function testFuzz_allocate_register_slashBeforeDelay(uint24 _r) public rand(_r) {
/// 1. Allocate
allocateParams = _genAllocation_AllAvailable(operator, operatorSet);
operator.modifyAllocations(allocateParams);
// Validate the allocation
check_IncrAlloc_State_NotSlashable(operator, allocateParams);
/// 2. Create and register operator
operator.registerForOperatorSet(operatorSet);
// Validate the registration
check_Registration_State_PendingAllocation(operator, allocateParams);
/// 3. Move time forward to _just before_ allocation delay
_rollForward_AllocationDelay(operator);
rollBackward(1); // make sure that allocation delay is not yet passed
/// 4. Slash operator
// Note: This slash does not revert as the operator, even though it is not allocated, is
// still registered. However, since there is no allocation, the slash has no material effect.
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
// Verify that the slash has no impact on the operator
// Note: emptySlashParams remains uninitialized, as the slash _should not_ impact the operator.
SlashingParams memory emptySlashParams;
check_Base_Slashing_State(operator, allocateParams, emptySlashParams);
}
function testFuzz_register_allocate_slashAfterDelay(uint24 _r) public rand(_r) {
/// 1. Create and register operator
operator.registerForOperatorSet(operatorSet);
// Validate the registration
check_Registration_State_NoAllocation(operator, operatorSet, allStrats);
/// 2. Allocate
allocateParams = _genAllocation_AllAvailable(operator, operatorSet);
operator.modifyAllocations(allocateParams);
// Validate the allocation
check_IncrAlloc_State_Slashable(operator, allocateParams);
/// 3. Move time forward to after the allocation delay completes
_rollForward_AllocationDelay(operator);
/// 4. Slash operator
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
// Verify that the slash was performed correctly
check_Base_Slashing_State(operator, allocateParams, slashParams);
}
function testFuzz_allocate_register_slashAfterDelay(uint24 _r) public rand(_r) {
/// 1. Allocate
allocateParams = _genAllocation_AllAvailable(operator, operatorSet);
operator.modifyAllocations(allocateParams);
// Validate the allocation
check_IncrAlloc_State_NotSlashable(operator, allocateParams);
/// 2. Create and register operator
operator.registerForOperatorSet(operatorSet);
// Validate the registration
check_Registration_State_PendingAllocation(operator, allocateParams);
/// 3. Move time forward to after the allocation delay completes
_rollForward_AllocationDelay(operator);
/// 4. Slash operator
slashParams = _genSlashing_Rand(operator, operatorSet);
avs.slashOperator(slashParams);
// Verify that the slash was performed correctly
check_Base_Slashing_State(operator, allocateParams, slashParams);
}
}
````
## File: src/test/integration/tests/Upgrade_Setup.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
contract IntegrationMainnetFork_UpgradeSetup is IntegrationCheckUtils {
// /// @notice Test upgrade setup is correct
// /// forge-config: default.fuzz.runs = 1
// function test_mainnet_upgrade_setup(uint24 _random) public {
// _configRand({
// _randomSeed: _random,
// _assetTypes: HOLDS_LST | HOLDS_ETH | HOLDS_ALL,
// _userTypes: DEFAULT | ALT_METHODS
// });
// // 1. Check proper state pre-upgrade
// _verifyContractPointers();
// _verifyImplementations();
// _verifyContractsInitialized(false);
// _verifyInitializationParams();
// // 2. Upgrade mainnet contracts
// _upgradeEigenLayerContracts();
// _parseInitialDeploymentParams("script/configs/mainnet/M2_mainnet_upgrade.config.json");
// // 2. Verify upgrade setup
// _verifyContractPointers();
// _verifyImplementations();
// _verifyContractsInitialized(false);
// _verifyInitializationParams();
// }
// /// @notice Test upgrade setup is correct
// /// forge-config: default.fuzz.runs = 1
// function test_holesky_upgrade_setup(uint24 _random) public {
// _configRand({
// _randomSeed: _random,
// _assetTypes: HOLDS_LST | HOLDS_ETH | HOLDS_ALL,
// _userTypes: DEFAULT | ALT_METHODS,
// _forkTypes: HOLESKY
// });
// // // 1. Check proper state pre-upgrade
// // _verifyContractPointers();
// // _verifyImplementations();
// // _verifyContractsInitialized(true);
// // _verifyInitializationParams();
// // 2. Upgrade holesky contracts
// _upgradeEigenLayerContracts();
// _parseInitialDeploymentParams("script/configs/holesky/M2_deploy_from_scratch.holesky.config.json");
// // 3. Verify upgrade setup
// _verifyContractPointers();
// _verifyImplementations();
// _verifyContractsInitialized(true);
// _verifyInitializationParams();
// }
/// @notice Ensure contracts point at each other correctly via constructors
/// override to remove ethPOSDeposit contract check
function _verifyContractPointers() internal view virtual override {
// AVSDirectory
require(avsDirectory.delegation() == delegationManager, "avsDirectory: delegationManager address not set correctly");
// DelegationManager
require(delegationManager.strategyManager() == strategyManager, "delegationManager: strategyManager address not set correctly");
require(delegationManager.eigenPodManager() == eigenPodManager, "delegationManager: eigenPodManager address not set correctly");
// StrategyManager
require(strategyManager.delegation() == delegationManager, "strategyManager: delegationManager address not set correctly");
// EPM
require(eigenPodManager.eigenPodBeacon() == eigenPodBeacon, "eigenPodManager: eigenPodBeacon contract address not set correctly");
require(
eigenPodManager.delegationManager() == delegationManager,
"eigenPodManager: delegationManager contract address not set correctly"
);
}
}
````
## File: src/test/integration/users/AVS.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "src/contracts/core/AllocationManager.sol";
import "src/contracts/permissions/PermissionController.sol";
import "src/contracts/strategies/StrategyFactory.sol";
import "src/test/mocks/ERC20Mock.sol";
import "src/test/integration/users/User.t.sol";
import "src/test/integration/TimeMachine.t.sol";
import "src/test/utils/Logger.t.sol";
import "src/test/utils/ArrayLib.sol";
import "src/contracts/interfaces/IAVSRegistrar.sol";
interface IAVSDeployer {
function delegationManager() external view returns (DelegationManager);
function allocationManager() external view returns (AllocationManager);
function strategyFactory() external view returns (StrategyFactory);
function permissionController() external view returns (PermissionController);
function timeMachine() external view returns (TimeMachine);
}
contract AVS is Logger, IAllocationManagerTypes, IAVSRegistrar {
using print for *;
using ArrayLib for *;
IStrategy constant beaconChainETHStrategy = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);
// TODO: fix later for same reason as User.t.sol
AllocationManager immutable allocationManager;
PermissionController immutable permissionController;
DelegationManager immutable delegationManager;
StrategyFactory immutable strategyFactory;
TimeMachine immutable timeMachine;
string _NAME;
uint32 totalOperatorSets;
constructor(string memory name) {
IAVSDeployer deployer = IAVSDeployer(msg.sender);
allocationManager = deployer.allocationManager();
permissionController = deployer.permissionController();
delegationManager = deployer.delegationManager();
strategyFactory = deployer.strategyFactory();
timeMachine = deployer.timeMachine();
_NAME = name;
cheats.label(address(this), NAME_COLORED());
}
modifier createSnapshot() virtual {
timeMachine.createSnapshot();
_;
}
receive() external payable {}
function NAME() public view override returns (string memory) {
return _NAME;
}
function supportsAVS(address) external pure override returns (bool) {
return true;
}
/// -----------------------------------------------------------------------
/// AllocationManager
/// -----------------------------------------------------------------------
function updateAVSMetadataURI(string memory uri) public createSnapshot {
print.method("updateAVSMetadataURI");
console.log("Setting AVS metadata URI to: %s", uri);
_tryPrankAppointee_AllocationManager(IAllocationManager.updateAVSMetadataURI.selector);
allocationManager.updateAVSMetadataURI(address(this), uri);
print.gasUsed();
}
function createOperatorSets(IStrategy[][] memory strategies) public createSnapshot returns (OperatorSet[] memory operatorSets) {
print.method("createOperatorSets");
uint len = strategies.length;
CreateSetParams[] memory p = new CreateSetParams[](len);
operatorSets = new OperatorSet[](len);
for (uint i; i < len; ++i) {
p[i] = CreateSetParams({operatorSetId: totalOperatorSets++, strategies: strategies[i]});
operatorSets[i] = OperatorSet(address(this), p[i].operatorSetId);
}
print.createOperatorSets(p);
allocationManager.createOperatorSets(address(this), p);
print.gasUsed();
}
function createOperatorSet(IStrategy[] memory strategies) public createSnapshot returns (OperatorSet memory operatorSet) {
print.method("createOperatorSets");
operatorSet = OperatorSet(address(this), totalOperatorSets++);
CreateSetParams[] memory p = CreateSetParams({operatorSetId: operatorSet.id, strategies: strategies}).toArray();
print.createOperatorSets(p);
allocationManager.createOperatorSets(address(this), p);
print.gasUsed();
}
function slashOperator(SlashingParams memory params) public createSnapshot {
for (uint i; i < params.strategies.length; ++i) {
string memory strategyName = params.strategies[i] == beaconChainETHStrategy
? "Native ETH"
: IERC20Metadata(address(params.strategies[i].underlyingToken())).name();
print.method(
"slashOperator",
string.concat(
"{operator: ",
User(payable(params.operator)).NAME_COLORED(),
", operatorSetId: ",
cheats.toString(params.operatorSetId),
", strategy: ",
strategyName,
", wadToSlash: ",
params.wadsToSlash[i].asWad(),
"}"
)
);
}
_tryPrankAppointee_AllocationManager(IAllocationManager.slashOperator.selector);
allocationManager.slashOperator(address(this), params);
print.gasUsed();
}
function slashOperator(User operator, uint32 operatorSetId, IStrategy[] memory strategies, uint[] memory wadsToSlash)
public
createSnapshot
returns (SlashingParams memory p)
{
p = SlashingParams({
operator: address(operator),
operatorSetId: operatorSetId,
strategies: strategies,
wadsToSlash: wadsToSlash,
description: "bad operator"
});
for (uint i; i < strategies.length; ++i) {
string memory strategyName =
strategies[i] == beaconChainETHStrategy ? "Native ETH" : IERC20Metadata(address(strategies[i].underlyingToken())).name();
print.method(
"slashOperator",
string.concat(
"{operator: ",
operator.NAME_COLORED(),
", operatorSetId: ",
cheats.toString(operatorSetId),
", strategy: ",
strategyName,
", wadToSlash: ",
wadsToSlash[i].asWad(),
"}"
)
);
}
_tryPrankAppointee_AllocationManager(IAllocationManager.slashOperator.selector);
allocationManager.slashOperator(address(this), p);
print.gasUsed();
}
function deregisterFromOperatorSets(User operator, uint32[] memory operatorSetIds) public createSnapshot {
print.method("deregisterFromOperatorSets");
DeregisterParams memory p = DeregisterParams({operator: address(operator), avs: address(this), operatorSetIds: operatorSetIds});
print.deregisterFromOperatorSets(p);
_tryPrankAppointee_AllocationManager(IAllocationManager.deregisterFromOperatorSets.selector);
allocationManager.deregisterFromOperatorSets(p);
print.gasUsed();
}
function setAVSRegistrar(IAVSRegistrar registrar) public createSnapshot {
print.method("setAVSRegistrar");
console.log("Setting AVS registrar to: %s", address(registrar));
_tryPrankAppointee_AllocationManager(IAllocationManager.setAVSRegistrar.selector);
allocationManager.setAVSRegistrar(address(this), registrar);
print.gasUsed();
}
function addStrategiesToOperatorSet(uint32 operatorSetId, IStrategy[] memory strategies) public createSnapshot {
print.method("addStrategiesToOperatorSet");
console.log("Adding strategies to operator set: %d", operatorSetId);
for (uint i; i < strategies.length; ++i) {
console.log(" strategy: %s", address(strategies[i]));
}
_tryPrankAppointee_AllocationManager(IAllocationManager.addStrategiesToOperatorSet.selector);
allocationManager.addStrategiesToOperatorSet(address(this), operatorSetId, strategies);
print.gasUsed();
}
function removeStrategiesFromOperatorSet(uint32 operatorSetId, IStrategy[] memory strategies) public createSnapshot {
print.method("removeStrategiesFromOperatorSet");
console.log("Removing strategies from operator set: %d", operatorSetId);
for (uint i; i < strategies.length; ++i) {
console.log(" strategy: %s", address(strategies[i]));
}
_tryPrankAppointee_AllocationManager(IAllocationManager.removeStrategiesFromOperatorSet.selector);
allocationManager.removeStrategiesFromOperatorSet(address(this), operatorSetId, strategies);
print.gasUsed();
}
/// -----------------------------------------------------------------------
/// IAVSRegistrar
/// -----------------------------------------------------------------------
function registerOperator(address operator, address avsIdentifier, uint32[] calldata operatorSetIds, bytes calldata data)
external
override
{}
function deregisterOperator(address operator, address avsIdentifier, uint32[] calldata operatorSetIds) external override {}
/// -----------------------------------------------------------------------
/// Internal Helpers
/// -----------------------------------------------------------------------
// function allocationManager public view returns (AllocationManager) {
// return AllocationManager(address(delegationManager.allocationManager));
// }
// function permissionController public view returns (PermissionController) {
// return PermissionController(address(delegationManager.permissionController));
// }
function _tryPrankAppointee(address target, bytes4 selector) internal {
address[] memory appointees = permissionController.getAppointees(address(this), target, selector);
if (appointees.length != 0) cheats.prank(appointees[0]);
}
function _tryPrankAppointee_AllocationManager(bytes4 selector) internal {
return _tryPrankAppointee(address(allocationManager), selector);
}
}
````
## File: src/test/integration/users/User_M1.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/deprecatedInterfaces/mainnet/IEigenPod.sol";
import "src/test/integration/deprecatedInterfaces/mainnet/IEigenPodManager.sol";
import "src/test/integration/deprecatedInterfaces/mainnet/IStrategyManager.sol";
import "src/test/integration/users/User.t.sol";
import "src/contracts/mixins/SignatureUtilsMixin.sol";
interface IUserM1MainnetForkDeployer {
function delegationManager() external view returns (DelegationManager);
function strategyManager() external view returns (StrategyManager);
function eigenPodManager() external view returns (EigenPodManager);
function timeMachine() external view returns (TimeMachine);
function beaconChain() external view returns (BeaconChainMock);
function strategyManager_M1() external view returns (IStrategyManager_DeprecatedM1);
function eigenPodManager_M1() external view returns (IEigenPodManager_DeprecatedM1);
}
/**
* @dev User_M1 used for performing mainnet M1 contract methods but also inherits User
* to perform current local contract methods after a upgrade of core contracts
*/
contract User_M1 is User {
IStrategyManager_DeprecatedM1 strategyManager_M1;
IEigenPodManager_DeprecatedM1 eigenPodManager_M1;
constructor(string memory name) User(name) {
IUserM1MainnetForkDeployer deployer = IUserM1MainnetForkDeployer(msg.sender);
strategyManager_M1 = IStrategyManager_DeprecatedM1(address(deployer.strategyManager()));
eigenPodManager_M1 = IEigenPodManager_DeprecatedM1(address(deployer.eigenPodManager()));
cheats.label(address(this), NAME_COLORED());
}
/**
* StrategyManager M1 mainnet methods:
*/
/// @notice Deposit into EigenLayer with M1 mainnet methods, only concerns LSTs
/// Note that this should not be called with BeaconChainStrat
function depositIntoEigenlayer_M1(IStrategy[] memory strategies, uint[] memory tokenBalances) public virtual createSnapshot {
print.method("depositIntoEigenlayer_M1");
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
uint tokenBalance = tokenBalances[i];
// Skip BeaconChainStrat, since BeaconChainStrat doesn't exist on M1 mainnet
if (strat == BEACONCHAIN_ETH_STRAT) continue;
IERC20 underlyingToken = strat.underlyingToken();
underlyingToken.approve(address(strategyManager), tokenBalance);
strategyManager.depositIntoStrategy(strat, underlyingToken, tokenBalance);
}
}
function _createPod() internal virtual override {
IEigenPodManager_DeprecatedM1(address(eigenPodManager)).createPod();
// get EigenPod address
pod = EigenPod(payable(address(IEigenPodManager_DeprecatedM1(address(eigenPodManager)).ownerToPod(address(this)))));
}
}
contract User_M1_AltMethods is User_M1 {
mapping(bytes32 => bool) public signedHashes;
constructor(string memory name) User_M1(name) {}
function depositIntoEigenlayer_M1(IStrategy[] memory strategies, uint[] memory tokenBalances) public override createSnapshot {
print.method(".depositIntoEigenlayer_M1_ALT");
uint expiry = type(uint).max;
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
uint tokenBalance = tokenBalances[i];
if (strat == BEACONCHAIN_ETH_STRAT) revert("Should not be depositing with BEACONCHAIN_ETH_STRAT for M1-mainnet User");
// Approve token
IERC20 underlyingToken = strat.underlyingToken();
underlyingToken.approve(address(strategyManager), tokenBalance);
// Get signature
uint nonceBefore = strategyManager.nonces(address(this));
bytes32 structHash =
keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), strat, underlyingToken, tokenBalance, nonceBefore, expiry));
bytes32 domain_separator = strategyManager.domainSeparator();
bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", domain_separator, structHash));
bytes memory signature = bytes(abi.encodePacked(digestHash)); // dummy sig data
// Mark hash as signed
signedHashes[digestHash] = true;
// Deposit
strategyManager.depositIntoStrategyWithSignature(strat, underlyingToken, tokenBalance, address(this), expiry, signature);
// Mark hash as used
signedHashes[digestHash] = false;
}
}
bytes4 internal constant MAGIC_VALUE = 0x1626ba7e;
function isValidSignature(bytes32 hash, bytes memory) external view returns (bytes4) {
if (signedHashes[hash]) return MAGIC_VALUE;
else return 0xffffffff;
}
}
````
## File: src/test/integration/users/User_M2.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "src/test/integration/deprecatedInterfaces/mainnet/IEigenPod.sol";
import "src/test/integration/deprecatedInterfaces/mainnet/IEigenPodManager.sol";
import "src/test/integration/deprecatedInterfaces/mainnet/IStrategyManager.sol";
import "src/test/integration/deprecatedInterfaces/mainnet/IDelegationManager.sol";
import "src/test/integration/users/User.t.sol";
import "src/test/integration/TimeMachine.t.sol";
import "src/test/integration/mocks/BeaconChainMock.t.sol";
import "src/test/utils/Logger.t.sol";
import "src/test/utils/ArrayLib.sol";
interface IUserM2MainnetForkDeployer {
function delegationManager() external view returns (DelegationManager);
function strategyManager() external view returns (StrategyManager);
function eigenPodManager() external view returns (EigenPodManager);
function delegationManager_M2() external view returns (IDelegationManager_DeprecatedM2);
function strategyManager_M2() external view returns (IStrategyManager_DeprecatedM2);
function eigenPodManager_M2() external view returns (IEigenPodManager_DeprecatedM2);
function timeMachine() external view returns (TimeMachine);
function beaconChain() external view returns (BeaconChainMock);
}
/**
* @dev User_M2 used for performing mainnet M2 contract methods but also inherits User
* to perform current local contract methods after a upgrade of core contracts
* @dev UserM2 interacts with pre-pectra beacon chain
*/
contract User_M2 is User {
using ArrayLib for *;
using print for *;
IDelegationManager_DeprecatedM2 delegationManager_M2;
IStrategyManager_DeprecatedM2 strategyManager_M2;
IEigenPodManager_DeprecatedM2 eigenPodManager_M2;
constructor(string memory name) User(name) {
IUserM2MainnetForkDeployer deployer = IUserM2MainnetForkDeployer(msg.sender);
delegationManager_M2 = IDelegationManager_DeprecatedM2(address(deployer.delegationManager()));
strategyManager_M2 = IStrategyManager_DeprecatedM2(address(deployer.strategyManager()));
eigenPodManager_M2 = IEigenPodManager_DeprecatedM2(address(deployer.eigenPodManager()));
cheats.label(address(this), NAME_COLORED());
}
/// -----------------------------------------------------------------------
/// Delegation Manager Methods
/// -----------------------------------------------------------------------
function registerAsOperator_M2() public virtual createSnapshot {
print.method("registerAsOperator_M2");
IDelegationManager_DeprecatedM2.OperatorDetails memory details = IDelegationManager_DeprecatedM2.OperatorDetails({
__deprecated_earningsReceiver: address(this),
delegationApprover: address(0),
stakerOptOutWindowBlocks: 0
});
delegationManager_M2.registerAsOperator(details, "metadata");
}
/// @dev Queues a single withdrawal for every share and strategy pair
/// @dev Returns the withdrawal struct of the new slashing interface
function queueWithdrawals(IStrategy[] memory strategies, uint[] memory shares)
public
virtual
override
createSnapshot
returns (Withdrawal[] memory)
{
print.method("queueWithdrawals_M2");
address operator = delegationManager_M2.delegatedTo(address(this));
address withdrawer = address(this);
uint nonce = delegationManager_M2.cumulativeWithdrawalsQueued(address(this));
// Create queueWithdrawals params
IDelegationManager_DeprecatedM2.QueuedWithdrawalParams[] memory params =
new IDelegationManager_DeprecatedM2.QueuedWithdrawalParams[](1);
params[0] = IDelegationManager_DeprecatedM2.QueuedWithdrawalParams({strategies: strategies, shares: shares, withdrawer: withdrawer});
// Create Withdrawal struct using same info
IDelegationManager_DeprecatedM2.Withdrawal[] memory withdrawals = new IDelegationManager_DeprecatedM2.Withdrawal[](1);
withdrawals[0] = IDelegationManager_DeprecatedM2.Withdrawal({
staker: address(this),
delegatedTo: operator,
withdrawer: withdrawer,
nonce: nonce,
startBlock: uint32(block.number),
strategies: strategies,
shares: shares
});
bytes32[] memory withdrawalRoots = delegationManager_M2.queueWithdrawals(params);
// Basic sanity check - we do all other checks outside this file
assertEq(withdrawals.length, withdrawalRoots.length, "User.queueWithdrawals: length mismatch");
Withdrawal[] memory withdrawalsToReturn = new Withdrawal[](1);
withdrawalsToReturn[0] = Withdrawal({
staker: address(this),
delegatedTo: operator,
withdrawer: withdrawer,
nonce: nonce,
startBlock: uint32(block.number),
strategies: strategies,
scaledShares: shares
});
return (withdrawalsToReturn);
}
/// -----------------------------------------------------------------------
/// Eigenpod Methods
/// -----------------------------------------------------------------------
function completeCheckpoint() public virtual override createSnapshot {
print.method("completeCheckpoint_M2");
_completeCheckpoint_M2();
}
/// -----------------------------------------------------------------------
/// Strategy Methods
/// -----------------------------------------------------------------------
function updateBalances(IStrategy[] memory strategies, int[] memory tokenDeltas) public virtual override createSnapshot {
print.method("updateBalances_M2");
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
int delta = tokenDeltas[i];
if (strat == BEACONCHAIN_ETH_STRAT) {
// If any balance update has occurred, a checkpoint will pick it up
_startCheckpoint();
if (pod.activeValidatorCount() != 0) _completeCheckpoint_M2();
} else {
uint tokens = uint(delta);
IERC20 underlyingToken = strat.underlyingToken();
underlyingToken.approve(address(strategyManager), tokens);
strategyManager_M2.depositIntoStrategy(strat, underlyingToken, tokens);
}
}
}
/// -----------------------------------------------------------------------
/// Internal Methods
/// -----------------------------------------------------------------------
/// @dev Uses any ETH held by the User to start validators on the beacon chain
/// @return A list of created validator indices
/// @return The amount of wei sent to the beacon chain
/// @return The number of validators that have the MaxEB
/// Note: If the user does not have enough ETH to start a validator, this method reverts
/// Note: This method also advances one epoch forward on the beacon chain, so that
/// withdrawal credential proofs are generated for each validator.
function _startValidators() internal virtual override returns (uint40[] memory, uint64, uint) {
uint balanceWei = address(this).balance;
// Number of full validators: balance / 32 ETH
uint numValidators = balanceWei / 32 ether;
balanceWei -= (numValidators * 32 ether);
// If we still have at least 1 ETH left over, we can create another (non-full) validator
// Note that in the mock beacon chain this validator will generate rewards like any other.
// The main point is to ensure pods are able to handle validators that have less than 32 ETH
uint lastValidatorBalance;
uint totalValidators = numValidators;
if (balanceWei >= 1 ether) {
lastValidatorBalance = balanceWei - (balanceWei % 1 gwei);
balanceWei -= lastValidatorBalance;
totalValidators++;
}
require(totalValidators != 0, "startValidators: not enough ETH to start a validator");
uint40[] memory newValidators = new uint40[](totalValidators);
uint64 totalBeaconBalanceGwei = uint64((address(this).balance - balanceWei) / GWEI_TO_WEI);
console.log("- creating new validators", newValidators.length);
console.log("- depositing balance to beacon chain (gwei)", totalBeaconBalanceGwei);
// Create each of the full validators
for (uint i = 0; i < numValidators; i++) {
uint40 validatorIndex = beaconChain.newValidator{value: 32 ether}(_podWithdrawalCredentials());
newValidators[i] = validatorIndex;
validators.push(validatorIndex);
}
// If we had a remainder, create the final, non-full validator
if (totalValidators == numValidators + 1) {
uint40 validatorIndex = beaconChain.newValidator{value: lastValidatorBalance}(_podWithdrawalCredentials());
newValidators[newValidators.length - 1] = validatorIndex;
validators.push(validatorIndex);
}
return (newValidators, totalBeaconBalanceGwei, numValidators);
}
function _completeCheckpoint_M2() internal {
cheats.pauseTracing();
IEigenPod_DeprecatedM2 pod = IEigenPod_DeprecatedM2(address(pod));
console.log("- active validator count", pod.activeValidatorCount());
console.log("- proofs remaining", pod.currentCheckpoint().proofsRemaining);
uint64 checkpointTimestamp = pod.currentCheckpointTimestamp();
if (checkpointTimestamp == 0) revert("User._completeCheckpoint: no existing checkpoint");
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(validators, checkpointTimestamp);
console.log("- submitting num checkpoint proofs", proofs.balanceProofs.length);
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
cheats.resumeTracing();
}
function _completeQueuedWithdrawal_M2(IDelegationManager_DeprecatedM2.Withdrawal memory withdrawal, bool receiveAsTokens)
internal
virtual
returns (IERC20[] memory)
{
IERC20[] memory tokens = new IERC20[](withdrawal.strategies.length);
for (uint i = 0; i < tokens.length; i++) {
IStrategy strat = withdrawal.strategies[i];
if (strat == BEACONCHAIN_ETH_STRAT) {
tokens[i] = NATIVE_ETH;
// If we're withdrawing native ETH as tokens, stop ALL validators
// and complete a checkpoint
if (receiveAsTokens) {
console.log("- exiting all validators and completing checkpoint");
_exitValidators(getActiveValidators());
beaconChain.advanceEpoch_NoRewards();
_startCheckpoint();
if (pod.activeValidatorCount() != 0) _completeCheckpoint();
}
} else {
tokens[i] = strat.underlyingToken();
}
}
delegationManager_M2.completeQueuedWithdrawal(withdrawal, tokens, 0, receiveAsTokens);
return tokens;
}
/// @notice Gets the expected withdrawals to be created when the staker is undelegated via a call to `delegationManager_M2.undelegate()`
/// @notice Assumes staker and withdrawer are the same and that all strategies and shares are withdrawn
function _getExpectedM2WithdrawalStructsForStaker(address staker)
internal
view
returns (IDelegationManager_DeprecatedM2.Withdrawal[] memory)
{
(IStrategy[] memory strategies, uint[] memory shares) = delegationManager_M2.getDelegatableShares(staker);
IDelegationManager_DeprecatedM2.Withdrawal[] memory expectedWithdrawals =
new IDelegationManager_DeprecatedM2.Withdrawal[](strategies.length);
address delegatedTo = delegationManager_M2.delegatedTo(staker);
uint nonce = delegationManager_M2.cumulativeWithdrawalsQueued(staker);
for (uint i = 0; i < strategies.length; ++i) {
IStrategy[] memory singleStrategy = new IStrategy[](1);
uint[] memory singleShares = new uint[](1);
singleStrategy[0] = strategies[i];
singleShares[0] = shares[i];
expectedWithdrawals[i] = IDelegationManager_DeprecatedM2.Withdrawal({
staker: staker,
delegatedTo: delegatedTo,
withdrawer: staker,
nonce: (nonce + i),
startBlock: uint32(block.number),
strategies: singleStrategy,
shares: singleShares
});
}
return expectedWithdrawals;
}
}
/// @notice A user contract that calls nonstandard methods (like xBySignature methods)
contract User_M2_AltMethods is User_M2 {
mapping(bytes32 => bool) public signedHashes;
constructor(string memory name) User_M2(name) {}
function depositIntoEigenlayer(IStrategy[] memory strategies, uint[] memory tokenBalances) public override createSnapshot {
print.method("depositIntoEigenlayer_ALT");
uint expiry = type(uint).max;
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
uint tokenBalance = tokenBalances[i];
if (strat == BEACONCHAIN_ETH_STRAT) {
(uint40[] memory newValidators,,) = _startValidators();
// Advance forward one epoch and generate credential and balance proofs for each validator
beaconChain.advanceEpoch_NoRewards();
_verifyWithdrawalCredentials(newValidators);
} else {
// Approve token
IERC20 underlyingToken = strat.underlyingToken();
underlyingToken.approve(address(strategyManager), tokenBalance);
// Get signature
uint nonceBefore = strategyManager_M2.nonces(address(this));
bytes32 structHash = keccak256(
abi.encode(
strategyManager_M2.DEPOSIT_TYPEHASH(), address(this), strat, underlyingToken, tokenBalance, nonceBefore, expiry
)
);
bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager_M2.domainSeparator(), structHash));
bytes memory signature = bytes(abi.encodePacked(digestHash)); // dummy sig data
// Mark hash as signed
signedHashes[digestHash] = true;
// Deposit
strategyManager_M2.depositIntoStrategyWithSignature(strat, underlyingToken, tokenBalance, address(this), expiry, signature);
// Mark hash as used
signedHashes[digestHash] = false;
}
}
}
bytes4 internal constant MAGIC_VALUE = 0x1626ba7e;
function isValidSignature(bytes32 hash, bytes memory) external view returns (bytes4) {
if (signedHashes[hash]) return MAGIC_VALUE;
else return 0xffffffff;
}
}
````
## File: src/test/integration/users/User.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "src/contracts/core/AllocationManager.sol";
import "src/contracts/core/DelegationManager.sol";
import "src/contracts/permissions/PermissionController.sol";
import "src/contracts/core/StrategyManager.sol";
import "src/contracts/pods/EigenPodManager.sol";
import "src/contracts/pods/EigenPod.sol";
import "src/test/integration/TimeMachine.t.sol";
import "src/test/integration/mocks/BeaconChainMock.t.sol";
import "src/test/utils/Logger.t.sol";
import "src/test/utils/ArrayLib.sol";
struct Validator {
uint40 index;
}
interface IUserDeployer {
function allocationManager() external view returns (AllocationManager);
function delegationManager() external view returns (DelegationManager);
function permissionController() external view returns (PermissionController);
function strategyManager() external view returns (StrategyManager);
function eigenPodManager() external view returns (EigenPodManager);
function timeMachine() external view returns (TimeMachine);
function beaconChain() external view returns (BeaconChainMock);
}
contract User is Logger, IDelegationManagerTypes, IAllocationManagerTypes {
using StdStyle for *;
using SlashingLib for *;
using ArrayLib for *;
using print for *;
IStrategy constant beaconChainETHStrategy = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);
// TODO: fix this and view function getters. These are newly added contracts so these are initially
// 0 addresses for fork tests. To work around this, we read these addresses directly off the delegationManager
// from its immutable addresses. This is a temporary solution until we can figure out a better way to handle this.
// AllocationManager allocationManager;
// PermissionController permissionController;
DelegationManager delegationManager;
StrategyManager strategyManager;
EigenPodManager eigenPodManager;
TimeMachine timeMachine;
BeaconChainMock beaconChain;
uint32 public allocationDelay = 1;
string _NAME;
// User's EigenPod and each of their validator indices within that pod
EigenPod public pod;
uint40[] validators;
constructor(string memory name) {
IUserDeployer deployer = IUserDeployer(msg.sender);
// TODO uncommented for reason above
// allocationManager = deployer.allocationManager();
// permissionController = deployer.permissionController();
delegationManager = deployer.delegationManager();
strategyManager = deployer.strategyManager();
eigenPodManager = deployer.eigenPodManager();
timeMachine = deployer.timeMachine();
beaconChain = deployer.beaconChain();
_createPod();
_NAME = name;
cheats.label(address(this), NAME_COLORED());
}
modifier createSnapshot() virtual {
timeMachine.createSnapshot();
_;
}
receive() external payable {}
function NAME() public view override returns (string memory) {
return _NAME;
}
/// -----------------------------------------------------------------------
/// Allocation Manager Methods
/// -----------------------------------------------------------------------
/// @dev Allocates randomly across the operator set's strategies with a sum of `magnitudeSum`.
/// NOTE: Calling more than once will lead to deallocations...
function modifyAllocations(AllocateParams memory params) public virtual createSnapshot {
print.method(
"modifyAllocations",
string.concat(
"{avs: ", Logger(params.operatorSet.avs).NAME_COLORED(), ", operatorSetId: ", cheats.toString(params.operatorSet.id), "}"
)
);
_tryPrankAppointee_AllocationManager(IAllocationManager.modifyAllocations.selector);
allocationManager().modifyAllocations(address(this), params.toArray());
print.gasUsed();
}
function deallocateAll(OperatorSet memory operatorSet) public virtual returns (AllocateParams memory) {
AllocateParams memory params;
params.operatorSet = operatorSet;
params.strategies = allocationManager().getStrategiesInOperatorSet(operatorSet);
params.newMagnitudes = new uint64[](params.strategies.length);
modifyAllocations(params);
return params;
}
function registerForOperatorSets(OperatorSet[] memory operatorSets) public virtual createSnapshot {
for (uint i; i < operatorSets.length; ++i) {
registerForOperatorSet(operatorSets[i]);
}
}
function registerForOperatorSet(OperatorSet memory operatorSet) public virtual createSnapshot {
print.method(
"registerForOperatorSet",
string.concat("{avs: ", Logger(operatorSet.avs).NAME_COLORED(), ", operatorSetId: ", cheats.toString(operatorSet.id), "}")
);
_tryPrankAppointee_AllocationManager(IAllocationManager.registerForOperatorSets.selector);
allocationManager().registerForOperatorSets(
address(this), RegisterParams({avs: operatorSet.avs, operatorSetIds: operatorSet.id.toArrayU32(), data: ""})
);
print.gasUsed();
}
function deregisterFromOperatorSet(OperatorSet memory operatorSet) public virtual createSnapshot {
print.method(
"deregisterFromOperatorSet",
string.concat("{avs: ", Logger(operatorSet.avs).NAME_COLORED(), ", operatorSetId: ", cheats.toString(operatorSet.id), "}")
);
_tryPrankAppointee_AllocationManager(IAllocationManager.deregisterFromOperatorSets.selector);
allocationManager().deregisterFromOperatorSets(
DeregisterParams({operator: address(this), avs: operatorSet.avs, operatorSetIds: operatorSet.id.toArrayU32()})
);
print.gasUsed();
}
function setAllocationDelay(uint32 delay) public virtual createSnapshot {
print.method("setAllocationDelay");
_tryPrankAppointee_AllocationManager(IAllocationManager.setAllocationDelay.selector);
allocationManager().setAllocationDelay(address(this), delay);
print.gasUsed();
allocationDelay = delay;
}
/// -----------------------------------------------------------------------
/// Delegation Manager Methods
/// -----------------------------------------------------------------------
function registerAsOperator() public virtual createSnapshot {
print.method("registerAsOperator");
delegationManager.registerAsOperator(address(0), allocationDelay, "metadata");
print.gasUsed();
}
/// @dev Delegate to the operator without a signature
function delegateTo(User operator) public virtual createSnapshot {
print.method("delegateTo", operator.NAME_COLORED());
ISignatureUtilsMixinTypes.SignatureWithExpiry memory emptySig;
delegationManager.delegateTo(address(operator), emptySig, bytes32(0));
print.gasUsed();
}
/// @dev Undelegate from operator
function undelegate() public virtual createSnapshot returns (Withdrawal[] memory) {
print.method("undelegate");
Withdrawal[] memory expectedWithdrawals = _getExpectedWithdrawalStructsForStaker(address(this));
_tryPrankAppointee_DelegationManager(IDelegationManager.undelegate.selector);
delegationManager.undelegate(address(this));
print.gasUsed();
for (uint i = 0; i < expectedWithdrawals.length; i++) {
IStrategy strat = expectedWithdrawals[i].strategies[0];
string memory name = strat == beaconChainETHStrategy ? "Native ETH" : IERC20Metadata(address(strat.underlyingToken())).name();
console.log(
" Expecting withdrawal with nonce %s of %s for %s scaled shares.",
expectedWithdrawals[i].nonce,
name,
expectedWithdrawals[i].scaledShares[0]
);
}
return expectedWithdrawals;
}
/// @dev Redelegate to a new operator
function redelegate(User newOperator) public virtual createSnapshot returns (Withdrawal[] memory) {
print.method("redelegate", newOperator.NAME_COLORED());
Withdrawal[] memory expectedWithdrawals = _getExpectedWithdrawalStructsForStaker(address(this));
ISignatureUtilsMixinTypes.SignatureWithExpiry memory emptySig;
_tryPrankAppointee_DelegationManager(IDelegationManager.redelegate.selector);
delegationManager.redelegate(address(newOperator), emptySig, bytes32(0));
print.gasUsed();
for (uint i = 0; i < expectedWithdrawals.length; i++) {
IStrategy strat = expectedWithdrawals[i].strategies[0];
string memory name = strat == beaconChainETHStrategy ? "Native ETH" : IERC20Metadata(address(strat.underlyingToken())).name();
console.log(
" Expecting withdrawal with nonce %s of %s for %s scaled shares.",
expectedWithdrawals[i].nonce,
name,
expectedWithdrawals[i].scaledShares[0]
);
}
return expectedWithdrawals;
}
/// @dev Force undelegate staker
function forceUndelegate(User staker) public virtual createSnapshot returns (Withdrawal[] memory) {
print.method("forceUndelegate", staker.NAME());
Withdrawal[] memory expectedWithdrawals = _getExpectedWithdrawalStructsForStaker(address(staker));
delegationManager.undelegate(address(staker));
print.gasUsed();
return expectedWithdrawals;
}
/// @dev Queues a single withdrawal for every share and strategy pair
function queueWithdrawals(IStrategy[] memory strategies, uint[] memory depositShares)
public
virtual
createSnapshot
returns (Withdrawal[] memory)
{
print.method("queueWithdrawals");
address operator = delegationManager.delegatedTo(address(this));
uint nonce = delegationManager.cumulativeWithdrawalsQueued(address(this));
// Create queueWithdrawals params
QueuedWithdrawalParams[] memory params = new QueuedWithdrawalParams[](1);
params[0] = QueuedWithdrawalParams({strategies: strategies, depositShares: depositShares, __deprecated_withdrawer: address(0)});
uint[] memory scaledSharesForWithdrawal = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; ++i) {
DepositScalingFactor memory dsf = DepositScalingFactor(delegationManager.depositScalingFactor(address(this), strategies[i]));
scaledSharesForWithdrawal[i] = dsf.scaleForQueueWithdrawal(depositShares[i]);
}
// Create Withdrawal struct using same info
Withdrawal[] memory withdrawals = new Withdrawal[](1);
withdrawals[0] = Withdrawal({
staker: address(this),
delegatedTo: operator,
withdrawer: address(this),
nonce: nonce,
startBlock: uint32(block.number),
strategies: strategies,
scaledShares: scaledSharesForWithdrawal
});
bytes32[] memory withdrawalRoots = delegationManager.queueWithdrawals(params);
print.gasUsed();
// Basic sanity check - we do all other checks outside this file
assertEq(withdrawals.length, withdrawalRoots.length, "User.queueWithdrawals: length mismatch");
return (withdrawals);
}
function completeWithdrawalsAsTokens(Withdrawal[] memory withdrawals)
public
virtual
createSnapshot
returns (IERC20[][] memory tokens)
{
print.method("completeWithdrawalsAsTokens");
tokens = new IERC20[][](withdrawals.length);
for (uint i = 0; i < withdrawals.length; i++) {
tokens[i] = _completeQueuedWithdrawal(withdrawals[i], true);
}
}
function completeWithdrawalAsTokens(Withdrawal memory withdrawal) public virtual createSnapshot returns (IERC20[] memory) {
print.method("completeWithdrawalsAsTokens");
return _completeQueuedWithdrawal(withdrawal, true);
}
function completeWithdrawalsAsShares(Withdrawal[] memory withdrawals)
public
virtual
createSnapshot
returns (IERC20[][] memory tokens)
{
print.method("completeWithdrawalsAsShares");
tokens = new IERC20[][](withdrawals.length);
for (uint i = 0; i < withdrawals.length; i++) {
tokens[i] = _completeQueuedWithdrawal(withdrawals[i], false);
}
}
function completeWithdrawalAsShares(Withdrawal memory withdrawal) public virtual createSnapshot returns (IERC20[] memory) {
print.method("completeWithdrawalAsShares");
return _completeQueuedWithdrawal(withdrawal, false);
}
/// -----------------------------------------------------------------------
/// Beacon Chain Methods
/// -----------------------------------------------------------------------
/// @dev Uses any ETH held by the User to start validators on the beacon chain
/// @return A list of created validator indices
/// @return The amount of wei sent to the beacon chain
/// @return The number of validators that have the MaxEB
/// Note: If the user does not have enough ETH to start a validator, this method reverts
/// Note: This method also advances one epoch forward on the beacon chain, so that
/// withdrawal credential proofs are generated for each validator.
function startValidators() public virtual createSnapshot returns (uint40[] memory, uint64, uint) {
print.method("startValidators");
return _startValidators();
}
/// @dev Starts a specified number of validators for 32 ETH each on the beacon chain
/// @param numValidators The number of validators to start
/// @return A list of created validator indices
/// @return The amount of wei sent to the beacon chain
function startValidators(uint8 numValidators) public virtual createSnapshot returns (uint40[] memory, uint64, uint) {
require(numValidators > 0 && numValidators <= 10, "startValidators: numValidators must be between 1 and 10");
uint balanceWei = address(this).balance;
// given a number of validators, the current balance, calculate the amount of ETH needed to start that many validators
uint ethNeeded = numValidators * 32 ether - balanceWei;
cheats.deal(address(this), ethNeeded);
print.method("startValidators");
return _startValidators();
}
function exitValidators(uint40[] memory _validators) public virtual createSnapshot returns (uint64 exitedBalanceGwei) {
print.method("exitValidators");
return _exitValidators(_validators);
}
/// -----------------------------------------------------------------------
/// Eigenpod Methods
/// -----------------------------------------------------------------------
function verifyWithdrawalCredentials(uint40[] memory _validators) public virtual createSnapshot {
print.method("verifyWithdrawalCredentials");
_verifyWithdrawalCredentials(_validators);
}
function startCheckpoint() public virtual createSnapshot {
print.method("startCheckpoint");
_startCheckpoint();
}
function completeCheckpoint() public virtual createSnapshot {
print.method("completeCheckpoint");
_completeCheckpoint();
}
function verifyStaleBalance(uint40 validatorIndex) public virtual createSnapshot {
print.method("verifyStaleBalance");
StaleBalanceProofs memory proof = beaconChain.getStaleBalanceProofs(validatorIndex);
try pod.verifyStaleBalance({
beaconTimestamp: proof.beaconTimestamp,
stateRootProof: proof.stateRootProof,
proof: proof.validatorProof
}) {} catch (bytes memory err) {
_revert(err);
}
}
/// -----------------------------------------------------------------------
/// Strategy Methods
/// -----------------------------------------------------------------------
/// @dev For each strategy/token balance, call the relevant deposit method
function depositIntoEigenlayer(IStrategy[] memory strategies, uint[] memory tokenBalances) public virtual createSnapshot {
print.method("depositIntoEigenlayer");
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
uint tokenBalance = tokenBalances[i];
if (strat == BEACONCHAIN_ETH_STRAT) {
(uint40[] memory newValidators,,) = _startValidators();
// Advance forward one epoch and generate credential and balance proofs for each validator
beaconChain.advanceEpoch_NoRewards();
_verifyWithdrawalCredentials(newValidators);
} else {
IERC20 underlyingToken = strat.underlyingToken();
underlyingToken.approve(address(strategyManager), tokenBalance);
strategyManager.depositIntoStrategy(strat, underlyingToken, tokenBalance);
print.gasUsed();
}
}
}
function updateBalances(IStrategy[] memory strategies, int[] memory tokenDeltas) public virtual createSnapshot {
print.method("updateBalances");
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
int delta = tokenDeltas[i];
if (strat == BEACONCHAIN_ETH_STRAT) {
// If any balance update has occurred, a checkpoint will pick it up
_startCheckpoint();
if (pod.activeValidatorCount() != 0) _completeCheckpoint();
} else {
uint tokens = uint(delta);
IERC20 underlyingToken = strat.underlyingToken();
underlyingToken.approve(address(strategyManager), tokens);
strategyManager.depositIntoStrategy(strat, underlyingToken, tokens);
print.gasUsed();
}
}
}
/// -----------------------------------------------------------------------
/// View Methods
/// -----------------------------------------------------------------------
function allocationManager() public view returns (AllocationManager) {
return AllocationManager(address(delegationManager.allocationManager()));
}
function permissionController() public view returns (PermissionController) {
return PermissionController(address(delegationManager.permissionController()));
}
function getSlashingFactor(IStrategy strategy) public view returns (uint) {
return _getSlashingFactor(address(this), strategy);
}
/// -----------------------------------------------------------------------
/// Internal Methods
/// -----------------------------------------------------------------------
function _completeQueuedWithdrawal(Withdrawal memory withdrawal, bool receiveAsTokens) internal virtual returns (IERC20[] memory) {
IERC20[] memory tokens = new IERC20[](withdrawal.strategies.length);
for (uint i = 0; i < tokens.length; i++) {
IStrategy strat = withdrawal.strategies[i];
if (strat == BEACONCHAIN_ETH_STRAT) {
tokens[i] = NATIVE_ETH;
// If we're withdrawing native ETH as tokens && do not have negative shares
// stop ALL validators and complete a checkpoint
if (receiveAsTokens && eigenPodManager.podOwnerDepositShares(address(this)) >= 0) {
console.log("- exiting all validators and completing checkpoint");
_exitValidators(getActiveValidators());
beaconChain.advanceEpoch_NoRewards();
_startCheckpoint();
if (pod.activeValidatorCount() != 0) _completeCheckpoint();
}
} else {
tokens[i] = strat.underlyingToken();
}
}
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens);
print.gasUsed();
return tokens;
}
function _createPod() internal virtual {
pod = EigenPod(payable(eigenPodManager.createPod()));
}
/// @dev Uses any ETH held by the User to start validators on the beacon chain
/// @dev Creates validators between 32 and 2048 ETH
/// @return A list of created validator indices
/// @return The amount of wei sent to the beacon chain
/// @return The number of validators that have the MaxEB
/// Note: If the user does not have enough ETH to start a validator, this method reverts
/// Note: This method also advances one epoch forward on the beacon chain, so that
/// withdrawal credential proofs are generated for each validator.
function _startValidators() internal virtual returns (uint40[] memory, uint64, uint) {
uint originalBalance = address(this).balance;
uint balanceWei = address(this).balance;
uint numValidators = 0;
uint maxEBValidators = 0;
// Get maximum possible number of validators. Add 1 to account for a validator with < 32 ETH
uint maxValidators = balanceWei / 32 ether;
uint40[] memory newValidators = new uint40[](maxValidators + 1);
// Create validators between 32 and 2048 ETH until we can't create more
while (balanceWei >= 32 ether) {
// Generate random validator balance between 32 and 2048 ETH
uint validatorEth = uint(keccak256(abi.encodePacked(block.timestamp, balanceWei, numValidators))) % 64 + 1; // 1-64 multiplier
validatorEth *= 32 ether; // Results in 32-2048 ETH
// If we don't have enough ETH for the random amount, use remaining balance
// as long as it's >= 32 ETH
if (balanceWei < validatorEth) {
if (balanceWei >= 32 ether) validatorEth = balanceWei - (balanceWei % 32 ether);
else break;
}
// Track validators with maximum effective balance
if (validatorEth == 2048 ether) maxEBValidators++;
// Create the validator
bytes memory withdrawalCredentials =
validatorEth == 32 ether ? _podWithdrawalCredentials() : _podCompoundingWithdrawalCredentials();
uint40 validatorIndex = beaconChain.newValidator{value: validatorEth}(withdrawalCredentials);
newValidators[numValidators] = validatorIndex;
validators.push(validatorIndex);
balanceWei -= validatorEth;
numValidators++;
}
// If we still have at least 1 ETH left over, we can create another (non-full) validator
// Note that in the mock beacon chain this validator will generate rewards like any other.
// The main point is to ensure pods are able to handle validators that have less than 32 ETH
if (balanceWei >= 1 ether) {
uint lastValidatorBalance = balanceWei - (balanceWei % 1 gwei);
uint40 validatorIndex = beaconChain.newValidator{value: lastValidatorBalance}(_podWithdrawalCredentials());
newValidators[numValidators] = validatorIndex;
validators.push(validatorIndex);
balanceWei -= lastValidatorBalance;
numValidators++;
}
// Resize the array to actual number of validators created
assembly {
mstore(newValidators, numValidators)
}
require(numValidators != 0, "startValidators: not enough ETH to start a validator");
uint64 totalBeaconBalanceGwei = uint64((originalBalance - balanceWei) / GWEI_TO_WEI);
console.log("- created new validators", newValidators.length);
console.log("- deposited balance to beacon chain (gwei)", totalBeaconBalanceGwei);
// Advance forward one epoch and generate withdrawal and balance proofs for each validator
beaconChain.advanceEpoch_NoRewards();
return (newValidators, totalBeaconBalanceGwei, maxEBValidators);
}
function _exitValidators(uint40[] memory _validators) internal returns (uint64 exitedBalanceGwei) {
console.log("- exiting num validators", _validators.length);
for (uint i = 0; i < _validators.length; i++) {
exitedBalanceGwei += beaconChain.exitValidator(_validators[i]);
}
console.log("- exited balance to pod (gwei)", exitedBalanceGwei);
return exitedBalanceGwei;
}
function _startCheckpoint() internal {
try pod.startCheckpoint(false) {}
catch (bytes memory err) {
_revert(err);
}
}
function _completeCheckpoint() internal {
cheats.pauseTracing();
console.log("- active validator count", pod.activeValidatorCount());
console.log("- proofs remaining", pod.currentCheckpoint().proofsRemaining);
uint64 checkpointTimestamp = pod.currentCheckpointTimestamp();
if (checkpointTimestamp == 0) revert("User._completeCheckpoint: no existing checkpoint");
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(validators, checkpointTimestamp);
console.log("- submitting num checkpoint proofs", proofs.balanceProofs.length);
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
cheats.resumeTracing();
}
function _verifyWithdrawalCredentials(uint40[] memory _validators) internal {
cheats.pauseTracing();
CredentialProofs memory proofs = beaconChain.getCredentialProofs(_validators);
try pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: _validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
}) {} catch (bytes memory err) {
_revert(err);
}
cheats.resumeTracing();
}
/// @dev Revert, passing through an error message
function _revert(bytes memory err) internal pure {
if (err.length != 0) {
assembly {
revert(add(32, err), mload(err))
}
}
revert("reverted with unknown error");
}
function _podWithdrawalCredentials() internal view returns (bytes memory) {
return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(pod));
}
function _podCompoundingWithdrawalCredentials() internal view returns (bytes memory) {
return abi.encodePacked(bytes1(uint8(2)), bytes11(0), address(pod));
}
function _getSlashingFactor(address staker, IStrategy strategy) internal view returns (uint) {
address operator = delegationManager.delegatedTo(staker);
uint64 maxMagnitude = allocationManager().getMaxMagnitudes(operator, strategy.toArray())[0];
if (strategy == beaconChainETHStrategy) return maxMagnitude.mulWad(eigenPodManager.beaconChainSlashingFactor(staker));
return maxMagnitude;
}
/// @notice Gets the expected withdrawals to be created when the staker is undelegated via a call to `DelegationManager.undelegate()`
/// @notice Assumes staker and withdrawer are the same and that all strategies and shares are withdrawn
function _getExpectedWithdrawalStructsForStaker(address staker) internal view returns (Withdrawal[] memory expectedWithdrawals) {
(IStrategy[] memory strategies,) = delegationManager.getDepositedShares(staker);
expectedWithdrawals = new Withdrawal[](strategies.length);
(, uint[] memory depositShares) = delegationManager.getWithdrawableShares(staker, strategies);
address delegatedTo = delegationManager.delegatedTo(staker);
uint nonce = delegationManager.cumulativeWithdrawalsQueued(staker);
for (uint i = 0; i < strategies.length; ++i) {
DepositScalingFactor memory dsf = DepositScalingFactor(delegationManager.depositScalingFactor(staker, strategies[i]));
uint scaledShares = dsf.scaleForQueueWithdrawal(depositShares[i]);
expectedWithdrawals[i] = Withdrawal({
staker: staker,
delegatedTo: delegatedTo,
withdrawer: staker,
nonce: (nonce + i),
startBlock: uint32(block.number),
strategies: strategies[i].toArray(),
scaledShares: scaledShares.toArrayU256()
});
}
}
function getActiveValidators() public view returns (uint40[] memory) {
uint40[] memory activeValidators = new uint40[](validators.length);
uint numActive;
uint pos;
for (uint i = 0; i < validators.length; i++) {
if (beaconChain.isActive(validators[i])) {
activeValidators[pos] = validators[i];
numActive++;
pos++;
}
}
// Manually update length
assembly {
mstore(activeValidators, numActive)
}
return activeValidators;
}
function _tryPrankAppointee(address target, bytes4 selector) internal {
address[] memory appointees = permissionController().getAppointees(address(this), target, selector);
if (appointees.length != 0) cheats.prank(appointees[0]);
}
function _tryPrankAppointee_AllocationManager(bytes4 selector) internal {
return _tryPrankAppointee(address(allocationManager()), selector);
}
function _tryPrankAppointee_DelegationManager(bytes4 selector) internal {
return _tryPrankAppointee(address(delegationManager), selector);
}
}
/// @notice A user contract that calls nonstandard methods (like xBySignature methods)
contract User_AltMethods is User {
mapping(bytes32 => bool) public signedHashes;
constructor(string memory name) User(name) {}
function depositIntoEigenlayer(IStrategy[] memory strategies, uint[] memory tokenBalances) public override createSnapshot {
print.method("depositIntoEigenlayer_ALT");
uint expiry = type(uint).max;
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
uint tokenBalance = tokenBalances[i];
if (strat == BEACONCHAIN_ETH_STRAT) {
(uint40[] memory newValidators,,) = _startValidators();
// Advance forward one epoch and generate credential and balance proofs for each validator
beaconChain.advanceEpoch_NoRewards();
_verifyWithdrawalCredentials(newValidators);
} else {
// Approve token
IERC20 underlyingToken = strat.underlyingToken();
underlyingToken.approve(address(strategyManager), tokenBalance);
// Get signature
uint nonceBefore = strategyManager.nonces(address(this));
bytes32 structHash = keccak256(
abi.encode(strategyManager.DEPOSIT_TYPEHASH(), address(this), strat, underlyingToken, tokenBalance, nonceBefore, expiry)
);
bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash));
bytes memory signature = bytes(abi.encodePacked(digestHash)); // dummy sig data
// Mark hash as signed
signedHashes[digestHash] = true;
// Deposit
strategyManager.depositIntoStrategyWithSignature(strat, underlyingToken, tokenBalance, address(this), expiry, signature);
// Mark hash as used
signedHashes[digestHash] = false;
}
}
}
bytes4 internal constant MAGIC_VALUE = 0x1626ba7e;
function isValidSignature(bytes32 hash, bytes memory) external view returns (bytes4) {
if (signedHashes[hash]) return MAGIC_VALUE;
else return 0xffffffff;
}
}
````
## File: src/test/integration/IntegrationBase.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "src/contracts/libraries/BeaconChainProofs.sol";
import "src/contracts/libraries/SlashingLib.sol";
import "src/test/integration/TypeImporter.t.sol";
import "src/test/integration/IntegrationDeployer.t.sol";
import "src/test/integration/TimeMachine.t.sol";
import "src/test/integration/users/User.t.sol";
import "src/test/integration/users/User_M1.t.sol";
abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
using StdStyle for *;
using SlashingLib for *;
using Math for uint;
using Strings for *;
using print for *;
using ArrayLib for *;
uint numStakers = 0;
uint numOperators = 0;
uint numAVSs = 0;
// Lists of operators created before the m2 (not slashing) upgrade
//
// When we call _upgradeEigenLayerContracts, we iterate over
// these lists and migrate perform the standard migration actions
// for each user
User[] operatorsToMigrate;
User[] stakersToMigrate;
/**
* Gen/Init methods:
*/
/**
* @dev Create a new user according to configured random variants.
* This user is ready to deposit into some strategies and has some underlying token balances
*/
function _newRandomStaker() internal returns (User, IStrategy[] memory, uint[] memory) {
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _randUser(_getStakerName());
if (!isUpgraded) stakersToMigrate.push(staker);
assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "_newRandomStaker: failed to award token balances");
return (staker, strategies, tokenBalances);
}
/// Given a list of strategies, creates a new user with random token balances in each underlying token
function _newStaker(IStrategy[] memory strategies) internal returns (User, uint[] memory) {
(User staker, uint[] memory tokenBalances) = _randUser(_getStakerName(), strategies);
if (!isUpgraded) stakersToMigrate.push(staker);
assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "_newStaker: failed to award token balances");
return (staker, tokenBalances);
}
/**
* @dev Create a new operator according to configured random variants.
* This user will immediately deposit their randomized assets into eigenlayer.
*/
function _newRandomOperator() internal returns (User, IStrategy[] memory, uint[] memory) {
/// TODO: Allow operators to have ETH
(User operator, IStrategy[] memory strategies, uint[] memory tokenBalances) = _randUser_NoETH(_getOperatorName());
/// Operators are created with all assets already deposited
uint[] memory addedShares = _calculateExpectedShares(strategies, tokenBalances);
operator.depositIntoEigenlayer(strategies, tokenBalances);
/// Registration flow differs for M2 vs Slashing release
if (!isUpgraded) {
User_M2(payable(operator)).registerAsOperator_M2();
operatorsToMigrate.push(operator);
} else {
operator.registerAsOperator();
rollForward({blocks: ALLOCATION_CONFIGURATION_DELAY + 1});
}
assert_Snap_Added_OperatorShares(operator, strategies, addedShares, "_newRandomOperator: failed to award shares to operator");
assertTrue(delegationManager.isOperator(address(operator)), "_newRandomOperator: operator should be registered");
assertEq(delegationManager.delegatedTo(address(operator)), address(operator), "_newRandomOperator: should be self-delegated");
return (operator, strategies, tokenBalances);
}
/// @dev Creates a new operator with no assets
function _newRandomOperator_NoAssets() internal returns (User) {
User operator = _randUser_NoAssets(_getOperatorName());
/// Registration flow differs for M2 vs Slashing release
if (!isUpgraded) {
User_M2(payable(operator)).registerAsOperator_M2();
operatorsToMigrate.push(operator);
} else {
operator.registerAsOperator();
rollForward({blocks: ALLOCATION_CONFIGURATION_DELAY + 1});
}
assertTrue(delegationManager.isOperator(address(operator)), "_newRandomOperator: operator should be registered");
assertEq(delegationManager.delegatedTo(address(operator)), address(operator), "_newRandomOperator: should be self-delegated");
return operator;
}
/// @dev Name a newly-created staker ("staker1", "staker2", ...)
function _getStakerName() private returns (string memory) {
numStakers++;
string memory stakerNum = cheats.toString(numStakers);
string memory namePrefix = isUpgraded ? "staker" : "m2-staker";
return string.concat(namePrefix, stakerNum);
}
/// @dev Name a newly-created operator ("operator1", "operator2", ...)
function _getOperatorName() private returns (string memory) {
numOperators++;
string memory operatorNum = cheats.toString(numOperators);
string memory namePrefix = isUpgraded ? "operator" : "m2-operator";
return string.concat(namePrefix, operatorNum);
}
function _newRandomAVS() internal returns (AVS avs, OperatorSet[] memory operatorSets) {
string memory avsName = string.concat("avs", numAVSs.toString());
avs = _genRandAVS(avsName);
avs.updateAVSMetadataURI("https://example.com");
operatorSets = avs.createOperatorSets(_randomStrategies());
++numAVSs;
}
/// @dev Send a random amount of ETH (up to 10 gwei) to the destination via `call`,
/// triggering its fallback function. Sends a gwei-divisible amount as well as a
/// non-gwei-divisible amount.
///
/// Total sent == `gweiSent + remainderSent`
function _sendRandomETH(address destination) internal returns (uint64 gweiSent, uint remainderSent) {
gweiSent = uint64(_randUint({min: 1, max: 10}));
remainderSent = _randUint({min: 1, max: 100});
uint totalSent = (gweiSent * GWEI_TO_WEI) + remainderSent;
cheats.deal(address(this), address(this).balance + totalSent);
bool r;
bytes memory d;
(r, d) = destination.call{value: totalSent}("");
return (gweiSent, remainderSent);
}
/// @dev Choose a random subset of validators (selects AT LEAST ONE)
function _choose(uint40[] memory validators) internal returns (uint40[] memory) {
uint _rand = _randUint({min: 1, max: (2 ** validators.length) - 1});
uint40[] memory result = new uint40[](validators.length);
uint newLen;
for (uint i = 0; i < validators.length; i++) {
// if bit set, add validator
if (_rand >> i & 1 == 1) {
result[newLen] = validators[i];
newLen++;
}
}
// Manually update length of result
assembly {
mstore(result, newLen)
}
return result;
}
function _getTokenName(IERC20 token) internal view returns (string memory) {
if (token == NATIVE_ETH) return "Native ETH";
return IERC20Metadata(address(token)).name();
}
/**
*
* COMMON ASSERTIONS
*
*/
function assert_HasNoDelegatableShares(User user, string memory err) internal view {
(IStrategy[] memory strategies, uint[] memory shares) = delegationManager.getDepositedShares(address(user));
assertEq(strategies.length, 0, err);
assertEq(strategies.length, shares.length, "assert_HasNoDelegatableShares: return length mismatch");
}
function assert_HasUnderlyingTokenBalances(User user, IStrategy[] memory strategies, uint[] memory expectedBalances, string memory err)
internal
view
{
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
uint expectedBalance = expectedBalances[i];
uint tokenBalance;
if (strat == BEACONCHAIN_ETH_STRAT) tokenBalance = address(user).balance;
else tokenBalance = strat.underlyingToken().balanceOf(address(user));
assertApproxEqAbs(expectedBalance, tokenBalance, 1, err);
}
}
function assert_HasNoUnderlyingTokenBalance(User user, IStrategy[] memory strategies, string memory err) internal view {
assert_HasUnderlyingTokenBalances(user, strategies, new uint[](strategies.length), err);
}
function assert_HasExpectedShares(User user, IStrategy[] memory strategies, uint[] memory expectedShares, string memory err)
internal
view
{
uint[] memory actualShares = _getStakerDepositShares(user, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertApproxEqAbs(expectedShares[i], actualShares[i], 1, err);
}
}
/// @dev Check that all the staker's deposit shares have been removed
function assert_RemovedAll_Staker_DepositShares(User user, IStrategy[] memory strategies, string memory err) internal view {
uint[] memory depositShares = _getStakerDepositShares(user, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(depositShares[i], 0, err);
}
}
/// @dev Check that all the staker's withdrawable shares have been removed
function assert_RemovedAll_Staker_WithdrawableShares(User staker, IStrategy[] memory strategies, string memory err) internal view {
uint[] memory curShares = _getStakerWithdrawableShares(staker, strategies);
// For each strategy, check all shares have been withdrawn
for (uint i = 0; i < strategies.length; i++) {
assertEq(0, curShares[i], err);
}
}
function assert_HasOperatorShares(User user, IStrategy[] memory strategies, uint[] memory expectedShares, string memory err)
internal
view
{
for (uint i = 0; i < strategies.length; i++) {
uint actualShares = delegationManager.operatorShares(address(user), strategies[i]);
assertEq(expectedShares[i], actualShares, err);
}
}
/// @dev Asserts that ALL of the `withdrawalRoots` is in `delegationManager.pendingWithdrawals`
function assert_AllWithdrawalsPending(bytes32[] memory withdrawalRoots, string memory err) internal view {
for (uint i = 0; i < withdrawalRoots.length; i++) {
assert_WithdrawalPending(withdrawalRoots[i], err);
}
}
/// @dev Asserts that NONE of the `withdrawalRoots` is in `delegationManager.pendingWithdrawals`
function assert_NoWithdrawalsPending(bytes32[] memory withdrawalRoots, string memory err) internal view {
for (uint i = 0; i < withdrawalRoots.length; i++) {
assert_WithdrawalNotPending(withdrawalRoots[i], err);
}
}
/// @dev Asserts that the hash of each withdrawal corresponds to the provided withdrawal root
function assert_WithdrawalPending(bytes32 withdrawalRoot, string memory err) internal view {
assertTrue(delegationManager.pendingWithdrawals(withdrawalRoot), err);
}
function assert_WithdrawalNotPending(bytes32 withdrawalRoot, string memory err) internal view {
assertFalse(delegationManager.pendingWithdrawals(withdrawalRoot), err);
}
function assert_ValidWithdrawalHashes(Withdrawal[] memory withdrawals, bytes32[] memory withdrawalRoots, string memory err)
internal
view
{
for (uint i = 0; i < withdrawals.length; i++) {
assert_ValidWithdrawalHash(withdrawals[i], withdrawalRoots[i], err);
}
}
function assert_ValidWithdrawalHash(Withdrawal memory withdrawal, bytes32 withdrawalRoot, string memory err) internal view {
assertEq(withdrawalRoot, delegationManager.calculateWithdrawalRoot(withdrawal), err);
}
function assert_StakerStrategyListEmpty(User staker, string memory err) internal view {
IStrategy[] memory strategies = _getStakerStrategyList(staker);
assertEq(strategies.length, 0, err);
}
function assert_StrategyNotInStakerStrategyList(User staker, IStrategy strategy, string memory err) internal view {
// BEACONCHAIN_ETH_STRAT is not in the staker's strategy list
if (strategy == BEACONCHAIN_ETH_STRAT) return;
IStrategy[] memory strategies = _getStakerStrategyList(staker);
assertFalse(strategies.contains(strategy), err);
}
function assert_StrategiesInStakerStrategyList(User staker, IStrategy[] memory strategies, string memory err) internal view {
for (uint i = 0; i < strategies.length; i++) {
assert_StrategyInStakerStrategyList(staker, strategies[i], err);
}
}
function assert_StrategyInStakerStrategyList(User staker, IStrategy strategy, string memory err) internal view {
// BEACONCHAIN_ETH_STRAT is not in the staker's strategy list
if (strategy == BEACONCHAIN_ETH_STRAT) return;
IStrategy[] memory strategies = _getStakerStrategyList(staker);
assertTrue(strategies.contains(strategy), err);
}
function assert_PodBalance_Eq(User staker, uint expectedBalance, string memory err) internal view {
EigenPod pod = staker.pod();
assertEq(address(pod).balance, expectedBalance, err);
}
function assert_ProofsRemainingEqualsActive(User staker, string memory err) internal view {
EigenPod pod = staker.pod();
console.log("proofsRemaining: ", pod.currentCheckpoint().proofsRemaining);
console.log("activeValidatorCount: ", pod.activeValidatorCount());
assertEq(pod.currentCheckpoint().proofsRemaining, pod.activeValidatorCount(), err);
}
function assert_CheckpointPodBalance(User staker, uint64 expectedPodBalanceGwei, string memory err) internal view {
EigenPod pod = staker.pod();
assertEq(pod.currentCheckpoint().podBalanceGwei, expectedPodBalanceGwei, err);
}
function assert_MaxEqualsAllocatablePlusEncumbered(User operator, string memory err) internal view {
Magnitudes[] memory mags = _getMagnitudes(operator, allStrats);
for (uint i = 0; i < allStrats.length; i++) {
Magnitudes memory m = mags[i];
assertEq(m.max, m.encumbered + m.allocatable, err);
}
}
function assert_CurMinSlashableEqualsMinAllocated(
User operator,
OperatorSet memory operatorSet,
IStrategy[] memory strategies,
string memory err
) internal view {
uint[] memory minSlashableStake = _getMinSlashableStake(operator, operatorSet, strategies);
uint[] memory minAllocatedStake = _getAllocatedStake(operator, operatorSet, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(minSlashableStake[i], minAllocatedStake[i], err);
}
}
function assert_MaxMagsEqualMaxMagsAtCurrentBlock(User operator, IStrategy[] memory strategies, string memory err) internal view {
uint64[] memory maxMagnitudes = _getMaxMagnitudes(operator, strategies);
uint64[] memory maxAtCurrentBlock = _getMaxMagnitudes(operator, strategies, uint32(block.number));
for (uint i = 0; i < strategies.length; i++) {
assertEq(maxMagnitudes[i], maxAtCurrentBlock[i], err);
}
}
function assert_CurrentMagnitude(User operator, AllocateParams memory params, string memory err) internal view {
Allocation[] memory allocations = _getAllocations(operator, params.operatorSet, params.strategies);
for (uint i = 0; i < allocations.length; i++) {
assertEq(allocations[i].currentMagnitude, params.newMagnitudes[i], err);
}
}
function assert_NoPendingModification(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies, string memory err)
internal
view
{
Allocation[] memory allocations = _getAllocations(operator, operatorSet, strategies);
for (uint i = 0; i < allocations.length; i++) {
assertEq(0, allocations[i].effectBlock, err);
}
}
function assert_HasPendingIncrease(User operator, AllocateParams memory params, string memory err) internal view {
uint32 delay = _getExistingAllocationDelay(operator);
Allocation[] memory allocations = _getAllocations(operator, params.operatorSet, params.strategies);
for (uint i = 0; i < allocations.length; i++) {
assertEq(allocations[i].effectBlock, uint32(block.number) + delay, err);
assertTrue(allocations[i].currentMagnitude != params.newMagnitudes[i], err);
assertGt(allocations[i].pendingDiff, 0, err);
}
}
function assert_HasPendingDecrease(User operator, AllocateParams memory params, string memory err) internal view {
uint32 deallocationDelay = allocationManager.DEALLOCATION_DELAY();
Allocation[] memory allocations = _getAllocations(operator, params.operatorSet, params.strategies);
for (uint i = 0; i < allocations.length; i++) {
assertEq(allocations[i].effectBlock, uint32(block.number) + deallocationDelay + 1, err);
assertTrue(allocations[i].currentMagnitude != params.newMagnitudes[i], err);
assertLt(allocations[i].pendingDiff, 0, err);
}
}
function assert_IsRegistered(User operator, OperatorSet memory operatorSet, string memory err) internal view {
assertTrue(allocationManager.isMemberOfOperatorSet(address(operator), operatorSet), err);
}
function assert_IsSlashable(User operator, OperatorSet memory operatorSet, string memory err) internal view {
assertTrue(allocationManager.isOperatorSlashable(address(operator), operatorSet), err);
}
function assert_NotSlashable(User operator, OperatorSet memory operatorSet, string memory err) internal view {
assertFalse(allocationManager.isOperatorSlashable(address(operator), operatorSet), err);
}
function assert_IsAllocatedToSet(User operator, OperatorSet memory operatorSet, string memory err) internal view {
assertTrue(allocationManager.getAllocatedSets(address(operator)).contains(operatorSet), err);
}
function assert_IsNotAllocated(User operator, OperatorSet memory operatorSet, string memory err) internal view {
assertEq(allocationManager.getAllocatedStrategies(address(operator), operatorSet).length, 0, err);
}
function assert_IsAllocatedToSetStrats(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies, string memory err)
internal
view
{
IStrategy[] memory allocatedStrategies = allocationManager.getAllocatedStrategies(address(operator), operatorSet);
for (uint i = 0; i < strategies.length; i++) {
assertTrue(allocatedStrategies.contains(strategies[i]), err);
}
}
function assert_HasAllocatedStake(User operator, AllocateParams memory params, string memory err) internal view {
OperatorSet memory operatorSet = params.operatorSet;
IStrategy[] memory strategies = params.strategies;
uint64[] memory curMagnitudes = params.newMagnitudes;
uint64[] memory maxMagnitudes = _getMaxMagnitudes(operator, params.strategies);
uint[] memory operatorShares = _getOperatorShares(operator, params.strategies);
uint[] memory allocatedStake = _getAllocatedStake(operator, operatorSet, strategies);
for (uint i = 0; i < strategies.length; i++) {
uint expectedAllocated;
if (maxMagnitudes[i] == 0) {
expectedAllocated = 0;
} else {
uint slashableProportion = uint(curMagnitudes[i]).divWad(maxMagnitudes[i]);
expectedAllocated = operatorShares[i].mulWad(slashableProportion);
}
assertEq(expectedAllocated, allocatedStake[i], err);
}
}
function assert_HasSlashableStake(User operator, AllocateParams memory params, string memory err) internal view {
OperatorSet memory operatorSet = params.operatorSet;
IStrategy[] memory strategies = params.strategies;
uint64[] memory curMagnitudes = params.newMagnitudes;
uint64[] memory maxMagnitudes = _getMaxMagnitudes(operator, params.strategies);
uint[] memory operatorShares = _getOperatorShares(operator, params.strategies);
uint[] memory slashableStake = _getMinSlashableStake(operator, operatorSet, strategies);
for (uint i = 0; i < strategies.length; i++) {
uint expectedSlashable;
if (maxMagnitudes[i] == 0) {
expectedSlashable = 0;
} else {
uint slashableProportion = uint(curMagnitudes[i]).divWad(maxMagnitudes[i]);
expectedSlashable = operatorShares[i].mulWad(slashableProportion);
}
assertEq(expectedSlashable, slashableStake[i], err);
}
}
function assert_NoSlashableStake(User operator, OperatorSet memory operatorSet, string memory err) internal view {
IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet);
uint[] memory slashableStake = _getMinSlashableStake(operator, operatorSet, strategies);
for (uint i = 0; i < slashableStake.length; i++) {
assertEq(slashableStake[i], 0, err);
}
}
function assert_DSF_WAD(User staker, IStrategy[] memory strategies, string memory err) internal view {
uint[] memory depositScalingFactors = _getDepositScalingFactors(staker, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(depositScalingFactors[i], WAD, err);
}
}
function assert_Zero_BCSF(User staker, string memory err) internal view {
uint64 curBCSF = _getBeaconChainSlashingFactor(staker);
assertEq(curBCSF, 0, err);
}
function assert_BCSF_WAD(User staker, string memory err) internal view {
uint64 curBCSF = _getBeaconChainSlashingFactor(staker);
assertEq(curBCSF, WAD, err);
}
function assert_ActiveValidatorCount(User staker, uint expectedCount, string memory err) internal view {
uint curActiveValidatorCount = _getActiveValidatorCount(staker);
assertEq(curActiveValidatorCount, expectedCount, err);
}
function assert_withdrawableSharesDecreasedByAtLeast(
User staker,
IStrategy[] memory strategies,
uint[] memory originalShares,
uint[] memory expectedDecreases,
string memory err
) internal view {
for (uint i = 0; i < strategies.length; i++) {
assert_withdrawableSharesDecreasedByAtLeast(staker, strategies[i], originalShares[i], expectedDecreases[i], err);
}
}
function assert_withdrawableSharesDecreasedByAtLeast(
User staker,
IStrategy strategy,
uint originalShares,
uint expectedDecrease,
string memory err
) internal view {
uint currentShares = _getWithdrawableShares(staker, strategy);
assertLt(currentShares, originalShares - expectedDecrease, err);
}
function assert_DepositShares_GTE_WithdrawableShares(User staker, IStrategy[] memory strategies, string memory err) internal view {
uint[] memory depositShares = _getStakerDepositShares(staker, strategies);
uint[] memory withdrawableShares = _getWithdrawableShares(staker, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertGe(depositShares[i], withdrawableShares[i], err);
}
}
function assert_Zero_WithdrawableShares(User staker, IStrategy strategy, string memory err) internal view {
assertEq(_getWithdrawableShares(staker, strategy), 0, err);
}
/**
*
* SNAPSHOT ASSERTIONS
* TIME TRAVELERS ONLY BEYOND THIS POINT
*
*/
/**
*
* SNAPSHOT ASSERTIONS: ALLOCATIONS
*
*/
function assert_Snap_Became_Registered(User operator, OperatorSet memory operatorSet, string memory err) internal {
bool curIsMemberOfSet = _getIsMemberOfSet(operator, operatorSet);
bool prevIsMemberOfSet = _getPrevIsMemberOfSet(operator, operatorSet);
assertFalse(prevIsMemberOfSet, err);
assertTrue(curIsMemberOfSet, err);
}
function assert_Snap_Became_Deregistered(User operator, OperatorSet memory operatorSet, string memory err) internal {
bool curIsMemberOfSet = _getIsMemberOfSet(operator, operatorSet);
bool prevIsMemberOfSet = _getPrevIsMemberOfSet(operator, operatorSet);
assertTrue(prevIsMemberOfSet, err);
assertFalse(curIsMemberOfSet, err);
}
function assert_Snap_Unchanged_Registration(User operator, OperatorSet memory operatorSet, string memory err) internal {
bool curIsMemberOfSet = _getIsMemberOfSet(operator, operatorSet);
bool prevIsMemberOfSet = _getPrevIsMemberOfSet(operator, operatorSet);
assertEq(prevIsMemberOfSet, curIsMemberOfSet, err);
}
function assert_Snap_Became_Slashable(User operator, OperatorSet memory operatorSet, string memory err) internal {
bool curIsSlashable = _getIsSlashable(operator, operatorSet);
bool prevIsSlashable = _getPrevIsSlashable(operator, operatorSet);
assertFalse(prevIsSlashable, err);
assertTrue(curIsSlashable, err);
}
function assert_Snap_Remains_Slashable(User operator, OperatorSet memory operatorSet, string memory err) internal {
bool curIsSlashable = _getIsSlashable(operator, operatorSet);
bool prevIsSlashable = _getPrevIsSlashable(operator, operatorSet);
assertTrue(prevIsSlashable, err);
assertTrue(curIsSlashable, err);
}
function assert_Snap_Unchanged_Slashability(User operator, OperatorSet memory operatorSet, string memory err) internal {
bool curIsSlashable = _getIsSlashable(operator, operatorSet);
bool prevIsSlashable = _getPrevIsSlashable(operator, operatorSet);
assertEq(prevIsSlashable, curIsSlashable, err);
}
function assert_Snap_Unchanged_AllocatedStrats(User operator, OperatorSet memory operatorSet, string memory err) internal {
IStrategy[] memory curAllocatedStrats = _getAllocatedStrats(operator, operatorSet);
IStrategy[] memory prevAllocatedStrats = _getPrevAllocatedStrats(operator, operatorSet);
assertEq(curAllocatedStrats.length, prevAllocatedStrats.length, err);
for (uint i = 0; i < curAllocatedStrats.length; i++) {
assertEq(address(curAllocatedStrats[i]), address(prevAllocatedStrats[i]), err);
}
}
function assert_Snap_Removed_AllocatedStrats(
User operator,
OperatorSet memory operatorSet,
IStrategy[] memory removedStrats,
string memory err
) internal {
IStrategy[] memory curAllocatedStrats = _getAllocatedStrats(operator, operatorSet);
IStrategy[] memory prevAllocatedStrats = _getPrevAllocatedStrats(operator, operatorSet);
assertEq(curAllocatedStrats.length + removedStrats.length, prevAllocatedStrats.length, err);
for (uint i = 0; i < removedStrats.length; i++) {
assertFalse(curAllocatedStrats.contains(removedStrats[i]), err);
}
}
function assert_Snap_Unchanged_StrategyAllocations(
User operator,
OperatorSet memory operatorSet,
IStrategy[] memory strategies,
string memory err
) internal {
Allocation[] memory curAllocations = _getAllocations(operator, operatorSet, strategies);
Allocation[] memory prevAllocations = _getPrevAllocations(operator, operatorSet, strategies);
for (uint i = 0; i < strategies.length; i++) {
Allocation memory curAllocation = curAllocations[i];
Allocation memory prevAllocation = prevAllocations[i];
assertEq(curAllocation.currentMagnitude, prevAllocation.currentMagnitude, err);
assertEq(curAllocation.pendingDiff, prevAllocation.pendingDiff, err);
assertEq(curAllocation.effectBlock, prevAllocation.effectBlock, err);
}
}
function assert_Snap_Added_AllocatedSet(User operator, OperatorSet memory operatorSet, string memory err) internal {
OperatorSet[] memory curAllocatedSets = _getAllocatedSets(operator);
OperatorSet[] memory prevAllocatedSets = _getPrevAllocatedSets(operator);
assertEq(curAllocatedSets.length, prevAllocatedSets.length + 1, err);
assertFalse(prevAllocatedSets.contains(operatorSet), err);
assertTrue(curAllocatedSets.contains(operatorSet), err);
}
function assert_Snap_Unchanged_AllocatedSets(User operator, string memory err) internal {
OperatorSet[] memory curAllocatedSets = _getAllocatedSets(operator);
OperatorSet[] memory prevAllocatedSets = _getPrevAllocatedSets(operator);
assertEq(curAllocatedSets.length, prevAllocatedSets.length, err);
}
function assert_Snap_Removed_AllocatedSet(User operator, OperatorSet memory operatorSet, string memory err) internal {
OperatorSet[] memory curAllocatedSets = _getAllocatedSets(operator);
OperatorSet[] memory prevAllocatedSets = _getPrevAllocatedSets(operator);
assertEq(curAllocatedSets.length + 1, prevAllocatedSets.length, err);
assertTrue(prevAllocatedSets.contains(operatorSet), err);
assertFalse(curAllocatedSets.contains(operatorSet), err);
}
function assert_Snap_Added_RegisteredSet(User operator, OperatorSet memory operatorSet, string memory err) internal {
OperatorSet[] memory curRegisteredSets = _getRegisteredSets(operator);
OperatorSet[] memory prevRegisteredSets = _getPrevRegisteredSets(operator);
assertEq(curRegisteredSets.length, prevRegisteredSets.length + 1, err);
assertFalse(prevRegisteredSets.contains(operatorSet), err);
assertTrue(curRegisteredSets.contains(operatorSet), err);
}
function assert_Snap_Removed_RegisteredSet(User operator, OperatorSet memory operatorSet, string memory err) internal {
OperatorSet[] memory curRegisteredSets = _getRegisteredSets(operator);
OperatorSet[] memory prevRegisteredSets = _getPrevRegisteredSets(operator);
assertEq(curRegisteredSets.length + 1, prevRegisteredSets.length, err);
assertTrue(prevRegisteredSets.contains(operatorSet), err);
assertFalse(curRegisteredSets.contains(operatorSet), err);
}
function assert_Snap_Unchanged_RegisteredSet(User operator, string memory err) internal {
OperatorSet[] memory curRegisteredSets = _getRegisteredSets(operator);
OperatorSet[] memory prevRegisteredSets = _getPrevRegisteredSets(operator);
assertEq(curRegisteredSets.length, prevRegisteredSets.length, err);
for (uint i = 0; i < curRegisteredSets.length; i++) {
assertEq(curRegisteredSets[i].avs, prevRegisteredSets[i].avs, err);
assertEq(curRegisteredSets[i].id, prevRegisteredSets[i].id, err);
}
}
function assert_Snap_Added_MemberOfSet(User operator, OperatorSet memory operatorSet, string memory err) internal {
address[] memory curOperators = _getMembers(operatorSet);
address[] memory prevOperators = _getPrevMembers(operatorSet);
assertEq(curOperators.length, prevOperators.length + 1, err);
assertFalse(prevOperators.contains(address(operator)), err);
assertTrue(curOperators.contains(address(operator)), err);
}
function assert_Snap_Removed_MemberOfSet(User operator, OperatorSet memory operatorSet, string memory err) internal {
address[] memory curOperators = _getMembers(operatorSet);
address[] memory prevOperators = _getPrevMembers(operatorSet);
assertEq(curOperators.length + 1, prevOperators.length, err);
assertTrue(prevOperators.contains(address(operator)), err);
assertFalse(curOperators.contains(address(operator)), err);
}
function assert_Snap_Unchanged_MemberOfSet(OperatorSet memory operatorSet, string memory err) internal {
address[] memory curOperators = _getMembers(operatorSet);
address[] memory prevOperators = _getPrevMembers(operatorSet);
assertEq(curOperators.length, prevOperators.length, err);
for (uint i = 0; i < curOperators.length; i++) {
assertEq(curOperators[i], prevOperators[i], err);
}
}
function assert_Snap_StakeBecameSlashable(
User operator,
OperatorSet memory operatorSet,
IStrategy[] memory strategies,
string memory err
) internal {
uint[] memory curSlashableStake = _getMinSlashableStake(operator, operatorSet, strategies);
uint[] memory prevSlashableStake = _getPrevMinSlashableStake(operator, operatorSet, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertTrue(prevSlashableStake[i] < curSlashableStake[i], err);
}
}
function assert_Snap_StakeBecomeUnslashable(
User operator,
OperatorSet memory operatorSet,
IStrategy[] memory strategies,
string memory err
) internal {
uint[] memory curSlashableStake = _getMinSlashableStake(address(operator), operatorSet, strategies);
uint[] memory prevSlashableStake = _getPrevMinSlashableStake(address(operator), operatorSet, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertTrue(prevSlashableStake[i] > curSlashableStake[i], err);
assertTrue(prevSlashableStake[i] > curSlashableStake[i], err);
}
}
function assert_Snap_Added_SlashableStake(
User operator,
OperatorSet memory operatorSet,
IStrategy[] memory strategies,
uint[] memory slashableShares,
string memory err
) internal {
uint[] memory curSlashableStake = _getMinSlashableStake(operator, operatorSet, strategies);
uint[] memory prevSlashableStake = _getPrevMinSlashableStake(operator, operatorSet, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(curSlashableStake[i], prevSlashableStake[i] + slashableShares[i], err);
}
}
function assert_Snap_Unchanged_SlashableStake(
User operator,
OperatorSet memory operatorSet,
IStrategy[] memory strategies,
string memory err
) internal {
uint[] memory curSlashableStake = _getMinSlashableStake(operator, operatorSet, strategies);
uint[] memory prevSlashableStake = _getPrevMinSlashableStake(operator, operatorSet, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(curSlashableStake[i], prevSlashableStake[i], err);
}
}
function assert_Snap_Removed_SlashableStake(
User operator,
OperatorSet memory operatorSet,
IStrategy[] memory strategies,
uint[] memory removedSlashableShares,
string memory err
) internal {
uint[] memory curSlashableStake = _getMinSlashableStake(operator, operatorSet, strategies);
uint[] memory prevSlashableStake = _getPrevMinSlashableStake(operator, operatorSet, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(curSlashableStake[i] + removedSlashableShares[i], prevSlashableStake[i], err);
}
}
function assert_Snap_Slashed_SlashableStake(
User operator,
OperatorSet memory operatorSet,
SlashingParams memory params,
string memory err
) internal {
uint[] memory curSlashableStake = _getMinSlashableStake(operator, operatorSet, params.strategies);
uint[] memory prevSlashableStake = _getPrevMinSlashableStake(operator, operatorSet, params.strategies);
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, params.strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, params.strategies);
for (uint i = 0; i < params.strategies.length; i++) {
// Slashing doesn't occur if the operator has no slashable magnitude
// This prevents a div by 0 when calculating expected slashed
uint expectedSlashed = prevMagnitudes[i].max == 0
? 0
: SlashingLib.calcSlashedAmount({
operatorShares: prevSlashableStake[i],
prevMaxMagnitude: prevMagnitudes[i].max,
newMaxMagnitude: curMagnitudes[i].max
});
assertEq(curSlashableStake[i], prevSlashableStake[i] - expectedSlashed, err);
}
}
//@dev requires slashparams strategies to be same as withdrawal strategies
// meant to be used in check_base_slashing_state
function assert_Snap_Decreased_SlashableSharesInQueue(
User operator,
SlashingParams memory slashParams,
Withdrawal[] memory withdrawals,
string memory err
) internal {
IStrategy[] memory strategies = slashParams.strategies;
uint[] memory curSlashableSharesInQueue = _getSlashableSharesInQueue(operator, strategies);
uint[] memory prevSlashableSharesInQueue = _getPrevSlashableSharesInQueue(operator, strategies);
uint[] memory totalScaledShares = new uint[](strategies.length);
for (uint i = 0; i < withdrawals.length; i++) {
for (uint j = 0; j < withdrawals[i].strategies.length; j++) {
totalScaledShares[j] = totalScaledShares[j] + withdrawals[i].scaledShares[j];
}
}
for (uint i = 0; i < strategies.length; i++) {
assertEq(
curSlashableSharesInQueue[i], prevSlashableSharesInQueue[i] - totalScaledShares[i].mulWad(slashParams.wadsToSlash[i]), err
);
}
}
function assert_Snap_Increased_SlashableSharesInQueue(User operator, Withdrawal[] memory withdrawals, string memory err) internal {
uint[] memory curSlashableSharesInQueue;
uint[] memory prevSlashableSharesInQueue;
uint64[] memory maxMagnitudes;
for (uint i = 0; i < withdrawals.length; i++) {
curSlashableSharesInQueue = _getSlashableSharesInQueue(operator, withdrawals[i].strategies);
prevSlashableSharesInQueue = _getPrevSlashableSharesInQueue(operator, withdrawals[i].strategies);
maxMagnitudes = _getMaxMagnitudes(operator, withdrawals[i].strategies);
for (uint j = 0; j < withdrawals[i].strategies.length; j++) {
assertEq(
curSlashableSharesInQueue[j],
prevSlashableSharesInQueue[j] + withdrawals[i].scaledShares[j].mulWad(maxMagnitudes[j]),
err
);
}
}
}
function assert_Snap_StakeBecameAllocated(
User operator,
OperatorSet memory operatorSet,
IStrategy[] memory strategies,
string memory err
) internal {
uint[] memory curMinAllocatedStake = _getAllocatedStake(operator, operatorSet, strategies);
uint[] memory prevMinAllocatedStake = _getPrevAllocatedStake(operator, operatorSet, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertGt(curMinAllocatedStake[i], prevMinAllocatedStake[i], err);
}
}
function assert_Snap_StakeBecameDeallocated(
User operator,
OperatorSet memory operatorSet,
IStrategy[] memory strategies,
string memory err
) internal {
uint[] memory curMinAllocatedStake = _getAllocatedStake(operator, operatorSet, strategies);
uint[] memory prevMinAllocatedStake = _getPrevAllocatedStake(operator, operatorSet, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertLt(curMinAllocatedStake[i], prevMinAllocatedStake[i], err);
}
}
function assert_Snap_Unchanged_AllocatedStake(
User operator,
OperatorSet memory operatorSet,
IStrategy[] memory strategies,
string memory err
) internal {
uint[] memory curAllocatedStake = _getAllocatedStake(operator, operatorSet, strategies);
uint[] memory prevAllocatedStake = _getPrevAllocatedStake(operator, operatorSet, strategies);
for (uint i = 0; i < curAllocatedStake.length; i++) {
assertEq(curAllocatedStake[i], prevAllocatedStake[i], err);
}
}
function assert_Snap_Slashed_AllocatedStake(
User operator,
OperatorSet memory operatorSet,
SlashingParams memory params,
string memory err
) internal {
uint[] memory curAllocatedStake = _getAllocatedStake(operator, operatorSet, params.strategies);
uint[] memory prevAllocatedStake = _getPrevAllocatedStake(operator, operatorSet, params.strategies);
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, params.strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, params.strategies);
for (uint i = 0; i < curAllocatedStake.length; i++) {
// Slashing doesn't occur if the operator has no slashable magnitude
// This prevents a div by 0 when calculating expected slashed
uint expectedSlashed = prevMagnitudes[i].max == 0
? 0
: SlashingLib.calcSlashedAmount({
operatorShares: prevAllocatedStake[i],
prevMaxMagnitude: prevMagnitudes[i].max,
newMaxMagnitude: curMagnitudes[i].max
});
assertEq(curAllocatedStake[i], prevAllocatedStake[i] - expectedSlashed, err);
}
}
function assert_Snap_Added_EncumberedMagnitude(
User operator,
IStrategy[] memory strategies,
uint64[] memory magnitudeAdded,
string memory err
) internal {
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(curMagnitudes[i].encumbered, prevMagnitudes[i].encumbered + magnitudeAdded[i], err);
}
}
function assert_Snap_Unchanged_EncumberedMagnitude(User operator, IStrategy[] memory strategies, string memory err) internal {
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(curMagnitudes[i].encumbered, prevMagnitudes[i].encumbered, err);
}
}
function assert_Snap_Removed_EncumberedMagnitude(
User operator,
IStrategy[] memory strategies,
uint64[] memory magnitudeRemoved,
string memory err
) internal {
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(curMagnitudes[i].encumbered + magnitudeRemoved[i], prevMagnitudes[i].encumbered, err);
}
}
function assert_Snap_Slashed_EncumberedMagnitude(User operator, SlashingParams memory params, string memory err) internal {
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, params.strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, params.strategies);
for (uint i = 0; i < params.strategies.length; i++) {
uint expectedSlashed = prevMagnitudes[i].encumbered.mulWadRoundUp(params.wadsToSlash[i]);
assertEq(curMagnitudes[i].encumbered, prevMagnitudes[i].encumbered - expectedSlashed, err);
}
}
function assert_Snap_Added_AllocatableMagnitude(
User operator,
IStrategy[] memory strategies,
uint64[] memory magnitudeFreed,
string memory err
) internal {
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(curMagnitudes[i].allocatable, prevMagnitudes[i].allocatable + magnitudeFreed[i], err);
}
}
function assert_Snap_Unchanged_AllocatableMagnitude(User operator, IStrategy[] memory strategies, string memory err) internal {
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(curMagnitudes[i].allocatable, prevMagnitudes[i].allocatable, err);
}
}
function assert_Snap_Removed_AllocatableMagnitude(
User operator,
IStrategy[] memory strategies,
uint64[] memory magnitudeRemoved,
string memory err
) internal {
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(curMagnitudes[i].allocatable, prevMagnitudes[i].allocatable - magnitudeRemoved[i], err);
}
}
function assert_Snap_Allocated_Magnitude(User operator, IStrategy[] memory strategies, string memory err) internal {
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, strategies);
/// Check:
/// allocatable increased
/// encumbered decreased
for (uint i = 0; i < strategies.length; i++) {
assertLt(curMagnitudes[i].allocatable, prevMagnitudes[i].allocatable, err);
assertGt(curMagnitudes[i].encumbered, prevMagnitudes[i].encumbered, err);
}
}
function assert_Snap_Deallocated_Magnitude(User operator, IStrategy[] memory strategies, string memory err) internal {
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, strategies);
/// Check:
/// allocatable increased
/// encumbered decreased
for (uint i = 0; i < strategies.length; i++) {
assertGt(curMagnitudes[i].allocatable, prevMagnitudes[i].allocatable, err);
assertLt(curMagnitudes[i].encumbered, prevMagnitudes[i].encumbered, err);
}
}
function assert_Snap_Set_CurrentMagnitude(User operator, AllocateParams memory params, string memory err) internal {
Allocation[] memory curAllocations = _getAllocations(operator, params.operatorSet, params.strategies);
Allocation[] memory prevAllocations = _getPrevAllocations(operator, params.operatorSet, params.strategies);
/// Prev allocation.currentMagnitude should NOT equal newly-set magnitude
/// Cur allocation.currentMagnitude SHOULD
for (uint i = 0; i < params.strategies.length; i++) {
assertTrue(prevAllocations[i].currentMagnitude != params.newMagnitudes[i], err);
assertEq(curAllocations[i].currentMagnitude, params.newMagnitudes[i], err);
}
}
function assert_Snap_Slashed_Allocation(User operator, OperatorSet memory operatorSet, SlashingParams memory params, string memory err)
internal
{
Allocation[] memory curAllocations = _getAllocations(operator, operatorSet, params.strategies);
Allocation[] memory prevAllocations = _getPrevAllocations(operator, operatorSet, params.strategies);
for (uint i = 0; i < params.strategies.length; i++) {
uint expectedSlashed = prevAllocations[i].currentMagnitude.mulWadRoundUp(params.wadsToSlash[i]);
assertEq(curAllocations[i].currentMagnitude, prevAllocations[i].currentMagnitude - expectedSlashed, err);
}
}
function assert_Snap_Unchanged_MaxMagnitude(User operator, IStrategy[] memory strategies, string memory err) internal {
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(curMagnitudes[i].max, prevMagnitudes[i].max, err);
}
}
function assert_Snap_Slashed_MaxMagnitude(
User operator,
OperatorSet memory operatorSet,
SlashingParams memory params,
string memory err
) internal {
Allocation[] memory prevAllocations = _getPrevAllocations(operator, operatorSet, params.strategies);
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, params.strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, params.strategies);
for (uint i = 0; i < params.strategies.length; i++) {
uint expectedSlashed = prevAllocations[i].currentMagnitude.mulWadRoundUp(params.wadsToSlash[i]);
assertEq(curMagnitudes[i].max, prevMagnitudes[i].max - expectedSlashed, err);
}
}
function assert_Snap_Allocations_Slashed(
SlashingParams memory slashingParams,
OperatorSet memory operatorSet,
bool completed,
string memory err
) internal {
User op = User(payable(slashingParams.operator));
Allocation[] memory curAllocs = _getAllocations(op, operatorSet, slashingParams.strategies);
Allocation[] memory prevAllocs = _getPrevAllocations(op, operatorSet, slashingParams.strategies);
Magnitudes[] memory curMagnitudes = _getMagnitudes(op, slashingParams.strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(op, slashingParams.strategies);
uint32 delay = _getExistingAllocationDelay(User(payable(slashingParams.operator)));
for (uint i = 0; i < slashingParams.strategies.length; i++) {
Allocation memory curAlloc = curAllocs[i];
Allocation memory prevAlloc = prevAllocs[i];
uint64 slashedMagnitude = uint64(uint(prevAlloc.currentMagnitude).mulWadRoundUp(slashingParams.wadsToSlash[i]));
// Check Allocations
assertEq(curAlloc.currentMagnitude, prevAlloc.currentMagnitude - slashedMagnitude, string.concat(err, " (currentMagnitude)"));
if (completed) {
assertEq(curAlloc.pendingDiff, 0, string.concat(err, " (pendingDiff)"));
assertEq(curAlloc.effectBlock, 0, string.concat(err, " (effectBlock)"));
} else {
assertEq(
curAlloc.currentMagnitude, prevAlloc.currentMagnitude - slashedMagnitude, string.concat(err, " (currentMagnitude)")
);
// If (isDeallocation) ...
if (prevAlloc.pendingDiff < 0) {
uint64 slashedPending = uint64(uint(uint128(-prevAlloc.pendingDiff)).mulWadRoundUp(slashingParams.wadsToSlash[i]));
assertEq(
curAlloc.pendingDiff, prevAlloc.pendingDiff + int128(uint128(slashedPending)), string.concat(err, " (pendingDiff)")
);
delay = DEALLOCATION_DELAY + 1;
}
assertEq(curAlloc.effectBlock, block.number + delay, string.concat(err, " (effectBlock)"));
}
// Check Magnitudes
Magnitudes memory curMagnitude = curMagnitudes[i];
Magnitudes memory prevMagnitude = prevMagnitudes[i];
assertEq(curMagnitude.encumbered, prevMagnitude.encumbered - slashedMagnitude, string.concat(err, " (encumbered magnitude)"));
assertEq(curMagnitude.allocatable, prevMagnitude.allocatable, string.concat(err, " (allocatable magnitude)"));
assertEq(curMagnitude.max, prevMagnitude.max - slashedMagnitude, string.concat(err, " (max magnitude)"));
}
}
function assert_Snap_StakerWithdrawableShares_AfterSlash(
User staker,
AllocateParams memory allocateParams,
SlashingParams memory slashingParams,
string memory err
) internal {
uint[] memory curShares = _getWithdrawableShares(staker, allocateParams.strategies);
uint[] memory prevShares = _getPrevWithdrawableShares(staker, allocateParams.strategies);
for (uint i = 0; i < allocateParams.strategies.length; i++) {
IStrategy strat = allocateParams.strategies[i];
uint slashedShares = 0;
if (slashingParams.strategies.contains(strat)) {
uint wadToSlash = slashingParams.wadsToSlash[slashingParams.strategies.indexOf(strat)];
slashedShares = prevShares[i].mulWadRoundUp(allocateParams.newMagnitudes[i].mulWadRoundUp(wadToSlash));
}
assertApproxEqAbs(prevShares[i] - slashedShares, curShares[i], 1, err);
}
}
/**
*
* SNAPSHOT ASSERTIONS: BEACON CHAIN AND AVS SLASHING
*
*/
/// @dev Same as `assert_Snap_StakerWithdrawableShares_AfterSlash`
/// @dev but when a BC slash occurs before an AVS slash
/// @dev There is additional rounding error when a BC and AVS slash occur together
function assert_Snap_StakerWithdrawableShares_AfterBCSlash_AVSSlash(
User staker,
AllocateParams memory allocateParams,
SlashingParams memory slashingParams,
string memory err
) internal {
require(allocateParams.strategies.length == 1 && slashingParams.strategies.length == 1, "only beacon strategy supported");
require(allocateParams.strategies[0] == BEACONCHAIN_ETH_STRAT, "only beacon strategy supported");
require(slashingParams.strategies[0] == BEACONCHAIN_ETH_STRAT, "only beacon strategy supported");
uint curShares = _getWithdrawableShares(staker, allocateParams.strategies)[0];
uint prevShares = _getPrevWithdrawableShares(staker, allocateParams.strategies)[0];
uint slashedShares = 0;
uint wadToSlash = slashingParams.wadsToSlash[0];
slashedShares = prevShares.mulWadRoundUp(allocateParams.newMagnitudes[0].mulWadRoundUp(wadToSlash));
assertApproxEqAbs(prevShares - slashedShares, curShares, 1e2, err);
}
/// @dev Validates behavior of "restaking", ie. that the funds can be slashed twice
function assert_Snap_StakerWithdrawableShares_AfterAVSSlash_BCSlash(
User staker,
AllocateParams memory allocateParams,
SlashingParams memory slashingParams,
string memory err
) internal {
require(allocateParams.strategies.length == 1 && slashingParams.strategies.length == 1, "only beacon strategy supported");
require(allocateParams.strategies[0] == BEACONCHAIN_ETH_STRAT, "only beacon strategy supported");
require(slashingParams.strategies[0] == BEACONCHAIN_ETH_STRAT, "only beacon strategy supported");
uint curShares = _getWithdrawableShares(staker, allocateParams.strategies)[0];
uint prevShares = _getPrevWithdrawableShares(staker, allocateParams.strategies)[0];
uint depositShares = _getStakerDepositShares(staker, allocateParams.strategies)[0];
// 1. The slashing factor and withdrawable shares should decrease by a factor of the BCSF
// We use assertApproxEq on shares since intermediate division on calculation of slashing factor introduces some additional rounding error
uint slashingFactor = _getSlashingFactor(staker, allocateParams.strategies[0]);
uint prevSlashingFactor = _getPrevSlashingFactor(staker, allocateParams.strategies[0]);
assertEq(prevSlashingFactor.mulWad(_getBeaconChainSlashingFactor(staker)), slashingFactor, err);
assertApproxEqAbs(prevShares.mulWad(_getBeaconChainSlashingFactor(staker)), curShares, 1e2, err);
/**
* 2. The delta in shares is given by:
* (depositShares * operatorMag) - (depositShares * operatorMag * BCSF)
* = depositShares * operatorMag * (1 - BCSF)
*/
uint beaconChainSlashingFactor = _getBeaconChainSlashingFactor(staker);
uint wadToSlash = slashingParams.wadsToSlash[0];
uint originalAVSSlashedShares = depositShares.mulWadRoundUp(allocateParams.newMagnitudes[0].mulWadRoundUp(wadToSlash));
{
uint withdrawableSharesAfterAVSSlash = depositShares - originalAVSSlashedShares;
uint expectedDelta = withdrawableSharesAfterAVSSlash.mulWad(WAD - beaconChainSlashingFactor);
assertApproxEqAbs(prevShares - expectedDelta, curShares, 1e2, err);
}
/**
* 3. The attributable avs slashed shares should decrease by a factor of the BCSF
* Attributable avs slashed shares = originalWithdrawableShares - bcSlashedShares - curShares
* Where bcSlashedShares = originalWithdrawableShares * (1 - BCSF)
*/
uint bcSlashedShares = depositShares.mulWad(WAD - beaconChainSlashingFactor);
uint attributableAVSSlashedShares = depositShares - bcSlashedShares - curShares;
assertApproxEqAbs(originalAVSSlashedShares.mulWad(beaconChainSlashingFactor), attributableAVSSlashedShares, 1e2, err);
}
/**
* @dev Validates behavior of "restaking", ie. that the funds can be slashed twice. Also validates
* the edge case where a validator is proven prior to the BC slash.
* @dev These bounds are based off of rounding when avs and bc slashing occur together
*/
function assert_Snap_StakerWithdrawableShares_AVSSlash_ValidatorProven_BCSlash(
User staker,
uint originalWithdrawableShares,
uint extraValidatorShares,
AllocateParams memory allocateParams,
SlashingParams memory slashingParams,
string memory err
) internal {
require(allocateParams.strategies.length == 1 && slashingParams.strategies.length == 1, "only beacon strategy supported");
require(allocateParams.strategies[0] == BEACONCHAIN_ETH_STRAT, "only beacon strategy supported");
require(slashingParams.strategies[0] == BEACONCHAIN_ETH_STRAT, "only beacon strategy supported");
uint curShares = _getWithdrawableShares(staker, allocateParams.strategies)[0];
uint prevShares = _getPrevWithdrawableShares(staker, allocateParams.strategies)[0];
// 1. The withdrawable shares should decrease by a factor of the BCSF
assertApproxEqAbs(prevShares.mulWad(_getBeaconChainSlashingFactor(staker)), curShares, 1e5, err);
/**
* 2. The delta in shares is given by:
* (originalWithdrawableShares * operatorMag) + extraValidatorShares - (depositShares * operatorMag * BCSF * dsf)
*/
uint beaconChainSlashingFactor = _getBeaconChainSlashingFactor(staker);
uint wadToSlash = slashingParams.wadsToSlash[0];
uint originalAVSSlashedShares = originalWithdrawableShares.mulWadRoundUp(allocateParams.newMagnitudes[0].mulWadRoundUp(wadToSlash));
uint withdrawableSharesAfterValidatorProven = originalWithdrawableShares - originalAVSSlashedShares + extraValidatorShares;
uint expectedDelta = withdrawableSharesAfterValidatorProven.mulWad(WAD - beaconChainSlashingFactor);
assertApproxEqAbs(prevShares - expectedDelta, curShares, 1e5, err);
/**
* 3. The attributable avs slashed shares should decrease by a factor of the BCSF
* Attributable avs slashed shares = depositShares - bcSlashedShares - curShars
* Where bcSlashedShares = depositShares * (1 - BCSF)
*/
uint depositShares = _getStakerDepositShares(staker, allocateParams.strategies)[0];
uint bcSlashedShares = depositShares.mulWad(WAD - beaconChainSlashingFactor);
uint attributableAVSSlashedShares = depositShares - bcSlashedShares - curShares;
assertApproxEqAbs(originalAVSSlashedShares.mulWad(beaconChainSlashingFactor), attributableAVSSlashedShares, 1e5, err);
}
// TODO: slashable stake
/**
*
* SNAPSHOT ASSERTIONS: OPERATOR SHARES
*
*/
/// @dev Check that the operator has `addedShares` additional operator shares
// for each strategy since the last snapshot
function assert_Snap_Added_OperatorShares(User operator, IStrategy[] memory strategies, uint[] memory addedShares, string memory err)
internal
{
uint[] memory curShares = _getOperatorShares(operator, strategies);
// Use timewarp to get previous operator shares
uint[] memory prevShares = _getPrevOperatorShares(operator, strategies);
// For each strategy, check (prev + added == cur)
for (uint i = 0; i < strategies.length; i++) {
assertEq(prevShares[i] + addedShares[i], curShares[i], err);
}
}
/// @dev Check that the operator has `removedShares` fewer operator shares
/// for each strategy since the last snapshot
function assert_Snap_Removed_OperatorShares(
User operator,
IStrategy[] memory strategies,
uint[] memory removedShares,
string memory err
) internal {
uint[] memory curShares = _getOperatorShares(operator, strategies);
// Use timewarp to get previous operator shares
uint[] memory prevShares = _getPrevOperatorShares(operator, strategies);
// For each strategy, check (prev - removed == cur)
for (uint i = 0; i < strategies.length; i++) {
assertEq(prevShares[i], curShares[i] + removedShares[i], err);
}
}
/// @dev Check that the operator's shares in ALL strategies have not changed
/// since the last snapshot
function assert_Snap_Unchanged_OperatorShares(User operator, string memory err) internal {
IStrategy[] memory strategies = allStrats;
uint[] memory curShares = _getOperatorShares(operator, strategies);
// Use timewarp to get previous operator shares
uint[] memory prevShares = _getPrevOperatorShares(operator, strategies);
// For each strategy, check (prev == cur)
for (uint i = 0; i < strategies.length; i++) {
assertEq(prevShares[i], curShares[i], err);
}
}
function assert_Snap_Delta_OperatorShares(User operator, IStrategy[] memory strategies, int[] memory shareDeltas, string memory err)
internal
{
uint[] memory curShares = _getOperatorShares(operator, strategies);
// Use timewarp to get previous operator shares
uint[] memory prevShares = _getPrevOperatorShares(operator, strategies);
// For each strategy, check (prev + added == cur)
for (uint i = 0; i < strategies.length; i++) {
uint expectedShares;
if (shareDeltas[i] < 0) expectedShares = prevShares[i] - uint(-shareDeltas[i]);
else expectedShares = prevShares[i] + uint(shareDeltas[i]);
assertEq(expectedShares, curShares[i], err);
}
}
function assert_Snap_Slashed_OperatorShares(User operator, SlashingParams memory params, string memory err) internal {
uint[] memory curShares = _getOperatorShares(operator, params.strategies);
uint[] memory prevShares = _getPrevOperatorShares(operator, params.strategies);
Magnitudes[] memory curMagnitudes = _getMagnitudes(operator, params.strategies);
Magnitudes[] memory prevMagnitudes = _getPrevMagnitudes(operator, params.strategies);
for (uint i = 0; i < params.strategies.length; i++) {
// Slashing doesn't occur if the operator has no slashable magnitude
// This prevents a div by 0 when calculating expected slashed
uint expectedSlashed = prevMagnitudes[i].max == 0
? 0
: SlashingLib.calcSlashedAmount({
operatorShares: prevShares[i],
prevMaxMagnitude: prevMagnitudes[i].max,
newMaxMagnitude: curMagnitudes[i].max
});
assertEq(curShares[i], prevShares[i] - expectedSlashed, err);
}
}
function assert_Snap_Increased_BurnableShares(User operator, SlashingParams memory params, string memory err) internal {
uint[] memory curBurnable = _getBurnableShares(params.strategies);
uint[] memory prevBurnable = _getPrevBurnableShares(params.strategies);
uint[] memory curShares = _getOperatorShares(operator, params.strategies);
uint[] memory prevShares = _getPrevOperatorShares(operator, params.strategies);
for (uint i = 0; i < params.strategies.length; i++) {
uint slashedAtLeast = prevShares[i] - curShares[i];
// Not factoring in slashable shares in queue here, because that gets more complex (TODO)
assertTrue(curBurnable[i] >= (prevBurnable[i] + slashedAtLeast), err);
}
}
/**
*
* SNAPSHOT ASSERTIONS: STAKER SHARES
*
*/
/// @dev Check that the staker has `addedShares` additional deposit shares
/// for each strategy since the last snapshot
function assert_Snap_Added_Staker_DepositShares(
User staker,
IStrategy[] memory strategies,
uint[] memory addedShares,
string memory err
) internal {
uint[] memory curShares = _getStakerDepositShares(staker, strategies);
// Use timewarp to get previous staker shares
uint[] memory prevShares = _getPrevStakerDepositShares(staker, strategies);
// For each strategy, check (prev + added == cur)
for (uint i = 0; i < strategies.length; i++) {
assertApproxEqAbs(prevShares[i] + addedShares[i], curShares[i], 1, err);
}
}
function assert_Snap_Added_Staker_DepositShares(User staker, IStrategy strat, uint addedShares, string memory err) internal {
assert_Snap_Added_Staker_DepositShares(staker, strat.toArray(), addedShares.toArrayU256(), err);
}
/// @dev Check that the staker has `removedShares` fewer delegatable shares
/// for each strategy since the last snapshot
function assert_Snap_Removed_Staker_DepositShares(
User staker,
IStrategy[] memory strategies,
uint[] memory removedShares,
string memory err
) internal {
uint[] memory curShares = _getStakerDepositShares(staker, strategies);
// Use timewarp to get previous staker shares
uint[] memory prevShares = _getPrevStakerDepositShares(staker, strategies);
// For each strategy, check (prev - removed == cur)
for (uint i = 0; i < strategies.length; i++) {
assertEq(prevShares[i] - removedShares[i], curShares[i], err);
}
}
function assert_Snap_Removed_Staker_DepositShares(User staker, IStrategy strat, uint removedShares, string memory err) internal {
assert_Snap_Removed_Staker_DepositShares(staker, strat.toArray(), removedShares.toArrayU256(), err);
}
/// @dev Check that the staker's delegatable shares in ALL strategies have not changed
/// since the last snapshot
function assert_Snap_Unchanged_Staker_DepositShares(User staker, string memory err) internal {
IStrategy[] memory strategies = allStrats;
uint[] memory curShares = _getStakerDepositShares(staker, strategies);
// Use timewarp to get previous staker shares
uint[] memory prevShares = _getPrevStakerDepositShares(staker, strategies);
// For each strategy, check (prev == cur)
for (uint i = 0; i < strategies.length; i++) {
assertEq(prevShares[i], curShares[i], err);
}
}
/// @dev Check that the staker's withdrawable shares have increased by `addedShares`
function assert_Snap_Added_Staker_WithdrawableShares(
User staker,
IStrategy[] memory strategies,
uint[] memory addedShares,
string memory err
) internal {
uint[] memory curShares = _getStakerWithdrawableShares(staker, strategies);
// Use timewarp to get previous staker shares
uint[] memory prevShares = _getPrevStakerWithdrawableShares(staker, strategies);
// For each strategy, check (prev - removed == cur)
for (uint i = 0; i < strategies.length; i++) {
assertEq(prevShares[i] + addedShares[i], curShares[i], err);
}
}
/// @dev This is currently used by dual slashing tests
/// TODO: potentially bound better
function assert_Snap_Added_Staker_WithdrawableShares_AtLeast(
User staker,
IStrategy[] memory strategies,
uint[] memory addedShares,
string memory err
) internal {
uint[] memory curShares = _getStakerWithdrawableShares(staker, strategies);
// Use timewarp to get previous staker shares
uint[] memory prevShares = _getPrevStakerWithdrawableShares(staker, strategies);
// For each strategy, check (prev - removed == cur)
for (uint i = 0; i < strategies.length; i++) {
assertApproxEqAbs(prevShares[i] + addedShares[i], curShares[i], 1e3, err);
}
}
/// @dev Check that the staker's withdrawable shares have decreased by `removedShares`
function assert_Snap_Removed_Staker_WithdrawableShares(
User staker,
IStrategy[] memory strategies,
uint[] memory removedShares,
string memory err
) internal {
uint[] memory curShares = _getStakerWithdrawableShares(staker, strategies);
// Use timewarp to get previous staker shares
uint[] memory prevShares = _getPrevStakerWithdrawableShares(staker, strategies);
// For each strategy, check (prev - removed == cur)
for (uint i = 0; i < strategies.length; i++) {
assertEq(prevShares[i] - removedShares[i], curShares[i], err);
}
}
function assert_Snap_Removed_Staker_WithdrawableShares(User staker, IStrategy strat, uint removedShares, string memory err) internal {
assert_Snap_Removed_Staker_WithdrawableShares(staker, strat.toArray(), removedShares.toArrayU256(), err);
}
function assert_Snap_Unchanged_Staker_WithdrawableShares(User staker, IStrategy[] memory strategies, string memory err) internal {
uint[] memory curShares = _getStakerWithdrawableShares(staker, strategies);
uint[] memory prevShares = _getPrevStakerWithdrawableShares(staker, strategies);
// For each strategy, check all shares have been withdrawn
for (uint i = 0; i < strategies.length; i++) {
assertEq(prevShares[i], curShares[i], err);
}
}
/// @dev Check that the staker's withdrawable shares have changed by the expected amount
function assert_Snap_Expected_Staker_WithdrawableShares_Delegation(
User staker,
User operator,
IStrategy[] memory strategies,
uint[] memory depositShares,
string memory err
) internal {
uint[] memory curShares = _getStakerWithdrawableShares(staker, strategies);
// Use timewarp to get previous staker shares
uint[] memory expectedShares = _getExpectedWithdrawableSharesDelegate(staker, operator, strategies, depositShares);
// For each strategy, check expected == current
for (uint i = 0; i < strategies.length; i++) {
assertEq(expectedShares[i], curShares[i], err);
}
}
function assert_Snap_Expected_Staker_WithdrawableShares_Deposit(
User staker,
User operator,
IStrategy[] memory strategies,
uint[] memory depositSharesAdded,
string memory err
) internal {
uint[] memory curShares = _getStakerWithdrawableShares(staker, strategies);
// Use timewarp to get previous staker shares
uint[] memory prevShares = _getPrevStakerWithdrawableShares(staker, strategies);
uint[] memory expectedWithdrawableShares = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
if (prevShares[i] == 0 && depositSharesAdded[i] > 0) {
expectedWithdrawableShares[i] =
_getExpectedWithdrawableSharesDeposit(staker, operator, strategies[i], depositSharesAdded[i]);
assertEq(curShares[i], expectedWithdrawableShares[i], err);
} else {
uint[] memory prevDepositShares = _getPrevStakerDepositShares(staker, strategies);
assertEq(
(prevDepositShares[i] + depositSharesAdded[i]).mulWad(_getDepositScalingFactor(staker, strategies[i])).mulWad(
_getSlashingFactor(staker, strategies[i])
),
curShares[i],
err
);
}
}
}
/// @dev Check that the staker's withdrawable shares have decreased by at least `removedShares`
/// @dev Used to handle overslashing of beacon chain
function assert_Snap_Removed_Staker_WithdrawableShares_AtLeast(
User staker,
IStrategy[] memory strategies,
uint[] memory removedShares,
string memory err
) internal {
uint[] memory curShares = _getStakerWithdrawableShares(staker, strategies);
// Use timewarp to get previous staker shares
uint[] memory prevShares = _getPrevStakerWithdrawableShares(staker, strategies);
// Assert that the decrease in withdrawable shares is at least as much as the removed shares
// Checking for expected rounding down behavior
for (uint i = 0; i < strategies.length; i++) {
assertGe(prevShares[i] - curShares[i], removedShares[i], err);
}
}
/// @dev Check that the staker's withdrawable shares have decreased by at least `removedShares`
/// @dev Used to handle overslashing of beacon chain with AVS slashings
function assert_Snap_Removed_Staker_WithdrawableShares_AtLeast(
User staker,
IStrategy[] memory strategies,
uint[] memory removedShares,
uint errBound,
string memory err
) internal {
uint[] memory curShares = _getStakerWithdrawableShares(staker, strategies);
// Use timewarp to get previous staker shares
uint[] memory prevShares = _getPrevStakerWithdrawableShares(staker, strategies);
// For each strategy, check diff between (prev-removed) and curr is at most 1 gwei
for (uint i = 0; i < strategies.length; i++) {
assertApproxEqAbs(prevShares[i] - removedShares[i], curShares[i], errBound, err);
}
}
function assert_Snap_Removed_Staker_WithdrawableShares_AtLeast(User staker, IStrategy strat, uint removedShares, string memory err)
internal
{
assert_Snap_Removed_Staker_WithdrawableShares_AtLeast(staker, strat.toArray(), removedShares.toArrayU256(), err);
}
function assert_Snap_Delta_StakerShares(User staker, IStrategy[] memory strategies, int[] memory shareDeltas, string memory err)
internal
{
int[] memory curShares = _getStakerDepositSharesInt(staker, strategies);
// Use timewarp to get previous staker shares
int[] memory prevShares = _getPrevStakerDepositSharesInt(staker, strategies);
// For each strategy, check (prev + added == cur)
for (uint i = 0; i < strategies.length; i++) {
assertEq(prevShares[i] + shareDeltas[i], curShares[i], err);
}
}
function assert_Snap_Unchanged_DSF(User staker, IStrategy[] memory strategies, string memory err) internal {
uint[] memory curDSFs = _getDepositScalingFactors(staker, strategies);
uint[] memory prevDSFs = _getPrevDepositScalingFactors(staker, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertEq(prevDSFs[i], curDSFs[i], err);
}
}
function assert_Snap_Increased_DSF(User staker, IStrategy[] memory strategies, string memory err) internal {
uint[] memory curDSFs = _getDepositScalingFactors(staker, strategies);
uint[] memory prevDSFs = _getPrevDepositScalingFactors(staker, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertGt(curDSFs[i], prevDSFs[i], err);
}
}
function assert_Snap_WithinErrorBounds_DSF(User staker, IStrategy[] memory strategies, string memory err) internal {
uint[] memory curDSFs = _getDepositScalingFactors(staker, strategies);
uint[] memory prevDSFs = _getPrevDepositScalingFactors(staker, strategies);
for (uint i = 0; i < strategies.length; i++) {
assertApproxEqAbs(curDSFs[i], prevDSFs[i], 1e2, err);
}
}
/// @dev Used to assert that the DSF is either increased or unchanged, depending on the slashing factor, on a deposit
function assert_Snap_DSF_State_Deposit(User staker, IStrategy[] memory strategies, string memory err) internal {
uint[] memory curDepositShares = _getStakerDepositShares(staker, strategies);
uint[] memory prevDepositShares = _getPrevStakerDepositShares(staker, strategies);
uint[] memory curDSFs = _getDepositScalingFactors(staker, strategies);
uint[] memory prevDSFs = _getPrevDepositScalingFactors(staker, strategies);
uint[] memory curSlashingFactors = _getSlashingFactors(staker, strategies);
uint[] memory prevSlashingFactors = _getPrevSlashingFactors(staker, strategies);
for (uint i = 0; i < strategies.length; i++) {
// If there was never a slashing, no need to normalize
if (curSlashingFactors[i] == WAD) {
assertEq(prevDSFs[i], curDSFs[i], err); // No slashing, so DSF is unchanged
assertEq(curDSFs[i], WAD, err); // DSF should have always been WAD
}
// If there was a slashing, and we deposit, normalize
else {
// If the DSF and slashing factor are already normalized against each other from a previous deposit (prevWithdrawableFactor very close to WAD)
// and there have been no subsequent slashings, DSF "should" stay the same, but recomputing decreases it slightly due to
// fixed point arithmetic rounding. Outer if is to prevent int underflow errors.
uint prevWithdrawableFactor = prevDSFs[i].mulWad(prevSlashingFactors[i]);
require(WAD >= prevWithdrawableFactor, "withdrawableFactor should always be less than or equal to WAD");
if (WAD - prevWithdrawableFactor < 1e2 && prevDepositShares[i] > 0 && curSlashingFactors[i] == prevSlashingFactors[i]) {
assertApproxEqAbs(curDSFs[i], prevDSFs[i], 1e2, err);
} else {
assertGt(curDSFs[i], prevDSFs[i], err); // Slashing, so DSF is increased
}
}
}
}
/// @dev When completing withdrawals as shares, we must also handle the case where a staker completes a withdrawal for 0 shares
function assert_Snap_DSF_State_WithdrawalAsShares(User staker, IStrategy[] memory strategies, string memory err) internal {
uint[] memory curDepositShares = _getStakerDepositShares(staker, strategies);
uint[] memory prevDepositShares = _getPrevStakerDepositShares(staker, strategies);
uint[] memory curDSFs = _getDepositScalingFactors(staker, strategies);
uint[] memory prevDSFs = _getPrevDepositScalingFactors(staker, strategies);
uint[] memory curSlashingFactors = _getSlashingFactors(staker, strategies);
uint[] memory prevSlashingFactors = _getPrevSlashingFactors(staker, strategies);
for (uint i = 0; i < strategies.length; i++) {
// If there was never a slashing, no need to normalize
// If there was a slashing, but we complete a withdrawal for 0 shares, no need to normalize
if (curSlashingFactors[i] == WAD || curDepositShares[i] == 0) {
assertEq(prevDSFs[i], curDSFs[i], err);
assertEq(curDSFs[i], WAD, err);
}
// If the staker has a slashingFactor of 0, any withdrawal as shares won't change the DSF
else if (curSlashingFactors[i] == 0) {
assertEq(prevDSFs[i], curDSFs[i], err);
}
// If there was a slashing and we complete a withdrawal for non-zero shares, normalize the DSF
else {
// If the DSF and slashing factor are already normalized against each other from a previous deposit (prevWithdrawableFactor very close to WAD)
// and there have been no subsequent slashings, DSF "should" stay the same, but recomputing decreases it slightly due to
// fixed point arithmetic rounding. Outer if is to prevent int underflow errors.
uint prevWithdrawableFactor = prevDSFs[i].mulWad(prevSlashingFactors[i]);
require(WAD >= prevWithdrawableFactor, "withdrawableFactor should always be less than or equal to WAD");
if (WAD - prevWithdrawableFactor < 1e2 && prevDepositShares[i] > 0 && curSlashingFactors[i] == prevSlashingFactors[i]) {
assertApproxEqAbs(curDSFs[i], prevDSFs[i], 1e2, err);
} else {
assertGt(curDSFs[i], prevDSFs[i], err); // Slashing, so DSF is increased
}
}
}
}
/// @dev On a delegation, the DSF should be increased if the operator magnitude is non-WAD
function assert_Snap_DSF_State_Delegation(
User staker,
IStrategy[] memory strategies,
uint[] memory delegatableShares,
string memory err
) internal {
address operator = delegationManager.delegatedTo(address(staker));
uint64[] memory maxMags = _getMaxMagnitudes(User(payable(operator)), strategies);
for (uint i = 0; i < strategies.length; i++) {
IStrategy[] memory stratArray = strategies[i].toArray();
// If you are delegating with 0 shares, no need to normalize
// If there was never an operator slashing, no need to normalize
if (delegatableShares[i] == 0 || maxMags[i] == WAD) {
assert_Snap_Unchanged_DSF(staker, stratArray, err);
// If we are not a beaconChainStrategy, we should also have a DSF of WAD
// We exclude BEACONCHAIN_ETH_STRAT because it could have had a non-WAD DSF from BC slashings
if (strategies[i] != BEACONCHAIN_ETH_STRAT) assert_DSF_WAD(staker, stratArray, err);
}
// If there was an operator slashing, and delegating with non-zero shares, normalize
else {
assert_Snap_Increased_DSF(staker, stratArray, err); // Slashing, so DSF is increased
}
}
}
/**
*
* SNAPSHOT ASSERTIONS: STRATEGY SHARES
*
*/
function assert_Snap_Removed_StrategyShares(IStrategy[] memory strategies, uint[] memory removedShares, string memory err) internal {
uint[] memory curShares = _getTotalStrategyShares(strategies);
// Use timewarp to get previous strategy shares
uint[] memory prevShares = _getPrevTotalStrategyShares(strategies);
for (uint i = 0; i < strategies.length; i++) {
// Ignore BeaconChainETH strategy since it doesn't keep track of global strategy shares
if (strategies[i] == BEACONCHAIN_ETH_STRAT) continue;
uint prevShare = prevShares[i];
uint curShare = curShares[i];
assertApproxEqAbs(prevShare - removedShares[i], curShare, 1, err);
}
}
function assert_Snap_Unchanged_StrategyShares(IStrategy[] memory strategies, string memory err) internal {
uint[] memory curShares = _getTotalStrategyShares(strategies);
// Use timewarp to get previous strategy shares
uint[] memory prevShares = _getPrevTotalStrategyShares(strategies);
for (uint i = 0; i < strategies.length; i++) {
uint prevShare = prevShares[i];
uint curShare = curShares[i];
assertEq(prevShare, curShare, err);
}
}
function assert_SlashableStake_Decrease_BCSlash(User staker) internal {
if (delegationManager.isDelegated(address(staker))) {
address operator = delegationManager.delegatedTo(address(staker));
(OperatorSet[] memory operatorSets, Allocation[] memory allocations) = _getStrategyAllocations(operator, BEACONCHAIN_ETH_STRAT);
for (uint i = 0; i < operatorSets.length; i++) {
if (allocations[i].currentMagnitude > 0) {
assert_Snap_StakeBecomeUnslashable(
User(payable(operator)),
operatorSets[i],
BEACONCHAIN_ETH_STRAT.toArray(),
"operator should have minSlashableStake decreased"
);
}
}
}
}
/**
*
* SNAPSHOT ASSERTIONS: UNDERLYING TOKEN
*
*/
/// @dev Check that the staker has `addedTokens` additional underlying tokens
// since the last snapshot
function assert_Snap_Added_TokenBalances(User staker, IERC20[] memory tokens, uint[] memory addedTokens, string memory err) internal {
uint[] memory curTokenBalances = _getTokenBalances(staker, tokens);
// Use timewarp to get previous token balances
uint[] memory prevTokenBalances = _getPrevTokenBalances(staker, tokens);
for (uint i = 0; i < tokens.length; i++) {
uint prevBalance = prevTokenBalances[i];
uint curBalance = curTokenBalances[i];
assertApproxEqAbs(prevBalance + addedTokens[i], curBalance, 1, err);
}
}
/// @dev Check that the staker has `removedTokens` fewer underlying tokens
// since the last snapshot
function assert_Snap_Removed_TokenBalances(User staker, IStrategy[] memory strategies, uint[] memory removedTokens, string memory err)
internal
{
IERC20[] memory tokens = _getUnderlyingTokens(strategies);
uint[] memory curTokenBalances = _getTokenBalances(staker, tokens);
// Use timewarp to get previous token balances
uint[] memory prevTokenBalances = _getPrevTokenBalances(staker, tokens);
for (uint i = 0; i < tokens.length; i++) {
uint prevBalance = prevTokenBalances[i];
uint curBalance = curTokenBalances[i];
assertEq(prevBalance - removedTokens[i], curBalance, err);
}
}
/// @dev Check that the staker's underlying token balance for ALL tokens have
/// not changed since the last snapshot
function assert_Snap_Unchanged_TokenBalances(User staker, string memory err) internal {
IERC20[] memory tokens = allTokens;
uint[] memory curTokenBalances = _getTokenBalances(staker, tokens);
// Use timewarp to get previous token balances
uint[] memory prevTokenBalances = _getPrevTokenBalances(staker, tokens);
for (uint i = 0; i < tokens.length; i++) {
assertEq(prevTokenBalances[i], curTokenBalances[i], err);
}
}
/**
*
* SNAPSHOT ASSERTIONS: QUEUED WITHDRAWALS
*
*/
function assert_Snap_Added_QueuedWithdrawals(User staker, Withdrawal[] memory withdrawals, string memory err) internal {
uint curQueuedWithdrawals = _getCumulativeWithdrawals(staker);
// Use timewarp to get previous cumulative withdrawals
uint prevQueuedWithdrawals = _getPrevCumulativeWithdrawals(staker);
assertEq(prevQueuedWithdrawals + withdrawals.length, curQueuedWithdrawals, err);
}
function assert_Snap_Added_QueuedWithdrawal(User staker, Withdrawal memory, /*withdrawal*/ string memory err) internal {
uint curQueuedWithdrawal = _getCumulativeWithdrawals(staker);
// Use timewarp to get previous cumulative withdrawals
uint prevQueuedWithdrawal = _getPrevCumulativeWithdrawals(staker);
assertEq(prevQueuedWithdrawal + 1, curQueuedWithdrawal, err);
}
/**
*
* SNAPSHOT ASSERTIONS: EIGENPODS
*
*/
function assert_Snap_Added_ActiveValidatorCount(User staker, uint addedValidators, string memory err) internal {
uint curActiveValidatorCount = _getActiveValidatorCount(staker);
uint prevActiveValidatorCount = _getPrevActiveValidatorCount(staker);
assertEq(prevActiveValidatorCount + addedValidators, curActiveValidatorCount, err);
}
function assert_Snap_Removed_ActiveValidatorCount(User staker, uint exitedValidators, string memory err) internal {
uint curActiveValidatorCount = _getActiveValidatorCount(staker);
uint prevActiveValidatorCount = _getPrevActiveValidatorCount(staker);
assertEq(curActiveValidatorCount + exitedValidators, prevActiveValidatorCount, err);
}
function assert_Snap_Unchanged_ActiveValidatorCount(User staker, string memory err) internal {
uint curActiveValidatorCount = _getActiveValidatorCount(staker);
uint prevActiveValidatorCount = _getPrevActiveValidatorCount(staker);
assertEq(curActiveValidatorCount, prevActiveValidatorCount, err);
}
function assert_Snap_Added_ActiveValidators(User staker, uint40[] memory addedValidators, string memory err) internal {
bytes32[] memory pubkeyHashes = beaconChain.getPubkeyHashes(addedValidators);
VALIDATOR_STATUS[] memory curStatuses = _getValidatorStatuses(staker, pubkeyHashes);
VALIDATOR_STATUS[] memory prevStatuses = _getPrevValidatorStatuses(staker, pubkeyHashes);
for (uint i = 0; i < curStatuses.length; i++) {
assertTrue(prevStatuses[i] == VALIDATOR_STATUS.INACTIVE, err);
assertTrue(curStatuses[i] == VALIDATOR_STATUS.ACTIVE, err);
}
}
function assert_Snap_Removed_ActiveValidators(User staker, uint40[] memory exitedValidators, string memory err) internal {
bytes32[] memory pubkeyHashes = beaconChain.getPubkeyHashes(exitedValidators);
VALIDATOR_STATUS[] memory curStatuses = _getValidatorStatuses(staker, pubkeyHashes);
VALIDATOR_STATUS[] memory prevStatuses = _getPrevValidatorStatuses(staker, pubkeyHashes);
for (uint i = 0; i < curStatuses.length; i++) {
assertTrue(prevStatuses[i] == VALIDATOR_STATUS.ACTIVE, err);
assertTrue(curStatuses[i] == VALIDATOR_STATUS.WITHDRAWN, err);
}
}
function assert_Snap_Created_Checkpoint(User staker, string memory err) internal {
uint64 curCheckpointTimestamp = _getCheckpointTimestamp(staker);
uint64 prevCheckpointTimestamp = _getPrevCheckpointTimestamp(staker);
assertEq(prevCheckpointTimestamp, 0, err);
assertTrue(curCheckpointTimestamp != 0, err);
}
function assert_Snap_Removed_Checkpoint(User staker, string memory err) internal {
uint64 curCheckpointTimestamp = _getCheckpointTimestamp(staker);
uint64 prevCheckpointTimestamp = _getPrevCheckpointTimestamp(staker);
assertEq(curCheckpointTimestamp, 0, err);
assertTrue(prevCheckpointTimestamp != 0, err);
}
function assert_Snap_Unchanged_Checkpoint(User staker, string memory err) internal {
uint64 curCheckpointTimestamp = _getCheckpointTimestamp(staker);
uint64 prevCheckpointTimestamp = _getPrevCheckpointTimestamp(staker);
assertEq(curCheckpointTimestamp, prevCheckpointTimestamp, err);
}
function assert_Snap_Updated_LastCheckpoint(User staker, string memory err) internal {
// Sorry for the confusing naming... the pod variable is called `lastCheckpointTimestamp`
uint64 curLastCheckpointTimestamp = _getLastCheckpointTimestamp(staker);
uint64 prevLastCheckpointTimestamp = _getPrevLastCheckpointTimestamp(staker);
assertTrue(curLastCheckpointTimestamp > prevLastCheckpointTimestamp, err);
}
function assert_Snap_Added_PodBalanceToWithdrawable(User staker, string memory err) internal {
uint64 curWithdrawableRestakedGwei = _getWithdrawableRestakedGwei(staker);
uint64 prevWithdrawableRestakedGwei = _getPrevWithdrawableRestakedGwei(staker);
uint64 prevCheckpointPodBalanceGwei = _getPrevCheckpointPodBalanceGwei(staker);
assertEq(prevWithdrawableRestakedGwei + prevCheckpointPodBalanceGwei, curWithdrawableRestakedGwei, err);
}
function assert_Snap_Added_WithdrawableGwei(User staker, uint64 addedGwei, string memory err) internal {
uint64 curWithdrawableRestakedGwei = _getWithdrawableRestakedGwei(staker);
uint64 prevWithdrawableRestakedGwei = _getPrevWithdrawableRestakedGwei(staker);
assertEq(prevWithdrawableRestakedGwei + addedGwei, curWithdrawableRestakedGwei, err);
}
function assert_Snap_Added_BalanceExitedGwei(User staker, uint64 addedGwei, string memory err) internal {
uint64 curCheckpointTimestamp = _getCheckpointTimestamp(staker);
uint64 prevCheckpointTimestamp = _getPrevCheckpointTimestamp(staker);
// If we just finalized a checkpoint, that's the timestamp we want to use
// to look up checkpoint balances exited
uint64 targetTimestamp = curCheckpointTimestamp;
if (curCheckpointTimestamp != prevCheckpointTimestamp) targetTimestamp = prevCheckpointTimestamp;
uint64 curExitedBalanceGwei = _getCheckpointBalanceExited(staker, targetTimestamp);
uint64 prevExitedBalanceGwei = _getPrevCheckpointBalanceExited(staker, targetTimestamp);
assertEq(prevExitedBalanceGwei + addedGwei, curExitedBalanceGwei, err);
}
function assert_Snap_Decreased_BCSF(User staker, string memory err) internal {
uint64 curBCSF = _getBeaconChainSlashingFactor(staker);
uint64 prevBCSF = _getPrevBeaconChainSlashingFactor(staker);
assertLt(curBCSF, prevBCSF, err);
}
function assert_Snap_Unchanged_BCSF(User staker, string memory err) internal {
uint64 curBCSF = _getBeaconChainSlashingFactor(staker);
uint64 prevBCSF = _getPrevBeaconChainSlashingFactor(staker);
assertEq(curBCSF, prevBCSF, err);
}
/**
*
* UTILITY METHODS
*
*/
/// @dev Fetches the opreator's allocation delay; asserts that it is set
function _getExistingAllocationDelay(User operator) internal view returns (uint32) {
(bool isSet, uint32 delay) = allocationManager.getAllocationDelay(address(operator));
assertTrue(isSet, "_getExistingAllocationDelay: expected allocation delay to be set");
return delay;
}
/// @dev Generate params to allocate all available magnitude to each strategy in the operator set
function _genAllocation_AllAvailable(User operator, OperatorSet memory operatorSet)
internal
view
returns (AllocateParams memory params)
{
return _genAllocation_AllAvailable({
operator: operator,
operatorSet: operatorSet,
strategies: allocationManager.getStrategiesInOperatorSet(operatorSet)
});
}
/// @dev Generate params to allocate all available magnitude to each strategy in the operator set
function _genAllocation_AllAvailable(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
view
returns (AllocateParams memory params)
{
params.operatorSet = operatorSet;
params.strategies = strategies;
params.newMagnitudes = new uint64[](params.strategies.length);
for (uint i = 0; i < params.strategies.length; i++) {
IStrategy strategy = params.strategies[i];
params.newMagnitudes[i] = allocationManager.getMaxMagnitude(address(operator), strategy);
}
}
/// @dev Gen params to allocate half of available magnitude to each strategy in the operator set
/// returns the params to complete this allocation
function _genAllocation_HalfAvailable(User operator, OperatorSet memory operatorSet)
internal
view
returns (AllocateParams memory params)
{
return _genAllocation_HalfAvailable({
operator: operator,
operatorSet: operatorSet,
strategies: allocationManager.getStrategiesInOperatorSet(operatorSet)
});
}
/// @dev Gen params to allocate half of available magnitude to each strategy in the operator set
/// returns the params to complete this allocation
function _genAllocation_HalfAvailable(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
view
returns (AllocateParams memory params)
{
params.operatorSet = operatorSet;
params.strategies = strategies;
params.newMagnitudes = new uint64[](params.strategies.length);
Allocation[] memory allocations = _getAllocations(operator, operatorSet, strategies);
Magnitudes[] memory magnitudes = _getMagnitudes(operator, strategies);
for (uint i = 0; i < params.strategies.length; i++) {
uint64 halfAvailable = uint64(magnitudes[i].allocatable) / 2;
params.newMagnitudes[i] = allocations[i].currentMagnitude + halfAvailable;
}
}
/// @dev Generate params to allocate a random portion of available magnitude to each strategy
/// in the operator set. All strategies will have a nonzero allocation, and the minimum allocation
/// will be 10% of available magnitude
function _genAllocation_Rand(User operator, OperatorSet memory operatorSet) internal returns (AllocateParams memory params) {
params.operatorSet = operatorSet;
params.strategies = allocationManager.getStrategiesInOperatorSet(operatorSet);
params.newMagnitudes = new uint64[](params.strategies.length);
Allocation[] memory allocations = _getAllocations(operator, operatorSet, params.strategies);
Magnitudes[] memory magnitudes = _getMagnitudes(operator, params.strategies);
for (uint i = 0; i < params.strategies.length; i++) {
// minimum of 10%, maximum of 100%. increments of 10%.
uint r = _randUint({min: 1, max: 10});
uint64 allocation = uint64(magnitudes[i].allocatable) / uint64(r);
params.newMagnitudes[i] = allocations[i].currentMagnitude + allocation;
}
}
/// @dev Generates params for a half deallocation from all strategies the operator is allocated to in the operator set
function _genDeallocation_HalfRemaining(User operator, OperatorSet memory operatorSet)
internal
view
returns (AllocateParams memory params)
{
return _genDeallocation_HalfRemaining({
operator: operator,
operatorSet: operatorSet,
strategies: allocationManager.getStrategiesInOperatorSet(operatorSet)
});
}
/// @dev Generates params for a half deallocation from all strategies the operator is allocated to in the operator set
function _genDeallocation_HalfRemaining(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
view
returns (AllocateParams memory params)
{
params.operatorSet = operatorSet;
params.strategies = strategies;
params.newMagnitudes = new uint64[](params.strategies.length);
for (uint i = 0; i < params.strategies.length; i++) {
IStrategy strategy = params.strategies[i];
params.newMagnitudes[i] = allocationManager.getEncumberedMagnitude(address(operator), strategy) / 2;
}
}
/// @dev Generates params for a full deallocation from all strategies the operator is allocated to in the operator set
function _genDeallocation_Full(User operator, OperatorSet memory operatorSet) internal view returns (AllocateParams memory params) {
return _genDeallocation_Full(operator, operatorSet, allocationManager.getStrategiesInOperatorSet(operatorSet));
}
/// @dev Generates params for a full deallocation from all strategies the operator is allocated to in the operator set
function _genDeallocation_Full(User, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
pure
returns (AllocateParams memory params)
{
params.operatorSet = operatorSet;
params.strategies = strategies;
params.newMagnitudes = new uint64[](params.strategies.length);
}
/// Generate random slashing between 1 and 99%
function _genSlashing_Rand(User operator, OperatorSet memory operatorSet) internal returns (SlashingParams memory params) {
params.operator = address(operator);
params.operatorSetId = operatorSet.id;
params.description = "genSlashing_Rand";
params.strategies = allocationManager.getStrategiesInOperatorSet(operatorSet).sort();
params.wadsToSlash = new uint[](params.strategies.length);
/// 1% * rand(1, 99)
uint slashWad = 1e16 * _randUint({min: 1, max: 99});
for (uint i = 0; i < params.wadsToSlash.length; i++) {
params.wadsToSlash[i] = slashWad;
}
}
function _genSlashing_Half(User operator, OperatorSet memory operatorSet) internal view returns (SlashingParams memory params) {
params.operator = address(operator);
params.operatorSetId = operatorSet.id;
params.description = "genSlashing_Half";
params.strategies = allocationManager.getStrategiesInOperatorSet(operatorSet).sort();
params.wadsToSlash = new uint[](params.strategies.length);
// slash 50%
for (uint i = 0; i < params.wadsToSlash.length; i++) {
params.wadsToSlash[i] = 5e17;
}
}
function _genSlashing_Full(User operator, OperatorSet memory operatorSet) internal view returns (SlashingParams memory params) {
params.operator = address(operator);
params.operatorSetId = operatorSet.id;
params.description = "_genSlashing_Full";
params.strategies = allocationManager.getStrategiesInOperatorSet(operatorSet).sort();
params.wadsToSlash = new uint[](params.strategies.length);
// slash 100%
for (uint i = 0; i < params.wadsToSlash.length; i++) {
params.wadsToSlash[i] = 1e18;
}
}
function _genSlashing_Custom(User operator, OperatorSet memory operatorSet, uint wadsToSlash)
internal
view
returns (SlashingParams memory params)
{
params.operator = address(operator);
params.operatorSetId = operatorSet.id;
params.description = "_genSlashing_Custom";
params.strategies = allocationManager.getStrategiesInOperatorSet(operatorSet).sort();
params.wadsToSlash = new uint[](params.strategies.length);
for (uint i = 0; i < params.wadsToSlash.length; i++) {
params.wadsToSlash[i] = wadsToSlash;
}
}
function _randWadToSlash() internal returns (uint) {
return _randUint({min: 0.01 ether, max: 1 ether});
}
function _strategiesAndWadsForFullSlash(OperatorSet memory operatorSet)
internal
view
returns (IStrategy[] memory strategies, uint[] memory wadsToSlash)
{
// Get list of all strategies in an operator set.
strategies = allocationManager.getStrategiesInOperatorSet(operatorSet);
wadsToSlash = new uint[](strategies.length);
for (uint i; i < strategies.length; ++i) {
wadsToSlash[i] = 1 ether;
}
return (strategies.sort(), wadsToSlash);
}
function _strategiesAndWadsForRandFullSlash(OperatorSet memory operatorSet)
internal
returns (IStrategy[] memory strategies, uint[] memory wadsToSlash)
{
// Get list of all strategies in an operator set.
strategies = allocationManager.getStrategiesInOperatorSet(operatorSet);
// Randomly select a subset of strategies to slash.
uint len = _randUint({min: 1, max: strategies.length});
// Update length of strategies array.
assembly {
mstore(strategies, len)
}
wadsToSlash = new uint[](len);
// Fully slash each selected strategy
for (uint i; i < len; ++i) {
wadsToSlash[i] = 1 ether;
}
return (strategies.sort(), wadsToSlash);
}
function _randMagnitudes(uint64 sum, uint len) internal returns (uint64[] memory magnitudes) {
magnitudes = new uint64[](len);
if (sum == 0 || len == 0) return magnitudes;
uint64 remaining = sum;
for (uint i; i < len; ++i) {
if (i == len - 1) {
magnitudes[i] = remaining;
} else {
magnitudes[i] = uint64(_randUint(0, remaining / (len - i)));
remaining -= magnitudes[i];
}
}
}
function _maxMagnitudes(OperatorSet memory operatorSet, User operator) internal view returns (uint64[] memory magnitudes) {
IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet);
uint len = strategies.length;
magnitudes = new uint64[](len);
if (len == 0) return magnitudes;
for (uint i; i < len; ++i) {
magnitudes[i] = allocationManager.getMaxMagnitude(address(operator), strategies[i]);
}
}
function _randWithdrawal(IStrategy[] memory strategies, uint[] memory shares) internal returns (IStrategy[] memory, uint[] memory) {
uint stratsToWithdraw = _randUint({min: 1, max: strategies.length});
IStrategy[] memory withdrawStrats = new IStrategy[](stratsToWithdraw);
uint[] memory withdrawShares = new uint[](stratsToWithdraw);
for (uint i = 0; i < stratsToWithdraw; i++) {
uint sharesToWithdraw;
if (strategies[i] == BEACONCHAIN_ETH_STRAT) {
// For native eth, withdraw a random amount of gwei (at least 1)
uint portion = _randUint({min: 1, max: shares[i] / GWEI_TO_WEI});
portion *= GWEI_TO_WEI;
sharesToWithdraw = shares[i] - portion;
} else {
// For LSTs, withdraw a random amount of shares (at least 1)
uint portion = _randUint({min: 1, max: shares[i]});
sharesToWithdraw = shares[i] - portion;
}
withdrawStrats[i] = strategies[i];
withdrawShares[i] = sharesToWithdraw;
}
return (withdrawStrats, withdrawShares);
}
/**
* Helpful getters:
*/
function _randSlashType() internal returns (BeaconChainMock.SlashType) {
return BeaconChainMock.SlashType(_randUint({min: 0, max: 2}));
}
function _randBalanceUpdate(User staker, IStrategy[] memory strategies) internal returns (int[] memory, int[] memory, int[] memory) {
int[] memory tokenDeltas = new int[](strategies.length);
int[] memory stakerShareDeltas = new int[](strategies.length);
int[] memory operatorShareDeltas = new int[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
if (strat == BEACONCHAIN_ETH_STRAT) {
// For native ETH, we're either going to slash the staker's validators,
// or award them consensus rewards. In either case, the magnitude of
// the balance update depends on the staker's active validator count
uint activeValidatorCount = staker.pod().activeValidatorCount();
int64 deltaGwei;
if (_randBool()) {
uint40[] memory validators = staker.getActiveValidators();
emit log_named_uint("slashing validators", validators.length);
deltaGwei = -int64(beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor));
beaconChain.advanceEpoch_NoRewards();
emit log_named_int("slashed amount", deltaGwei);
} else {
emit log("generating consensus rewards for validators");
deltaGwei = int64(uint64(activeValidatorCount) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI());
beaconChain.advanceEpoch_NoWithdraw();
}
tokenDeltas[i] = int(deltaGwei) * int(GWEI_TO_WEI);
stakerShareDeltas[i] = tokenDeltas[i];
operatorShareDeltas[i] = _calcNativeETHOperatorShareDelta(staker, stakerShareDeltas[i]);
emit log_named_int("beacon balance delta (gwei): ", deltaGwei);
emit log_named_int("staker share delta (gwei): ", stakerShareDeltas[i] / int(GWEI_TO_WEI));
emit log_named_int("operator share delta (gwei): ", operatorShareDeltas[i] / int(GWEI_TO_WEI));
} else {
// For LSTs, mint a random token amount
uint portion = _randUint({min: MIN_BALANCE, max: MAX_BALANCE});
StdCheats.deal(address(strat.underlyingToken()), address(staker), portion);
int delta = int(portion);
tokenDeltas[i] = delta;
stakerShareDeltas[i] = int(strat.underlyingToShares(uint(delta)));
operatorShareDeltas[i] = int(strat.underlyingToShares(uint(delta)));
}
}
return (tokenDeltas, stakerShareDeltas, operatorShareDeltas);
}
function _calcNativeETHOperatorShareDelta(User staker, int shareDelta) internal view returns (int) {
// TODO: Maybe we update parent method to have an M2 and Slashing version?
int curPodOwnerShares;
if (!isUpgraded) curPodOwnerShares = IEigenPodManager_DeprecatedM2(address(eigenPodManager)).podOwnerShares(address(staker));
else curPodOwnerShares = eigenPodManager.podOwnerDepositShares(address(staker));
int newPodOwnerShares = curPodOwnerShares + shareDelta;
if (curPodOwnerShares <= 0) {
// if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares
if (newPodOwnerShares <= 0) return 0;
// if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount
else return newPodOwnerShares;
} else {
// if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount
if (newPodOwnerShares <= 0) return (-curPodOwnerShares);
// if the shares started positive and stayed positive, then the change in delegateable shares
// is the difference between starting and ending amounts
else return (newPodOwnerShares - curPodOwnerShares);
}
}
function _calculateExpectedShares(Withdrawal memory withdrawal) internal view returns (uint[] memory) {
bytes32 root = delegationManager.calculateWithdrawalRoot(withdrawal);
(, uint[] memory shares) = delegationManager.getQueuedWithdrawal(root);
return shares;
}
/// @dev For some strategies/underlying token balances, calculate the expected shares received
/// from depositing all tokens
function _calculateExpectedShares(IStrategy[] memory strategies, uint[] memory tokenBalances) internal returns (uint[] memory) {
uint[] memory expectedShares = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
uint tokenBalance = tokenBalances[i];
if (strat == BEACONCHAIN_ETH_STRAT) expectedShares[i] = tokenBalance;
else expectedShares[i] = strat.underlyingToShares(tokenBalance);
}
return expectedShares;
}
/// @dev For some strategies/underlying token balances, calculate the expected shares received
/// from depositing all tokens
function _calculateExpectedTokens(IStrategy[] memory strategies, uint[] memory shares) internal returns (uint[] memory) {
uint[] memory expectedTokens = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
if (strat == BEACONCHAIN_ETH_STRAT) {
// We round down expected tokens to the nearest gwei
expectedTokens[i] = (shares[i] / GWEI_TO_WEI) * GWEI_TO_WEI;
} else {
expectedTokens[i] = strat.sharesToUnderlying(shares[i]);
}
}
return expectedTokens;
}
function _getWithdrawalHashes(Withdrawal[] memory withdrawals) internal view returns (bytes32[] memory) {
bytes32[] memory withdrawalRoots = new bytes32[](withdrawals.length);
for (uint i = 0; i < withdrawals.length; i++) {
withdrawalRoots[i] = delegationManager.calculateWithdrawalRoot(withdrawals[i]);
}
return withdrawalRoots;
}
/// @dev Converts a list of strategies to underlying tokens
function _getUnderlyingTokens(IStrategy[] memory strategies) internal view returns (IERC20[] memory) {
IERC20[] memory tokens = new IERC20[](strategies.length);
for (uint i = 0; i < tokens.length; i++) {
IStrategy strat = strategies[i];
if (strat == BEACONCHAIN_ETH_STRAT) tokens[i] = NATIVE_ETH;
else tokens[i] = strat.underlyingToken();
}
return tokens;
}
modifier timewarp() {
uint curState = timeMachine.travelToLast();
_;
timeMachine.travel(curState);
}
/// @dev Rolls forward by the minimum withdrawal delay blocks.
function _rollBlocksForCompleteWithdrawals(Withdrawal[] memory withdrawals) internal {
uint latest;
for (uint i = 0; i < withdrawals.length; ++i) {
if (withdrawals[i].startBlock > latest) latest = withdrawals[i].startBlock;
}
cheats.roll(latest + delegationManager.minWithdrawalDelayBlocks() + 1);
}
function _rollForward_AllocationDelay(User operator) internal {
uint32 delay = _getExistingAllocationDelay(operator);
rollForward(delay);
}
function _rollBackward_AllocationDelay(User operator) internal {
uint32 delay = _getExistingAllocationDelay(operator);
rollBackward(delay);
}
function _rollForward_DeallocationDelay() internal {
rollForward(allocationManager.DEALLOCATION_DELAY() + 1);
}
function _rollBackward_DeallocationDelay() internal {
rollBackward(allocationManager.DEALLOCATION_DELAY() + 1);
}
/// @dev Rolls forward by the default allocation delay blocks.
function _rollBlocksForCompleteAllocation(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies) internal {
uint latest;
for (uint i = 0; i < strategies.length; ++i) {
uint effectBlock = allocationManager.getAllocation(address(operator), operatorSet, strategies[i]).effectBlock;
if (effectBlock > latest) latest = effectBlock;
}
cheats.roll(latest + 1);
}
/// @dev Rolls forward by the default allocation delay blocks.
function _rollBlocksForCompleteAllocation(User operator, OperatorSet[] memory operatorSets, IStrategy[] memory strategies) internal {
uint latest;
for (uint i = 0; i < operatorSets.length; ++i) {
for (uint j = 0; j < strategies.length; ++j) {
uint effectBlock = allocationManager.getAllocation(address(operator), operatorSets[i], strategies[j]).effectBlock;
if (effectBlock > latest) latest = effectBlock;
}
}
cheats.roll(latest + 1);
}
/// @dev Uses timewarp modifier to get the operator set strategy allocations at the last snapshot.
function _getPrevAllocations(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
timewarp
returns (Allocation[] memory)
{
return _getAllocations(operator, operatorSet, strategies);
}
/// @dev Looks up each strategy for an operator set and returns a list of operator allocations.
function _getAllocations(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
view
returns (Allocation[] memory allocations)
{
allocations = new Allocation[](strategies.length);
for (uint i = 0; i < strategies.length; ++i) {
allocations[i] = allocationManager.getAllocation(address(operator), operatorSet, strategies[i]);
}
}
function _getPrevAllocatedStrats(User operator, OperatorSet memory operatorSet) internal timewarp returns (IStrategy[] memory) {
return _getAllocatedStrats(operator, operatorSet);
}
function _getAllocatedStrats(User operator, OperatorSet memory operatorSet) internal view returns (IStrategy[] memory) {
return allocationManager.getAllocatedStrategies(address(operator), operatorSet);
}
function _getPrevAllocatedSets(User operator) internal timewarp returns (OperatorSet[] memory) {
return _getAllocatedSets(operator);
}
function _getAllocatedSets(User operator) internal view returns (OperatorSet[] memory) {
return allocationManager.getAllocatedSets(address(operator));
}
function _getPrevRegisteredSets(User operator) internal timewarp returns (OperatorSet[] memory) {
return _getRegisteredSets(operator);
}
function _getRegisteredSets(User operator) internal view returns (OperatorSet[] memory) {
return allocationManager.getRegisteredSets(address(operator));
}
function _getPrevMembers(OperatorSet memory operatorSet) internal timewarp returns (address[] memory) {
return _getMembers(operatorSet);
}
function _getMembers(OperatorSet memory operatorSet) internal view returns (address[] memory) {
return allocationManager.getMembers(operatorSet);
}
struct Magnitudes {
uint encumbered;
uint allocatable;
uint max;
}
function _getPrevMagnitudes(User operator, IStrategy[] memory strategies) internal timewarp returns (Magnitudes[] memory) {
return _getMagnitudes(operator, strategies);
}
function _getMagnitudes(User operator, IStrategy[] memory strategies) internal view returns (Magnitudes[] memory magnitudes) {
magnitudes = new Magnitudes[](strategies.length);
for (uint i = 0; i < strategies.length; ++i) {
magnitudes[i] = Magnitudes({
encumbered: allocationManager.getEncumberedMagnitude(address(operator), strategies[i]),
allocatable: allocationManager.getAllocatableMagnitude(address(operator), strategies[i]),
max: allocationManager.getMaxMagnitude(address(operator), strategies[i])
});
}
}
function _getMaxMagnitudes(User operator, IStrategy[] memory strategies) internal view returns (uint64[] memory) {
return allocationManager.getMaxMagnitudes(address(operator), strategies);
}
function _getMaxMagnitudes(User operator, IStrategy[] memory strategies, uint32 blockNum) internal view returns (uint64[] memory) {
return allocationManager.getMaxMagnitudesAtBlock(address(operator), strategies, blockNum);
}
function _getPrevMinSlashableStake(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
timewarp
returns (uint[] memory)
{
return _getMinSlashableStake(operator, operatorSet, strategies);
}
function _getPrevMinSlashableStake(address operator, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
timewarp
returns (uint[] memory)
{
return _getMinSlashableStake(operator, operatorSet, strategies);
}
function _getMinSlashableStake(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
view
returns (uint[] memory)
{
return allocationManager.getMinimumSlashableStake({
operatorSet: operatorSet,
operators: address(operator).toArray(),
strategies: strategies,
futureBlock: uint32(block.number)
})[0];
}
function _getMinSlashableStake(address operator, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
view
returns (uint[] memory)
{
return allocationManager.getMinimumSlashableStake({
operatorSet: operatorSet,
operators: address(operator).toArray(),
strategies: strategies,
futureBlock: uint32(block.number)
})[0];
}
function _getPrevAllocatedStake(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
timewarp
returns (uint[] memory)
{
return _getAllocatedStake(operator, operatorSet, strategies);
}
function _getAllocatedStake(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
view
returns (uint[] memory)
{
return allocationManager.getAllocatedStake({
operatorSet: operatorSet,
operators: address(operator).toArray(),
strategies: strategies
})[0];
}
function _getStrategyAllocations(User operator, IStrategy strategy)
internal
view
returns (OperatorSet[] memory operatorSets, Allocation[] memory allocations)
{
(operatorSets, allocations) = allocationManager.getStrategyAllocations(address(operator), strategy);
}
function _getStrategyAllocations(address operator, IStrategy strategy)
internal
view
returns (OperatorSet[] memory operatorSets, Allocation[] memory allocations)
{
(operatorSets, allocations) = allocationManager.getStrategyAllocations(operator, strategy);
}
function _getPrevIsSlashable(User operator, OperatorSet memory operatorSet) internal timewarp returns (bool) {
return _getIsSlashable(operator, operatorSet);
}
function _getIsSlashable(User operator, OperatorSet memory operatorSet) internal view returns (bool) {
return allocationManager.isOperatorSlashable(address(operator), operatorSet);
}
function _getPrevIsMemberOfSet(User operator, OperatorSet memory operatorSet) internal timewarp returns (bool) {
return _getIsMemberOfSet(operator, operatorSet);
}
function _getIsMemberOfSet(User operator, OperatorSet memory operatorSet) internal view returns (bool) {
return allocationManager.isMemberOfOperatorSet(address(operator), operatorSet);
}
function _getPrevBurnableShares(IStrategy[] memory strategies) internal timewarp returns (uint[] memory) {
return _getBurnableShares(strategies);
}
function _getBurnableShares(IStrategy[] memory strategies) internal view returns (uint[] memory) {
uint[] memory burnableShares = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
if (strategies[i] == beaconChainETHStrategy) burnableShares[i] = eigenPodManager.burnableETHShares();
else burnableShares[i] = strategyManager.getBurnableShares(strategies[i]);
}
return burnableShares;
}
function _getPrevSlashableSharesInQueue(User operator, IStrategy[] memory strategies) internal timewarp returns (uint[] memory) {
return _getSlashableSharesInQueue(operator, strategies);
}
function _getSlashableSharesInQueue(User operator, IStrategy[] memory strategies) internal view returns (uint[] memory) {
uint[] memory slashableShares = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
slashableShares[i] = delegationManager.getSlashableSharesInQueue(address(operator), strategies[i]);
}
return slashableShares;
}
/// @dev Uses timewarp modifier to get operator shares at the last snapshot
function _getPrevOperatorShares(User operator, IStrategy[] memory strategies) internal timewarp returns (uint[] memory) {
return _getOperatorShares(operator, strategies);
}
/// @dev Looks up each strategy and returns a list of the operator's shares
function _getOperatorShares(User operator, IStrategy[] memory strategies) internal view returns (uint[] memory) {
uint[] memory curShares = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
curShares[i] = delegationManager.operatorShares(address(operator), strategies[i]);
}
return curShares;
}
/// @dev Uses timewarp modifier to get staker shares at the last snapshot
function _getPrevStakerDepositShares(User staker, IStrategy[] memory strategies) internal timewarp returns (uint[] memory) {
return _getStakerDepositShares(staker, strategies);
}
/// @dev Looks up each strategy and returns a list of the staker's shares
function _getStakerDepositShares(User staker, IStrategy[] memory strategies) internal view returns (uint[] memory) {
uint[] memory curShares = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
if (strat == BEACONCHAIN_ETH_STRAT) {
// This method should only be used for tests that handle positive
// balances. Negative balances are an edge case that require
// the own tests and helper methods.
int shares = eigenPodManager.podOwnerDepositShares(address(staker));
if (shares < 0) revert("_getStakerDepositShares: negative shares");
curShares[i] = uint(shares);
} else {
curShares[i] = strategyManager.stakerDepositShares(address(staker), strat);
}
}
return curShares;
}
/// @dev Uses timewarp modifier to get staker shares at the last snapshot
function _getPrevStakerDepositSharesInt(User staker, IStrategy[] memory strategies) internal timewarp returns (int[] memory) {
return _getStakerDepositSharesInt(staker, strategies);
}
/// @dev Looks up each strategy and returns a list of the staker's shares
function _getStakerDepositSharesInt(User staker, IStrategy[] memory strategies) internal view returns (int[] memory) {
int[] memory curShares = new int[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
if (strat == BEACONCHAIN_ETH_STRAT) curShares[i] = eigenPodManager.podOwnerDepositShares(address(staker));
else curShares[i] = int(strategyManager.stakerDepositShares(address(staker), strat));
}
return curShares;
}
function _getStakerStrategyList(User staker) internal view returns (IStrategy[] memory) {
return strategyManager.getStakerStrategyList(address(staker));
}
function _getPrevStakerWithdrawableShares(User staker, IStrategy[] memory strategies) internal timewarp returns (uint[] memory) {
return _getStakerWithdrawableShares(staker, strategies);
}
function _getStakerWithdrawableShares(User staker, IStrategy[] memory strategies) internal view returns (uint[] memory) {
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(address(staker), strategies);
return withdrawableShares;
}
function _calcWithdrawable(User staker, IStrategy[] memory strategies, uint[] memory depositSharesToWithdraw)
internal
view
returns (uint[] memory)
{
uint[] memory withdrawableShares = new uint[](strategies.length);
uint[] memory depositScalingFactors = _getDepositScalingFactors(staker, strategies);
for (uint i = 0; i < strategies.length; i++) {
withdrawableShares[i] =
depositSharesToWithdraw[i].mulWad(depositScalingFactors[i]).mulWad(_getSlashingFactor(staker, strategies[i]));
}
return withdrawableShares;
}
/// @dev Uses timewarp modifier to get staker beacon chain scaling factor at the last snapshot
function _getPrevBeaconChainSlashingFactor(User staker) internal timewarp returns (uint64) {
return _getBeaconChainSlashingFactor(staker);
}
/// @dev Looks up the staker's beacon chain scaling factor
function _getBeaconChainSlashingFactor(User staker) internal view returns (uint64) {
return eigenPodManager.beaconChainSlashingFactor(address(staker));
}
function _getPrevCumulativeWithdrawals(User staker) internal timewarp returns (uint) {
return _getCumulativeWithdrawals(staker);
}
function _getCumulativeWithdrawals(User staker) internal view returns (uint) {
return delegationManager.cumulativeWithdrawalsQueued(address(staker));
}
function _getPrevTokenBalances(User staker, IERC20[] memory tokens) internal timewarp returns (uint[] memory) {
return _getTokenBalances(staker, tokens);
}
function _getTokenBalances(User staker, IERC20[] memory tokens) internal view returns (uint[] memory) {
uint[] memory balances = new uint[](tokens.length);
for (uint i = 0; i < tokens.length; i++) {
if (tokens[i] == NATIVE_ETH) balances[i] = address(staker).balance;
else balances[i] = tokens[i].balanceOf(address(staker));
}
return balances;
}
function _getPrevTotalStrategyShares(IStrategy[] memory strategies) internal timewarp returns (uint[] memory) {
return _getTotalStrategyShares(strategies);
}
function _getTotalStrategyShares(IStrategy[] memory strategies) internal view returns (uint[] memory) {
uint[] memory shares = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
if (strategies[i] != BEACONCHAIN_ETH_STRAT) shares[i] = strategies[i].totalShares();
// BeaconChainETH strategy doesn't keep track of global strategy shares, so we ignore
}
return shares;
}
function _getDepositScalingFactors(User staker, IStrategy[] memory strategies) internal view returns (uint[] memory) {
uint[] memory depositScalingFactors = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
depositScalingFactors[i] = _getDepositScalingFactor(staker, strategies[i]);
}
return depositScalingFactors;
}
function _getDepositScalingFactor(User staker, IStrategy strategy) internal view returns (uint) {
return delegationManager.depositScalingFactor(address(staker), strategy);
}
function _getPrevDepositScalingFactors(User staker, IStrategy[] memory strategies) internal timewarp returns (uint[] memory) {
return _getDepositScalingFactors(staker, strategies);
}
function _getExpectedDSFUndelegate(User staker) internal view returns (uint expectedDepositScalingFactor) {
return WAD.divWad(_getBeaconChainSlashingFactor(staker));
}
function _getExpectedDSFDeposit(User staker, User operator, IStrategy strategy)
internal
view
returns (uint expectedDepositScalingFactor)
{
if (strategy == BEACONCHAIN_ETH_STRAT) {
return WAD.divWad(allocationManager.getMaxMagnitude(address(operator), strategy).mulWad(_getBeaconChainSlashingFactor(staker)));
} else {
return WAD.divWad(allocationManager.getMaxMagnitude(address(operator), strategy));
}
}
function _getExpectedWithdrawableSharesUndelegate(User staker, IStrategy[] memory strategies, uint[] memory shares)
internal
view
returns (uint[] memory)
{
uint[] memory expectedWithdrawableShares = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
if (strategies[i] == BEACONCHAIN_ETH_STRAT) {
expectedWithdrawableShares[i] =
shares[i].mulWad(_getExpectedDSFUndelegate(staker)).mulWad(_getBeaconChainSlashingFactor(staker));
} else {
expectedWithdrawableShares[i] = shares[i];
}
}
return expectedWithdrawableShares;
}
function _getExpectedDSFsDelegate(User staker, User operator, IStrategy[] memory strategies) internal returns (uint[] memory) {
uint[] memory expectedDepositScalingFactors = new uint[](strategies.length);
uint[] memory oldDepositScalingFactors = _getPrevDepositScalingFactors(staker, strategies);
uint64[] memory maxMagnitudes = _getMaxMagnitudes(operator, strategies);
for (uint i = 0; i < strategies.length; i++) {
expectedDepositScalingFactors[i] = oldDepositScalingFactors[i].divWad(maxMagnitudes[i]);
}
return expectedDepositScalingFactors;
}
function _getExpectedWithdrawableSharesDelegate(User staker, User operator, IStrategy[] memory strategies, uint[] memory depositShares)
internal
returns (uint[] memory)
{
uint[] memory expectedWithdrawableShares = new uint[](strategies.length);
uint[] memory expectedDSFs = _getExpectedDSFsDelegate(staker, operator, strategies);
uint64[] memory maxMagnitudes = _getMaxMagnitudes(operator, strategies);
for (uint i = 0; i < strategies.length; i++) {
if (strategies[i] == BEACONCHAIN_ETH_STRAT) {
expectedWithdrawableShares[i] =
depositShares[i].mulWad(expectedDSFs[i]).mulWad(maxMagnitudes[i].mulWad(_getBeaconChainSlashingFactor(staker)));
} else {
expectedWithdrawableShares[i] = depositShares[i].mulWad(expectedDSFs[i]).mulWad(maxMagnitudes[i]);
}
}
return expectedWithdrawableShares;
}
function _getExpectedWithdrawableSharesDeposit(User staker, User operator, IStrategy strategy, uint depositShares)
internal
view
returns (uint)
{
return depositShares.mulWad(_getExpectedDSFDeposit(staker, operator, strategy)).mulWad(_getSlashingFactor(staker, strategy));
}
function _getSlashingFactor(User staker, IStrategy strategy) internal view returns (uint) {
address operator = delegationManager.delegatedTo(address(staker));
uint64 maxMagnitude = allocationManager.getMaxMagnitudes(operator, strategy.toArray())[0];
if (strategy == beaconChainETHStrategy) return maxMagnitude.mulWad(eigenPodManager.beaconChainSlashingFactor(address(staker)));
return maxMagnitude;
}
function _getPrevSlashingFactor(User staker, IStrategy strategy) internal timewarp returns (uint) {
return _getSlashingFactor(staker, strategy);
}
function _getSlashingFactors(User staker, IStrategy[] memory strategies) internal view returns (uint[] memory) {
address operator = delegationManager.delegatedTo(address(staker));
uint64[] memory maxMagnitudes = allocationManager.getMaxMagnitudes(operator, strategies);
uint[] memory slashingFactors = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
if (strategies[i] == beaconChainETHStrategy) {
slashingFactors[i] = maxMagnitudes[i].mulWad(eigenPodManager.beaconChainSlashingFactor(address(staker)));
} else {
slashingFactors[i] = maxMagnitudes[i];
}
}
return slashingFactors;
}
function _getPrevSlashingFactors(User staker, IStrategy[] memory strategies) internal timewarp returns (uint[] memory) {
return _getSlashingFactors(staker, strategies);
}
function _getPrevWithdrawableShares(User staker, IStrategy[] memory strategies) internal timewarp returns (uint[] memory) {
return _getWithdrawableShares(staker, strategies);
}
function _getWithdrawableShares(User staker, IStrategy[] memory strategies) internal view returns (uint[] memory withdrawableShares) {
(withdrawableShares,) = delegationManager.getWithdrawableShares(address(staker), strategies);
}
function _getWithdrawableShares(User staker, IStrategy strategy) internal view returns (uint withdrawableShares) {
(uint[] memory _withdrawableShares,) = delegationManager.getWithdrawableShares(address(staker), strategy.toArray());
return _withdrawableShares[0];
}
/// @dev Assumes that the staker has one withdrawal queued
function _getWithdrawableSharesAfterCompletion(User staker) internal view returns (uint[] memory withdrawableShares) {
bytes32 root = delegationManager.getQueuedWithdrawalRoots(address(staker))[0];
(, withdrawableShares) = delegationManager.getQueuedWithdrawal(root);
}
function _getActiveValidatorCount(User staker) internal view returns (uint) {
EigenPod pod = staker.pod();
return pod.activeValidatorCount();
}
function _getPrevActiveValidatorCount(User staker) internal timewarp returns (uint) {
return _getActiveValidatorCount(staker);
}
function _getValidatorStatuses(User staker, bytes32[] memory pubkeyHashes) internal view returns (VALIDATOR_STATUS[] memory) {
EigenPod pod = staker.pod();
VALIDATOR_STATUS[] memory statuses = new VALIDATOR_STATUS[](pubkeyHashes.length);
for (uint i = 0; i < statuses.length; i++) {
statuses[i] = pod.validatorStatus(pubkeyHashes[i]);
}
return statuses;
}
function _getPrevValidatorStatuses(User staker, bytes32[] memory pubkeyHashes) internal timewarp returns (VALIDATOR_STATUS[] memory) {
return _getValidatorStatuses(staker, pubkeyHashes);
}
function _getCheckpointTimestamp(User staker) internal view returns (uint64) {
EigenPod pod = staker.pod();
return pod.currentCheckpointTimestamp();
}
function _getPrevCheckpointTimestamp(User staker) internal timewarp returns (uint64) {
return _getCheckpointTimestamp(staker);
}
function _getLastCheckpointTimestamp(User staker) internal view returns (uint64) {
EigenPod pod = staker.pod();
return pod.lastCheckpointTimestamp();
}
function _getPrevLastCheckpointTimestamp(User staker) internal timewarp returns (uint64) {
return _getLastCheckpointTimestamp(staker);
}
function _getWithdrawableRestakedGwei(User staker) internal view returns (uint64) {
EigenPod pod = staker.pod();
return pod.withdrawableRestakedExecutionLayerGwei();
}
function _getPrevWithdrawableRestakedGwei(User staker) internal timewarp returns (uint64) {
return _getWithdrawableRestakedGwei(staker);
}
function _getCheckpointPodBalanceGwei(User staker) internal view returns (uint64) {
EigenPod pod = staker.pod();
return uint64(pod.currentCheckpoint().podBalanceGwei);
}
function _getPrevCheckpointPodBalanceGwei(User staker) internal timewarp returns (uint64) {
return _getCheckpointPodBalanceGwei(staker);
}
function _getCheckpointBalanceExited(User staker, uint64 checkpointTimestamp) internal view returns (uint64) {
EigenPod pod = staker.pod();
return pod.checkpointBalanceExitedGwei(checkpointTimestamp);
}
function _getPrevCheckpointBalanceExited(User staker, uint64 checkpointTimestamp) internal timewarp returns (uint64) {
return _getCheckpointBalanceExited(staker, checkpointTimestamp);
}
function _getQueuedWithdrawals(User staker) internal view returns (Withdrawal[] memory) {
(Withdrawal[] memory withdrawals,) = delegationManager.getQueuedWithdrawals(address(staker));
return withdrawals;
}
}
````
## File: src/test/integration/IntegrationChecks.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationBase.t.sol";
import "src/test/integration/users/User.t.sol";
import "src/test/integration/users/User_M1.t.sol";
import "src/test/integration/users/User_M2.t.sol";
/// @notice Contract that provides utility functions to reuse common test blocks & checks
contract IntegrationCheckUtils is IntegrationBase {
using ArrayLib for *;
using SlashingLib for *;
using StdStyle for *;
/**
*
* EIGENPOD CHECKS
*
*/
function check_VerifyWC_State(User staker, uint40[] memory validators, uint64 beaconBalanceGwei) internal {
uint beaconBalanceWei = beaconBalanceGwei * GWEI_TO_WEI;
assert_DepositShares_GTE_WithdrawableShares(
staker, BEACONCHAIN_ETH_STRAT.toArray(), "deposit shares should be greater than or equal to withdrawable shares"
);
assert_Snap_Added_Staker_DepositShares(
staker, BEACONCHAIN_ETH_STRAT, beaconBalanceWei, "staker should have added deposit shares to beacon chain strat"
);
// TODO: fix this
// assert_Snap_Added_Staker_WithdrawableShares(staker, BEACONCHAIN_ETH_STRAT.toArray(), beaconBalanceWei.toArrayU256(), "staker should have added withdrawable shares to beacon chain strat");
assert_Snap_Added_ActiveValidatorCount(staker, validators.length, "staker should have increased active validator count");
assert_Snap_Added_ActiveValidators(staker, validators, "validators should each be active");
}
function check_StartCheckpoint_State(User staker) internal {
assert_ProofsRemainingEqualsActive(staker, "checkpoint proofs remaining should equal active validator count");
assert_Snap_Created_Checkpoint(staker, "staker should have created a new checkpoint");
}
function check_StartCheckpoint_WithPodBalance_State(User staker, uint64 expectedPodBalanceGwei) internal {
check_StartCheckpoint_State(staker);
assert_CheckpointPodBalance(staker, expectedPodBalanceGwei, "checkpoint podBalanceGwei should equal expected");
}
function check_StartCheckpoint_NoValidators_State(User staker, uint64 sharesAddedGwei) internal {
assert_Snap_Added_Staker_DepositShares(
staker, BEACONCHAIN_ETH_STRAT, sharesAddedGwei * GWEI_TO_WEI, "should have added staker shares"
);
assert_Snap_Added_WithdrawableGwei(staker, sharesAddedGwei, "should have added to withdrawable restaked gwei");
assert_Snap_Unchanged_ActiveValidatorCount(staker, "active validator count should remain 0");
assert_Snap_Updated_LastCheckpoint(staker, "last checkpoint timestamp should have increased");
assert_Snap_Unchanged_Checkpoint(staker, "current checkpoint timestamp should be unchanged");
}
function check_CompleteCheckpoint_State(User staker) internal {
assert_DepositShares_GTE_WithdrawableShares(
staker, BEACONCHAIN_ETH_STRAT.toArray(), "deposit shares should be greater than or equal to withdrawable shares"
);
assert_Snap_Removed_Checkpoint(staker, "should have deleted active checkpoint");
assert_Snap_Updated_LastCheckpoint(staker, "last checkpoint timestamp should be updated");
assert_Snap_Added_PodBalanceToWithdrawable(staker, "pod balance should have been added to withdrawable restaked exec layer gwei");
}
function check_CompleteCheckpoint_EarnOnBeacon_State(User staker, uint64 beaconBalanceAdded) internal {
check_CompleteCheckpoint_State(staker);
uint balanceAddedWei = beaconBalanceAdded * GWEI_TO_WEI;
assert_Snap_Added_Staker_DepositShares(
staker, BEACONCHAIN_ETH_STRAT, balanceAddedWei, "should have increased shares by excess beacon balance"
);
assert_Snap_Unchanged_BCSF(staker, "BCSF should be unchanged");
}
function check_CompleteCheckpoint_WithPodBalance_State(User staker, uint64 expectedPodBalanceGwei) internal {
check_CompleteCheckpoint_State(staker);
assert_Snap_Added_WithdrawableGwei(
staker, expectedPodBalanceGwei, "should have added expected gwei to withdrawable restaked exec layer gwei"
);
}
/// @dev Common checks for all slashing states, irrespective of validator exits
function check_CompleteCheckpoint_WithSlashing_State_Base(User staker) internal {
check_CompleteCheckpoint_State(staker);
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker shares should not have decreased");
assert_Snap_Decreased_BCSF(staker, "BCSF should decrease");
assert_Snap_Unchanged_DSF(staker, BEACONCHAIN_ETH_STRAT.toArray(), "DSF should be unchanged");
assert_SlashableStake_Decrease_BCSlash(staker);
}
function check_CompleteCheckpoint_WithSlashing_Exits_State_Base(User staker, uint40[] memory slashedValidators) internal {
check_CompleteCheckpoint_WithSlashing_State_Base(staker);
// Validator exits
assert_Snap_Removed_ActiveValidatorCount(staker, slashedValidators.length, "should have decreased active validator count");
assert_Snap_Removed_ActiveValidators(staker, slashedValidators, "exited validators should each be WITHDRAWN");
// System Vars
assert_Snap_Decreased_BCSF(staker, "BCSF should decrease");
assert_Snap_Unchanged_DSF(staker, BEACONCHAIN_ETH_STRAT.toArray(), "DSF should be unchanged");
assert_SlashableStake_Decrease_BCSlash(staker);
}
/// @dev Includes validator exits
function check_CompleteCheckpoint_WithSlashing_State(User staker, uint40[] memory slashedValidators, uint64 slashedAmountGwei)
internal
{
check_CompleteCheckpoint_WithSlashing_Exits_State_Base(staker, slashedValidators);
// Prove withdrawable shares decrease
assert_Snap_Removed_Staker_WithdrawableShares(
staker, BEACONCHAIN_ETH_STRAT, slashedAmountGwei * GWEI_TO_WEI, "should have decreased withdrawable shares by slashed amount"
);
}
/// @dev Same as above, but BCSF must be zero on a full slash
function check_CompleteCheckpoint_FullySlashed_State(User staker, uint40[] memory slashedValidators, uint64 slashedAmountGwei)
internal
{
check_CompleteCheckpoint_WithSlashing_State(staker, slashedValidators, slashedAmountGwei);
assert_Zero_BCSF(staker, "BCSF should be 0");
}
function check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(
User staker,
uint40[] memory slashedValidators,
uint64 slashedAmountGwei
) internal {
check_CompleteCheckpoint_WithSlashing_Exits_State_Base(staker, slashedValidators);
assert_Snap_Removed_Staker_WithdrawableShares_AtLeast(
staker,
BEACONCHAIN_ETH_STRAT,
slashedAmountGwei * GWEI_TO_WEI,
"should have decreased withdrawable shares by at least slashed amount"
);
}
/// @notice Used for edge cases where rounding behaviors of magnitudes close to 1 are tested.
/// Normal
function check_CompleteCheckPoint_WithSlashing_LowMagnitude_State(User staker, uint64 slashedAmountGwei) internal {
check_CompleteCheckpoint_State(staker);
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker shares should not have decreased");
assert_Snap_Removed_Staker_WithdrawableShares_AtLeast(
staker,
BEACONCHAIN_ETH_STRAT,
slashedAmountGwei * GWEI_TO_WEI,
"should have decreased withdrawable shares by at least slashed amount"
);
assert_Snap_Unchanged_ActiveValidatorCount(staker, "should not have changed active validator count");
assert_Snap_Decreased_BCSF(staker, "BCSF should decrease");
assert_Snap_Unchanged_DSF(staker, BEACONCHAIN_ETH_STRAT.toArray(), "DSF should be unchanged");
}
function check_CompleteCheckpoint_WithCLSlashing_State(User staker, uint64 slashedAmountGwei) internal {
check_CompleteCheckpoint_WithSlashing_State_Base(staker);
assert_Snap_Removed_Staker_WithdrawableShares(
staker, BEACONCHAIN_ETH_STRAT, slashedAmountGwei * GWEI_TO_WEI, "should have decreased withdrawable shares by slashed amount"
);
assert_Snap_Unchanged_ActiveValidatorCount(staker, "should not have changed active validator count");
}
function check_CompleteCheckpoint_WithCLSlashing_HandleRoundDown_State(User staker, uint64 slashedAmountGwei) internal {
check_CompleteCheckpoint_WithSlashing_State_Base(staker);
assert_Snap_Removed_Staker_WithdrawableShares_AtLeast(
staker,
BEACONCHAIN_ETH_STRAT,
slashedAmountGwei * GWEI_TO_WEI,
"should have decreased withdrawable shares by at least slashed amount"
);
assert_Snap_Unchanged_ActiveValidatorCount(staker, "should not have changed active validator count");
}
function check_CompleteCheckpoint_WithExits_State(User staker, uint40[] memory exitedValidators, uint64 exitedBalanceGwei) internal {
check_CompleteCheckpoint_WithPodBalance_State(staker, exitedBalanceGwei);
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker should not have changed shares");
assert_Snap_Added_BalanceExitedGwei(staker, exitedBalanceGwei, "should have attributed expected gwei to exited balance");
assert_Snap_Removed_ActiveValidatorCount(staker, exitedValidators.length, "should have decreased active validator count");
assert_Snap_Removed_ActiveValidators(staker, exitedValidators, "exited validators should each be WITHDRAWN");
}
function check_CompleteCheckpoint_ZeroBalanceDelta_State(User staker) internal {
check_CompleteCheckpoint_State(staker);
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker deposit shares should not have decreased");
assert_Snap_Unchanged_Staker_WithdrawableShares(
staker, BEACONCHAIN_ETH_STRAT.toArray(), "staker withdrawable shares should not have decreased"
);
assert_Snap_Unchanged_DSF(staker, BEACONCHAIN_ETH_STRAT.toArray(), "staker DSF should not have changed");
}
/**
*
* LST/DELEGATION CHECKS
*
*/
function check_Deposit_State(User staker, IStrategy[] memory strategies, uint[] memory shares) internal {
/// Deposit into strategies:
// For each of the assets held by the staker (either StrategyManager or EigenPodManager),
// the staker calls the relevant deposit function, depositing all held assets.
//
// ... check that all underlying tokens were transferred to the correct destination
// and that the staker now has the expected amount of delegated shares in each strategy
assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker should have transferred all underlying tokens");
assert_DepositShares_GTE_WithdrawableShares(
staker, strategies, "deposit shares should be greater than or equal to withdrawable shares"
);
assert_Snap_Added_Staker_DepositShares(staker, strategies, shares, "staker should expect shares in each strategy after depositing");
assert_StrategiesInStakerStrategyList(staker, strategies, "staker strategy list should contain all strategies");
if (delegationManager.isDelegated(address(staker))) {
User operator = User(payable(delegationManager.delegatedTo(address(staker))));
assert_Snap_Expected_Staker_WithdrawableShares_Deposit(
staker, operator, strategies, shares, "staker should have received expected withdrawable shares"
);
} else {
assert_Snap_Added_Staker_WithdrawableShares(staker, strategies, shares, "deposit should increase withdrawable shares");
}
assert_Snap_DSF_State_Deposit(staker, strategies, "staker's DSF not updated correctly");
}
function check_Deposit_State_PartialDeposit(
User staker,
IStrategy[] memory strategies,
uint[] memory shares,
uint[] memory tokenBalances
) internal {
/// Deposit into strategies:
// For each of the assets held by the staker (either StrategyManager or EigenPodManager),
// the staker calls the relevant deposit function, depositing some subset of held assets
//
// ... check that some underlying tokens were transferred to the correct destination
// and that the staker now has the expected amount of delegated shares in each strategy
assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "staker should have transferred some underlying tokens");
assert_DepositShares_GTE_WithdrawableShares(
staker, strategies, "deposit shares should be greater than or equal to withdrawable shares"
);
assert_StrategiesInStakerStrategyList(staker, strategies, "staker strategy list should contain all strategies");
assert_Snap_Added_Staker_DepositShares(
staker, strategies, shares, "staker should expected shares in each strategy after depositing"
);
if (delegationManager.isDelegated(address(staker))) {
User operator = User(payable(delegationManager.delegatedTo(address(staker))));
assert_Snap_Expected_Staker_WithdrawableShares_Deposit(
staker, operator, strategies, shares, "staker should have received expected withdrawable shares"
);
} else {
assert_Snap_Added_Staker_WithdrawableShares(staker, strategies, shares, "deposit should increase withdrawable shares");
}
assert_Snap_DSF_State_Deposit(staker, strategies, "staker's DSF not updated correctly");
}
function check_Delegation_State(User staker, User operator, IStrategy[] memory strategies, uint[] memory depositShares) internal {
/// Delegate to an operator:
//
// ... check that the staker is now delegated to the operator, and that the operator
// was awarded the staker shares
assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated");
assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should be delegated to operator");
assert_HasExpectedShares(staker, strategies, depositShares, "staker should still have expected shares after delegating");
assert_DepositShares_GTE_WithdrawableShares(
staker, strategies, "deposit shares should be greater than or equal to withdrawable shares"
);
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker shares should be unchanged after delegating");
assert_Snap_Expected_Staker_WithdrawableShares_Delegation(
staker, operator, strategies, depositShares, "withdrawable shares should be unchanged within rounding error after delegating"
);
uint[] memory delegatableShares = _getPrevStakerWithdrawableShares(staker, strategies);
assert_Snap_Added_OperatorShares(operator, strategies, delegatableShares, "operator should have received shares");
check_Added_SlashableStake(operator, strategies);
assert_Snap_DSF_State_Delegation(staker, strategies, delegatableShares, "staker's DSF not updated correctly");
}
function check_Added_SlashableStake(User operator, IStrategy[] memory strategies) internal {
for (uint i = 0; i < strategies.length; i++) {
(OperatorSet[] memory operatorSets, Allocation[] memory allocations) = _getStrategyAllocations(operator, strategies[i]);
for (uint j = 0; j < operatorSets.length; j++) {
if (allocations[j].currentMagnitude > 0) {
assert_Snap_StakeBecameSlashable(
operator, operatorSets[j], strategies[i].toArray(), "allocated strategies should have minSlashableStake increased"
);
}
}
}
}
function check_QueuedWithdrawal_State(
User staker,
User operator,
IStrategy[] memory strategies,
uint[] memory depositShares,
uint[] memory withdrawableShares,
Withdrawal[] memory withdrawals,
bytes32[] memory withdrawalRoots
) internal {
// The staker will queue one or more withdrawals for the selected strategies and shares
//
// ... check that each withdrawal was successfully enqueued, that the returned roots
// match the hashes of each withdrawal, and that the staker and operator have
// reduced shares.
_check_QueuedWithdrawal_State_NotDelegated(staker, strategies, depositShares, withdrawableShares, withdrawals, withdrawalRoots);
if (delegationManager.isDelegated(address(staker))) {
assert_Snap_Removed_OperatorShares(
operator, strategies, withdrawableShares, "check_QueuedWithdrawal_State: failed to remove operator shares"
);
assert_Snap_Increased_SlashableSharesInQueue(
operator, withdrawals, "check_QueuedWithdrawal_State: failed to increase slashable shares in queue"
);
check_Decreased_SlashableStake(operator, withdrawableShares, strategies);
}
}
/// @dev Basic queued withdrawal checks if the staker is not delegated, should be called by the above function only
function _check_QueuedWithdrawal_State_NotDelegated(
User staker,
IStrategy[] memory strategies,
uint[] memory depositShares,
uint[] memory withdrawableShares,
Withdrawal[] memory withdrawals,
bytes32[] memory withdrawalRoots
) private {
assertEq(withdrawalRoots.length, 1, "check_QueuedWithdrawal_State: should only have 1 withdrawal root after queueing");
assert_AllWithdrawalsPending(withdrawalRoots, "check_QueuedWithdrawal_State: staker withdrawals should now be pending");
assert_ValidWithdrawalHashes(
withdrawals, withdrawalRoots, "check_QueuedWithdrawal_State: calculated withdrawals should match returned roots"
);
assert_DepositShares_GTE_WithdrawableShares(
staker, strategies, "deposit shares should be greater than or equal to withdrawable shares"
);
assert_Snap_Added_QueuedWithdrawals(
staker, withdrawals, "check_QueuedWithdrawal_State: staker should have increased nonce by withdrawals.length"
);
assert_Snap_Removed_Staker_DepositShares(
staker, strategies, depositShares, "check_QueuedWithdrawal_State: failed to remove staker shares"
);
assert_Snap_Removed_Staker_WithdrawableShares(
staker, strategies, withdrawableShares, "check_QueuedWithdrawal_State: failed to remove staker withdrawable shares"
);
// Check that the dsf is either reset to wad or unchanged
for (uint i = 0; i < strategies.length; i++) {
// For a full withdrawal, the dsf should be reset to wad & the staker strategy list should not contain the strategy
if (_getStakerDepositShares(staker, strategies[i].toArray())[0] == 0) {
assert_DSF_WAD(staker, strategies[i].toArray(), "check_QueuedWithdrawal_State: dsf should be reset to wad");
assert_StrategyNotInStakerStrategyList(
staker, strategies[i], "check_QueuedWithdrawal_State: staker strategy list should not contain strategy"
);
}
// For a partial withdrawal, the dsf should not be changed & the strategy should still be in the staker strategy list
else {
assert_Snap_Unchanged_DSF(staker, strategies[i].toArray(), "check_QueuedWithdrawal_State: dsf should not be changed");
assert_StrategyInStakerStrategyList(
staker, strategies[i], "check_QueuedWithdrawal_State: staker strategy list should contain strategy"
);
}
}
}
function check_Decreased_SlashableStake(User operator, uint[] memory withdrawableShares, IStrategy[] memory strategies) internal {
for (uint i = 0; i < strategies.length; i++) {
if (withdrawableShares[i] > 0) {
(OperatorSet[] memory operatorSets, Allocation[] memory allocations) = _getStrategyAllocations(operator, strategies[i]);
for (uint j = 0; j < operatorSets.length; j++) {
if (allocations[j].currentMagnitude > 0) {
assert_Snap_StakeBecomeUnslashable(
operator,
operatorSets[j],
strategies[i].toArray(),
"allocated strategies should have minSlashableStake decreased"
);
}
}
}
}
}
function check_Undelegate_State(
User staker,
User operator,
Withdrawal[] memory withdrawals,
bytes32[] memory withdrawalRoots,
IStrategy[] memory strategies,
uint[] memory stakerDelegatedShares
) internal {
/// Undelegate from an operator
//
// ... check that the staker is undelegated, all strategies from which the staker is deposited are unqueued,
// that the returned root matches the hashes for each strategy and share amounts, and that the staker
// and operator have reduced shares
assert_HasNoDelegatableShares(staker, "staker should have withdrawn all shares");
assertFalse(delegationManager.isDelegated(address(staker)), "check_Undelegate_State: staker should not be delegated");
assert_ValidWithdrawalHashes(
withdrawals, withdrawalRoots, "check_Undelegate_State: calculated withdrawal should match returned root"
);
assert_AllWithdrawalsPending(withdrawalRoots, "check_Undelegate_State: stakers withdrawal should now be pending");
assert_DSF_WAD(staker, strategies, "check_Undelegate_State: staker dsfs should be reset to wad");
assert_StakerStrategyListEmpty(staker, "check_Undelegate_State: staker strategy list should be empty");
assert_Snap_Added_QueuedWithdrawals(
staker, withdrawals, "check_Undelegate_State: staker should have increased nonce by withdrawals.length"
);
assert_Snap_Removed_OperatorShares(
operator, strategies, stakerDelegatedShares, "check_Undelegate_State: failed to remove operator shares"
);
assert_RemovedAll_Staker_DepositShares(staker, strategies, "check_Undelegate_State: failed to remove staker shares");
assert_RemovedAll_Staker_WithdrawableShares(
staker, strategies, "check_Undelegate_State: failed to remove staker withdrawable shares"
);
}
function check_Redelegate_State(
User staker,
User oldOperator,
User newOperator,
IDelegationManagerTypes.Withdrawal[] memory withdrawals,
bytes32[] memory withdrawalRoots,
IStrategy[] memory strategies,
uint[] memory stakerDelegatedShares
) internal {
/// Redelegate to a new operator
//
// ... check that the staker is delegated to new operator, all strategies from which the staker is deposited are unqueued,
// that the returned root matches the hashes for each strategy and share amounts, and that the staker
// and operator have reduced shares
assertTrue(delegationManager.isDelegated(address(staker)), "check_Redelegate_State: staker should not be delegated");
assertEq(address(newOperator), delegationManager.delegatedTo(address(staker)), "staker should be delegated to operator");
assert_HasExpectedShares(
staker, strategies, new uint[](strategies.length), "staker should not have deposit shares after redelegation"
);
assert_Snap_Unchanged_OperatorShares(newOperator, "new operator should not have received any shares");
assert_ValidWithdrawalHashes(
withdrawals, withdrawalRoots, "check_Redelegate_State: calculated withdrawal should match returned root"
);
assert_AllWithdrawalsPending(withdrawalRoots, "check_Redelegate_State: stakers withdrawal should now be pending");
assert_Snap_Added_QueuedWithdrawals(
staker, withdrawals, "check_Redelegate_State: staker should have increased nonce by withdrawals.length"
);
assert_StakerStrategyListEmpty(staker, "check_Redelegate_State: staker strategy list should be empty");
assert_Snap_Removed_OperatorShares(
oldOperator, strategies, stakerDelegatedShares, "check_Redelegate_State: failed to remove operator shares"
);
assert_RemovedAll_Staker_DepositShares(staker, strategies, "check_Undelegate_State: failed to remove staker shares");
assert_RemovedAll_Staker_WithdrawableShares(
staker, strategies, "check_Redelegate_State: failed to remove staker withdrawable shares"
);
assert_Snap_Unchanged_OperatorShares(newOperator, "check_Redelegate_State: new operator shares should not have changed");
assert_DSF_WAD(staker, strategies, "check_Redelegate_State: staker dsfs should be reset to wad");
}
/**
* @notice Overloaded function to check the state after a withdrawal as tokens, accepting a non-user type for the operator.
* @param staker The staker who completed the withdrawal.
* @param operator The operator address, which can be a non-user type like address(0).
* @param withdrawal The details of the withdrawal that was completed.
* @param strategies The strategies from which the withdrawal was made.
* @param shares The number of shares involved in the withdrawal.
* @param tokens The tokens received after the withdrawal.
* @param expectedTokens The expected tokens to be received after the withdrawal.
*/
function check_Withdrawal_AsTokens_State(
User staker,
User operator,
Withdrawal memory withdrawal,
IStrategy[] memory strategies,
uint[] memory shares,
IERC20[] memory tokens,
uint[] memory expectedTokens
) internal {
// Common checks
assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending");
assert_DepositShares_GTE_WithdrawableShares(
staker, strategies, "deposit shares should be greater than or equal to withdrawable shares"
);
assert_Snap_Added_TokenBalances(staker, tokens, expectedTokens, "staker should have received expected tokens");
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker shares should not have changed");
assert_Snap_Unchanged_DSF(staker, strategies, "dsf should not be changed");
assert_Snap_Removed_StrategyShares(strategies, shares, "strategies should have total shares decremented");
// Checks specific to an operator that the Staker has delegated to
if (operator != User(payable(0))) {
if (operator != staker) assert_Snap_Unchanged_TokenBalances(operator, "operator token balances should not have changed");
assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed");
}
}
function check_Withdrawal_AsShares_State(
User staker,
User operator,
Withdrawal memory withdrawal,
IStrategy[] memory strategies,
uint[] memory withdrawableShares
) internal {
// Common checks applicable to both user and non-user operator types
assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending");
assert_DepositShares_GTE_WithdrawableShares(
staker, strategies, "deposit shares should be greater than or equal to withdrawable shares"
);
assert_Snap_Unchanged_TokenBalances(staker, "staker should not have any change in underlying token balances");
assert_Snap_Added_Staker_DepositShares(staker, strategies, withdrawableShares, "staker should have received expected shares");
assert_Snap_Expected_Staker_WithdrawableShares_Deposit(
staker, operator, strategies, withdrawableShares, "staker should have received expected withdrawable shares"
);
assert_Snap_Unchanged_StrategyShares(strategies, "strategies should have total shares unchanged");
// Additional checks or handling for the non-user operator scenario
if (operator != User(User(payable(0)))) {
if (operator != staker) {
assert_Snap_Unchanged_TokenBalances(operator, "operator should not have any change in underlying token balances");
}
assert_Snap_Added_OperatorShares(operator, strategies, withdrawableShares, "operator should have received shares");
}
assert_Snap_DSF_State_WithdrawalAsShares(staker, strategies, "staker's DSF not updated correctly");
}
/// @notice Difference from above is that operator shares do not increase since staker is not delegated
function check_Withdrawal_AsShares_Undelegated_State(
User staker,
User operator,
Withdrawal memory withdrawal,
IStrategy[] memory strategies,
uint[] memory withdrawableShares
) internal {
/// Complete withdrawal(s):
// The staker will complete the withdrawal as shares
//
// ... check that the withdrawal is not pending, that the token balances of the staker and operator are unchanged,
// that the withdrawer received the expected shares, and that that the total shares of each o
// strategy withdrawn remains unchanged
assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending");
assert_DepositShares_GTE_WithdrawableShares(
staker, strategies, "deposit shares should be greater than or equal to withdrawable shares"
);
assert_Snap_Unchanged_TokenBalances(staker, "staker should not have any change in underlying token balances");
assert_Snap_Unchanged_TokenBalances(operator, "operator should not have any change in underlying token balances");
assert_Snap_Added_Staker_DepositShares(
staker, strategies, withdrawableShares, "staker should have received expected deposit shares"
);
uint[] memory expectedWithdrawableShares = _getExpectedWithdrawableSharesUndelegate(staker, strategies, withdrawableShares);
assert_Snap_Added_Staker_WithdrawableShares(
staker, strategies, expectedWithdrawableShares, "staker should have received expected withdrawable shares"
);
assert_Snap_Unchanged_OperatorShares(operator, "operator should have shares unchanged");
assert_Snap_Unchanged_StrategyShares(strategies, "strategies should have total shares unchanged");
assert_Snap_DSF_State_WithdrawalAsShares(staker, strategies, "staker's DSF not updated correctly");
}
function check_Withdrawal_AsShares_Redelegated_State(
User staker,
User operator,
User newOperator,
Withdrawal memory withdrawal,
IStrategy[] memory strategies,
uint[] memory withdrawableShares
) internal {
/// Complete withdrawal(s):
// The staker will complete the withdrawal as shares
//
// ... check that the withdrawal is not pending, that the token balances of the staker and operator are unchanged,
// that the withdrawer received the expected shares, and that that the total shares of each o
// strategy withdrawn remains unchanged
assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending");
assert_DepositShares_GTE_WithdrawableShares(
staker, strategies, "deposit shares should be greater than or equal to withdrawable shares"
);
assert_Snap_Unchanged_TokenBalances(staker, "staker should not have any change in underlying token balances");
assert_Snap_Unchanged_TokenBalances(operator, "operator should not have any change in underlying token balances");
assert_Snap_Added_Staker_DepositShares(staker, strategies, withdrawableShares, "staker should have received expected shares");
assert_Snap_Unchanged_OperatorShares(operator, "old operator should have shares unchanged");
assert_Snap_Added_OperatorShares(newOperator, strategies, withdrawableShares, "new operator should have received shares");
assert_Snap_Unchanged_StrategyShares(strategies, "strategies should have total shares unchanged");
assert_Snap_Expected_Staker_WithdrawableShares_Deposit(
staker, newOperator, strategies, withdrawableShares, "staker should have received expected withdrawable shares"
);
assert_Snap_DSF_State_WithdrawalAsShares(staker, strategies, "staker's DSF not updated correctly");
}
/**
*
* ALM - BASIC INVARIANTS
*
*/
/// @dev Run a method as if the user's allocation delay had passed
/// When done, reset block number so other tests are not affected
modifier activateAllocation(User operator) {
_rollForward_AllocationDelay(operator);
_;
_rollBackward_AllocationDelay(operator);
}
/// @dev Run a method as if the deallocation delay has passed
/// When done, reset block number so other tests are not affected
modifier activateDeallocation() {
_rollForward_DeallocationDelay();
_;
_rollBackward_DeallocationDelay();
}
/// @dev Run a method ONLY IF the operator has a nonzero activation delay
modifier skipIfInstantAlloc(User operator) {
/// Note: if the ALM says the allocation delay is "not set", this will revert
uint32 delay = _getExistingAllocationDelay(operator);
if (delay != 0) _;
else console.log("%s", "skipping checks for operator with allocation delay of 0".italic());
}
/// @dev Check global max magnitude invariants - these should ALWAYS hold
function check_MaxMag_Invariants(User operator) internal view {
assert_MaxMagsEqualMaxMagsAtCurrentBlock(operator, allStrats, "max magnitudes should equal upperlookup at current block");
assert_MaxEqualsAllocatablePlusEncumbered(operator, "max magnitude should equal encumbered plus allocatable");
}
/// @dev Check that the last call to modifyAllocations resulted in a non-pending modification
function check_ActiveModification_State(User operator, AllocateParams memory params) internal view {
OperatorSet memory operatorSet = params.operatorSet;
IStrategy[] memory strategies = params.strategies;
assert_CurrentMagnitude(operator, params, "current magnitude should match allocate params");
assert_NoPendingModification(operator, operatorSet, strategies, "there should not be a pending modification for any strategy");
}
function check_IsSlashable_State(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies) internal view {
assert_IsSlashable(operator, operatorSet, "operator should be slashable for operator set");
assert_CurMinSlashableEqualsMinAllocated(
operator, operatorSet, strategies, "minimum slashable stake should equal allocated stake at current block"
);
}
function check_NotSlashable_State(User operator, OperatorSet memory operatorSet) internal view {
assert_NotSlashable(operator, operatorSet, "operator should not be slashable for operator set");
assert_NoSlashableStake(operator, operatorSet, "operator should not have any slashable stake");
}
/**
*
* ALM - REGISTRATION
*
*/
/// @dev Basic invariants that should hold after EVERY call to `registerForOperatorSets`
/// NOTE: These are only slightly modified from check_Base_Deregistration_State
/// If you add invariants here, consider adding them there (and vice-versa)
function check_Base_Registration_State(User operator, OperatorSet memory operatorSet) internal {
check_MaxMag_Invariants(operator);
check_IsSlashable_State(operator, operatorSet, allocationManager.getStrategiesInOperatorSet(operatorSet));
// Registration SHOULD register the operator, making them slashable and adding them as a member of the set
assert_Snap_Became_Registered(operator, operatorSet, "operator should not have been registered before, and is now registered");
assert_Snap_Became_Slashable(operator, operatorSet, "operator should not have been slashable before, and is now slashable");
assert_Snap_Added_RegisteredSet(operator, operatorSet, "should have added operator sets to list of registered sets");
assert_Snap_Added_MemberOfSet(operator, operatorSet, "should have added operator to list of set members");
// Registration should NOT change anything about magnitude, allocations, or allocated sets
assert_Snap_Unchanged_AllocatedSets(operator, "should not have updated allocated sets");
assert_Snap_Unchanged_AllocatedStrats(operator, operatorSet, "should not have updated allocated strategies");
assert_Snap_Unchanged_MaxMagnitude(operator, allStrats, "should not have updated max magnitudes in any way");
assert_Snap_Unchanged_AllocatedStake(operator, operatorSet, allStrats, "should not have updated allocated stake in any way");
assert_Snap_Unchanged_StrategyAllocations(operator, operatorSet, allStrats, "should not have updated any individual allocations");
assert_Snap_Unchanged_EncumberedMagnitude(operator, allStrats, "should not have updated encumbered magnitude");
assert_Snap_Unchanged_AllocatableMagnitude(operator, allStrats, "should not have updated allocatable magnitude");
}
/// @dev Check invariants for registerForOperatorSets given a set of strategies
/// for which NO allocation exists (currentMag/pendingDiff are 0)
/// @param unallocated For the given operatorSet, a list of strategies for which NO allocation exists
function check_Registration_State_NoAllocation(User operator, OperatorSet memory operatorSet, IStrategy[] memory unallocated)
internal
{
check_Base_Registration_State(operator, operatorSet);
/// The operator is NOT allocated, ensure their slashable stake and magnitudes are unchanged
assert_Snap_Unchanged_AllocatedStake(operator, operatorSet, unallocated, "should not have updated allocated stake in any way");
assert_Snap_Unchanged_SlashableStake(
operator, operatorSet, unallocated, "operator should not have increased slashable stake for any given strategy"
);
}
/// @dev Check invariants for registerForOperatorSets AFTER a prior allocation becomes active
/// @param active allocation params to the last call to modifyAllocations
///
/// ASSUMES:
/// - the effect block for `params` has already passed
/// - params.newMagnitudes does NOT contain any `0` entries
function check_Registration_State_ActiveAllocation(User operator, AllocateParams memory active) internal {
OperatorSet memory operatorSet = active.operatorSet;
IStrategy[] memory strategies = active.strategies;
/// Basic registerForOperatorSets invariants
check_Base_Registration_State(operator, operatorSet);
/// Given an active allocation, check that the allocation is reflected in state
assert_IsAllocatedToSet(operator, operatorSet, "operatorSet should be included in allocatedSets");
assert_IsAllocatedToSetStrats(operator, operatorSet, strategies, "strategies should be included in allocatedStrategies");
assert_CurrentMagnitude(operator, active, "queried allocation should equal active allocation");
/// Check that additional stake just became slashable
assert_Snap_Unchanged_AllocatedStake(operator, operatorSet, strategies, "should not have updated allocated stake in any way");
assert_Snap_StakeBecameSlashable(
operator, operatorSet, strategies, "registration should make entirety of active allocation slashable"
);
}
/// @dev Check registration invariants. Assumes the operator has a PENDING allocation
/// to the set, but that the allocation's effect block has not yet been reached
function check_Registration_State_PendingAllocation(User operator, AllocateParams memory params) internal {
OperatorSet memory operatorSet = params.operatorSet;
IStrategy[] memory strategies = params.strategies;
check_Base_Registration_State(operator, operatorSet);
assert_IsAllocatedToSet(operator, operatorSet, "operator should be allocated to set, even while pending");
assert_IsAllocatedToSetStrats(operator, operatorSet, strategies, "strategies should be included in allocatedStrategies");
/// Skip pending checks if operator has no allocation delay
uint32 delay = _getExistingAllocationDelay(operator);
if (delay == 0) return;
assert_Snap_Unchanged_AllocatedStake(operator, operatorSet, strategies, "should not have updated allocated stake in any way");
assert_Snap_Unchanged_SlashableStake(operator, operatorSet, allStrats, "operator should not have increased slashable stake");
}
/**
*
* ALM - DEREGISTRATION
*
*/
/// @dev Basic invariants that should hold after EVERY call to `deregisterFromOperatorSets`
/// NOTE: These are only slightly modified from check_Base_Registration_State
/// If you add invariants here, consider adding them there (and vice-versa)
function check_Base_Deregistration_State(User operator, OperatorSet memory operatorSet) internal {
check_MaxMag_Invariants(operator);
// Deregistration SHOULD remove the operator as a member of the set
assert_Snap_Became_Deregistered(operator, operatorSet, "operator should have been registered before, and is now deregistered");
assert_Snap_Removed_RegisteredSet(operator, operatorSet, "should have removed operator set from list of registered sets");
assert_Snap_Removed_MemberOfSet(operator, operatorSet, "should have removed operator from list of set members");
// Deregistration should NOT change slashability, magnitude, allocations, or allocated sets
assert_Snap_Remains_Slashable(operator, operatorSet, "operator should have been slashable already, and should still be slashable");
assert_Snap_Unchanged_AllocatedSets(operator, "should not have updated allocated sets");
assert_Snap_Unchanged_AllocatedStrats(operator, operatorSet, "should not have updated allocated strategies");
assert_Snap_Unchanged_MaxMagnitude(operator, allStrats, "should not have updated max magnitudes in any way");
assert_Snap_Unchanged_AllocatedStake(operator, operatorSet, allStrats, "should not have updated allocated stake in any way");
assert_Snap_Unchanged_StrategyAllocations(operator, operatorSet, allStrats, "should not have updated any individual allocations");
assert_Snap_Unchanged_EncumberedMagnitude(operator, allStrats, "should not have updated encumbered magnitude");
assert_Snap_Unchanged_AllocatableMagnitude(operator, allStrats, "should not have updated allocatable magnitude");
_rollForward_DeallocationDelay();
{
check_NotSlashable_State(operator, operatorSet);
}
_rollBackward_DeallocationDelay();
}
function check_Deregistration_State_NoAllocation(User operator, OperatorSet memory operatorSet) internal {
check_Base_Deregistration_State(operator, operatorSet);
assert_Snap_Unchanged_AllocatedStake(operator, operatorSet, allStrats, "should not have updated allocated stake in any way");
assert_Snap_Unchanged_SlashableStake(
operator, operatorSet, allStrats, "operator should not have increased slashable stake for any given strategy"
);
}
function check_Deregistration_State_ActiveAllocation(User operator, OperatorSet memory operatorSet) internal {
check_Base_Deregistration_State(operator, operatorSet);
assert_Snap_Unchanged_AllocatedStake(operator, operatorSet, allStrats, "should not have updated allocated stake in any way");
assert_Snap_Unchanged_SlashableStake(
operator, operatorSet, allStrats, "operator should not have increased slashable stake for any given strategy"
);
}
function check_Deregistration_State_PendingAllocation(User operator, OperatorSet memory operatorSet) internal {
check_Base_Deregistration_State(operator, operatorSet);
assert_Snap_Unchanged_AllocatedStake(operator, operatorSet, allStrats, "should not have updated allocated stake in any way");
assert_Snap_Unchanged_SlashableStake(
operator, operatorSet, allStrats, "operator should not have increased slashable stake for any given strategy"
);
}
/**
*
* ALM - INCREASE ALLOCATION
*
*/
/// @dev Basic invariants that should hold after all calls to `modifyAllocations`
/// where the input `params` represent an _increase_ in magnitude
function check_Base_IncrAlloc_State(User operator, AllocateParams memory params) internal {
check_MaxMag_Invariants(operator);
OperatorSet memory operatorSet = params.operatorSet;
IStrategy[] memory strategies = params.strategies;
// Increasing Allocation should NOT change operator set registration, max magnitude
assert_Snap_Unchanged_Registration(operator, operatorSet, "operator registration status should be unchanged");
assert_Snap_Unchanged_Slashability(operator, operatorSet, "operator slashability should be unchanged");
assert_Snap_Unchanged_RegisteredSet(operator, "list of registered sets should remain unchanged");
assert_Snap_Unchanged_MemberOfSet(operatorSet, "list of set members should remain unchanged");
assert_Snap_Unchanged_MaxMagnitude(operator, allStrats, "should not have updated max magnitudes in any way");
// Increasing Allocation SHOULD consume magnitude and mark the operator as being allocated to the set
assert_IsAllocatedToSet(operator, operatorSet, "operator should be allocated to set");
assert_IsAllocatedToSetStrats(operator, operatorSet, strategies, "operator should be allocated to strategies for set");
assert_Snap_Allocated_Magnitude(operator, strategies, "operator should have allocated magnitude");
}
/// @dev Invariants for modifyAllocations. Use when:
/// - operator is NOT slashable for this operator set
/// - last call to modifyAllocations created an INCREASE in allocation
function check_IncrAlloc_State_NotSlashable(User operator, AllocateParams memory params) internal {
check_Base_IncrAlloc_State(operator, params);
check_NotSlashable_State(operator, params.operatorSet);
/// Run checks on pending allocation, if the operator has a nonzero delay
check_IncrAlloc_State_NotSlashable_Pending(operator, params);
/// Run checks on active allocation
check_IncrAlloc_State_NotSlashable_Active(operator, params);
}
/// @dev Invariants for modifyAllocations. Used when:
/// - operator is NOT slashable for this operator set
/// - last call to modifyAllocations created an INCREASE in allocation
/// - effectBlock for the increase HAS NOT been reached
function check_IncrAlloc_State_NotSlashable_Pending(User operator, AllocateParams memory params) private skipIfInstantAlloc(operator) {
// Validate operator allocation is pending
assert_HasPendingIncrease(operator, params, "params should reflect a pending modification");
// Should not have allocated magnitude
assert_Snap_Unchanged_AllocatedStake(
operator, params.operatorSet, params.strategies, "should not have updated allocated stake in any way"
);
assert_Snap_Unchanged_SlashableStake(
operator, params.operatorSet, params.strategies, "should not have updated allocated stake in any way"
);
}
/// @dev Invariants for modifyAllocations. Used when:
/// - operator is NOT slashable for this operator set
/// - last call to modifyAllocations created an INCREASE in allocation
/// - effectBlock for the increase HAS been reached
function check_IncrAlloc_State_NotSlashable_Active(User operator, AllocateParams memory params) private activateAllocation(operator) {
// Validate allocation is active
check_ActiveModification_State(operator, params);
// SHOULD set current magnitude and increase allocated stake
assert_Snap_Set_CurrentMagnitude(operator, params, "should have updated the operator's magnitude");
assert_HasAllocatedStake(operator, params, "operator should have expected allocated stake for each strategy");
assert_Snap_StakeBecameAllocated(operator, params.operatorSet, params.strategies, "allocated stake should have increased");
// Should NOT change slashable stake
assert_Snap_Unchanged_SlashableStake(operator, params.operatorSet, params.strategies, "slashable stake should not be changed");
}
/// @dev Invariants for modifyAllocations. Use when:
/// - operator IS slashable for this operator set
/// - last call to modifyAllocations created an INCREASE in allocation
function check_IncrAlloc_State_Slashable(User operator, AllocateParams memory params) internal {
check_Base_IncrAlloc_State(operator, params);
check_IsSlashable_State(operator, params.operatorSet, params.strategies);
/// Run checks on pending allocation, if the operator has a nonzero delay
check_IncrAlloc_State_Slashable_Pending(operator, params);
/// Run checks on active allocation
check_IncrAlloc_State_Slashable_Active(operator, params);
}
/// @dev Invariants for modifyAllocations. Use when:
/// - operator IS slashable for this operator set
/// - last call to modifyAllocations created an INCREASE in allocation
/// - operator has no delegated shares/stake so their slashable stake remains UNCHANGED
function check_IncrAlloc_State_Slashable_NoDelegatedStake(User operator, AllocateParams memory params) internal {
check_Base_IncrAlloc_State(operator, params);
check_IsSlashable_State(operator, params.operatorSet, params.strategies);
/// Run checks on pending allocation, if the operator has a nonzero delay
check_IncrAlloc_State_Slashable_Pending(operator, params);
// Validate operator has no pending modification and has increased allocation
check_IncrAlloc_State_Slashable_Active_NoDelegatedStake(operator, params);
}
/// @dev Invariants for modifyAllocations. Used when:
/// - operator IS slashable for this operator set
/// - last call to modifyAllocations created an INCREASE in allocation
/// - effectBlock for the increase HAS NOT been reached
function check_IncrAlloc_State_Slashable_Pending(User operator, AllocateParams memory params) private skipIfInstantAlloc(operator) {
OperatorSet memory operatorSet = params.operatorSet;
IStrategy[] memory strategies = params.strategies;
// Validate operator has pending allocation and unchanged allocated/slashable stake
assert_HasPendingIncrease(operator, params, "params should reflect a pending modification");
// Should not have allocated magnitude
assert_Snap_Unchanged_AllocatedStake(operator, operatorSet, strategies, "should not have updated allocated stake in any way");
assert_Snap_Unchanged_SlashableStake(operator, operatorSet, strategies, "should not have updated allocated stake in any way");
}
/// @dev Invariants for modifyAllocations. Used when:
/// - operator IS slashable for this operator set
/// - last call to modifyAllocations created an INCREASE in allocation
/// - effectBlock for the increase HAS been reached
function check_IncrAlloc_State_Slashable_Active(User operator, AllocateParams memory params) private activateAllocation(operator) {
// Validate operator does not have a pending modification, and has expected slashable stake
check_ActiveModification_State(operator, params);
// SHOULD set current magnitude and increase slashable/allocated stake
assert_Snap_Set_CurrentMagnitude(operator, params, "should have updated the operator's magnitude");
assert_HasAllocatedStake(operator, params, "operator should have expected allocated stake for each strategy");
assert_HasSlashableStake(operator, params, "operator should have expected slashable stake for each strategy");
assert_Snap_StakeBecameAllocated(operator, params.operatorSet, params.strategies, "allocated stake should have increased");
assert_Snap_StakeBecameSlashable(operator, params.operatorSet, params.strategies, "slashable stake should have increased");
}
/// @dev Invariants for modifyAllocations. Used when:
/// - operator IS slashable for this operator set
/// - last call to modifyAllocations created an INCREASE in allocation
/// - effectBlock for the increase HAS been reached
function check_IncrAlloc_State_Slashable_Active_NoDelegatedStake(User operator, AllocateParams memory params)
private
activateAllocation(operator)
{
// Validate operator does not have a pending modification, and has expected slashable stake
check_ActiveModification_State(operator, params);
// SHOULD set current magnitude and increase slashable/allocated stake
assert_Snap_Set_CurrentMagnitude(operator, params, "should have updated the operator's magnitude");
assert_HasAllocatedStake(operator, params, "operator should have expected allocated stake for each strategy");
assert_HasSlashableStake(operator, params, "operator should have expected slashable stake for each strategy");
}
/**
*
* ALM - DECREASE ALLOCATION
*
*/
/// @dev Basic invariants that should hold after all calls to `modifyAllocations`
/// where the input `params` represent a decrease in magnitude
function check_Base_DecrAlloc_State(User operator, AllocateParams memory params) internal {
check_MaxMag_Invariants(operator);
OperatorSet memory operatorSet = params.operatorSet;
// Decreasing Allocation should NOT change operator set registration, max magnitude
assert_Snap_Unchanged_Registration(operator, operatorSet, "operator registration status should be unchanged");
assert_Snap_Unchanged_Slashability(operator, operatorSet, "operator slashability should be unchanged");
assert_Snap_Unchanged_RegisteredSet(operator, "list of registered sets should remain unchanged");
assert_Snap_Unchanged_MemberOfSet(operatorSet, "list of set members should remain unchanged");
assert_Snap_Unchanged_MaxMagnitude(operator, allStrats, "should not have updated max magnitudes in any way");
}
function check_DecrAlloc_State_NotSlashable(User operator, AllocateParams memory params) internal {
OperatorSet memory operatorSet = params.operatorSet;
IStrategy[] memory strategies = params.strategies;
check_Base_DecrAlloc_State(operator, params);
check_NotSlashable_State(operator, operatorSet);
check_ActiveModification_State(operator, params);
// SHOULD set current magnitude and decrease allocated stake
assert_HasAllocatedStake(operator, params, "operator should have expected allocated stake for each strategy");
assert_Snap_Set_CurrentMagnitude(operator, params, "should have updated the operator's magnitude");
assert_Snap_StakeBecameDeallocated(operator, operatorSet, strategies, "allocated stake should have increased");
assert_Snap_Deallocated_Magnitude(operator, strategies, "should have deallocated magnitude");
}
function check_DecrAlloc_State_Slashable(User operator, AllocateParams memory params) internal {
check_Base_DecrAlloc_State(operator, params);
check_IsSlashable_State(operator, params.operatorSet, params.strategies);
// Run checks on pending deallocation
check_DecrAlloc_State_Slashable_Pending(operator, params);
// Run checks on active deallocation
check_DecrAlloc_State_Slashable_Active(operator, params);
}
function check_DecrAlloc_State_Slashable_Pending(User operator, AllocateParams memory params) private {
// Validate deallocation is pending
assert_HasPendingDecrease(operator, params, "params should reflect a pending modification");
// Should NOT have changed allocated magnitude or stake
assert_Snap_Unchanged_EncumberedMagnitude(operator, params.strategies, "should not have changed encumbered magnitude");
assert_Snap_Unchanged_AllocatableMagnitude(operator, params.strategies, "should not have changed allocatable magnitude");
assert_Snap_Unchanged_AllocatedSets(operator, "should not have removed operator set from allocated sets");
assert_Snap_Unchanged_AllocatedStake(
operator, params.operatorSet, params.strategies, "should not have updated allocated stake in any way"
);
assert_Snap_Unchanged_SlashableStake(
operator, params.operatorSet, params.strategies, "should not have updated allocated stake in any way"
);
}
function check_DecrAlloc_State_Slashable_Active(User operator, AllocateParams memory params) private activateDeallocation {
OperatorSet memory operatorSet = params.operatorSet;
IStrategy[] memory strategies = params.strategies;
check_ActiveModification_State(operator, params);
// SHOULD set current magnitude and decrease allocated stake
assert_Snap_Set_CurrentMagnitude(operator, params, "should have updated the operator's magnitude");
assert_HasAllocatedStake(operator, params, "operator should have expected allocated stake for each strategy");
assert_HasSlashableStake(operator, params, "operator should have expected slashable stake for each strategy");
assert_Snap_StakeBecomeUnslashable(operator, operatorSet, strategies, "slashable stake should have decreased");
assert_Snap_StakeBecameDeallocated(operator, params.operatorSet, params.strategies, "allocated stake should have decreased");
assert_Snap_Deallocated_Magnitude(operator, strategies, "should have deallocated magnitude");
}
function check_FullyDeallocated_State(User operator, AllocateParams memory allocateParams, AllocateParams memory deallocateParams)
internal
{
OperatorSet memory operatorSet = allocateParams.operatorSet;
assert_NoSlashableStake(operator, operatorSet, "should not have any slashable stake");
// TODO - broken; do we want to fix this?
// assert_Snap_Removed_AllocatedSet(operator, operatorSet, "should have removed operator set from allocated sets");
// Any instant deallocation
assert_Snap_Removed_EncumberedMagnitude(
operator, allocateParams.strategies, allocateParams.newMagnitudes, "should have removed allocation from encumbered magnitude"
);
assert_Snap_Added_AllocatableMagnitude(
operator, allocateParams.strategies, allocateParams.newMagnitudes, "should have added allocation to allocatable magnitude"
);
assert_Snap_Unchanged_MaxMagnitude(operator, allStrats, "max magnitude should not have changed");
assert_MaxEqualsAllocatablePlusEncumbered(operator, "max magnitude should equal encumbered plus allocatable");
check_ActiveModification_State(operator, deallocateParams);
}
/**
*
* ALM - SLASHING
*
*/
function check_Base_Slashing_State(User operator, AllocateParams memory allocateParams, SlashingParams memory slashParams) internal {
OperatorSet memory operatorSet = allocateParams.operatorSet;
check_MaxMag_Invariants(operator);
check_IsSlashable_State(operator, operatorSet, allocateParams.strategies);
// Slashing SHOULD change max magnitude and current allocation
assert_Snap_Slashed_MaxMagnitude(operator, operatorSet, slashParams, "slash should lower max magnitude");
assert_Snap_Slashed_EncumberedMagnitude(operator, slashParams, "slash should lower encumbered magnitude");
assert_Snap_Slashed_AllocatedStake(operator, operatorSet, slashParams, "slash should lower allocated stake");
assert_Snap_Slashed_SlashableStake(operator, operatorSet, slashParams, "slash should lower slashable stake");
assert_Snap_Slashed_OperatorShares(operator, slashParams, "slash should remove operator shares");
assert_Snap_Slashed_Allocation(operator, operatorSet, slashParams, "slash should reduce current magnitude");
assert_Snap_Increased_BurnableShares(operator, slashParams, "slash should increase burnable shares");
// Slashing SHOULD NOT change allocatable magnitude, registration, and slashability status
assert_Snap_Unchanged_AllocatableMagnitude(operator, allStrats, "slashing should not change allocatable magnitude");
assert_Snap_Unchanged_Registration(operator, operatorSet, "slash should not change registration status");
assert_Snap_Unchanged_Slashability(operator, operatorSet, "slash should not change slashability status");
// assert_Snap_Unchanged_AllocatedSets(operator, "should not have updated allocated sets");
// assert_Snap_Unchanged_AllocatedStrats(operator, operatorSet, "should not have updated allocated strategies");
}
function check_Base_Slashing_State(
User operator,
AllocateParams memory allocateParams,
SlashingParams memory slashParams,
Withdrawal[] memory withdrawals
) internal {
check_Base_Slashing_State(operator, allocateParams, slashParams);
assert_Snap_Decreased_SlashableSharesInQueue(operator, slashParams, withdrawals, "slash should decrease slashable shares in queue");
}
/// Slashing invariants when the operator has been fully slashed for every strategy in the operator set
function check_FullySlashed_State(User operator, AllocateParams memory allocateParams, SlashingParams memory slashParams) internal {
check_Base_Slashing_State(operator, allocateParams, slashParams);
assert_Snap_Removed_AllocatedSet(operator, allocateParams.operatorSet, "should not have updated allocated sets");
assert_Snap_Removed_AllocatedStrats(
operator, allocateParams.operatorSet, slashParams.strategies, "should not have updated allocated strategies"
);
}
/**
*
* DUAL SLASHING CHECKS
*
*/
function check_CompleteCheckpoint_AfterAVSSlash_BCSlash(
User staker,
uint40[] memory slashedValidators,
uint depositShares,
uint64 slashedBalanceGwei,
AllocateParams memory allocateParams,
SlashingParams memory slashingParams
) internal {
check_CompleteCheckpoint_WithSlashing_Exits_State_Base(staker, slashedValidators);
// From the original shares to the BC slash (AVS slash in between), the shares should have decreased by at least the BC slash amount
assert_withdrawableSharesDecreasedByAtLeast(
staker,
BEACONCHAIN_ETH_STRAT,
depositShares,
uint(slashedBalanceGwei * GWEI_TO_WEI),
"should have decreased withdrawable shares by at least the BC slash amount"
);
// Calculate the withdrawable shares
assert_Snap_StakerWithdrawableShares_AfterAVSSlash_BCSlash(
staker, allocateParams, slashingParams, "should have decreased withdrawable shares correctly"
);
}
function check_CompleteCheckpoint_AfterAVSSlash_ValidatorProven_BCSlash(
User staker,
uint40[] memory slashedValidators,
uint originalWithdrawableShares,
uint extraValidatorShares,
AllocateParams memory allocateParams,
SlashingParams memory slashingParams
) internal {
// Checkpoint State
check_CompleteCheckpoint_WithSlashing_Exits_State_Base(staker, slashedValidators);
assert_Snap_StakerWithdrawableShares_AVSSlash_ValidatorProven_BCSlash(
staker,
originalWithdrawableShares,
extraValidatorShares,
allocateParams,
slashingParams,
"should have decreased withdrawable shares correctly"
);
}
/// @dev Assumes the eth deposit was greater than the amount slashed
function check_CompleteCheckpoint_AfterAVSSlash_ETHDeposit_BCSlash(
User staker,
uint40[] memory slashedValidators,
uint64 slashedBalanceGwei,
uint beaconSharesAddedGwei
) internal {
// Checkpoint State - can't use base check since a BC balance decrease isn't occurring
check_CompleteCheckpoint_State(staker);
assert_Snap_Removed_ActiveValidatorCount(staker, slashedValidators.length, "should have decreased active validator count");
assert_Snap_Removed_ActiveValidators(staker, slashedValidators, "exited validators should each be WITHDRAWN");
// Share check.
// DSF and deposit shares should increase because our deposit amount is larger than the slash amount
uint sharesAdded = uint(beaconSharesAddedGwei - slashedBalanceGwei) * GWEI_TO_WEI;
assert_Snap_Unchanged_BCSF(staker, "BCSF should be unchanged");
assert_Snap_Increased_DSF(staker, BEACONCHAIN_ETH_STRAT.toArray(), "DSF should be increases");
assert_Snap_Added_Staker_DepositShares(staker, BEACONCHAIN_ETH_STRAT, sharesAdded, "staker deposit shares should have increased");
assert_Snap_Added_Staker_WithdrawableShares_AtLeast(
staker,
BEACONCHAIN_ETH_STRAT.toArray(),
sharesAdded.toArrayU256(),
"staker withdrawable shares should increase by diff of deposit and slash"
);
}
function check_CompleteCheckpoint_FullDualSlashes(
User staker,
uint40[] memory slashedValidators,
AllocateParams memory allocateParams,
SlashingParams memory slashingParams
) internal {
check_CompleteCheckpoint_WithSlashing_Exits_State_Base(staker, slashedValidators);
// Assert no withdrawable shares
assert_Zero_WithdrawableShares(staker, BEACONCHAIN_ETH_STRAT, "should not have any withdrawable shares");
assert_Snap_Unchanged_Staker_WithdrawableShares(
staker, BEACONCHAIN_ETH_STRAT.toArray(), "should not have any change in withdrawable shares"
);
assert_Snap_StakerWithdrawableShares_AfterAVSSlash_BCSlash(
staker, allocateParams, slashingParams, "should have decreased withdrawable shares correctly"
);
}
}
````
## File: src/test/integration/IntegrationDeployer.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
// Imports
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/proxy/beacon/IBeacon.sol";
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "forge-std/Test.sol";
import "src/contracts/core/DelegationManager.sol";
import "src/contracts/core/AllocationManager.sol";
import "src/contracts/core/StrategyManager.sol";
import "src/contracts/strategies/StrategyFactory.sol";
import "src/contracts/strategies/StrategyBase.sol";
import "src/contracts/strategies/StrategyBaseTVLLimits.sol";
import "src/contracts/pods/EigenPodManager.sol";
import "src/contracts/pods/EigenPod.sol";
import "src/contracts/permissions/PauserRegistry.sol";
import "src/contracts/permissions/PermissionController.sol";
import "src/test/mocks/EmptyContract.sol";
import "src/test/mocks/ETHDepositMock.sol";
import "src/test/integration/mocks/BeaconChainMock.t.sol";
import "src/test/integration/users/AVS.t.sol";
import "src/test/integration/users/User.t.sol";
import "src/test/integration/users/User_M1.t.sol";
import "src/test/integration/users/User_M2.t.sol";
import "script/utils/ExistingDeploymentParser.sol";
IStrategy constant beaconChainETHStrategy = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);
abstract contract IntegrationDeployer is ExistingDeploymentParser {
using StdStyle for *;
using ArrayLib for *;
// Fork ids for specific fork tests
bool isUpgraded;
uint mainnetForkBlock = 21_616_692; // Post Protocol Council upgrade
string version = "v9.9.9";
// Beacon chain genesis time when running locally
// Multiple of 12 for sanity's sake
uint64 constant GENESIS_TIME_LOCAL = 1 hours * 12;
uint64 constant GENESIS_TIME_MAINNET = 1_606_824_023;
uint64 BEACON_GENESIS_TIME; // set after forkType is decided
// Beacon chain deposit contract. The BeaconChainMock contract etchs ETHPOSDepositMock code here.
IETHPOSDeposit constant DEPOSIT_CONTRACT = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa);
uint8 constant NUM_LST_STRATS = 32;
// Lists of strategies used in the system
//
// When we select random user assets, we use the `assetType` to determine
// which of these lists to select user assets from.
IStrategy[] lstStrats;
IStrategy[] ethStrats; // only has one strat tbh
IStrategy[] allStrats; // just a combination of the above 2 lists
IERC20[] allTokens; // `allStrats`, but contains all of the underlying tokens instead
uint maxUniqueAssetsHeld;
// If a token is in this mapping, then we will ignore this LST as it causes issues with reading balanceOf
mapping(address => bool) public tokensNotTested;
// Mock Contracts to deploy
TimeMachine public timeMachine;
BeaconChainMock public beaconChain;
// Admin Addresses
address constant pauser = address(555);
address constant unpauser = address(556);
// Randomness state vars
bytes32 random;
// After calling `_configRand`, these are the allowed "variants" on users that will
// be returned from `_randUser`.
bytes assetTypes;
bytes userTypes;
// Set only once in setUp, if FORK_MAINNET env is set
uint forkType;
/// @dev used to configure randomness and default user/asset types
///
/// Tests that want alternate user/asset types can still use this modifier,
/// and then configure user/asset types individually using the methods:
/// _configAssetTypes(...)
/// _configUserTypes(...)
///
/// (Alternatively, this modifier can be overwritten)
modifier rand(uint24 r) virtual {
_configRand({_randomSeed: r, _assetTypes: HOLDS_LST | HOLDS_ETH | HOLDS_ALL, _userTypes: DEFAULT | ALT_METHODS});
// Used to create shared setups between tests
_init();
_;
}
constructor() {
address stETH_Holesky = 0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034;
address stETH_Mainnet = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
address OETH_Mainnet = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3;
address osETH_Holesky = 0xF603c5A3F774F05d4D848A9bB139809790890864;
address osETH_Mainnet = 0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38;
address cbETH_Holesky = 0x8720095Fa5739Ab051799211B146a2EEE4Dd8B37;
tokensNotTested[stETH_Holesky] = true;
tokensNotTested[stETH_Mainnet] = true;
tokensNotTested[OETH_Mainnet] = true;
tokensNotTested[osETH_Holesky] = true;
tokensNotTested[osETH_Mainnet] = true;
tokensNotTested[cbETH_Holesky] = true;
// Use current contracts by default. Upgrade tests are only run with mainnet fork tests
// using the `UpgradeTest.t.sol` mixin.
isUpgraded = true;
}
function NAME() public view virtual override returns (string memory) {
return "Integration Deployer";
}
/**
* @dev Anyone who wants to test using this contract in a separate repo via submodules may have to
* override this function to set the correct paths for the deployment info files.
*
* This setUp function will account for specific --fork-url flags and deploy/upgrade contracts accordingly.
* Note that forkIds are also created so you can make explicit fork tests using cheats.selectFork(forkId)
*/
function setUp() public virtual {
bool forkMainnet = isForktest();
if (forkMainnet) {
forkType = MAINNET;
_setUpMainnet();
} else {
forkType = LOCAL;
_setUpLocal();
}
}
/// @dev Used to create shared setup between tests. This method is called
/// when the `rand` modifier is run, before a test starts
function _init() internal virtual {
return;
}
/**
* env FOUNDRY_PROFILE=forktest forge t --mc Integration
*
* Running foundry like this will trigger the fork test profile,
* lowering fuzz runs and using a remote RPC to test against mainnet state
*/
function isForktest() public view returns (bool) {
return _hash("forktest") == _hash(cheats.envOr(string("FOUNDRY_PROFILE"), string("default")));
}
/// Deploy EigenLayer locally
function _setUpLocal() public virtual noTracing {
console.log("Setting up `%s` integration tests:", "LOCAL".yellow().bold());
// Deploy ProxyAdmin
eigenLayerProxyAdmin = new ProxyAdmin();
executorMultisig = address(eigenLayerProxyAdmin.owner());
// Deploy PauserRegistry
address[] memory pausers = new address[](1);
pausers[0] = pauser;
eigenLayerPauserReg = new PauserRegistry(pausers, unpauser);
// Deploy mocks
emptyContract = new EmptyContract();
// Matching parameters to testnet
DELEGATION_MANAGER_MIN_WITHDRAWAL_DELAY_BLOCKS = 50;
DEALLOCATION_DELAY = 50;
ALLOCATION_CONFIGURATION_DELAY = 75;
REWARDS_COORDINATOR_CALCULATION_INTERVAL_SECONDS = 86_400;
REWARDS_COORDINATOR_MAX_REWARDS_DURATION = 6_048_000;
REWARDS_COORDINATOR_MAX_RETROACTIVE_LENGTH = 7_776_000;
REWARDS_COORDINATOR_MAX_FUTURE_LENGTH = 2_592_000;
REWARDS_COORDINATOR_GENESIS_REWARDS_TIMESTAMP = 1_710_979_200;
_deployProxies();
_deployImplementations();
_upgradeProxies();
_initializeProxies();
// Place native ETH first in `allStrats`
// This ensures when we select a nonzero number of strategies from this array, we always
// have beacon chain ETH
ethStrats.push(BEACONCHAIN_ETH_STRAT);
allStrats.push(BEACONCHAIN_ETH_STRAT);
allTokens.push(NATIVE_ETH);
// Deploy and configure strategies and tokens
for (uint i = 1; i < NUM_LST_STRATS + 1; ++i) {
string memory name = string.concat("LST-Strat", cheats.toString(i), " token");
string memory symbol = string.concat("lstStrat", cheats.toString(i));
// Deploy half of the strategies using the factory.
_newStrategyAndToken(name, symbol, 10e50, address(this), i % 2 == 0);
}
maxUniqueAssetsHeld = allStrats.length;
// Create time machine and beacon chain. Set block time to beacon chain genesis time and starting block number
BEACON_GENESIS_TIME = GENESIS_TIME_LOCAL;
cheats.warp(BEACON_GENESIS_TIME);
cheats.roll(10_000);
timeMachine = new TimeMachine();
beaconChain = new BeaconChainMock(eigenPodManager, BEACON_GENESIS_TIME);
// Set the `pectraForkTimestamp` on the EigenPodManager. Use pectra state
cheats.startPrank(executorMultisig);
eigenPodManager.setProofTimestampSetter(executorMultisig);
eigenPodManager.setPectraForkTimestamp(BEACON_GENESIS_TIME);
cheats.stopPrank();
}
/// Parse existing contracts from mainnet
function _setUpMainnet() public virtual noTracing {
console.log("Setting up `%s` integration tests:", "MAINNET_FORK".green().bold());
console.log("RPC:", cheats.rpcUrl("mainnet"));
console.log("Block:", mainnetForkBlock);
cheats.createSelectFork(cheats.rpcUrl("mainnet"), mainnetForkBlock);
string memory deploymentInfoPath = "script/configs/mainnet/mainnet-addresses.config.json";
_parseDeployedContracts(deploymentInfoPath);
string memory existingDeploymentParams = "script/configs/mainnet.json";
_parseParamsForIntegrationUpgrade(existingDeploymentParams);
// Place native ETH first in `allStrats`
// This ensures when we select a nonzero number of strategies from this array, we always
// have beacon chain ETH
ethStrats.push(BEACONCHAIN_ETH_STRAT);
allStrats.push(BEACONCHAIN_ETH_STRAT);
allTokens.push(NATIVE_ETH);
// Add deployed strategies to lstStrats and allStrats
for (uint i; i < deployedStrategyArray.length; i++) {
IStrategy strategy = IStrategy(deployedStrategyArray[i]);
if (tokensNotTested[address(strategy.underlyingToken())]) continue;
// Add to lstStrats and allStrats
lstStrats.push(strategy);
allStrats.push(strategy);
allTokens.push(strategy.underlyingToken());
}
maxUniqueAssetsHeld = allStrats.length;
// Create time machine and mock beacon chain
BEACON_GENESIS_TIME = GENESIS_TIME_MAINNET;
timeMachine = new TimeMachine();
beaconChain = new BeaconChainMock(eigenPodManager, BEACON_GENESIS_TIME);
// Since we haven't done the slashing upgrade on mainnet yet, upgrade mainnet contracts
// prior to test. `isUpgraded` is true by default, but is set to false in `UpgradeTest.t.sol`
if (isUpgraded) {
_upgradeMainnetContracts();
// Set the `pectraForkTimestamp` on the EigenPodManager. Use pectra state
cheats.startPrank(executorMultisig);
eigenPodManager.setProofTimestampSetter(executorMultisig);
eigenPodManager.setPectraForkTimestamp(BEACON_GENESIS_TIME);
cheats.stopPrank();
}
}
function _upgradeMainnetContracts() public virtual {
cheats.startPrank(address(executorMultisig));
// First, deploy the new contracts as empty contracts
emptyContract = new EmptyContract();
allocationManager =
AllocationManager(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")));
permissionController =
PermissionController(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")));
emit log_named_uint("EPM pause status", eigenPodManager.paused());
// Deploy new implementation contracts and upgrade all proxies to point to them
_deployImplementations();
_upgradeProxies();
emit log_named_uint("EPM pause status", eigenPodManager.paused());
// Initialize the newly-deployed proxy
allocationManager.initialize({initialOwner: executorMultisig, initialPausedStatus: 0});
cheats.stopPrank();
}
function _deployProxies() public {
delegationManager =
DelegationManager(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")));
strategyManager =
StrategyManager(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")));
eigenPodManager =
EigenPodManager(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")));
rewardsCoordinator =
RewardsCoordinator(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")));
avsDirectory = AVSDirectory(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")));
strategyFactory =
StrategyFactory(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")));
allocationManager =
AllocationManager(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")));
permissionController =
PermissionController(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")));
eigenPodBeacon = new UpgradeableBeacon(address(emptyContract));
strategyBeacon = new UpgradeableBeacon(address(emptyContract));
}
/// Deploy an implementation contract for each contract in the system
function _deployImplementations() public {
allocationManagerImplementation = new AllocationManager(
delegationManager, eigenLayerPauserReg, permissionController, DEALLOCATION_DELAY, ALLOCATION_CONFIGURATION_DELAY, version
);
permissionControllerImplementation = new PermissionController(version);
delegationManagerImplementation = new DelegationManager(
strategyManager,
eigenPodManager,
allocationManager,
eigenLayerPauserReg,
permissionController,
DELEGATION_MANAGER_MIN_WITHDRAWAL_DELAY_BLOCKS,
version
);
strategyManagerImplementation = new StrategyManager(delegationManager, eigenLayerPauserReg, version);
rewardsCoordinatorImplementation = new RewardsCoordinator(
IRewardsCoordinatorTypes.RewardsCoordinatorConstructorParams({
delegationManager: delegationManager,
strategyManager: strategyManager,
allocationManager: allocationManager,
pauserRegistry: eigenLayerPauserReg,
permissionController: permissionController,
CALCULATION_INTERVAL_SECONDS: REWARDS_COORDINATOR_CALCULATION_INTERVAL_SECONDS,
MAX_REWARDS_DURATION: REWARDS_COORDINATOR_MAX_REWARDS_DURATION,
MAX_RETROACTIVE_LENGTH: REWARDS_COORDINATOR_MAX_RETROACTIVE_LENGTH,
MAX_FUTURE_LENGTH: REWARDS_COORDINATOR_MAX_FUTURE_LENGTH,
GENESIS_REWARDS_TIMESTAMP: REWARDS_COORDINATOR_GENESIS_REWARDS_TIMESTAMP,
version: version
})
);
avsDirectoryImplementation = new AVSDirectory(delegationManager, eigenLayerPauserReg, version);
eigenPodManagerImplementation =
new EigenPodManager(DEPOSIT_CONTRACT, eigenPodBeacon, delegationManager, eigenLayerPauserReg, "v9.9.9");
strategyFactoryImplementation = new StrategyFactory(strategyManager, eigenLayerPauserReg, "v9.9.9");
// Beacon implementations
eigenPodImplementation = new EigenPod(DEPOSIT_CONTRACT, eigenPodManager, BEACON_GENESIS_TIME, "v9.9.9");
baseStrategyImplementation = new StrategyBase(strategyManager, eigenLayerPauserReg, "v9.9.9");
// Pre-longtail StrategyBaseTVLLimits implementation
// TODO - need to update ExistingDeploymentParser
}
function _upgradeProxies() public noTracing {
// DelegationManager
eigenLayerProxyAdmin.upgrade(
ITransparentUpgradeableProxy(payable(address(delegationManager))), address(delegationManagerImplementation)
);
// StrategyManager
eigenLayerProxyAdmin.upgrade(
ITransparentUpgradeableProxy(payable(address(strategyManager))), address(strategyManagerImplementation)
);
// EigenPodManager
eigenLayerProxyAdmin.upgrade(
ITransparentUpgradeableProxy(payable(address(eigenPodManager))), address(eigenPodManagerImplementation)
);
// RewardsCoordinator
eigenLayerProxyAdmin.upgrade(
ITransparentUpgradeableProxy(payable(address(rewardsCoordinator))), address(rewardsCoordinatorImplementation)
);
// AVSDirectory
eigenLayerProxyAdmin.upgrade(ITransparentUpgradeableProxy(payable(address(avsDirectory))), address(avsDirectoryImplementation));
// AllocationManager
eigenLayerProxyAdmin.upgrade(
ITransparentUpgradeableProxy(payable(address(allocationManager))), address(allocationManagerImplementation)
);
// PermissionController
eigenLayerProxyAdmin.upgrade(
ITransparentUpgradeableProxy(payable(address(permissionController))), address(permissionControllerImplementation)
);
// StrategyFactory
eigenLayerProxyAdmin.upgrade(
ITransparentUpgradeableProxy(payable(address(strategyFactory))), address(strategyFactoryImplementation)
);
// EigenPod beacon
eigenPodBeacon.upgradeTo(address(eigenPodImplementation));
// StrategyBase Beacon
strategyBeacon.upgradeTo(address(baseStrategyImplementation));
// Upgrade All deployed strategy contracts to new base strategy
for (uint i = 0; i < numStrategiesDeployed; i++) {
// Upgrade existing strategy
eigenLayerProxyAdmin.upgrade(
ITransparentUpgradeableProxy(payable(address(deployedStrategyArray[i]))), address(baseStrategyImplementation)
);
}
}
function _initializeProxies() public noTracing {
delegationManager.initialize({initialOwner: executorMultisig, initialPausedStatus: 0});
strategyManager.initialize({
initialOwner: executorMultisig,
initialStrategyWhitelister: address(strategyFactory),
initialPausedStatus: 0
});
eigenPodManager.initialize({initialOwner: executorMultisig, _initPausedStatus: 0});
avsDirectory.initialize({initialOwner: executorMultisig, initialPausedStatus: 0});
allocationManager.initialize({initialOwner: executorMultisig, initialPausedStatus: 0});
strategyFactory.initialize({_initialOwner: executorMultisig, _initialPausedStatus: 0, _strategyBeacon: strategyBeacon});
}
/// @dev Deploy a strategy and its underlying token, push to global lists of tokens/strategies, and whitelist
/// strategy in strategyManager
function _newStrategyAndToken(string memory tokenName, string memory tokenSymbol, uint initialSupply, address owner, bool useFactory)
internal
noTracing
{
IERC20 underlyingToken = new ERC20PresetFixedSupply(tokenName, tokenSymbol, initialSupply, owner);
StrategyBase strategy;
if (useFactory) {
strategy = StrategyBase(address(strategyFactory.deployNewStrategy(underlyingToken)));
} else {
strategy = StrategyBase(
address(
new TransparentUpgradeableProxy(
address(baseStrategyImplementation),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(StrategyBase.initialize.selector, underlyingToken)
)
)
);
}
// Whitelist strategy
IStrategy[] memory strategies = new IStrategy[](1);
strategies[0] = strategy;
cheats.prank(strategyManager.strategyWhitelister());
strategyManager.addStrategiesToDepositWhitelist(strategies);
// Add to lstStrats and allStrats
lstStrats.push(strategy);
allStrats.push(strategy);
allTokens.push(underlyingToken);
}
function _configRand(uint24 _randomSeed, uint _assetTypes, uint _userTypes) private noTracing {
// Using uint24 for the seed type so that if a test fails, it's easier
// to manually use the seed to replay the same test.
random = _hash(_randomSeed);
// Convert flag bitmaps to bytes of set bits for easy use with _randUint
_configAssetTypes(_assetTypes);
_configUserTypes(_userTypes);
}
function _configAssetTypes(uint _assetTypes) internal {
assetTypes = _bitmapToBytes(_assetTypes);
assertTrue(assetTypes.length != 0, "_configRand: no asset types selected");
}
function _configAssetAmounts(uint _maxUniqueAssetsHeld) internal {
if (_maxUniqueAssetsHeld > allStrats.length) _maxUniqueAssetsHeld = allStrats.length;
maxUniqueAssetsHeld = _maxUniqueAssetsHeld;
require(maxUniqueAssetsHeld != 0, "_configAssetAmounts: invalid 0");
}
function _configUserTypes(uint _userTypes) internal {
userTypes = _bitmapToBytes(_userTypes);
assertTrue(userTypes.length != 0, "_configRand: no user types selected");
}
/**
* @dev Create a new User with a random config using the range defined in `_configRand`
*
* Assets are pulled from `strategies` based on a random staker/operator `assetType`
*/
function _randUser(string memory name) internal noTracing returns (User, IStrategy[] memory, uint[] memory) {
// Deploy new User contract
uint userType = _randUserType();
User user = _genRandUser(name, userType);
// For the specific asset selection we made, get a random assortment of strategies
// and deal the user some corresponding underlying token balances
uint assetType = _randAssetType();
IStrategy[] memory strategies = _selectRandAssets(assetType);
uint[] memory tokenBalances = _dealRandAmounts(user, strategies);
print.user(name, assetType, userType, strategies, tokenBalances);
return (user, strategies, tokenBalances);
}
function _randUser(string memory name, IStrategy[] memory strategies) internal noTracing returns (User, uint[] memory) {
// Deploy new User contract
uint userType = _randUserType();
User user = _genRandUser(name, userType);
// Deal the user some corresponding underlying token balances
uint[] memory tokenBalances = _dealRandAmounts(user, strategies);
print.user(name, HOLDS_ALL, userType, strategies, tokenBalances);
return (user, tokenBalances);
}
/// @dev Create a new user without native ETH. See _randUser above for standard usage
function _randUser_NoETH(string memory name) internal noTracing returns (User, IStrategy[] memory, uint[] memory) {
// Deploy new User contract
uint userType = _randUserType();
User user = _genRandUser(name, userType);
// Pick the user's asset distribution, removing "native ETH" as an option
// I'm sorry if this eventually leads to a bug that's really hard to track down
uint assetType = _randAssetType();
if (assetType == HOLDS_ETH) assetType = NO_ASSETS;
else if (assetType == HOLDS_ALL || assetType == HOLDS_MAX) assetType = HOLDS_LST;
// For the specific asset selection we made, get a random assortment of strategies
// and deal the user some corresponding underlying token balances
IStrategy[] memory strategies = _selectRandAssets(assetType);
uint[] memory tokenBalances = _dealRandAmounts(user, strategies);
print.user(name, assetType, userType, strategies, tokenBalances);
return (user, strategies, tokenBalances);
}
/// @dev Creates a new user without any assets
function _randUser_NoAssets(string memory name) internal noTracing returns (User) {
// Deploy new User contract
uint userType = _randUserType();
User user = _genRandUser(name, userType);
print.user(name, NO_ASSETS, userType, new IStrategy[](0), new uint[](0));
return user;
}
function _genRandUser(string memory name, uint userType) internal returns (User user) {
// Create User contract based on userType:
if (forkType == LOCAL || (forkType == MAINNET && isUpgraded)) {
user = new User(name);
if (userType == DEFAULT) {
user = new User(name);
} else if (userType == ALT_METHODS) {
// User will use nonstandard methods like `depositIntoStrategyWithSignature`
user = User(new User_AltMethods(name));
} else {
revert("_randUser: unimplemented userType");
}
} else if (forkType == MAINNET && !isUpgraded) {
if (userType == DEFAULT) {
user = User(new User_M2(name));
} else if (userType == ALT_METHODS) {
// User will use nonstandard methods like `depositIntoStrategyWithSignature`
user = User(new User_M2(name));
} else {
revert("_randUser: unimplemented userType");
}
} else {
revert("_randUser: unimplemented forkType");
}
}
function _genRandAVS(string memory name) internal returns (AVS avs) {
if (forkType == LOCAL) avs = new AVS(name);
else if (forkType == MAINNET) avs = new AVS(name);
else revert("_genRandAVS: unimplemented forkType");
}
/// Given an assetType, select strategies the user will be dealt assets in
function _selectRandAssets(uint assetType) internal noTracing returns (IStrategy[] memory) {
if (assetType == NO_ASSETS) return new IStrategy[](0);
/// Select only ETH
if (assetType == HOLDS_ETH) return beaconChainETHStrategy.toArray();
/// Select multiple LSTs, and maybe add ETH:
// Select number of assets:
// HOLDS_LST can hold at most all LSTs. HOLDS_ALL and HOLDS_MAX also hold ETH.
// Clamp number of assets to maxUniqueAssetsHeld (guaranteed to be at least 1)
uint assetPoolSize = assetType == HOLDS_LST ? lstStrats.length : allStrats.length;
uint maxAssets = assetPoolSize > maxUniqueAssetsHeld ? maxUniqueAssetsHeld : assetPoolSize;
uint numAssets = assetType == HOLDS_MAX ? maxAssets : _randUint(1, maxAssets);
IStrategy[] memory strategies = new IStrategy[](numAssets);
for (uint i = 0; i < strategies.length; i++) {
if (assetType == HOLDS_LST) {
strategies[i] = lstStrats[i];
} else {
// allStrats[0] is the beaconChainETHStrategy
strategies[i] = allStrats[i];
}
}
return strategies;
}
/// Given an input list of strategies, deal random underlying token amounts to a user
function _dealRandAmounts(User user, IStrategy[] memory strategies) internal noTracing returns (uint[] memory) {
uint[] memory tokenBalances = new uint[](strategies.length);
for (uint i = 0; i < tokenBalances.length; i++) {
IStrategy strategy = strategies[i];
uint balance;
if (strategy == BEACONCHAIN_ETH_STRAT) {
// Award the user with a random amount of ETH
// This guarantees a multiple of 32 ETH (at least 1, up to/incl 2080)
uint amount = 32 ether * _randUint({min: 1, max: 65});
balance = 32 ether * _randUint({min: 1, max: 5});
cheats.deal(address(user), balance);
} else {
IERC20 underlyingToken = strategy.underlyingToken();
balance = _randUint({min: MIN_BALANCE, max: MAX_BALANCE});
StdCheats.deal(address(underlyingToken), address(user), balance);
}
tokenBalances[i] = balance;
}
return tokenBalances;
}
/// Given an array of strategies and an array of amounts, deal the amounts to the user
function _dealAmounts(User user, IStrategy[] memory strategies, uint[] memory amounts) internal noTracing {
for (uint i = 0; i < amounts.length; i++) {
IStrategy strategy = strategies[i];
if (strategy == BEACONCHAIN_ETH_STRAT) {
cheats.deal(address(user), amounts[i]);
} else {
IERC20 underlyingToken = strategy.underlyingToken();
StdCheats.deal(address(underlyingToken), address(user), amounts[i]);
}
}
}
/// @dev Uses `random` to return a random uint, with a range given by `min` and `max` (inclusive)
/// @return `min` <= result <= `max`
function _randUint(uint min, uint max) internal returns (uint) {
uint range = max - min + 1;
// calculate the number of bits needed for the range
uint bitsNeeded = 0;
uint tempRange = range;
while (tempRange > 0) {
bitsNeeded++;
tempRange >>= 1;
}
// create a mask for the required number of bits
// and extract the value from the hash
uint mask = (1 << bitsNeeded) - 1;
uint value = uint(random) & mask;
// in case value is out of range, wrap around or retry
while (value >= range) value = (value - range) & mask;
// Hash `random` with itself so the next value we generate is different
random = _hash(uint(random));
return min + value;
}
function _randBool() internal returns (bool) {
return _randUint({min: 0, max: 1}) == 0;
}
function _randAssetType() internal returns (uint) {
uint idx = _randUint({min: 0, max: assetTypes.length - 1});
uint assetType = uint(uint8(assetTypes[idx]));
return assetType;
}
function _randUserType() internal returns (uint) {
uint idx = _randUint({min: 0, max: userTypes.length - 1});
uint userType = uint(uint8(userTypes[idx]));
return userType;
}
function _shuffle(IStrategy[] memory strats) internal returns (IStrategy[] memory) {
// Fisher-Yates shuffle algorithm
for (uint i = strats.length - 1; i > 0; i--) {
uint randomIndex = _randUint({min: 0, max: i});
// Swap elements
IStrategy temp = strats[i];
strats[i] = strats[randomIndex];
strats[randomIndex] = temp;
}
return strats;
}
function _randomStrategies() internal returns (IStrategy[][] memory strategies) {
uint numOpSets = _randUint({min: 1, max: 5});
strategies = new IStrategy[][](numOpSets);
for (uint i; i < numOpSets; ++i) {
IStrategy[] memory randomStrategies = _shuffle(allStrats);
uint numStrategies = _randUint({min: 1, max: maxUniqueAssetsHeld});
// Modify the length of the array in memory (thus ignoring remaining elements).
assembly {
mstore(randomStrategies, numStrategies)
}
strategies[i] = randomStrategies;
}
}
/**
* @dev Converts a bitmap into an array of bytes
* @dev Each byte in the input is processed as indicating a single bit to flip in the bitmap
*/
function _bitmapToBytes(uint bitmap) internal pure returns (bytes memory bytesArray) {
for (uint i = 0; i < 256; ++i) {
// Mask for i-th bit
uint mask = uint(1 << i);
// If the i-th bit is flipped, add a byte to the return array
if (bitmap & mask != 0) bytesArray = bytes.concat(bytesArray, bytes1(uint8(1 << i)));
}
return bytesArray;
}
function _hash(string memory x) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(x));
}
function _hash(uint x) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(x));
}
}
````
## File: src/test/integration/README.md
````markdown
## EigenLayer Core Integration Testing
### What the Hell?
Good question.
This folder contains the integration framework and tests for Eigenlayer core, which orchestrates the deployment of all EigenLayer core contracts to fuzz high-level user flows across multiple user and asset types, and supports time-travelling state lookups to quickly compare past and present states (please try to avoid preventing your own birth).
**If you want to know how to run the tests**:
* Local: `forge t --mc Integration`
* Mainnet fork tests: `env FOUNDRY_PROFILE=forktest forge t --mc Integration`
Note that for mainnet fork tests, you'll need to set the `RPC_MAINNET` environment variable to your RPC provider of choice!
**If you want to know where the tests are**, take a look at `/tests`. We're doing one test contract per top-level flow, and defining multiple test functions for variants on that flow.
e.g. if you're testing the flow "deposit into strategies -> delegate to operator -> queue withdrawal -> complete withdrawal", that's it's own test contract. For variants where withdrawals are completed "as tokens" vs "as shares," those are their own functions inside that contract.
Looking at the current tests is a good place to start.
**If you want to know how we're fuzzing these flows**, take a look at how we're using the `_configRand` method at the start of each test, which accepts bitmaps for the types of users and assets you want to spawn during the test.
During the test, the config passed into `_configRand` will randomly generate only the values you configure:
* `assetTypes` affect the assets granted to Users when they are first created. You can use this to ensure your flows and assertions work when users are holding only LSTs, native ETH, or some combination.
* `userTypes` affect the actual `User` contract being deployed. The `DEFAULT` flag deploys the base `User` contract, while `ALT_METHODS` deploys a version that derives from the same contract, but overrides some methods to use "functionWithSignature" and other variants.
Here's an example:
```solidity
function testFuzz_deposit_delegate_EXAMPLE(uint24 _random) public {
// When new Users are created, they will choose a random configuration from these params.
// `_randomSeed` will be the starting seed for all random lookups.
_configRand({
_randomSeed: _random,
_assetTypes: HOLDS_LST,
_userTypes: DEFAULT | ALT_METHODS
});
// Because of the `assetTypes` flags above, this will create two Users for our test,
// each of which holds some random assortment of LSTs.
(
User staker,
IStrategy[] memory strategies,
uint[] memory tokenBalances
) = _newRandomStaker();
(User operator, ,) = _newRandomOperator();
// Because of the `userTypes` flags above, this user might be using either:
// - `strategyManager.depositIntoStrategy`
// - `strategyManager.depositIntoStrategyWithSignature`
staker.depositIntoEigenlayer(strategies, tokenBalances);
// assertions go here
}
```
### `print` Library Overview
The `print` library provides utilities for structured and stylized logging to streamline debugging in EigenLayer contracts. It includes helpers for common tasks such as logging user actions, integration test details. The library works in conjunction with the `Logger` abstract contract, which must be inherited by contracts that use `print`.
NOTE: `print` is intended to be used in conjunction with `console.log`.
```solidity
import "src/test/utils/Logger.t.sol";
// The `User` contract inherits `Logger` to enable `print` functionalities.
contract User is Logger {
string public _name;
constructor(string memory name) {
_name = name;
}
function NAME() public override view returns (string memory) {
return _name;
}
function vote(User who, uint256 votes) public {
print.method(
"vote",
string.concat(
"{who: ", who.NAME_COLORED(),
", votes: ", votes.asWad(),
"}"
)
);
}
}
User alice = User("Alice");
User bob = User("Bob");
// Will emit a log equal to: `console.log("Alice.vote({who: Bob, votes: 1.0 (wad)})");`
alice.vote(bob, 1 ether);
```
**If you want to know about the time travel**, there's a few things to note:
The main feature we're using is foundry's `cheats.snapshotState()` and `cheats.revertToState(snapshot)` to zip around in time. You can look at the [Cheatcodes Reference](https://book.getfoundry.sh/cheatcodes/#cheatcodes-interface) to get some idea, but the docs aren't actually correct. The best thing to do is look through our tests and see how it's being used. If you see an assertion called `assert_Snap_...`, that's using the `TimeMachine` under the hood.
Speaking of, the `TimeMachine` is a global contract that controls the time, fate, and destiny of all who use it.
* `Users` use the `TimeMachine` to snapshot chain state *before* every action they perform. (see the [`User.createSnapshot`](https://github.com/layr-labs/eigenlayer-contracts/blob/c5193f7bff00903a4323be2a1500cbf7137a83e9/src/test/integration/User.t.sol#L43-L46) modifier).
* `IntegrationBase` uses a `timewarp` modifier to quickly fetch state "from before the last user action". These are leveraged within various `assert_Snap_XYZ` methods to allow the test to quickly compare previous and current values. ([example assertion method](https://github.com/layr-labs/eigenlayer-contracts/blob/c99e847709852d7246c73b7d72d44bba368b760e/src/test/integration/IntegrationBase.t.sol#L146-L148))
This means that tests can perform user actions with very little setup or "reading prior state", and perform all the important assertions after each action. For example:
```solidity
function testFuzz_deposit_delegate_EXAMPLE(uint24 _random) public {
// ... test setup goes above here
// This snapshots state before the deposit.
staker.depositIntoEigenlayer(strategies, tokenBalances);
// This checks the staker's shares from before `depositIntoEigenlayer`, and compares
// them to their shares after `depositIntoEigenlayer`.
assert_Snap_AddedStakerShares(staker, strategies, expectedShares, "failed to award staker shares");
// This snapshots state before delegating.
staker.delegateTo(operator);
// This checks the operator's `operatorShares` before the staker delegated to them, and
// compares those shares to the `operatorShares` after the staker delegated.
assert_Snap_AddedOperatorShares(operator, strategies, expectedShares, "failed to award operator shares");
}
```
### Additional Important Concepts
* Most testing logic and checks are performed at the test level. `IntegrationBase` has primarily helpers and a few sanity checks, but the current structure exists to make it clear what's being tested by reading the test itself.
* Minimal logic/assertions/cheats used in User contract. These are for carrying out user behaviors, only. Exception:
* User methods snapshot state before performing actions
* Top-level error messages are passed into helper assert methods so that it's always clear where an error came from
* User contract should have an interface as similar as possible to the contract interfaces, so it feels like calling an EigenLayer method rather than some weird abstraction. Exceptions for things like:
* `user.depositIntoEigenLayer(strats, tokenBalances)` - because this deposits all strategies/shares and may touch either Smgr or Emgr
### What needs to be done?
* Suggest or PR cleanup if you have ideas. Currently, the `IntegrationDeployer` contract is pretty messy.
* Coordinate in Slack to pick out some user flows to write tests for!
#### Reduce RPC spam for fork tests
Currently our mainnet fork tests spam whatever RPC we use. We can improve this in the future - apparently the meta is:
> Use an anvil node to fork the network, you can write a script to make some changes to the forked network for setup etc, then fork your local node in your test.
> Effectively you just setup an anvil node with the command
`anvil -f RPC_URL `
You can use `anvil -h` for more info on what it can do.
> Then in your test you use the vm.createSelectFork command in your setup with the argument to point to your local anvil node which is basically a copy of the rpc you set it up as.
> If you want to do some setup before running your tests you can write a script file and broadcast the setup transactions to your local anvil node (make sure to use one of the private keys anvil gives you)
````
## File: src/test/integration/TimeMachine.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "src/test/utils/Logger.t.sol";
contract TimeMachine is Test, Logger {
uint[] public snapshots;
function NAME() public view virtual override returns (string memory) {
return "TimeMachine";
}
/// -----------------------------------------------------------------------
/// Setters
/// -----------------------------------------------------------------------
function createSnapshot() public returns (uint snapshot) {
snapshots.push(snapshot = cheats.snapshotState());
print.method("createSnapshot", cheats.toString(snapshot));
}
function travelToLast() public returns (uint currentSnapshot) {
// Safety check to make sure createSnapshot is called before attempting
// to warp so we don't accidentally prevent our own births.
assertTrue(pastExists(), "Global.warpToPast: invalid usage, past does not exist");
uint last = lastSnapshot();
// print.method("travelToLast", cheats.toString(last));
currentSnapshot = createSnapshot();
cheats.revertToState(last);
}
function travel(uint snapshot) public {
// print.method("travel", cheats.toString(snapshot));
cheats.revertToState(snapshot);
}
/// -----------------------------------------------------------------------
/// Getters
/// -----------------------------------------------------------------------
function lastSnapshot() public view returns (uint) {
return snapshots[snapshots.length - 1];
}
function pastExists() public view returns (bool) {
return snapshots.length != 0;
}
}
````
## File: src/test/integration/TypeImporter.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/contracts/interfaces/IAllocationManager.sol";
import "src/contracts/interfaces/IAVSDirectory.sol";
import "src/contracts/interfaces/IDelegationManager.sol";
import "src/contracts/interfaces/IEigenPod.sol";
import "src/contracts/interfaces/IEigenPodManager.sol";
import "src/contracts/interfaces/IStrategyManager.sol";
/// @dev A master interface contract that imports types defined in our
/// contract interfaces so they can be used without needing to refer to
/// the interface, e.g:
///
/// `AllocateParams memory params;`
/// vs
/// `IAllocationManagerTypes.AllocateParams memory params;`
interface TypeImporter is IAllocationManagerTypes, IAVSDirectoryTypes, IDelegationManagerTypes, IEigenPodManagerTypes, IEigenPodTypes {}
````
## File: src/test/integration/UpgradeTest.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationDeployer.t.sol";
import "src/test/integration/IntegrationChecks.t.sol";
import "src/test/integration/mocks/BeaconChainMock_Deneb.t.sol";
abstract contract UpgradeTest is IntegrationCheckUtils {
/// Only run upgrade tests on mainnet forks
function setUp() public virtual override {
if (!isForktest()) {
cheats.skip(true);
} else {
isUpgraded = false;
super.setUp();
// Use Deneb Beacon Chain Mock as Pectra state is not live on mainnet
beaconChain = BeaconChainMock(new BeaconChainMock_DenebForkable(eigenPodManager, BEACON_GENESIS_TIME));
}
}
/// Deploy current implementation contracts and upgrade existing proxies
function _upgradeEigenLayerContracts() public virtual {
require(forkType == MAINNET, "_upgradeEigenLayerContracts: somehow running upgrade test locally");
require(!isUpgraded, "_upgradeEigenLayerContracts: already performed upgrade");
emit log("_upgradeEigenLayerContracts: upgrading mainnet to slashing");
_upgradeMainnetContracts();
_handlePectraFork();
// Bump block.timestamp forward to allow verifyWC proofs for migrated pods
emit log("advancing block time to start of next epoch:");
beaconChain.advanceEpoch_NoRewards();
emit log("======");
isUpgraded = true;
emit log("_upgradeEigenLayerContracts: slashing upgrade complete");
}
// Set the fork timestamp sufficiently in the future to keep using Deneb proofs
// `Prooftra.t.sol` will handle the Deneb -> Pectra transition
function _handlePectraFork() internal {
// 1. Set proof timestamp setter to operations multisig
cheats.prank(eigenPodManager.owner());
eigenPodManager.setProofTimestampSetter(address(operationsMultisig));
// 2. Set Proof timestamp
cheats.prank(eigenPodManager.proofTimestampSetter());
eigenPodManager.setPectraForkTimestamp(type(uint64).max);
}
}
````
## File: src/test/mocks/AllocationManagerMock.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.9;
import "forge-std/Test.sol";
import "src/contracts/interfaces/IStrategy.sol";
import "src/contracts/libraries/Snapshots.sol";
import "src/contracts/libraries/OperatorSetLib.sol";
contract AllocationManagerMock is Test {
using Snapshots for Snapshots.DefaultWadHistory;
using OperatorSetLib for OperatorSet;
receive() external payable {}
fallback() external payable {}
mapping(bytes32 operatorSetKey => bool) public _isOperatorSet;
mapping(address avs => uint) public getOperatorSetCount;
mapping(address => mapping(IStrategy => Snapshots.DefaultWadHistory)) internal _maxMagnitudeHistory;
function setIsOperatorSet(OperatorSet memory operatorSet, bool boolean) external {
_isOperatorSet[operatorSet.key()] = boolean;
}
function isOperatorSet(OperatorSet memory operatorSet) external view returns (bool) {
return _isOperatorSet[operatorSet.key()];
}
function setMaxMagnitudes(address operator, IStrategy[] calldata strategies, uint64[] calldata maxMagnitudes) external {
for (uint i = 0; i < strategies.length; ++i) {
setMaxMagnitude(operator, strategies[i], maxMagnitudes[i]);
}
}
function setMaxMagnitude(address operator, IStrategy strategy, uint64 maxMagnitude) public {
_maxMagnitudeHistory[operator][strategy].push({key: uint32(block.number), value: maxMagnitude});
}
function getMaxMagnitude(address operator, IStrategy strategy) external view returns (uint64) {
return _maxMagnitudeHistory[operator][strategy].latest();
}
function getMaxMagnitudes(address operator, IStrategy[] calldata strategies) external view returns (uint64[] memory) {
uint64[] memory maxMagnitudes = new uint64[](strategies.length);
for (uint i = 0; i < strategies.length; ++i) {
maxMagnitudes[i] = _maxMagnitudeHistory[operator][strategies[i]].latest();
}
return maxMagnitudes;
}
function getMaxMagnitudesAtBlock(address operator, IStrategy[] calldata strategies, uint32 blockNumber)
external
view
returns (uint64[] memory)
{
uint64[] memory maxMagnitudes = new uint64[](strategies.length);
for (uint i = 0; i < strategies.length; ++i) {
maxMagnitudes[i] = _maxMagnitudeHistory[operator][strategies[i]].upperLookup({key: blockNumber});
}
return maxMagnitudes;
}
function setAVSSetCount(address avs, uint numSets) external {
getOperatorSetCount[avs] = numSets;
}
}
````
## File: src/test/mocks/AVSDirectoryMock.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.9;
import "forge-std/Test.sol";
import "src/contracts/interfaces/IAVSDirectory.sol";
import "src/contracts/libraries/OperatorSetLib.sol";
contract AVSDirectoryMock is Test {
receive() external payable {}
fallback() external payable {}
}
````
## File: src/test/mocks/DelegationManagerMock.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "src/contracts/interfaces/IDelegationManager.sol";
import "src/contracts/interfaces/IStrategyManager.sol";
import "src/contracts/libraries/SlashingLib.sol";
contract DelegationManagerMock is Test {
receive() external payable {}
fallback() external payable {}
mapping(address => bool) public isOperator;
mapping(address => address) public delegatedTo;
mapping(address => mapping(IStrategy => uint)) public operatorShares;
function getDelegatableShares(address staker) external view returns (IStrategy[] memory, uint[] memory) {}
function setMinWithdrawalDelayBlocks(uint newMinWithdrawalDelayBlocks) external {}
function setStrategyWithdrawalDelayBlocks(IStrategy[] calldata strategies, uint[] calldata withdrawalDelayBlocks) external {}
function setIsOperator(address operator, bool _isOperatorReturnValue) external {
isOperator[operator] = _isOperatorReturnValue;
}
function slashOperatorShares(address operator, IStrategy strategy, uint64 prevMaxMagnitude, uint64 newMaxMagnitude) external {
uint amountSlashed = SlashingLib.calcSlashedAmount({
operatorShares: operatorShares[operator][strategy],
prevMaxMagnitude: prevMaxMagnitude,
newMaxMagnitude: newMaxMagnitude
});
operatorShares[operator][strategy] -= amountSlashed;
}
/// @notice returns the total number of shares in `strategy` that are delegated to `operator`.
function setOperatorShares(address operator, IStrategy strategy, uint shares) external {
operatorShares[operator][strategy] = shares;
}
/// @notice returns the total number of shares in `strategy` that are delegated to `operator`.
function setOperatorsShares(address operator, IStrategy[] memory strategies, uint shares) external {
for (uint i = 0; i < strategies.length; i++) {
operatorShares[operator][strategies[i]] = shares;
}
}
function delegateTo(
address operator,
ISignatureUtilsMixinTypes.SignatureWithExpiry memory, /*approverSignatureAndExpiry*/
bytes32 /*approverSalt*/
) external {
delegatedTo[msg.sender] = operator;
}
function undelegate(address staker) external returns (bytes32[] memory withdrawalRoot) {
delegatedTo[staker] = address(0);
return withdrawalRoot;
}
function getOperatorsShares(address[] memory operators, IStrategy[] memory strategies) external view returns (uint[][] memory) {
uint[][] memory operatorSharesArray = new uint[][](operators.length);
for (uint i = 0; i < operators.length; i++) {
operatorSharesArray[i] = new uint[](strategies.length);
for (uint j = 0; j < strategies.length; j++) {
operatorSharesArray[i][j] = operatorShares[operators[i]][strategies[j]];
}
}
return operatorSharesArray;
}
function operatorDetails(address operator) external pure returns (IDelegationManagerTypes.OperatorDetails memory) {
IDelegationManagerTypes.OperatorDetails memory returnValue = IDelegationManagerTypes.OperatorDetails({
__deprecated_earningsReceiver: operator,
delegationApprover: operator,
__deprecated_stakerOptOutWindowBlocks: 0
});
return returnValue;
}
function isDelegated(address staker) external view returns (bool) {
return (delegatedTo[staker] != address(0));
}
// onlyDelegationManager functions in StrategyManager
function addShares(IStrategyManager strategyManager, address staker, IStrategy strategy, uint shares) external {
strategyManager.addShares(staker, strategy, shares);
}
function removeDepositShares(IStrategyManager strategyManager, address staker, IStrategy strategy, uint shares) external {
strategyManager.removeDepositShares(staker, strategy, shares);
}
function withdrawSharesAsTokens(IStrategyManager strategyManager, address recipient, IStrategy strategy, uint shares, IERC20 token)
external
{
strategyManager.withdrawSharesAsTokens(recipient, strategy, token, shares);
}
}
````
## File: src/test/mocks/Dummy.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
contract EmptyContract {
function foo() public pure returns (uint) {
return 0;
}
}
````
## File: src/test/mocks/EigenPodManagerMock.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.9;
import "forge-std/Test.sol";
import "../../contracts/interfaces/IStrategy.sol";
import "../../contracts/permissions/Pausable.sol";
contract EigenPodManagerMock is Test, Pausable {
receive() external payable {}
fallback() external payable {}
mapping(address => int) public podOwnerDepositShares;
mapping(address => uint) public podOwnerSharesWithdrawn;
uint64 public pectraForkTimestamp;
struct BeaconChainSlashingFactor {
bool isSet;
uint64 slashingFactor;
}
mapping(address => BeaconChainSlashingFactor) _beaconChainSlashingFactor;
constructor(IPauserRegistry _pauserRegistry) Pausable(_pauserRegistry) {
_setPausedStatus(0);
pectraForkTimestamp = 1 hours * 12;
}
function podOwnerShares(address podOwner) external view returns (int) {
return podOwnerDepositShares[podOwner];
}
function stakerDepositShares(address user, address) public view returns (uint depositShares) {
return podOwnerDepositShares[user] < 0 ? 0 : uint(podOwnerDepositShares[user]);
}
function setPodOwnerShares(address podOwner, int shares) external {
podOwnerDepositShares[podOwner] = shares;
}
function addShares(address podOwner, IStrategy, uint shares) external returns (uint, uint) {
uint existingDepositShares = uint(podOwnerDepositShares[podOwner]);
podOwnerDepositShares[podOwner] += int(shares);
return (existingDepositShares, shares);
}
function removeDepositShares(
address podOwner,
IStrategy, // strategy
uint shares
) external returns (uint) {
int updatedShares = podOwnerDepositShares[podOwner] - int(shares);
podOwnerDepositShares[podOwner] = updatedShares;
return uint(updatedShares);
}
function denebForkTimestamp() external pure returns (uint64) {
return type(uint64).max;
}
function withdrawSharesAsTokens(
address podOwner,
address,
/**
* strategy
*/
address,
/**
* token
*/
uint shares
) external {
podOwnerSharesWithdrawn[podOwner] += shares;
}
function setBeaconChainSlashingFactor(address staker, uint64 bcsf) external {
_beaconChainSlashingFactor[staker] = BeaconChainSlashingFactor({isSet: true, slashingFactor: bcsf});
}
function beaconChainSlashingFactor(address staker) external view returns (uint64) {
BeaconChainSlashingFactor memory bsf = _beaconChainSlashingFactor[staker];
return bsf.isSet ? bsf.slashingFactor : WAD;
}
function setPectraForkTimestamp(uint64 _pectraForkTimestamp) external {
pectraForkTimestamp = _pectraForkTimestamp;
}
}
````
## File: src/test/mocks/EigenPodMock.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.9;
import "forge-std/Test.sol";
import "../../contracts/interfaces/IEigenPod.sol";
import "../../contracts/mixins/SemVerMixin.sol";
contract EigenPodMock is IEigenPod, SemVerMixin, Test {
constructor() SemVerMixin("v9.9.9") {}
function nonBeaconChainETHBalanceWei() external view returns (uint) {}
/// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from beaconchain but not EigenLayer),
function withdrawableRestakedExecutionLayerGwei() external view returns (uint64) {}
/// @notice Used to initialize the pointers to contracts crucial to the pod's functionality, in beacon proxy construction from EigenPodManager
function initialize(address owner) external {}
/// @notice Called by EigenPodManager when the owner wants to create another ETH validator.
function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable {}
/**
* @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address
* @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain.
* @dev Called during withdrawal or slashing.
* @dev Note that this function is marked as non-reentrant to prevent the recipient calling back into it
*/
function withdrawRestakedBeaconChainETH(address recipient, uint amount) external {}
/// @notice The single EigenPodManager for EigenLayer
function eigenPodManager() external view returns (IEigenPodManager) {}
/// @notice The owner of this EigenPod
function podOwner() external view returns (address) {}
/// @notice an indicator of whether or not the podOwner has ever "fully restaked" by successfully calling `verifyCorrectWithdrawalCredentials`.
function hasRestaked() external view returns (bool) {}
/// @notice block timestamp of the most recent withdrawal
function mostRecentWithdrawalTimestamp() external view returns (uint64) {}
/// @notice Returns the validatorInfo struct for the provided pubkeyHash
function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) {}
/// @notice This returns the status of a given validator
function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) {}
/// @notice Number of validators with proven withdrawal credentials, who do not have proven full withdrawals
function activeValidatorCount() external view returns (uint) {}
/// @notice The timestamp of the last checkpoint finalized
function lastCheckpointTimestamp() external view returns (uint64) {}
/// @notice The timestamp of the currently-active checkpoint. Will be 0 if there is not active checkpoint
function currentCheckpointTimestamp() external view returns (uint64) {}
/// @notice Returns the currently-active checkpoint
function currentCheckpoint() external view returns (Checkpoint memory) {}
function checkpointBalanceExitedGwei(uint64) external view returns (uint64) {}
function startCheckpoint(bool revertIfNoBalance) external {}
function verifyCheckpointProofs(
BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof,
BeaconChainProofs.BalanceProof[] calldata proofs
) external {}
function verifyStaleBalance(
uint64 beaconTimestamp,
BeaconChainProofs.StateRootProof calldata stateRootProof,
BeaconChainProofs.ValidatorProof calldata proof
) external {}
function verifyWithdrawalCredentials(
uint64 oracleTimestamp,
BeaconChainProofs.StateRootProof calldata stateRootProof,
uint40[] calldata validatorIndices,
bytes[] calldata withdrawalCredentialProofs,
bytes32[][] calldata validatorFields
) external {}
/// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false
function activateRestaking() external {}
/// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false
function withdrawBeforeRestaking() external {}
/// @notice Called by the pod owner to withdraw the nonBeaconChainETHBalanceWei
function withdrawNonBeaconChainETHBalanceWei(address recipient, uint amountToWithdraw) external {}
/// @notice called by owner of a pod to remove any ERC20s deposited in the pod
function recoverTokens(IERC20[] memory tokenList, uint[] memory amountsToWithdraw, address recipient) external {}
function setProofSubmitter(address newProofSubmitter) external {}
function proofSubmitter() external view returns (address) {}
function validatorStatus(bytes calldata pubkey) external view returns (VALIDATOR_STATUS) {}
function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory) {}
/// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp`
/// @param timestamp of the block for which the parent block root will be returned. MUST correspond
/// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method
/// will revert.
function getParentBlockRoot(uint64 timestamp) external view returns (bytes32) {}
function getPectraForkTimestamp() external view returns (uint64) {}
}
````
## File: src/test/mocks/EmptyContract.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
contract EmptyContract {
function foo() public pure returns (uint) {
return 0;
}
}
````
## File: src/test/mocks/ERC20_OneWeiFeeOnTransfer.sol
````
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
// copy-pasted OZ code with _balances mapping made *internal* instead of private
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
// pragma solidity ^0.8.0;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract OpenZeppelin_Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
}
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)
// pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface OpenZeppelin_IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint value);
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint);
/**
* @dev Moves `amount` tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `from` to `to` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint amount) external returns (bool);
}
// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol)
// pragma solidity ^0.8.0;
// import "../IERC20.sol";
/**
* @dev Interface for the optional metadata functions from the ERC20 standard.
*
* _Available since v4.1._
*/
interface OpenZeppelin_IERC20Metadata is OpenZeppelin_IERC20 {
/**
* @dev Returns the name of the token.
*/
function name() external view returns (string memory);
/**
* @dev Returns the symbol of the token.
*/
function symbol() external view returns (string memory);
/**
* @dev Returns the decimals places of the token.
*/
function decimals() external view returns (uint8);
}
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol)
// import "./IERC20.sol";
// import "./extensions/IERC20Metadata.sol";
// import "../../utils/Context.sol";
/**
* @dev Implementation of the {IERC20} interface.
*
* This implementation is agnostic to the way tokens are created. This means
* that a supply mechanism has to be added in a derived contract using {_mint}.
* For a generic mechanism see {ERC20PresetMinterPauser}.
*
* TIP: For a detailed writeup see our guide
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
* to implement supply mechanisms].
*
* The default value of {decimals} is 18. To change this, you should override
* this function so it returns a different value.
*
* We have followed general OpenZeppelin Contracts guidelines: functions revert
* instead returning `false` on failure. This behavior is nonetheless
* conventional and does not conflict with the expectations of ERC20
* applications.
*
* Additionally, an {Approval} event is emitted on calls to {transferFrom}.
* This allows applications to reconstruct the allowance for all accounts just
* by listening to said events. Other implementations of the EIP may not emit
* these events, as it isn't required by the specification.
*
* Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
* functions have been added to mitigate the well-known issues around setting
* allowances. See {IERC20-approve}.
*/
contract OpenZeppelin_ERC20 is OpenZeppelin_Context, OpenZeppelin_IERC20, OpenZeppelin_IERC20Metadata {
mapping(address => uint) internal _balances;
mapping(address => mapping(address => uint)) private _allowances;
uint private _totalSupply;
string private _name;
string private _symbol;
/**
* @dev Sets the values for {name} and {symbol}.
*
* All two of these values are immutable: they can only be set once during
* construction.
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/**
* @dev Returns the name of the token.
*/
function name() public view virtual override returns (string memory) {
return _name;
}
/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei. This is the default value returned by this function, unless
* it's overridden.
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view virtual override returns (uint8) {
return 18;
}
/**
* @dev See {IERC20-totalSupply}.
*/
function totalSupply() public view virtual override returns (uint) {
return _totalSupply;
}
/**
* @dev See {IERC20-balanceOf}.
*/
function balanceOf(address account) public view virtual override returns (uint) {
return _balances[account];
}
/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - the caller must have a balance of at least `amount`.
*/
function transfer(address to, uint amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}
/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual override returns (uint) {
return _allowances[owner][spender];
}
/**
* @dev See {IERC20-approve}.
*
* NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
* `transferFrom`. This is semantically equivalent to an infinite approval.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
/**
* @dev See {IERC20-transferFrom}.
*
* Emits an {Approval} event indicating the updated allowance. This is not
* required by the EIP. See the note at the beginning of {ERC20}.
*
* NOTE: Does not update the allowance if the current allowance
* is the maximum `uint256`.
*
* Requirements:
*
* - `from` and `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
* - the caller must have allowance for ``from``'s tokens of at least
* `amount`.
*/
function transferFrom(address from, address to, uint amount) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
/**
* @dev Atomically increases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function increaseAllowance(address spender, uint addedValue) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}
/**
* @dev Atomically decreases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `spender` must have allowance for the caller of at least
* `subtractedValue`.
*/
function decreaseAllowance(address spender, uint subtractedValue) public virtual returns (bool) {
address owner = _msgSender();
uint currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}
return true;
}
/**
* @dev Moves `amount` of tokens from `from` to `to`.
*
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
*/
function _transfer(address from, address to, uint amount) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
/**
* @dev Creates `amount` tokens and assigns them to `account`, increasing
* the total supply.
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
*/
function _mint(address account, uint amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply += amount;
unchecked {
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
_balances[account] += amount;
}
emit Transfer(address(0), account, amount);
_afterTokenTransfer(address(0), account, amount);
}
/**
* @dev Destroys `amount` tokens from `account`, reducing the
* total supply.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
* - `account` must have at least `amount` tokens.
*/
function _burn(address account, uint amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
uint accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountBalance <= totalSupply.
_totalSupply -= amount;
}
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
}
/**
* @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*/
function _approve(address owner, address spender, uint amount) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
/**
* @dev Updates `owner` s allowance for `spender` based on spent `amount`.
*
* Does not update the allowance amount in case of infinite allowance.
* Revert if not enough allowance is available.
*
* Might emit an {Approval} event.
*/
function _spendAllowance(address owner, address spender, uint amount) internal virtual {
uint currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
/**
* @dev Hook that is called before any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* will be transferred to `to`.
* - when `from` is zero, `amount` tokens will be minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens will be burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(address from, address to, uint amount) internal virtual {}
/**
* @dev Hook that is called after any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* has been transferred to `to`.
* - when `from` is zero, `amount` tokens have been minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens have been burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _afterTokenTransfer(address from, address to, uint amount) internal virtual {}
}
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/extensions/ERC20Burnable.sol)
// pragma solidity ^0.8.0;
// import "../ERC20.sol";
// import "../../../utils/Context.sol";
/**
* @dev Extension of {ERC20} that allows token holders to destroy both their own
* tokens and those that they have an allowance for, in a way that can be
* recognized off-chain (via event analysis).
*/
abstract contract OpenZeppelin_ERC20Burnable is OpenZeppelin_Context, OpenZeppelin_ERC20 {
/**
* @dev Destroys `amount` tokens from the caller.
*
* See {ERC20-_burn}.
*/
function burn(uint amount) public virtual {
_burn(_msgSender(), amount);
}
/**
* @dev Destroys `amount` tokens from `account`, deducting from the caller's
* allowance.
*
* See {ERC20-_burn} and {ERC20-allowance}.
*
* Requirements:
*
* - the caller must have allowance for ``accounts``'s tokens of at least
* `amount`.
*/
function burnFrom(address account, uint amount) public virtual {
_spendAllowance(account, _msgSender(), amount);
_burn(account, amount);
}
}
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/presets/ERC20PresetFixedSupply.sol)
// pragma solidity ^0.8.0;
// import "../extensions/ERC20Burnable.sol";
/**
* @dev {ERC20} token, including:
*
* - Preminted initial supply
* - Ability for holders to burn (destroy) their tokens
* - No access control mechanism (for minting/pausing) and hence no governance
*
* This contract uses {ERC20Burnable} to include burn capabilities - head to
* its documentation for details.
*
* _Available since v3.4._
*
* _Deprecated in favor of https://wizard.openzeppelin.com/[Contracts Wizard]._
*/
contract OpenZeppelin_ERC20PresetFixedSupply is OpenZeppelin_ERC20Burnable {
/**
* @dev Mints `initialSupply` amount of token and transfers them to `owner`.
*
* See {ERC20-constructor}.
*/
constructor(string memory name, string memory symbol, uint initialSupply, address owner) OpenZeppelin_ERC20(name, symbol) {
_mint(owner, initialSupply);
}
}
// actual mock code
contract ERC20_OneWeiFeeOnTransfer is OpenZeppelin_ERC20PresetFixedSupply {
constructor(uint initSupply, address initOwner)
OpenZeppelin_ERC20PresetFixedSupply("ERC20_OneWeiFeeOnTransfer_Mock", "ERC20_OneWeiFeeOnTransfer_Mock", initSupply, initOwner)
{}
/**
* @dev Moves `amount` of tokens from `from` to `to`.
*
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
*/
function _transfer(address from, address to, uint amount) internal virtual override {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
if (amount != 0) {
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += (amount - 1);
}
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
}
````
## File: src/test/mocks/ERC20_SetTransferReverting_Mock.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol";
contract ERC20_SetTransferReverting_Mock is ERC20PresetFixedSupply {
bool public transfersRevert;
constructor(uint initSupply, address initOwner)
ERC20PresetFixedSupply("ERC20_SetTransferReverting_Mock", "ERC20_SetTransferReverting_Mock", initSupply, initOwner)
{}
function setTransfersRevert(bool _transfersRevert) public {
transfersRevert = _transfersRevert;
}
function _beforeTokenTransfer(address, address, uint) internal view override {
if (transfersRevert) {
// revert without message
revert();
// revert("ERC20_SetTransferReverting_Mock._beforeTokenTransfer: transfersRevert set");
}
}
}
````
## File: src/test/mocks/ERC20Mock.sol
````
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/interfaces/IERC20.sol";
import "@openzeppelin/contracts/utils/Context.sol";
/**
* @dev Implementation of the {IERC20} interface.
*
* This implementation is agnostic to the way tokens are created. This means
* that a supply mechanism has to be added in a derived contract using {_mint}.
* For a generic mechanism see {ERC20PresetMinterPauser}.
*
* TIP: For a detailed writeup see our guide
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
* to implement supply mechanisms].
*
* The default value of {decimals} is 18. To change this, you should override
* this function so it returns a different value.
*
* We have followed general OpenZeppelin Contracts guidelines: functions revert
* instead returning `false` on failure. This behavior is nonetheless
* conventional and does not conflict with the expectations of ERC20
* applications.
*
* Additionally, an {Approval} event is emitted on calls to {transferFrom}.
* This allows applications to reconstruct the allowance for all accounts just
* by listening to said events. Other implementations of the EIP may not emit
* these events, as it isn't required by the specification.
*
* Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
* functions have been added to mitigate the well-known issues around setting
* allowances. See {IERC20-approve}.
*/
contract ERC20Mock is Context, IERC20 {
mapping(address => uint) private _balances;
mapping(address => mapping(address => uint)) private _allowances;
uint private _totalSupply;
uint initSupply = type(uint88).max;
constructor() {
_mint(msg.sender, initSupply);
}
/**
* @dev Returns the decimals places of the token.
*/
function decimals() external pure returns (uint8) {
return 18;
}
/**
* @dev See {IERC20-totalSupply}.
*/
function totalSupply() public view virtual override returns (uint) {
return _totalSupply;
}
/**
* @dev See {IERC20-balanceOf}.
*/
function balanceOf(address account) public view virtual override returns (uint) {
return _balances[account];
}
/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - the caller must have a balance of at least `amount`.
*/
function transfer(address to, uint amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}
/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual override returns (uint) {
return _allowances[owner][spender];
}
function mint(address, /*to*/ uint amount) public virtual {
address owner = _msgSender();
_mint(owner, amount);
}
/**
* @dev See {IERC20-approve}.
*
* NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
* `transferFrom`. This is semantically equivalent to an infinite approval.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address, /*spender*/ uint /*amount*/ ) public virtual override returns (bool) {
return true;
}
/**
* @dev See {IERC20-transferFrom}.
*
* Emits an {Approval} event indicating the updated allowance. This is not
* required by the EIP. See the note at the beginning of {ERC20}.
*
* NOTE: Does not update the allowance if the current allowance
* is the maximum `uint256`.
*
* Requirements:
*
* - `from` and `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
* - the caller must have allowance for ``from``'s tokens of at least
* `amount`.
*/
function transferFrom(address from, address to, uint amount) public virtual override returns (bool) {
_transfer(from, to, amount);
return true;
}
/**
* @dev Moves `amount` of tokens from `from` to `to`.
*
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
*/
function _transfer(address from, address to, uint amount) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
_mint(from, amount);
unchecked {
_balances[from] = _balances[from] - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
/**
* @dev Creates `amount` tokens and assigns them to `account`, increasing
* the total supply.
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
*/
function _mint(address account, uint amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");
_totalSupply += amount;
unchecked {
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
_balances[account] += amount;
}
emit Transfer(address(0), account, amount);
}
/**
* @dev Destroys `amount` tokens from `account`, reducing the
* total supply.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
* - `account` must have at least `amount` tokens.
*/
function _burn(address account, uint amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
uint accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountBalance <= totalSupply.
_totalSupply -= amount;
}
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
}
/**
* @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*/
function _approve(address owner, address spender, uint amount) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
/**
* @dev Updates `owner` s allowance for `spender` based on spent `amount`.
*
* Does not update the allowance amount in case of infinite allowance.
* Revert if not enough allowance is available.
*
* Might emit an {Approval} event.
*/
function _spendAllowance(address owner, address spender, uint amount) internal virtual {
uint currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint).max) require(currentAllowance >= amount, "ERC20: insufficient allowance");
}
/**
* @dev Hook that is called before any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* will be transferred to `to`.
* - when `from` is zero, `amount` tokens will be minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens will be burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(address from, address to, uint amount) internal virtual {}
/**
* @dev Hook that is called after any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* has been transferred to `to`.
* - when `from` is zero, `amount` tokens have been minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens have been burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _afterTokenTransfer(address from, address to, uint amount) internal virtual {}
}
````
## File: src/test/mocks/ETHDepositMock.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../../contracts/interfaces/IETHPOSDeposit.sol";
contract ETHPOSDepositMock is IETHPOSDeposit {
function deposit(bytes calldata pubkey, bytes calldata withdrawal_credentials, bytes calldata signature, bytes32 deposit_data_root)
external
payable
{}
function get_deposit_root() external pure returns (bytes32) {
bytes32 root;
return root;
}
/// @notice Query the current deposit count.
/// @return The deposit count encoded as a little endian 64-bit number.
function get_deposit_count() external pure returns (bytes memory) {
bytes memory root;
return root;
}
}
````
## File: src/test/mocks/LiquidStakingToken.sol
````
// SPDX-License-Identifier: BUSL-1.1
// modified version of https://github.com/itstargetconfirmed/wrapped-ether/blob/master/contracts/WETH.sol
pragma solidity >=0.4.22 <0.9.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
/// @notice An implementation of Wrapped Ether.
/// @author Anderson Singh.
contract WETH is ERC20 {
constructor() ERC20("Wrapped Ether", "WETH") {}
/// @dev mint tokens for sender based on amount of ether sent.
function deposit() public payable {
_mint(msg.sender, msg.value);
}
/// @dev withdraw ether based on requested amount and user balance.
function withdraw(uint _amount) external {
require(balanceOf(msg.sender) >= _amount, "insufficient balance.");
_burn(msg.sender, _amount);
payable(msg.sender).transfer(_amount);
}
fallback() external payable {
deposit();
}
receive() external payable {
deposit();
}
}
````
## File: src/test/mocks/MockAVSRegistrar.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
contract MockAVSRegistrar {
function supportsAVS(address /*avs*/ ) external pure returns (bool) {
return true;
}
fallback() external {}
}
````
## File: src/test/mocks/MockDecimals.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
contract MockDecimals {
function decimals() public pure returns (uint8) {
return 18;
}
}
````
## File: src/test/mocks/OwnableMock.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/access/Ownable.sol";
contract OwnableMock is Ownable {}
````
## File: src/test/mocks/Reenterer.sol
````
// SPDX-License-Identifier: MIT
// contract is *modified* from Seaport's test file: https://github.com/ProjectOpenSea/seaport/blob/891b5d4f52b58eb7030597fbb22dca67fd86c4c8/contracts/test/Reenterer.sol
pragma solidity ^0.8.9;
import "forge-std/Test.sol";
contract Reenterer is Test {
Vm cheats = Vm(VM_ADDRESS);
address public target;
uint public msgValue;
bytes public callData;
bytes public expectedRevertData;
bytes public dataToReturn;
event Reentered(bytes returnData);
function prepare(address targetToUse, uint msgValueToUse, bytes memory callDataToUse) external {
target = targetToUse;
msgValue = msgValueToUse;
callData = callDataToUse;
}
// added function that allows writing to `expectedRevertData`
function prepare(address targetToUse, uint msgValueToUse, bytes memory callDataToUse, bytes memory expectedRevertDataToUse) external {
target = targetToUse;
msgValue = msgValueToUse;
callData = callDataToUse;
expectedRevertData = expectedRevertDataToUse;
}
// added function that allows writing to `dataToReturn`
function prepareReturnData(bytes memory returnDataToUse) external {
dataToReturn = returnDataToUse;
}
receive() external payable {
// added expectrevert logic
if (expectedRevertData.length != 0) cheats.expectRevert(expectedRevertData);
(bool success, bytes memory returnData) = target.call{value: msgValue}(callData);
if (!success) {
assembly {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}
emit Reentered(returnData);
// added dataToReturn logic
uint dataToReturnLength = dataToReturn.length;
if (dataToReturnLength > 0) {
bytes memory _dataToReturn = dataToReturn;
assembly {
return(add(_dataToReturn, 32), dataToReturnLength)
}
}
}
// added fallback function that is a copy of the `receive` function
fallback() external payable {
// added expectRevert logic
if (expectedRevertData.length != 0) cheats.expectRevert(expectedRevertData);
(bool success, bytes memory returnData) = target.call{value: msgValue}(callData);
if (!success) {
assembly {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}
emit Reentered(returnData);
// added dataToReturn logic
uint dataToReturnLength = dataToReturn.length;
if (dataToReturnLength > 0) {
bytes memory _dataToReturn = dataToReturn;
assembly {
return(add(_dataToReturn, 32), dataToReturnLength)
}
}
}
}
````
## File: src/test/mocks/Reverter.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.9;
contract Reverter {
fallback() external {
revert("Reverter: I am a contract that always reverts");
}
}
contract ReverterWithDecimals is Reverter {
function decimals() external pure returns (uint8) {
return 18;
}
}
````
## File: src/test/mocks/StrategyManagerMock.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "../../contracts/interfaces/IDelegationManager.sol";
contract StrategyManagerMock is Test {
IDelegationManager public delegation;
address public strategyWhitelister;
mapping(address => IStrategy[]) public strategiesToReturn;
mapping(address => uint[]) public sharesToReturn;
/// @notice Mapping staker => strategy => shares withdrawn after a withdrawal has been completed
mapping(address => mapping(IStrategy => uint)) public strategySharesWithdrawn;
mapping(IStrategy => bool) public strategyIsWhitelistedForDeposit;
/// @notice Mapping: staker => cumulative number of queued withdrawals they have ever initiated. only increments (doesn't decrement)
mapping(address => uint) public cumulativeWithdrawalsQueued;
constructor(IDelegationManager _delegation) {
delegation = _delegation;
}
/**
* @notice mocks the return value of getDeposits
* @param staker staker whose deposits are being mocked
* @param _strategiesToReturn strategies to return in getDeposits
* @param _sharesToReturn shares to return in getDeposits
*/
function setDeposits(address staker, IStrategy[] calldata _strategiesToReturn, uint[] calldata _sharesToReturn) external {
require(_strategiesToReturn.length == _sharesToReturn.length, "StrategyManagerMock: length mismatch");
strategiesToReturn[staker] = _strategiesToReturn;
sharesToReturn[staker] = _sharesToReturn;
}
/**
* @notice Adds deposit to the staker's deposits. Note that this function does not check if the staker
* has already deposited for the strategy.
*/
function addDeposit(address staker, IStrategy strategy, uint shares) external {
strategiesToReturn[staker].push(strategy);
sharesToReturn[staker].push(shares);
}
/**
* @notice Get all details on the staker's deposits and corresponding shares
* @return (staker's strategies, shares in these strategies)
*/
function getDeposits(address staker) external view returns (IStrategy[] memory, uint[] memory) {
return (strategiesToReturn[staker], sharesToReturn[staker]);
}
function stakerDepositShares(address staker, IStrategy strategy) public view returns (uint) {
uint strategyIndex = _getStrategyIndex(staker, strategy);
return sharesToReturn[staker][strategyIndex];
}
uint public stakerStrategyListLengthReturnValue;
/// @notice Simple getter function that returns `stakerStrategyList[staker].length`.
function stakerStrategyListLength(address /*staker*/ ) external view returns (uint) {
return stakerStrategyListLengthReturnValue;
}
function setStakerStrategyListLengthReturnValue(uint valueToSet) public {
stakerStrategyListLengthReturnValue = valueToSet;
}
function setStrategyWhitelist(IStrategy strategy, bool value) external {
strategyIsWhitelistedForDeposit[strategy] = value;
}
function addStrategiesToDepositWhitelist(IStrategy[] calldata strategiesToWhitelist) external {
for (uint i = 0; i < strategiesToWhitelist.length; ++i) {
strategyIsWhitelistedForDeposit[strategiesToWhitelist[i]] = true;
}
}
function removeDepositShares(address staker, IStrategy strategy, uint sharesToRemove) external returns (uint) {
uint strategyIndex = _getStrategyIndex(staker, strategy);
uint updatedShares = sharesToReturn[staker][strategyIndex] - sharesToRemove;
sharesToReturn[staker][strategyIndex] = updatedShares;
return updatedShares;
}
function removeStrategiesFromDepositWhitelist(IStrategy[] calldata /*strategiesToRemoveFromWhitelist*/ ) external pure {}
function withdrawSharesAsTokens(
address staker,
IStrategy strategy,
address, // token
uint shares
) external {
strategySharesWithdrawn[staker][strategy] += shares;
}
function addShares(address staker, IStrategy strategy, uint addedShares) external returns (uint, uint) {
// Increase the staker's shares
uint strategyIndex = _getStrategyIndex(staker, strategy);
uint existingShares = sharesToReturn[staker][strategyIndex];
sharesToReturn[staker][strategyIndex] += addedShares;
return (existingShares, addedShares);
}
function burnShares(IStrategy strategy, uint sharesToBurn) external {}
function _getStrategyIndex(address staker, IStrategy strategy) internal view returns (uint) {
IStrategy[] memory strategies = strategiesToReturn[staker];
uint strategyIndex = type(uint).max;
for (uint i = 0; i < strategies.length; ++i) {
if (strategies[i] == strategy) {
strategyIndex = i;
break;
}
}
if (strategyIndex == type(uint).max) revert("StrategyManagerMock: strategy not found");
return strategyIndex;
}
function setDelegationManager(IDelegationManager _delegation) external {
delegation = _delegation;
}
fallback() external payable {}
receive() external payable {}
}
````
## File: src/test/test-data/rewardsCoordinator/processClaim_Preprod_Test.json
````json
{
"Root": "0x5e3227269ddcd5ddc7bc76ef42243a16166e86f9adab6183ee49b9875f2c7002",
"RootIndex": 0,
"EarnerIndex": 1,
"EarnerTreeProof": "0x468cfb9f23175803c35daccc788454e82e192aa69bd770b26861dbe1fb42336d44b3fc6ad9da7ba33382006677e1808659ccd50c35705233f0fedca7b34c0e07d8894c83317a23b1f54108d86045843d34a127d92fb262452a636d0d40b577e1",
"EarnerLeaf": {
"Earner": "0x2222aac0c980cc029624b7ff55b88bc6f63c538f",
"EarnerTokenRoot": "0x987aa019b1caf3fb0eaf02ebc1b8ce46840aee48d94ee21a2753045805ae38a8"
},
"LeafIndices": [
0
],
"TokenTreeProofs": [
"0x"
],
"TokenLeaves": [
{
"Token": "0x94373a4919b3240d86ea41593d5eba789fef3848",
"CumulativeEarnings": 1003827094673442500
}
],
"TokenTreeProofsNum": 1,
"TokenLeavesNum": 1
}
````
## File: src/test/test-data/rewardsCoordinator/processClaimProofs_MaxEarnerAndLeafIndices.json
````json
{
"Root": "0x37550707c80f3d8907c467999730e52127ab89be3f17a5017a3f1ffb73a1445f",
"RootIndex": 0,
"EarnerIndex": 7,
"EarnerTreeProof": "0x4bf5e16eaabbc36964f1e1639808669420f55d60e51adb7e9695b77145c479fd6777be59643947bb24d78e69d6605bf369c515b479f3a8967dd68a97c5bb4a4a262b28002eeb6cbbffb7e79e5741bf2be189a6073440a62fabcd8af4dbda94e3",
"EarnerLeaf": {
"Earner": "0x25a1b7322f9796b26a4bec125913b34c292b28d6",
"EarnerTokenRoot": "0xf8e7e20b32aae1d818dcb593b98982841e9a0ed12c161ad603e3ee3948746cba"
},
"LeafIndices": [
7
],
"TokenTreeProofs": [
"0x3cd04e8fc6f23812c570fe12292a30bb9e105e00f5913ac4b4938f23e65d8d10e6b1403d58c9d5450952e7d96c81305dad9fb966e8a27d3a42058e3958a0d30033148e91b455542d05deb81b8305b672e742cd3145f7022a0089bad2e6af9173"
],
"TokenLeaves": [
{
"Token": "0x7fbfdd1dfd80730385aee232cc9f79b8ae12a654",
"CumulativeEarnings": 3000000000000000000
}
],
"TokenTreeProofsNum": 1,
"TokenLeavesNum": 1
}
````
## File: src/test/test-data/rewardsCoordinator/processClaimProofs_Root1.json
````json
{
"Root": "0xc5d6bb1073f9040366851b5971493165893558f1cdc0b0046b6703baa85cddfc",
"RootIndex": 0,
"EarnerIndex": 3,
"EarnerTreeProof": "0x04da796e892869089dfdaae7269c8eb12548f6aa3774a747322b65c42d1ca0050d38d6d5e7e9e65ba24c275fcc0f0c377d8fb6ed089770d849c39a1829d1edf27e78a529aa8f867a3777f97541a23fb1844d6ae24c3b8ca1cc981510e5d08bda",
"EarnerLeaf": {
"Earner": "0xf2288d736d27c1584ebf7be5f52f9e4d47251aee",
"EarnerTokenRoot": "0xa81e82a39e6da197c3c83b8b1343eb7e8a969db52d4bfc424fd04d60350d76e3"
},
"LeafIndices": [
0,
1,
2,
3,
4,
5
],
"TokenTreeProofs": [
"0x6166682a9a29283a51a1c1575de82334227cc45b1ce686973039a44eb9a6b008e3e64bd80597510ac0b737fc645f15801cc3835b279412e1a09ba66d50c2aa82e3f64b05b0f9ee23e853d9c134984c27b5b58d14b70c4dacea9a5e40600e17a3",
"0x167f18d55815451f979244946b7eb2ce019c323a9e02fba1b2e05e19a27b91b5e3e64bd80597510ac0b737fc645f15801cc3835b279412e1a09ba66d50c2aa82e3f64b05b0f9ee23e853d9c134984c27b5b58d14b70c4dacea9a5e40600e17a3",
"0x2faa07574e263370227b938c72ca18ae40edf215067ce325ebfc36f11b1f19484923b477146618e0f36993536e7bbec8ef5346613df2fb9d53caf8d9365b4c68e3f64b05b0f9ee23e853d9c134984c27b5b58d14b70c4dacea9a5e40600e17a3",
"0x4dccc729db7b3ad40fc9acfe257d45196427285382332a4bc6704e1ae42785474923b477146618e0f36993536e7bbec8ef5346613df2fb9d53caf8d9365b4c68e3f64b05b0f9ee23e853d9c134984c27b5b58d14b70c4dacea9a5e40600e17a3",
"0x1e9348730aee854752d72b32f4eed96ad80093807b53f3a07068536ce96d0e9aad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb51ab784a49394ced1b194ec2cac2163a6a5a0108e31a2510f82162f3d41b79962",
"0x7a570fedf4656f3240f44fb4771c946ea688c554f82e204778b792013b29ded3ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb51ab784a49394ced1b194ec2cac2163a6a5a0108e31a2510f82162f3d41b79962"
],
"TokenLeaves": [
{
"Token": "0x1006dd1b8c3d0ef53489bed27577c75299f71473",
"CumulativeEarnings": 1000000000000000000
},
{
"Token": "0x11a4b85eab283c98d27c8ae64469224d55ed1894",
"CumulativeEarnings": 400000000000000000
},
{
"Token": "0x43afffbe0afacdabe9ce7dbc4f07407a2b788a84",
"CumulativeEarnings": 30000000000000000000
},
{
"Token": "0x748a3ed7e6b04239150d7ebe12d7aef3e3994a23",
"CumulativeEarnings": 250000000000
},
{
"Token": "0xd275b23e0a5b68ae251b0dc4c81104cba36e7cd6",
"CumulativeEarnings": 884300000000000000
},
{
"Token": "0xec562acb9e470de27dca2495950660fa9fbd85f8",
"CumulativeEarnings": 42000000000000
}
],
"TokenTreeProofsNum": 6,
"TokenLeavesNum": 6
}
````
## File: src/test/test-data/rewardsCoordinator/processClaimProofs_Root2.json
````json
{
"Root": "0x4d58d15093f392176c29504b94be56fb4969cf100083b6e8d3c79373f2d25974",
"RootIndex": 0,
"EarnerIndex": 3,
"EarnerTreeProof": "0xa9ed93ecba6058bc5cfab91319b40d344e7c829fb40eb26208e2898cb90071ed3bcd0aa5a1fbb1782053d35aaec2c6203ff53f6d6493100bafbf9506c7a3edf2bbcc90923e3f236d88ec22330aa91f3f50c425bc56ad3ff9707c72dcf853e206",
"EarnerLeaf": {
"Earner": "0xf2288d736d27c1584ebf7be5f52f9e4d47251aee",
"EarnerTokenRoot": "0x6eeac100b8cd705b92cda3015844005d918e118a3c7ba20046b6523cdc203d48"
},
"LeafIndices": [
0,
1,
2,
3,
4,
5
],
"TokenTreeProofs": [
"0x57898ab69c4024d674e8c18eea33fef5cd76c5b01e0f93ef168c602298f7b27ee2b46f44bc74268bc383609078e0217a0ea21d6a658975171f5233c1cd3133c0797252a751b45d9f15ef34a230f200a1c8bb69cdbbfb1fead4a3fd0f792fda58",
"0x97a04c80233ddc54eccd67a57f442c63c8f741079f41d95dd6242e275dcbe871e2b46f44bc74268bc383609078e0217a0ea21d6a658975171f5233c1cd3133c0797252a751b45d9f15ef34a230f200a1c8bb69cdbbfb1fead4a3fd0f792fda58",
"0xb0f239391b892c4b6f4ce0fea0a15de171f977f6798713e540b3493b93b557ffb9cf5ff8530c7a7932e4349c3c4d0172466b77e791a4e8bb1523fdb57c1fe96a797252a751b45d9f15ef34a230f200a1c8bb69cdbbfb1fead4a3fd0f792fda58",
"0x71a2384092132151928d8a13f599e52ad0aebe5587aa1cac9199d782c08847e2b9cf5ff8530c7a7932e4349c3c4d0172466b77e791a4e8bb1523fdb57c1fe96a797252a751b45d9f15ef34a230f200a1c8bb69cdbbfb1fead4a3fd0f792fda58",
"0xf8b6a59d2da40bb3c9a9fed1f77336c45e3550f34b8bcb76fa64adcf6d9a409dad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb508231e83c23fa7d9b9a2820a400601f21f946161a3817010724592cf53cf4ad9",
"0x87fa7a4daeedebd0069679c39d095a21293b412f6f9fdae3398b454160703e3bad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb508231e83c23fa7d9b9a2820a400601f21f946161a3817010724592cf53cf4ad9"
],
"TokenLeaves": [
{
"Token": "0x1006dd1b8c3d0ef53489bed27577c75299f71473",
"CumulativeEarnings": 1630000000000000000
},
{
"Token": "0x11a4b85eab283c98d27c8ae64469224d55ed1894",
"CumulativeEarnings": 780000000000000000
},
{
"Token": "0x43afffbe0afacdabe9ce7dbc4f07407a2b788a84",
"CumulativeEarnings": 37800000000000000000
},
{
"Token": "0x748a3ed7e6b04239150d7ebe12d7aef3e3994a23",
"CumulativeEarnings": 5050000000000
},
{
"Token": "0xd275b23e0a5b68ae251b0dc4c81104cba36e7cd6",
"CumulativeEarnings": 2284300000000000000
},
{
"Token": "0xec562acb9e470de27dca2495950660fa9fbd85f8",
"CumulativeEarnings": 886400000000000
}
],
"TokenTreeProofsNum": 6,
"TokenLeavesNum": 6
}
````
## File: src/test/test-data/rewardsCoordinator/processClaimProofs_Root3.json
````json
{
"Root": "0x53d76498642862250c1caa8b14df55642d977200467a3dfce62e6da30b4820c6",
"RootIndex": 0,
"EarnerIndex": 3,
"EarnerTreeProof": "0xecd8c0e6d2d221742f8025acd6f1f0a5ff8482fe8f1cb135439e346df6fd56acce4f8a04a8bcb37dcbc11cb6984ba73cfa4da51dd037f7be27c257cf0605673b38d2f9a729da60cd7c3c2b7ff53bebfdef61de0871518eb36654368ac584b6b8",
"EarnerLeaf": {
"Earner": "0xf2288d736d27c1584ebf7be5f52f9e4d47251aee",
"EarnerTokenRoot": "0x36c11c299ad4a8b95a795d6afc1f5f958b9d1a1ea5cc13ea2fc59b6ccd4b6ee4"
},
"LeafIndices": [
0,
1,
2,
3,
4,
5
],
"TokenTreeProofs": [
"0xfcf8546a323ba4d4bcfbdb5930b3bede1e93ec4bdd8d1372ed84db3126df143fb306484eeaece55c5cd8db0ce3f3a77f4aa6fa5150b95db06b9c1cec4825aae157a8dfba810f55acf97f229426649e26f6fe58886e1edcb404535334da43d92d",
"0xde4e83fd8b6b5e44f72fd511cd1a9ce6704f16378e5ca20c15f4d52efe27aa57b306484eeaece55c5cd8db0ce3f3a77f4aa6fa5150b95db06b9c1cec4825aae157a8dfba810f55acf97f229426649e26f6fe58886e1edcb404535334da43d92d",
"0x67f846894f21973d7db36e6b363141b291b2f0f57bfc4b9a74b4f5335c0d068a17aac423247fab5821f643830b49570dfdecd0f30a9ae24ff5fd59bd4e9e8a4e57a8dfba810f55acf97f229426649e26f6fe58886e1edcb404535334da43d92d",
"0xc27217da67ce7b0418f5c8bef4ceeefe04c5561ddd73ab9750798d8f981d981d17aac423247fab5821f643830b49570dfdecd0f30a9ae24ff5fd59bd4e9e8a4e57a8dfba810f55acf97f229426649e26f6fe58886e1edcb404535334da43d92d",
"0xd084816ad70c189dbfa8be4e6b1f912edb6fe3b39ba1c19c4ed16d91b84f903dad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb550d72c280c9bd978b5ef45c3647daa2ab564342c473cb977f35b053706ed19d1",
"0x2bec75e7737677b586943a2292393137cad6617ac248d249d220afb3bf31f4dcad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb550d72c280c9bd978b5ef45c3647daa2ab564342c473cb977f35b053706ed19d1"
],
"TokenLeaves": [
{
"Token": "0x1006dd1b8c3d0ef53489bed27577c75299f71473",
"CumulativeEarnings": 3500000000000000000
},
{
"Token": "0x11a4b85eab283c98d27c8ae64469224d55ed1894",
"CumulativeEarnings": 1240000000000000000
},
{
"Token": "0x43afffbe0afacdabe9ce7dbc4f07407a2b788a84",
"CumulativeEarnings": 56800000000000000000
},
{
"Token": "0x748a3ed7e6b04239150d7ebe12d7aef3e3994a23",
"CumulativeEarnings": 8900000000000
},
{
"Token": "0xd275b23e0a5b68ae251b0dc4c81104cba36e7cd6",
"CumulativeEarnings": 4343000000000000000
},
{
"Token": "0xec562acb9e470de27dca2495950660fa9fbd85f8",
"CumulativeEarnings": 1064000000000000
}
],
"TokenTreeProofsNum": 6,
"TokenLeavesNum": 6
}
````
## File: src/test/test-data/rewardsCoordinator/processClaimProofs_SingleEarnerLeaf.json
````json
{
"Root": "0xd4aa4d1bdb95eb78f061238d587609407301a05bd952334ad6b5e8ca60bb347e",
"RootIndex": 0,
"EarnerIndex": 0,
"EarnerTreeProof": "0x",
"EarnerLeaf": {
"Earner": "0x0d6ba28b9919cfcdb6b233469cc5ce30b979e08e",
"EarnerTokenRoot": "0x12104ce9deb6e62ef479656476be1da6d71905234252cf9a5c9733e6cb115d77"
},
"LeafIndices": [
0,
2,
3
],
"TokenTreeProofs": [
"0xf027f6a1f6522a5ede64f16448001378272bcc3b4d9b2660f241ea87d4cafc880d5fa8201813790dcfddb37966811813117ab78db2cce941c8b1d1f3888cccca6271c73f26f5e492cc990b25f85410beaf6f04958410162dc16eb5e3ce4791ce",
"0x9ae7a20b0f244cb25a1c18339432139587ecc0058130e9ebb10a98049981395d5bef23d3c6b22f7b1265e30a68e83b675394ea57b7fa150f5ecc07bae7e3b88d6271c73f26f5e492cc990b25f85410beaf6f04958410162dc16eb5e3ce4791ce",
"0x31335a07ff9047e08d712967b2ef1720f8682cb696d9df15902602b0c5eebdde5bef23d3c6b22f7b1265e30a68e83b675394ea57b7fa150f5ecc07bae7e3b88d6271c73f26f5e492cc990b25f85410beaf6f04958410162dc16eb5e3ce4791ce"
],
"TokenLeaves": [
{
"Token": "0x1006dd1b8c3d0ef53489bed27577c75299f71473",
"CumulativeEarnings": 1000000000000000000
},
{
"Token": "0x43afffbe0afacdabe9ce7dbc4f07407a2b788a84",
"CumulativeEarnings": 3000000000000000000
},
{
"Token": "0x748a3ed7e6b04239150d7ebe12d7aef3e3994a23",
"CumulativeEarnings": 1000000000000000000
}
],
"TokenTreeProofsNum": 3,
"TokenLeavesNum": 3
}
````
## File: src/test/test-data/rewardsCoordinator/processClaimProofs_SingleTokenLeaf.json
````json
{
"Root": "0x86867b737e68a56280554c0447ac6b43ba1cb68f4a46d1d9a0ecb919f912959b",
"RootIndex": 0,
"EarnerIndex": 3,
"EarnerTreeProof": "0x04da796e892869089dfdaae7269c8eb12548f6aa3774a747322b65c42d1ca0050d38d6d5e7e9e65ba24c275fcc0f0c377d8fb6ed089770d849c39a1829d1edf27e78a529aa8f867a3777f97541a23fb1844d6ae24c3b8ca1cc981510e5d08bda",
"EarnerLeaf": {
"Earner": "0xf2288d736d27c1584ebf7be5f52f9e4d47251aee",
"EarnerTokenRoot": "0x167f18d55815451f979244946b7eb2ce019c323a9e02fba1b2e05e19a27b91b5"
},
"LeafIndices": [
0
],
"TokenTreeProofs": [
"0x"
],
"TokenLeaves": [
{
"Token": "0x1006dd1b8c3d0ef53489bed27577c75299f71473",
"CumulativeEarnings": 1000000000000000000
}
],
"TokenTreeProofsNum": 1,
"TokenLeavesNum": 1
}
````
## File: src/test/test-data/slashedProofs/balanceUpdateProof_notOvercommitted_61511_incrementedBlockBy100.json
````json
{
"validatorIndex": 61511,
"beaconStateRoot": "0xe9537d65c51eb2c5b09330a10eacac64dce5a1c5298e455dd8705aef2a2a0c90",
"slotRoot": "0x4733030000000000000000000000000000000000000000000000000000000000",
"balanceRoot": "0x000000000000000043b2983307000000b9d8243007000000e5015b7307000000",
"latestBlockHeaderRoot": "0x44e54c4f1192d7cceaea02be1f3e680b480ff3474e004aaa5c78a8e1543f0d54",
"slotProof": [
"0x79610ba1d515acf1fc35d9aa230071dfac431ccd2d2d54109df6930baa9e6417",
"0x074b37317c897ad7f622e4f3a783a129f8bf1fb60fb0e96dff12f47bc76ed504",
"0x98515b0e3e59aaf4758c87e5b204c884f7ab9e874ae1063ebb00b16ae3c4a30d",
"0x5024e63e2e208d43769955eeeeb3db9dfed9b2d65e965ace30a60096dbeccede",
"0x8ce2046003484702394d963638c95fb59d0f32da8e2060a33918836be89ff074"
],
"ValidatorBalanceProof": [
"0x000000000000000068d059730700000000000000000000000000000000000000",
"0x7f5d365ccd831acbbbda52bca33268dd3153d69d29602a4f1b27c28838dd2ccf",
"0xa8556dd2b415efe9ea077acd637059f02112fe070e26e29bd56a1c1a281f3edb",
"0x11e8a9a28b280f0ea331f9f34a963aa493087147001ab12812df39f82258000f",
"0x128ef0eb187fb5343e67b9d4c60826cb3ccba55f32169d4d5d0e203e0abf51c8",
"0x8de5b7a778b0cc4f3cc5fcfdfe00a2a7b326c6096300fc7a2e132fe7328003a9",
"0x9050635d2238a7ddd92f956cb6c444805881635901510604a2ccb2c6d709455d",
"0xda56d579f6cecdf3ea6a31550aaba7f1233b07f58db6730ecdd0c929ed445f53",
"0x4a7aadbb3b9721060adbe14ce124730853dc8ddadb8fe1608c0784f1ef35e262",
"0xce105221b70ce96b926af197858c7f703f78f6f772565e49a977426e3622d527",
"0xa73aa71c2dcc22d07a4d636bea5e207b0610206bd095d1715ea8cf394aeca03d",
"0x482c8b89f16d57347c143f3e9102b63c4e56d0ef11f7fc68a498407b845ff1ae",
"0xef9636e6ce536e011368ef2950aa56b8c2551af78c65e6076a16ff34ae984530",
"0x38f4e0d211be2dc4aff4f85c215f042369f2ac780831369a2287eacb6d30104d",
"0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784",
"0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0x1ff9000000000000000000000000000000000000000000000000000000000000",
"0xe3a4221e582bef1ce66e057107d2423d8a4af2ceef50973ab5c5b9a2c1fba14b",
"0x64b4054633cff8782678efebd075563f7eb509431378ec880a8280c897e13190",
"0xbdabb1a249ef55a056128614d113e85bb6f6899e7169050dcc88407406f0ace0",
"0x234714ee1e90e736bbabeec9d26875e76b86300d657304aa762e5f5a05d57f55",
"0x8ce2046003484702394d963638c95fb59d0f32da8e2060a33918836be89ff074"
],
"ValidatorFields": [
"0x15c3c57049dbbd86d6a6cb7cd7637512c5c0ba23b582e3e9b5a9bd71e2fb6146",
"0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443",
"0xe5015b7307000000000000000000000000000000000000000000000000000000",
"0x0100000000000000000000000000000000000000000000000000000000000000",
"0x6502000000000000000000000000000000000000000000000000000000000000",
"0x6c02000000000000000000000000000000000000000000000000000000000000",
"0x0908000000000000000000000000000000000000000000000000000000000000",
"0x0428000000000000000000000000000000000000000000000000000000000000"
],
"LatestBlockHeaderProof": [
"0x8c0d5a3b8b10a624d22494560f6a657d2ef8513f4e05e3d1a9b4e51589ccb3d6",
"0x4e70fb71e677e500e00ca1234f5e540aaffa0df03c12a0f92f921e76845187b3",
"0x5a6544be083c70c87ff98d129a5bf434d62c73999c25230c2bff2f2e2d555b21",
"0x5024e63e2e208d43769955eeeeb3db9dfed9b2d65e965ace30a60096dbeccede",
"0x8ce2046003484702394d963638c95fb59d0f32da8e2060a33918836be89ff074"
],
"WithdrawalCredentialProof": [
"0x71bd8878bd97df4ceb7c6b5132d8b6745a6780262d5a0fa9dcb0d6f5c98115e7",
"0x27e6e87163079873e4a956e687e2f5f852775c6c69cc1646d59ff9de87ffdfec",
"0x7d3ec7b5b0603ec79644358dbeea0613df717f2e90271efeee665c25f99806a9",
"0x198cf23e2a594f99cc6fb0bd71728ae43f64d3aeecbbbd73b42e7f9d91ab112b",
"0x1a2ae162dcb33b34412a1b76d296d043b6e4a092ac17a31ff688f125a71efc5c",
"0x22b7f97d9cf16bb59b5bd032833bc428eba10283db93184e3e8f58e840376c4a",
"0x2d7294482a48d1fa33db6d09459c5ef98c5b4f063f1d8abb2968f6ff2553cea1",
"0x3cf02972a98b6349570d638662c6c359fd67d51da4ebb84c78a616bf57031be3",
"0x91efbc4e73550db8404d9df7819c1a93474a2dac866fe503936751f9c8aa52ce",
"0x5e4d2fde7fd736c48b6d997064515f12c5b870573f822571d575ad9ec4330d37",
"0x65cdebb1d68f7e5f9f4b367cf30b781ffc2c630afd87821d98dcdf31f8216147",
"0x7d35223de45ed1780d9c5fa6d8c4f777b819433c3e585a41d1ebfcb33de180f8",
"0xa248de567404ec7a47f3c27b498c2b9e6318d2de870b0e076a4689ccd32f4713",
"0xf00f90e9671a1decbdf18b1ad2247fd9192f1be793818c2c303ee607f43876f0",
"0xe5ae53bbdf410ecdecbbf58c7b3a4f9ee0885535ef95a3d5a34b69b32da31c07",
"0x04c321478671bf8864f28399bfd02a4d534ed2d35a402ae97b5a223483bbaebd",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x1ff9000000000000000000000000000000000000000000000000000000000000",
"0x7c0b000000000000000000000000000000000000000000000000000000000000",
"0x23453ac8b60d1e35c7b111a0a5a5fb5621007fc454a17d5099d4290c99d7cf5b",
"0x3626feb0eee255fd0fb5296d2984a905827a482a26234a8632e810d2355eeeaf",
"0x234714ee1e90e736bbabeec9d26875e76b86300d657304aa762e5f5a05d57f55",
"0x8ce2046003484702394d963638c95fb59d0f32da8e2060a33918836be89ff074"
]
}
````
## File: src/test/test-data/slashedProofs/balanceUpdateProof_notOvercommitted_61511.json
````json
{
"validatorIndex": 61511,
"beaconStateRoot": "0x4ee7c0f1277c3675d938680918b5b495ba5a9825ded9f007aa8a820548240520",
"slotRoot": "0xe332030000000000000000000000000000000000000000000000000000000000",
"balanceRoot": "0x000000000000000043b2983307000000b9d8243007000000e5015b7307000000",
"latestBlockHeaderRoot": "0x44e54c4f1192d7cceaea02be1f3e680b480ff3474e004aaa5c78a8e1543f0d54",
"slotProof": [
"0x79610ba1d515acf1fc35d9aa230071dfac431ccd2d2d54109df6930baa9e6417",
"0x074b37317c897ad7f622e4f3a783a129f8bf1fb60fb0e96dff12f47bc76ed504",
"0x98515b0e3e59aaf4758c87e5b204c884f7ab9e874ae1063ebb00b16ae3c4a30d",
"0x5024e63e2e208d43769955eeeeb3db9dfed9b2d65e965ace30a60096dbeccede",
"0x8ce2046003484702394d963638c95fb59d0f32da8e2060a33918836be89ff074"
],
"ValidatorBalanceProof": [
"0x000000000000000068d059730700000000000000000000000000000000000000",
"0x7f5d365ccd831acbbbda52bca33268dd3153d69d29602a4f1b27c28838dd2ccf",
"0xa8556dd2b415efe9ea077acd637059f02112fe070e26e29bd56a1c1a281f3edb",
"0x11e8a9a28b280f0ea331f9f34a963aa493087147001ab12812df39f82258000f",
"0x128ef0eb187fb5343e67b9d4c60826cb3ccba55f32169d4d5d0e203e0abf51c8",
"0x8de5b7a778b0cc4f3cc5fcfdfe00a2a7b326c6096300fc7a2e132fe7328003a9",
"0x9050635d2238a7ddd92f956cb6c444805881635901510604a2ccb2c6d709455d",
"0xda56d579f6cecdf3ea6a31550aaba7f1233b07f58db6730ecdd0c929ed445f53",
"0x4a7aadbb3b9721060adbe14ce124730853dc8ddadb8fe1608c0784f1ef35e262",
"0xce105221b70ce96b926af197858c7f703f78f6f772565e49a977426e3622d527",
"0xa73aa71c2dcc22d07a4d636bea5e207b0610206bd095d1715ea8cf394aeca03d",
"0x482c8b89f16d57347c143f3e9102b63c4e56d0ef11f7fc68a498407b845ff1ae",
"0xef9636e6ce536e011368ef2950aa56b8c2551af78c65e6076a16ff34ae984530",
"0x38f4e0d211be2dc4aff4f85c215f042369f2ac780831369a2287eacb6d30104d",
"0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784",
"0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0x1ff9000000000000000000000000000000000000000000000000000000000000",
"0xe3a4221e582bef1ce66e057107d2423d8a4af2ceef50973ab5c5b9a2c1fba14b",
"0x64b4054633cff8782678efebd075563f7eb509431378ec880a8280c897e13190",
"0xbdabb1a249ef55a056128614d113e85bb6f6899e7169050dcc88407406f0ace0",
"0x4092a1ba83f037b714a1d8ce4cbdcbeacc0b4af46535d35327f824c2aa0f6bc6",
"0x8ce2046003484702394d963638c95fb59d0f32da8e2060a33918836be89ff074"
],
"ValidatorFields": [
"0x15c3c57049dbbd86d6a6cb7cd7637512c5c0ba23b582e3e9b5a9bd71e2fb6146",
"0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443",
"0xe5015b7307000000000000000000000000000000000000000000000000000000",
"0x0100000000000000000000000000000000000000000000000000000000000000",
"0x6502000000000000000000000000000000000000000000000000000000000000",
"0x6c02000000000000000000000000000000000000000000000000000000000000",
"0x0908000000000000000000000000000000000000000000000000000000000000",
"0x0428000000000000000000000000000000000000000000000000000000000000"
],
"LatestBlockHeaderProof": [
"0x8c0d5a3b8b10a624d22494560f6a657d2ef8513f4e05e3d1a9b4e51589ccb3d6",
"0x4e70fb71e677e500e00ca1234f5e540aaffa0df03c12a0f92f921e76845187b3",
"0xb416255e46f0d0233446299dc4a66f39b045b05aefa97591acc98d9b043d4aa5",
"0x5024e63e2e208d43769955eeeeb3db9dfed9b2d65e965ace30a60096dbeccede",
"0x8ce2046003484702394d963638c95fb59d0f32da8e2060a33918836be89ff074"
],
"WithdrawalCredentialProof": [
"0x71bd8878bd97df4ceb7c6b5132d8b6745a6780262d5a0fa9dcb0d6f5c98115e7",
"0x27e6e87163079873e4a956e687e2f5f852775c6c69cc1646d59ff9de87ffdfec",
"0x7d3ec7b5b0603ec79644358dbeea0613df717f2e90271efeee665c25f99806a9",
"0x198cf23e2a594f99cc6fb0bd71728ae43f64d3aeecbbbd73b42e7f9d91ab112b",
"0x1a2ae162dcb33b34412a1b76d296d043b6e4a092ac17a31ff688f125a71efc5c",
"0x22b7f97d9cf16bb59b5bd032833bc428eba10283db93184e3e8f58e840376c4a",
"0x2d7294482a48d1fa33db6d09459c5ef98c5b4f063f1d8abb2968f6ff2553cea1",
"0x3cf02972a98b6349570d638662c6c359fd67d51da4ebb84c78a616bf57031be3",
"0x91efbc4e73550db8404d9df7819c1a93474a2dac866fe503936751f9c8aa52ce",
"0x5e4d2fde7fd736c48b6d997064515f12c5b870573f822571d575ad9ec4330d37",
"0x65cdebb1d68f7e5f9f4b367cf30b781ffc2c630afd87821d98dcdf31f8216147",
"0x7d35223de45ed1780d9c5fa6d8c4f777b819433c3e585a41d1ebfcb33de180f8",
"0xa248de567404ec7a47f3c27b498c2b9e6318d2de870b0e076a4689ccd32f4713",
"0xf00f90e9671a1decbdf18b1ad2247fd9192f1be793818c2c303ee607f43876f0",
"0xe5ae53bbdf410ecdecbbf58c7b3a4f9ee0885535ef95a3d5a34b69b32da31c07",
"0x04c321478671bf8864f28399bfd02a4d534ed2d35a402ae97b5a223483bbaebd",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x1ff9000000000000000000000000000000000000000000000000000000000000",
"0x7c0b000000000000000000000000000000000000000000000000000000000000",
"0x23453ac8b60d1e35c7b111a0a5a5fb5621007fc454a17d5099d4290c99d7cf5b",
"0x3626feb0eee255fd0fb5296d2984a905827a482a26234a8632e810d2355eeeaf",
"0x4092a1ba83f037b714a1d8ce4cbdcbeacc0b4af46535d35327f824c2aa0f6bc6",
"0x8ce2046003484702394d963638c95fb59d0f32da8e2060a33918836be89ff074"
]
}
````
## File: src/test/test-data/slashedProofs/balanceUpdateProof_Overcommitted_61511.json
````json
{
"validatorIndex": 61511,
"beaconStateRoot": "0xedfa8c363b420fd244477dc823f58f33f5d431f78cdbb7faac607597b5efad94",
"slotRoot": "0xe332030000000000000000000000000000000000000000000000000000000000",
"balanceRoot": "0x000000000000000043b2983307000000b9d82430070000006c0cec3007000000",
"latestBlockHeaderRoot": "0x44e54c4f1192d7cceaea02be1f3e680b480ff3474e004aaa5c78a8e1543f0d54",
"slotProof": [
"0x79610ba1d515acf1fc35d9aa230071dfac431ccd2d2d54109df6930baa9e6417",
"0x074b37317c897ad7f622e4f3a783a129f8bf1fb60fb0e96dff12f47bc76ed504",
"0x98515b0e3e59aaf4758c87e5b204c884f7ab9e874ae1063ebb00b16ae3c4a30d",
"0xaa07ea4616f5ee64aae9543c79e7370b2f9cdd82f0b94525cd7bab13d88b5cd7",
"0x8ce2046003484702394d963638c95fb59d0f32da8e2060a33918836be89ff074"
],
"ValidatorBalanceProof": [
"0x000000000000000068d059730700000000000000000000000000000000000000",
"0x7f5d365ccd831acbbbda52bca33268dd3153d69d29602a4f1b27c28838dd2ccf",
"0xa8556dd2b415efe9ea077acd637059f02112fe070e26e29bd56a1c1a281f3edb",
"0x11e8a9a28b280f0ea331f9f34a963aa493087147001ab12812df39f82258000f",
"0x128ef0eb187fb5343e67b9d4c60826cb3ccba55f32169d4d5d0e203e0abf51c8",
"0x8de5b7a778b0cc4f3cc5fcfdfe00a2a7b326c6096300fc7a2e132fe7328003a9",
"0x9050635d2238a7ddd92f956cb6c444805881635901510604a2ccb2c6d709455d",
"0xda56d579f6cecdf3ea6a31550aaba7f1233b07f58db6730ecdd0c929ed445f53",
"0x4a7aadbb3b9721060adbe14ce124730853dc8ddadb8fe1608c0784f1ef35e262",
"0xce105221b70ce96b926af197858c7f703f78f6f772565e49a977426e3622d527",
"0xa73aa71c2dcc22d07a4d636bea5e207b0610206bd095d1715ea8cf394aeca03d",
"0x482c8b89f16d57347c143f3e9102b63c4e56d0ef11f7fc68a498407b845ff1ae",
"0xef9636e6ce536e011368ef2950aa56b8c2551af78c65e6076a16ff34ae984530",
"0x38f4e0d211be2dc4aff4f85c215f042369f2ac780831369a2287eacb6d30104d",
"0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784",
"0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0x1ff9000000000000000000000000000000000000000000000000000000000000",
"0xe3a4221e582bef1ce66e057107d2423d8a4af2ceef50973ab5c5b9a2c1fba14b",
"0x64b4054633cff8782678efebd075563f7eb509431378ec880a8280c897e13190",
"0x86d33e3372cac6ef0ff823dc3a3ea92e02ea2bee1600549bdda5a914875a6e10",
"0x4092a1ba83f037b714a1d8ce4cbdcbeacc0b4af46535d35327f824c2aa0f6bc6",
"0x8ce2046003484702394d963638c95fb59d0f32da8e2060a33918836be89ff074"
],
"ValidatorFields": [
"0x15c3c57049dbbd86d6a6cb7cd7637512c5c0ba23b582e3e9b5a9bd71e2fb6146",
"0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443",
"0x0076be3707000000000000000000000000000000000000000000000000000000",
"0x0100000000000000000000000000000000000000000000000000000000000000",
"0x6502000000000000000000000000000000000000000000000000000000000000",
"0x6c02000000000000000000000000000000000000000000000000000000000000",
"0x0908000000000000000000000000000000000000000000000000000000000000",
"0x0428000000000000000000000000000000000000000000000000000000000000"
],
"LatestBlockHeaderProof": [
"0x8c0d5a3b8b10a624d22494560f6a657d2ef8513f4e05e3d1a9b4e51589ccb3d6",
"0x4e70fb71e677e500e00ca1234f5e540aaffa0df03c12a0f92f921e76845187b3",
"0xb416255e46f0d0233446299dc4a66f39b045b05aefa97591acc98d9b043d4aa5",
"0xaa07ea4616f5ee64aae9543c79e7370b2f9cdd82f0b94525cd7bab13d88b5cd7",
"0x8ce2046003484702394d963638c95fb59d0f32da8e2060a33918836be89ff074"
],
"WithdrawalCredentialProof": [
"0x71bd8878bd97df4ceb7c6b5132d8b6745a6780262d5a0fa9dcb0d6f5c98115e7",
"0x27e6e87163079873e4a956e687e2f5f852775c6c69cc1646d59ff9de87ffdfec",
"0x7d3ec7b5b0603ec79644358dbeea0613df717f2e90271efeee665c25f99806a9",
"0x198cf23e2a594f99cc6fb0bd71728ae43f64d3aeecbbbd73b42e7f9d91ab112b",
"0x1a2ae162dcb33b34412a1b76d296d043b6e4a092ac17a31ff688f125a71efc5c",
"0x22b7f97d9cf16bb59b5bd032833bc428eba10283db93184e3e8f58e840376c4a",
"0x2d7294482a48d1fa33db6d09459c5ef98c5b4f063f1d8abb2968f6ff2553cea1",
"0x3cf02972a98b6349570d638662c6c359fd67d51da4ebb84c78a616bf57031be3",
"0x91efbc4e73550db8404d9df7819c1a93474a2dac866fe503936751f9c8aa52ce",
"0x5e4d2fde7fd736c48b6d997064515f12c5b870573f822571d575ad9ec4330d37",
"0x65cdebb1d68f7e5f9f4b367cf30b781ffc2c630afd87821d98dcdf31f8216147",
"0x7d35223de45ed1780d9c5fa6d8c4f777b819433c3e585a41d1ebfcb33de180f8",
"0xa248de567404ec7a47f3c27b498c2b9e6318d2de870b0e076a4689ccd32f4713",
"0xf00f90e9671a1decbdf18b1ad2247fd9192f1be793818c2c303ee607f43876f0",
"0xe5ae53bbdf410ecdecbbf58c7b3a4f9ee0885535ef95a3d5a34b69b32da31c07",
"0x04c321478671bf8864f28399bfd02a4d534ed2d35a402ae97b5a223483bbaebd",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x1ff9000000000000000000000000000000000000000000000000000000000000",
"0x7c0b000000000000000000000000000000000000000000000000000000000000",
"0x23453ac8b60d1e35c7b111a0a5a5fb5621007fc454a17d5099d4290c99d7cf5b",
"0x9225de1bd47ad6255b27cb59eaaeb7eabacb3cff9e7316ca06933559ef882b86",
"0x4092a1ba83f037b714a1d8ce4cbdcbeacc0b4af46535d35327f824c2aa0f6bc6",
"0x8ce2046003484702394d963638c95fb59d0f32da8e2060a33918836be89ff074"
]
}
````
## File: src/test/test-data/balanceUpdateProof_balance28ETH_302913.json
````json
{"validatorIndex":302913,"beaconStateRoot":"0x8276d4d448d27e7d41da771ca7729f7a25c01b5fcd1f18f9bd9632dc7fde1388","slotRoot":"0xfea7610000000000000000000000000000000000000000000000000000000000","balanceRoot":"0x6cba5d7307000000e5d9ef840600000008bd5d7307000000239e5d7307000000","latestBlockHeaderRoot":"0x56461a3931812e3341030121e7db0f298112576ac9e898c00f4f1338c2ad647e","ValidatorBalanceProof":["0x26805d73070000002aa55d7307000000218e5d730700000056d35d7307000000","0x24846a54724508d8394a9e30f819bc19fe6727745be4f19e1e3924b985b4b8e9","0x48fb4849c31d88b413f650c17bd55bb11fe38bcef5d234dba8532737ae78f776","0x0d7d38f90a04ef49d15ca10af0e818e227134e4b46c7b22dd10e149f2a2bc612","0xdd345235f28f92719c39c9a5042b91f814d535ae5df574c0f6cd38f77791fc2b","0x0855928643f4210d9d8d0986847674139be32ebf3db984b6d81f3ed136475b6d","0x28460d51c1ce75299f8c0dff8bca9f498fef0edc57c343bbd76592b3d6b984a3","0x068a141709053afb48698290176068f448867cba9c6ff10f986b1d02691f6e70","0x9f0958275fe6ea80b1825bcd11907d1076600719ce574ae2a609e51a975dde54","0xd43fbb18b30449f5c81148848fd6579b4f6cebfe66187bd1c36a293b22085fc2","0x7bf0aac502053da7dfd5356960db63ab48d3cc6ee5f36f41639eadacd34ac272","0x866e3b213d02f1cb86149c2472789ab9d7fb3fb52371a893dc8b6f8936ebd0c1","0xcec7cc4b82fb8dd889257128af3e4433700fcc0fb0505b431aa5f592111e785b","0xdd08667067baa43e2b41b793464a591a29643b5582c8fc9c2c045f984c5848d7","0x842ec80062a4f2f7e9d80ab408374ae42817acf5036aff40b3ade345ab196090","0xfe1ae22b8ba21fce84b25f5da3e37a91db49158b7025d6371d5a54c03adba125","0x8c039a81d68952077db2cc11713ae6f5a9ecc5ebe859c0612ff7215c0ccd7aba","0x5415d3199f1da31287d646d03c2691728396d87f7ef52c142596301e27a343ca","0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4","0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0x846b080000000000000000000000000000000000000000000000000000000000","0x7d46d32db9a97974916a2114aeffc11fc031a954b2110742267054a438055c1a","0x81e1234eb27473f180f890c98019ecb7655a3e58105c7182d8daf726d75aaf8d","0x275abf1ff862082807cafed8d0a318c27439ec7159d81c831ee2832b5d6b57f6","0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b","0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"],"ValidatorFields":["0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d","0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443","0xe5d9ef8406000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0xea65010000000000000000000000000000000000000000000000000000000000","0xf265010000000000000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000"],"StateRootAgainstLatestBlockHeaderProof":["0x0000000000000000000000000000000000000000000000000000000000000000","0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"],"WithdrawalCredentialProof":["0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e","0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee","0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1","0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1","0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac","0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537","0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38","0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748","0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a","0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c","0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b","0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e","0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee","0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9","0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74","0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3","0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f","0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6","0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528","0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76","0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f","0x846b080000000000000000000000000000000000000000000000000000000000","0x5a6c050000000000000000000000000000000000000000000000000000000000","0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e","0x9fade50f5a88a8f7a027b4e7fa019f2289075e38668e926f9909dfcd5bcb3574","0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b","0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"]}
````
## File: src/test/test-data/balanceUpdateProof_notOverCommitted_302913_incrementedBlockBy100.json
````json
{"validatorIndex":302913,"beaconStateRoot":"0x5f137c26afeed8bb6385aa78e03242767f3b63e9b6bbe9f1e96190863100162f","slotRoot":"0x62a8610000000000000000000000000000000000000000000000000000000000","latestBlockHeaderRoot":"0xae005b0407313e551634dbaa1d746813eba6df508952e66184ab3616e14d63b8","ValidatorFields":["0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d","0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443","0x0040597307000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0xea65010000000000000000000000000000000000000000000000000000000000","0xf265010000000000000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000"],"StateRootAgainstLatestBlockHeaderProof":["0x0000000000000000000000000000000000000000000000000000000000000000","0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"],"WithdrawalCredentialProof":["0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e","0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee","0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1","0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1","0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac","0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537","0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38","0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748","0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a","0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c","0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b","0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e","0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee","0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9","0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74","0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3","0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f","0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6","0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528","0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76","0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f","0x846b080000000000000000000000000000000000000000000000000000000000","0x5a6c050000000000000000000000000000000000000000000000000000000000","0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e","0xce3d1b002aa817e3718132b7ffe6cea677f81b1b3b690b8052732d8e1a70d06b","0x687a08449d00a14e3143183adb3f66082d1459ba7f7d1209df02d3bfaa539031","0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"]}
````
## File: src/test/test-data/balanceUpdateProof_notOverCommitted_302913.json
````json
{
"validatorIndex": 302913,
"beaconStateRoot": "0x4fa780c32b4cf43ade769a51d738d889858c23197e4567ad537270220ab923e0",
"slotRoot": "0xfea7610000000000000000000000000000000000000000000000000000000000",
"latestBlockHeaderRoot": "0xdc501e6d6439fb13ee2020b4013374be51e35c58d611115a96856cbf71edaf73",
"ValidatorFields": [
"0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d",
"0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443",
"0x0040597307000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xea65010000000000000000000000000000000000000000000000000000000000",
"0xf265010000000000000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000"
],
"StateRootAgainstLatestBlockHeaderProof": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"
],
"WithdrawalCredentialProof": [
"0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e",
"0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee",
"0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1",
"0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1",
"0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac",
"0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537",
"0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38",
"0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748",
"0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a",
"0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c",
"0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b",
"0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e",
"0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee",
"0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9",
"0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74",
"0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3",
"0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f",
"0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6",
"0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528",
"0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x846b080000000000000000000000000000000000000000000000000000000000",
"0x5a6c050000000000000000000000000000000000000000000000000000000000",
"0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e",
"0xce3d1b002aa817e3718132b7ffe6cea677f81b1b3b690b8052732d8e1a70d06b",
"0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b",
"0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"
]
}
````
## File: src/test/test-data/balanceUpdateProof_updated_to_0ETH_302913.json
````json
{
"validatorIndex": 302913,
"beaconStateRoot": "0xd6c32aa93c58fd5ca1f36246234878ec6b0c36b2c218d8bd2521b4c4f2f8cd23",
"slotRoot": "0xfea7610000000000000000000000000000000000000000000000000000000000",
"latestBlockHeaderRoot": "0xc8fd8d84d59b2fe5e331e72466e237ba5014d9f078b0d65271aa00d2689fec9b",
"ValidatorFields": [
"0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d",
"0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xea65010000000000000000000000000000000000000000000000000000000000",
"0xf265010000000000000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000"
],
"StateRootAgainstLatestBlockHeaderProof": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"
],
"WithdrawalCredentialProof": [
"0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e",
"0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee",
"0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1",
"0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1",
"0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac",
"0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537",
"0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38",
"0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748",
"0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a",
"0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c",
"0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b",
"0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e",
"0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee",
"0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9",
"0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74",
"0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3",
"0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f",
"0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6",
"0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528",
"0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x846b080000000000000000000000000000000000000000000000000000000000",
"0x5a6c050000000000000000000000000000000000000000000000000000000000",
"0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e",
"0xd740083592ddcf0930ad47cc8e7758d567c2dc63767624bda4d50cafcbfe7093",
"0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b",
"0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"
]
}
````
## File: src/test/test-data/balanceUpdateProof_updated_to_30ETH_302913.json
````json
{"validatorIndex":302913,"beaconStateRoot":"0x9761c69195aeb853adc72f41b00232e0dccdfaaf5828bc1db56416eb1d4396df","slotRoot":"0xfea7610000000000000000000000000000000000000000000000000000000000","latestBlockHeaderRoot":"0xbc2f3b78cf1d4d5b47d6cc987864860035088de6316b4eb1ccbe8677f31e98e1","ValidatorFields":["0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d","0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443","0xe56d25fc06000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0xea65010000000000000000000000000000000000000000000000000000000000","0xf265010000000000000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000"],"StateRootAgainstLatestBlockHeaderProof":["0x0000000000000000000000000000000000000000000000000000000000000000","0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"],"WithdrawalCredentialProof":["0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e","0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee","0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1","0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1","0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac","0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537","0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38","0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748","0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a","0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c","0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b","0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e","0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee","0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9","0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74","0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3","0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f","0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6","0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528","0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76","0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f","0x846b080000000000000000000000000000000000000000000000000000000000","0x5a6c050000000000000000000000000000000000000000000000000000000000","0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e","0x8efea8a7f00e6411ae6187a6d9a2c22ad033c0ad749ce0940e5bf2fd76ac35c7","0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b","0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"]}
````
## File: src/test/test-data/fullWithdrawalCapellaAgainstDenebRoot.json
````json
{
"slot": 6397852,
"validatorIndex": 302913,
"historicalSummaryIndex": 146,
"withdrawalIndex": 0,
"blockHeaderRootIndex": 8092,
"beaconStateRoot": "0x426cc7e4b6a9be3a44e671c99eb62f27dd956d5db33aa8f05d07c3e8b05cb38f",
"slotRoot": "0x9c9f610000000000000000000000000000000000000000000000000000000000",
"timestampRoot": "0xb06fed6400000000000000000000000000000000000000000000000000000000",
"blockHeaderRoot": "0x8b036996f94e940c80c5c692fd0e25467a5d55f1cf92b7808f92090fc7be1d17",
"blockBodyRoot": "0x542df356d51eb305cff8282abe6909504b8c6d7bd4598b43aa7d54be13e44e9c",
"executionPayloadRoot": "0xe628472355543b53917635e60c1f924f111f7a3cd58f2d947e8631b9d9924cb1",
"latestBlockHeaderRoot": "0xb00368eaa2de6ca1e83d610be190a397668215a337837e1ad23241373d1c2dd0",
"SlotProof": [
"0x89c5010000000000000000000000000000000000000000000000000000000000",
"0xab4a015ca78ff722e478d047b19650dc6fc92a4270c6cd34401523d3d6a1d9f2",
"0xf4e65df697eb85f3ab176ac93b6ad4d96bd6b04bdddcc4f6c98f0fa94effc553"
],
"WithdrawalProof": [
"0xa3d843f57c18ee3dac0eb263e446fe5d0110059137807d3cae4a2e60ccca013f",
"0x87441da495942a4af734cbca4dbcf0b96b2d83137ce595c9f29495aae6a8d99e",
"0xae0dc609ecbfb26abc191227a76efb332aaea29725253756f2cad136ef5837a6",
"0x765bcd075991ecad96203020d1576fdb9b45b41dad3b5adde11263ab9f6f56b8",
"0x1000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x9d9b56c23faa6a8abf0a49105eb12bbdfdf198a9c6c616b8f24f6e91ad79de92",
"0xac5e32ea973e990d191039e91c7d9fd9830599b8208675f159c3df128228e729",
"0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"
],
"ValidatorProof": [
"0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e",
"0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee",
"0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1",
"0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1",
"0xdfe7aa4a49359b80e66fbdc8cbbf5d2014868aaf8dee25848f1b6494cb56231f",
"0xeec50554b6994496920a265643520c78ff05beec8015c6432f72b0cac5af510c",
"0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38",
"0x9c797e45839da75fea2190bf7a2735e68d8e4bd3cdbc82498491debb3acce128",
"0x2c693b02fbff3f4f659bfb8414a9ba27bbfdc1baf65317f05a84a85552628bd6",
"0x6f853a43c11ab3ac27f5ea8dd9e6dc825216114dee8e5db2306b97723048daaa",
"0x620fe1c260fcea9fb7ca0b7cd64f88aa906c88a8efc136317d88483f8a99d5ae",
"0xccfc09edfaa6bb8f9a1db781f1104b3aeb2a45799c898df57f3610b9ffc255de",
"0x8c5a00d96b9eb727a5a4aec2fd6b9771cb0c5be3a8b5588aff54b2ee36792af2",
"0x48fc48699053f4bd8f986f4216e2728f11e0d53ebeaf13bc9d287d2e099e7196",
"0xac88ce300b12047d9131a651417b624f206b742080c28c347873a7706897b379",
"0xe373b48074ce47d30b57509e85e905e42e8dbc869bb7c436553326a7a65e22ec",
"0x358c6950cfb1fb035e1e2506bddf5a1dc1f87d699a464a7eb05b87ce699942ce",
"0x040e77e06c4d45802b2093e445e56a1ed5d5afbd1c3393806b006b7f40a17148",
"0xcfb3f924f2e8810a349041676060bbf55113cbc00270f778723dbac055c8ba2b",
"0xa4b4bda96e8ae5a441b128b339ed3776f6105d24fcaa288cad2a3153e430a9ea",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x0b430a0000000000000000000000000000000000000000000000000000000000",
"0x3b4f070000000000000000000000000000000000000000000000000000000000",
"0x7e71eb9ab70a9ac86179e04cc156a3a6efd5d49e864b1def60d045fe88ae53db",
"0x042f04ac64635d44bd43c48c5504689a119901947ed7cfca6ce6f7171d29b696",
"0x560c8c92a1425b1c928f582f64de643e17290760d4ecb242afb53b62d51ea918",
"0xf426ceebf070d088972f46060f72b9fa35ebb90e8d18af341b698d359ab8366f"
],
"TimestampProof": [
"0x28a2c80000000000000000000000000000000000000000000000000000000000",
"0xa749df3368741198702798435eea361b1b1946aa9456587a2be377c8472ea2df",
"0xb1c03097a5a24f46cdb684df37e3ac0536a76428be1a6c6d6450378701ab1f3d",
"0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"
],
"ExecutionPayloadProof": [
"0xb6a435ffd17014d1dad214ba466aaa7fba5aa247945d2c29fd53e90d554f4474",
"0x336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0e",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0x5ec9aaf0a3571602f4704d4471b9af564caf17e4d22a7c31017293cb95949053",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0xc1f7cb289e44e9f711d7d7c05b67b84b8c6dd0394b688922a490cfd3fe216db1"
],
"ValidatorFields": [
"0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d",
"0x0100000000000000000000008e35f095545c56b07c942a4f3b055ef1ec4cb148",
"0x0040597307000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xea65010000000000000000000000000000000000000000000000000000000000",
"0xf265010000000000000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000"
],
"WithdrawalFields": [
"0x45cee50000000000000000000000000000000000000000000000000000000000",
"0x419f040000000000000000000000000000000000000000000000000000000000",
"0x59b0d71688da01057c08e4c1baa8faa629819c2a000000000000000000000000",
"0xe5015b7307000000000000000000000000000000000000000000000000000000"
],
"StateRootAgainstLatestBlockHeaderProof": [
"0xc055061b6674f3bac542b141b441b476130d17dacc3e61d6ce01a6b8528f7de7",
"0xfd8af94401badce5dd4588a22c605e197a51b17e696f761aed06819a98645f03",
"0xdeb9a60cdd0908457ff962da3895ffcf1a1e479b633e3427ac9b1166e006c8f7"
],
"HistoricalSummaryProof": [
"0x96d85b451bb1df5314fd3fd3fa4caa3285796905bc8bba6eee5e3e1539cf2695",
"0x354f5aaff1cde08c6b8a7ef8610abf62a0b50339d0dd26b0152699ebc2bdf785",
"0xb8c4c9f1dec537f4c4652e6bf519bac768e85b2590b7683e392aab698b50d529",
"0x1cae4e7facb6883024359eb1e9212d36e68a106a7733dc1c099017a74e5f465a",
"0x616439c1379e10fc6ad2fa192a2e49c2d5a7155fdde95911f37fcfb75952fcb2",
"0x9d95eefbaff153d6ad9050327e21a254750d80ff89315ab4056574275ce2a751",
"0x20cdc9ec1006e8bc27ab3e0a9c88a8e14c8a9da2470f8eaf673ee67abcfd0342",
"0x4bdcbe543f9ef7348855aac43d6b6286f9c0c7be53de8a1300bea1ba5ba0758e",
"0xb6631640d626ea9523ae619a42633072614326cc9220462dffdeb63e804ef05f",
"0xf19a76e33ca189a8682ece523c2afda138db575955b7af31a427c9b8adb41e15",
"0x221b43ad87d7410624842cad296fc48360b5bf4e835f6ff610db736774d2f2d3",
"0x297c51f4ff236db943bebeb35538e207c8de6330d26aa8138a9ca206f42154bf",
"0x129a0644f33b9ee4e9a36a11dd59d1dedc64012fbb7a79263d07f82d647ffba8",
"0x763794c2042b9c8381ac7c4d7f4d38b0abb38069b2810b522f87951f25d9d2be",
"0x4ee09be5d00612fc8807752842f0839656107b50699330ecf09825466576a8e5",
"0xee0639a2ada97368e8b3493f9d2141e16c3cd9fe54e13691bb7a2c376c56c7c8",
"0xc9394eeb778485920ae0bc45a6f0e5f1b43f3aeadc24b24e7a655582aa87aede",
"0xfc3f3efb849bf652aa632502c004af0f78dfe1a75902f33bbce3ca3d1cc3bfea",
"0xba2bc704559c23541f0c9efa0b522454e8cd06cd504d0e45724709cf5672640f",
"0x4644f74cc7fedf973ef015906df9ba97729b04395a70ff170bee8c40ffed8f0f",
"0xb5202e0c2d48a246c5b45e30d3bf0a89f3d3ea5e4591db5ec39efc1ed1e0a2a2",
"0xff857f4c17c9fb2e544e496685ebd8e2258c761e4636cfb031ba73a4430061c7",
"0x72cf6a5869e83ea39846cc892946c8a5e6bf3191df19ae922664504e4cf38c6b",
"0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1",
"0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b",
"0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220",
"0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f",
"0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e",
"0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784",
"0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x1101000000000000000000000000000000000000000000000000000000000000",
"0xcbe0080000000000000000000000000000000000000000000000000000000000",
"0x8ff2572846d80ce4be83e1638164331c30cd7eadb3488f00ba2507c072929e3a",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0xa6c60794743172c4a55a9fcee4a7832e5ef7b31aac1b9489b5baa1db78df5c60",
"0x1c8017e95f68e52210618f3802699b49a2a13a40257e97b74ee864824a73a280"
]
}
````
## File: src/test/test-data/fullWithdrawalDeneb.json
````json
{
"slot": 7421951,
"validatorIndex": 0,
"historicalSummaryIndex": 271,
"withdrawalIndex": 0,
"blockHeaderRootIndex": 8191,
"beaconStateRoot": "0xaf91e832d495c6d0e877bdf61b2bb6614621addb789ab492ceff9eef1696a64b",
"slotRoot": "0xff3f710000000000000000000000000000000000000000000000000000000000",
"timestampRoot": "0x54f4a86500000000000000000000000000000000000000000000000000000000",
"blockHeaderRoot": "0x68d267d7566c4829a1560fea6cce668f8fbf2e126e59e2556288bcbff92b64f2",
"blockBodyRoot": "0x88a28082491645022ce8d6af49dca10325fc979d179d0135f0f7d0937fbbbfeb",
"executionPayloadRoot": "0xc1f6b92b0e40c5cf43de8fe08f68434736dd9d42f7620436df40320f4eb65286",
"latestBlockHeaderRoot": "0x5a35a89568f3323481764c70b1bad0880d4d0114f185e43a42c96a8e45fa2a0f",
"SlotProof": [
"0x8d6a010000000000000000000000000000000000000000000000000000000000",
"0x0d4c303f43d35612a043d17114edde94bdc94ee369b761067bb85bd347c94c4c",
"0x8dbd7ba3acb83e5e9a00d908e8d05e0dc99569d2135d24affc44e325b0f7911d"
],
"WithdrawalProof": [
"0x3effc719333b3a5052a34bd1ed124dce49445905dbe18af5aa947bfe25a94dd8",
"0xf8470ba001831654956a6f12a8ffd6a1f1004e08557268d86477945cd3989531",
"0x55f96eba696026f9d8389019bf3c2f61ab741f968c01744540b170a4fb0f25a4",
"0x47ab534e81180bcf81d1ef541132b84826f1f34e2a0fde736a313a6ed5557459",
"0x1000000000000000000000000000000000000000000000000000000000000000",
"0x00000a0000000000000000000000000000000000000000000000000000000000",
"0x5c1bb5d0e2afe397c9fb9e275aa97209ba4c01e13d181e66311b42aed62559f7",
"0x058ad237cbc009d8b6f426aaa40709e508753fa90c6858e147e1c1066127dc69",
"0x4750fb0e389da83ea89697969498c02f840a2b21c7ece905e0f284f7e5b179c4",
"0xd2252e6aa60b6dbca15826f459db1e89ec584c2a2aa89bccd1715b9942633e00"
],
"ValidatorProof": [
"0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e",
"0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee",
"0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1",
"0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1",
"0xdfe7aa4a49359b80e66fbdc8cbbf5d2014868aaf8dee25848f1b6494cb56231f",
"0xeec50554b6994496920a265643520c78ff05beec8015c6432f72b0cac5af510c",
"0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38",
"0x9c797e45839da75fea2190bf7a2735e68d8e4bd3cdbc82498491debb3acce128",
"0x2c693b02fbff3f4f659bfb8414a9ba27bbfdc1baf65317f05a84a85552628bd6",
"0x6f853a43c11ab3ac27f5ea8dd9e6dc825216114dee8e5db2306b97723048daaa",
"0x620fe1c260fcea9fb7ca0b7cd64f88aa906c88a8efc136317d88483f8a99d5ae",
"0xccfc09edfaa6bb8f9a1db781f1104b3aeb2a45799c898df57f3610b9ffc255de",
"0x8c5a00d96b9eb727a5a4aec2fd6b9771cb0c5be3a8b5588aff54b2ee36792af2",
"0x48fc48699053f4bd8f986f4216e2728f11e0d53ebeaf13bc9d287d2e099e7196",
"0xac88ce300b12047d9131a651417b624f206b742080c28c347873a7706897b379",
"0xe373b48074ce47d30b57509e85e905e42e8dbc869bb7c436553326a7a65e22ec",
"0x358c6950cfb1fb035e1e2506bddf5a1dc1f87d699a464a7eb05b87ce699942ce",
"0x040e77e06c4d45802b2093e445e56a1ed5d5afbd1c3393806b006b7f40a17148",
"0xcfb3f924f2e8810a349041676060bbf55113cbc00270f778723dbac055c8ba2b",
"0xa4b4bda96e8ae5a441b128b339ed3776f6105d24fcaa288cad2a3153e430a9ea",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x0b430a0000000000000000000000000000000000000000000000000000000000",
"0x3b4f070000000000000000000000000000000000000000000000000000000000",
"0x7e71eb9ab70a9ac86179e04cc156a3a6efd5d49e864b1def60d045fe88ae53db",
"0x042f04ac64635d44bd43c48c5504689a119901947ed7cfca6ce6f7171d29b696",
"0x560c8c92a1425b1c928f582f64de643e17290760d4ecb242afb53b62d51ea918",
"0xfe7b7941293bb1a833e6be99db3175a8ab0af7e44f42d0c9dcdf34ae916db490"
],
"TimestampProof": [
"0x79958c0000000000000000000000000000000000000000000000000000000000",
"0xb0949007c306f2de2257c598d935ca16be928532e866698c2561bf4cf1e08b6f",
"0x11b7c6b7b01e2a21a682cf18e431dc78efa32300bfb5eba5374420f11cbcb751",
"0x4750fb0e389da83ea89697969498c02f840a2b21c7ece905e0f284f7e5b179c4",
"0xd2252e6aa60b6dbca15826f459db1e89ec584c2a2aa89bccd1715b9942633e00"
],
"ExecutionPayloadProof": [
"0xe5e633a5ba845ad1ede8be35fed9ea6d37e39d09061398190eac885703ff5cbd",
"0x260336bbff9ef0540c4497ed3e946ba0ca2080b668a1bdcb033496e56c40d451",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0xd2dc492d263b7c106c7a5012f6f2c138c28e1cd37962d49a30031c16964f6bb8",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0x5c2aa56042580b615d81c829f006de5e7b2a21fc330119ddc7600a5a28692069"
],
"ValidatorFields": [
"0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d",
"0x0100000000000000000000008e35f095545c56b07c942a4f3b055ef1ec4cb148",
"0x0040597307000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xea65010000000000000000000000000000000000000000000000000000000000",
"0xf265010000000000000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000"
],
"WithdrawalFields": [
"0xe599a70100000000000000000000000000000000000000000000000000000000",
"0x419f040000000000000000000000000000000000000000000000000000000000",
"0xd982a5927741bfd9b8cf16234061d7a592ca2b1c000000000000000000000000",
"0xe5015b7307000000000000000000000000000000000000000000000000000000"
],
"StateRootAgainstLatestBlockHeaderProof": [
"0xc055061b6674f3bac542b141b441b476130d17dacc3e61d6ce01a6b8528f7de7",
"0xfd8af94401badce5dd4588a22c605e197a51b17e696f761aed06819a98645f03",
"0xdeb9a60cdd0908457ff962da3895ffcf1a1e479b633e3427ac9b1166e006c8f7"
],
"HistoricalSummaryProof": [
"0x273d5cec9b06327f6b83f079e34df0678d905713326e4ac2fe3afe8b12d4af22",
"0xf39c43458894e29168c729f714c66b3aef24d43ea37c46ece64f736fbcbcb1d1",
"0xeca6748ed11dc62dbecc0eaca339af8929bfa9b962149257af7c30cb947ec642",
"0x7cd5fc51dbcc6c6666c00e0ddda4f095f848c70fdc6fa378c5eb9b9108e59efd",
"0xe6215943dc342defe9fb5c1474eb6f3ab3db8d1e04dd11707394cf026f0bf780",
"0x650c78d820aad1d0b31b94e1d15c2e4998aeffd5e3399646809064042e4f923e",
"0xefe753d3d111fa3603f090f5f08c2f12ae9324e8fbe2862162fc6c1587a5ab4b",
"0x3e62b06efce16432b6495d9d7fb51f0c3c745036e0c77dc5fc78a70e54d64d93",
"0x0aae9c557ef9a8c3d7b1dd2788f484b94a9cf04312cf274353e3c19d5beb8541",
"0x6d716c0e4864c59df7bc3319eb4ccac269242e9a1634cf04d4c8df8f9b53f4da",
"0x1eecd8c195eb8c158d0dd3e6d7303aee05cc9d9fdfe7c9135ac19453ee8d7bed",
"0x93b6c13c69ea8e5c921ac4b895db6f8ebc66d3b01daff16a1658f2526eb79ed9",
"0x4e0b3c661d827f71fca9f2fedc3a72d9e4907599577b7149123d5328afe091c9",
"0xae456e2a1b0f20ebda87d9e3e7f87f7dcc0860aae65c53657a51f876b862f9a9",
"0x3f8e2a5e35171009027df8b882d861e09d70a334a0db14b0f3c920fc6e4c4e23",
"0x0cad2edea11734fc388afd6bc9b9125be12edd7e4df6f05e2fdc5a622c0138fb",
"0x735f927c57108d1de8547c9d49ecdbf8661a481d6374ca6e25a103ea728b1916",
"0xf9513c49e7d50b6311372f787ab3ec7a112e384115d340b0d9f74bccb3562c33",
"0xd3573d59f23ed8018d754c166d987e60ac4018ed6a0c187e01439c10e449511f",
"0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30",
"0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1",
"0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c",
"0x1ef5d0b4795711d6aaf89b6eb2e5ca1c8c729ad9acb5b58c2b700a857c3512a0",
"0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1",
"0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b",
"0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220",
"0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f",
"0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e",
"0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784",
"0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x1101000000000000000000000000000000000000000000000000000000000000",
"0xcbe0080000000000000000000000000000000000000000000000000000000000",
"0x8ff2572846d80ce4be83e1638164331c30cd7eadb3488f00ba2507c072929e3a",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0xa6c60794743172c4a55a9fcee4a7832e5ef7b31aac1b9489b5baa1db78df5c60",
"0x1c8017e95f68e52210618f3802699b49a2a13a40257e97b74ee864824a73a280"
]
}
````
## File: src/test/test-data/fullWithdrawalProof_Latest_1SlotAdvanced.json
````json
{
"slot": 6397852,
"validatorIndex": 0,
"historicalSummaryIndex": 146,
"withdrawalIndex": 0,
"blockHeaderRootIndex": 8092,
"beaconStateRoot": "0x347e794382bd27d071b471af5047a2d97f8080a0f029e1fb9e3a13665924cdd1",
"slotRoot": "0x9c9f610000000000000000000000000000000000000000000000000000000000",
"timestampRoot": "0xb16fed6400000000000000000000000000000000000000000000000000000000",
"blockHeaderRoot": "0x8b44963e24101b8a5dadf759524d69084f142aaeded6e261e2ae4892c66e3bdf",
"blockBodyRoot": "0x8538e5b68a113c7c2cefcbd7b08b23af8f098b03dd1cd40108be7a0bad39f7b4",
"executionPayloadRoot": "0x126dc97fb335244643609b61b64ab8694f41a077cfadff97de390e9f708c80be",
"latestBlockHeaderRoot": "0x6674590fc92d3ad53a128a56f1b189f6c4111d6bad8976b57fa5858e057f0e07",
"SlotProof": [
"0x89c5010000000000000000000000000000000000000000000000000000000000",
"0xab4a015ca78ff722e478d047b19650dc6fc92a4270c6cd34401523d3d6a1d9f2",
"0x3424c924845b7698ec1ee6a4350e03dc5a00dd32f1b2fb21c9d25e17ef61b64f"
],
"WithdrawalProof": [
"0xa3d843f57c18ee3dac0eb263e446fe5d0110059137807d3cae4a2e60ccca013f",
"0x87441da495942a4af734cbca4dbcf0b96b2d83137ce595c9f29495aae6a8d99e",
"0xae0dc609ecbfb26abc191227a76efb332aaea29725253756f2cad136ef5837a6",
"0x765bcd075991ecad96203020d1576fdb9b45b41dad3b5adde11263ab9f6f56b8",
"0x1000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x9d9b56c23faa6a8abf0a49105eb12bbdfdf198a9c6c616b8f24f6e91ad79de92",
"0x232d0d58faceee6ed21f85527a6d6b54148bfabb0665e580fdd6ec862ab5cc56",
"0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"
],
"ValidatorProof": [
"0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e",
"0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee",
"0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1",
"0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1",
"0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac",
"0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537",
"0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38",
"0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748",
"0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a",
"0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c",
"0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b",
"0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e",
"0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee",
"0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9",
"0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74",
"0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3",
"0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f",
"0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6",
"0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528",
"0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x846b080000000000000000000000000000000000000000000000000000000000",
"0x5a6c050000000000000000000000000000000000000000000000000000000000",
"0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e",
"0xce3d1b002aa817e3718132b7ffe6cea677f81b1b3b690b8052732d8e1a70d06b",
"0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b",
"0xce38745b03a4351bbea18f0aadd0001d1c4f1b23b250ed40204591910a77bae2"
],
"TimestampProof": [
"0x28a2c80000000000000000000000000000000000000000000000000000000000",
"0xa749df3368741198702798435eea361b1b1946aa9456587a2be377c8472ea2df",
"0xb1c03097a5a24f46cdb684df37e3ac0536a76428be1a6c6d6450378701ab1f3d",
"0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"
],
"ExecutionPayloadProof": [
"0xb6a435ffd17014d1dad214ba466aaa7fba5aa247945d2c29fd53e90d554f4474",
"0x336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0e",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0x5ec9aaf0a3571602f4704d4471b9af564caf17e4d22a7c31017293cb95949053",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0xc1f7cb289e44e9f711d7d7c05b67b84b8c6dd0394b688922a490cfd3fe216db1"
],
"ValidatorFields": [
"0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d",
"0x0100000000000000000000008e35f095545c56b07c942a4f3b055ef1ec4cb148",
"0x0040597307000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xea65010000000000000000000000000000000000000000000000000000000000",
"0xf265010000000000000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000"
],
"WithdrawalFields": [
"0x45cee50000000000000000000000000000000000000000000000000000000000",
"0x419f040000000000000000000000000000000000000000000000000000000000",
"0x59b0d71688da01057c08e4c1baa8faa629819c2a000000000000000000000000",
"0xe5015b7307000000000000000000000000000000000000000000000000000000"
],
"StateRootAgainstLatestBlockHeaderProof": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"
],
"HistoricalSummaryProof": [
"0x050b5923fe2e470849a7d467e4cbed4803d3a46d516b84552567976960ff4ccc",
"0x6ab9b5fc357c793cc5339398016c28ea21f0be4d56a68b554a29766dd312eeeb",
"0xb8c4c9f1dec537f4c4652e6bf519bac768e85b2590b7683e392aab698b50d529",
"0x1cae4e7facb6883024359eb1e9212d36e68a106a7733dc1c099017a74e5f465a",
"0x616439c1379e10fc6ad2fa192a2e49c2d5a7155fdde95911f37fcfb75952fcb2",
"0x301ab0d5d5ada4bd2e859658fc31743b5a502b26bc9b8b162e5a161e21218048",
"0x9d2bc97fffd61659313009e67a4c729a10274f2af26914a53bc7af6717da211e",
"0x4bdcbe543f9ef7348855aac43d6b6286f9c0c7be53de8a1300bea1ba5ba0758e",
"0xb6631640d626ea9523ae619a42633072614326cc9220462dffdeb63e804ef05f",
"0xf19a76e33ca189a8682ece523c2afda138db575955b7af31a427c9b8adb41e15",
"0x221b43ad87d7410624842cad296fc48360b5bf4e835f6ff610db736774d2f2d3",
"0x297c51f4ff236db943bebeb35538e207c8de6330d26aa8138a9ca206f42154bf",
"0x129a0644f33b9ee4e9a36a11dd59d1dedc64012fbb7a79263d07f82d647ffba8",
"0x763794c2042b9c8381ac7c4d7f4d38b0abb38069b2810b522f87951f25d9d2be",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xee0639a2ada97368e8b3493f9d2141e16c3cd9fe54e13691bb7a2c376c56c7c8",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c",
"0xba2bc704559c23541f0c9efa0b522454e8cd06cd504d0e45724709cf5672640f",
"0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30",
"0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1",
"0xff857f4c17c9fb2e544e496685ebd8e2258c761e4636cfb031ba73a4430061c7",
"0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193",
"0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1",
"0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b",
"0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220",
"0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f",
"0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e",
"0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784",
"0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x9300000000000000000000000000000000000000000000000000000000000000",
"0xd9ed050000000000000000000000000000000000000000000000000000000000",
"0xd824a89a9d5dd329a069b394ddc6618c70ed784982061959ac77d58d48e9d7c8",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0x8dc0b81ebe27fb91663a74713252d2eae5deb3b983374afc3c2d2f6b254128f1",
"0x09c7a863a253b4163a10916db2781eb35f58c016f0a24b3331ed84af3822c341"
]
}
````
## File: src/test/test-data/fullWithdrawalProof_Latest_28ETH.json
````json
{"slot":6397852,"validatorIndex":302913,"historicalSummaryIndex":146,"withdrawalIndex":0,"blockHeaderRootIndex":8092,"beaconStateRoot":"0xc9bc855bfd67190b654dc87cfe6945cdafa1d8bba66c3c2e81748f98c0c0e95b","slotRoot":"0x9c9f610000000000000000000000000000000000000000000000000000000000","timestampRoot":"0xb06fed6400000000000000000000000000000000000000000000000000000000","blockHeaderRoot":"0xbd83e79a38e454ece98a2f9e661792047984889db19234d11143322522e444fb","blockBodyRoot":"0x3e5c78192898e4f24aeceb181e58e4c5f390059ff6eebf0cbee6a096969a59a8","executionPayloadRoot":"0x8478de9992c86ab2617576c4b32267a2de50fd2bd35c8481a3d4a1731c0d5fe4","latestBlockHeaderRoot":"0x9ce41a41052e65ec6a0b3b021dc2f6f98f6060f22d70ac92ade6bcc3d4acca54","SlotProof":["0x89c5010000000000000000000000000000000000000000000000000000000000","0xab4a015ca78ff722e478d047b19650dc6fc92a4270c6cd34401523d3d6a1d9f2","0x1dfeede5317301d17b8e3ea54e055bd0567240cc27f76af3adc823ad7f6b6833"],"WithdrawalProof":["0xa3d843f57c18ee3dac0eb263e446fe5d0110059137807d3cae4a2e60ccca013f","0x87441da495942a4af734cbca4dbcf0b96b2d83137ce595c9f29495aae6a8d99e","0xae0dc609ecbfb26abc191227a76efb332aaea29725253756f2cad136ef5837a6","0x765bcd075991ecad96203020d1576fdb9b45b41dad3b5adde11263ab9f6f56b8","0x1000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x9d9b56c23faa6a8abf0a49105eb12bbdfdf198a9c6c616b8f24f6e91ad79de92","0xac5e32ea973e990d191039e91c7d9fd9830599b8208675f159c3df128228e729","0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"],"ValidatorProof":["0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e","0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee","0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1","0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1","0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac","0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537","0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38","0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748","0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a","0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c","0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b","0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e","0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee","0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9","0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74","0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3","0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f","0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6","0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528","0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76","0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f","0x846b080000000000000000000000000000000000000000000000000000000000","0x5a6c050000000000000000000000000000000000000000000000000000000000","0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e","0xce3d1b002aa817e3718132b7ffe6cea677f81b1b3b690b8052732d8e1a70d06b","0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b","0xf91f8669ffcb2c5c46ef5c23e2fe3308154469b55494145b26ca111c173e1184"],"TimestampProof":["0x28a2c80000000000000000000000000000000000000000000000000000000000","0xa749df3368741198702798435eea361b1b1946aa9456587a2be377c8472ea2df","0x251a5e0b34e9eb58a44a231cfb3e817e3cf3f82cde755dc5fe94cbe36e146b5b","0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"],"ExecutionPayloadProof":["0xb6a435ffd17014d1dad214ba466aaa7fba5aa247945d2c29fd53e90d554f4474","0x336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0e","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71","0x5ec9aaf0a3571602f4704d4471b9af564caf17e4d22a7c31017293cb95949053","0x0000000000000000000000000000000000000000000000000000000000000000","0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","0xc1f7cb289e44e9f711d7d7c05b67b84b8c6dd0394b688922a490cfd3fe216db1"],"ValidatorFields":["0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d","0x0100000000000000000000008e35f095545c56b07c942a4f3b055ef1ec4cb148","0x0040597307000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0xea65010000000000000000000000000000000000000000000000000000000000","0xf265010000000000000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000"],"WithdrawalFields":["0x45cee50000000000000000000000000000000000000000000000000000000000","0x419f040000000000000000000000000000000000000000000000000000000000","0x59b0d71688da01057c08e4c1baa8faa629819c2a000000000000000000000000","0xe5d9ef8406000000000000000000000000000000000000000000000000000000"],"StateRootAgainstLatestBlockHeaderProof":["0x0000000000000000000000000000000000000000000000000000000000000000","0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"],"HistoricalSummaryProof":["0x050b5923fe2e470849a7d467e4cbed4803d3a46d516b84552567976960ff4ccc","0x6ab9b5fc357c793cc5339398016c28ea21f0be4d56a68b554a29766dd312eeeb","0xb8c4c9f1dec537f4c4652e6bf519bac768e85b2590b7683e392aab698b50d529","0x1cae4e7facb6883024359eb1e9212d36e68a106a7733dc1c099017a74e5f465a","0x616439c1379e10fc6ad2fa192a2e49c2d5a7155fdde95911f37fcfb75952fcb2","0x301ab0d5d5ada4bd2e859658fc31743b5a502b26bc9b8b162e5a161e21218048","0x9d2bc97fffd61659313009e67a4c729a10274f2af26914a53bc7af6717da211e","0x4bdcbe543f9ef7348855aac43d6b6286f9c0c7be53de8a1300bea1ba5ba0758e","0xb6631640d626ea9523ae619a42633072614326cc9220462dffdeb63e804ef05f","0xf19a76e33ca189a8682ece523c2afda138db575955b7af31a427c9b8adb41e15","0x221b43ad87d7410624842cad296fc48360b5bf4e835f6ff610db736774d2f2d3","0x297c51f4ff236db943bebeb35538e207c8de6330d26aa8138a9ca206f42154bf","0x129a0644f33b9ee4e9a36a11dd59d1dedc64012fbb7a79263d07f82d647ffba8","0x763794c2042b9c8381ac7c4d7f4d38b0abb38069b2810b522f87951f25d9d2be","0x0000000000000000000000000000000000000000000000000000000000000000","0xee0639a2ada97368e8b3493f9d2141e16c3cd9fe54e13691bb7a2c376c56c7c8","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71","0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c","0xba2bc704559c23541f0c9efa0b522454e8cd06cd504d0e45724709cf5672640f","0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30","0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1","0xff857f4c17c9fb2e544e496685ebd8e2258c761e4636cfb031ba73a4430061c7","0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193","0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1","0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b","0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220","0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f","0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e","0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784","0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb","0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb","0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab","0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4","0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x9300000000000000000000000000000000000000000000000000000000000000","0xd9ed050000000000000000000000000000000000000000000000000000000000","0xd824a89a9d5dd329a069b394ddc6618c70ed784982061959ac77d58d48e9d7c8","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71","0x8dc0b81ebe27fb91663a74713252d2eae5deb3b983374afc3c2d2f6b254128f1","0x09c7a863a253b4163a10916db2781eb35f58c016f0a24b3331ed84af3822c341"]}
````
## File: src/test/test-data/fullWithdrawalProof_Latest.json
````json
{
"slot": 6397852,
"validatorIndex": 302913,
"historicalSummaryIndex": 146,
"withdrawalIndex": 0,
"blockHeaderRootIndex": 8092,
"beaconStateRoot": "0xe562b064fa5f17412bf13cd3b650f9d84b912f127c3f4c09879864d51a8b4daf",
"slotRoot": "0x9c9f610000000000000000000000000000000000000000000000000000000000",
"timestampRoot": "0xb06fed6400000000000000000000000000000000000000000000000000000000",
"blockHeaderRoot": "0x8b036996f94e940c80c5c692fd0e25467a5d55f1cf92b7808f92090fc7be1d17",
"executionPayloadRoot": "0xe628472355543b53917635e60c1f924f111f7a3cd58f2d947e8631b9d9924cb1",
"latestBlockHeaderRoot": "0xa81fa0ec796b5f84e6435745245f6d24279a11a74e29666560355507c441332d",
"SlotProof": [
"0x89c5010000000000000000000000000000000000000000000000000000000000",
"0xab4a015ca78ff722e478d047b19650dc6fc92a4270c6cd34401523d3d6a1d9f2",
"0xf4e65df697eb85f3ab176ac93b6ad4d96bd6b04bdddcc4f6c98f0fa94effc553"
],
"WithdrawalProof": [
"0xa3d843f57c18ee3dac0eb263e446fe5d0110059137807d3cae4a2e60ccca013f",
"0x87441da495942a4af734cbca4dbcf0b96b2d83137ce595c9f29495aae6a8d99e",
"0xae0dc609ecbfb26abc191227a76efb332aaea29725253756f2cad136ef5837a6",
"0x765bcd075991ecad96203020d1576fdb9b45b41dad3b5adde11263ab9f6f56b8",
"0x1000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x9d9b56c23faa6a8abf0a49105eb12bbdfdf198a9c6c616b8f24f6e91ad79de92",
"0xac5e32ea973e990d191039e91c7d9fd9830599b8208675f159c3df128228e729",
"0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"
],
"ValidatorProof": [
"0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e",
"0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee",
"0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1",
"0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1",
"0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac",
"0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537",
"0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38",
"0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748",
"0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a",
"0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c",
"0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b",
"0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e",
"0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee",
"0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9",
"0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74",
"0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3",
"0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f",
"0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6",
"0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528",
"0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x846b080000000000000000000000000000000000000000000000000000000000",
"0x5a6c050000000000000000000000000000000000000000000000000000000000",
"0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e",
"0xce3d1b002aa817e3718132b7ffe6cea677f81b1b3b690b8052732d8e1a70d06b",
"0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b",
"0xba25997a12576cc460c39cfc6cc87d7dc215b237a0f3c4dc8cb7473125b2e138"
],
"TimestampProof": [
"0x28a2c80000000000000000000000000000000000000000000000000000000000",
"0xa749df3368741198702798435eea361b1b1946aa9456587a2be377c8472ea2df",
"0xb1c03097a5a24f46cdb684df37e3ac0536a76428be1a6c6d6450378701ab1f3d",
"0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"
],
"ExecutionPayloadProof": [
"0xb6a435ffd17014d1dad214ba466aaa7fba5aa247945d2c29fd53e90d554f4474",
"0x336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0e",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0x5ec9aaf0a3571602f4704d4471b9af564caf17e4d22a7c31017293cb95949053",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0xc1f7cb289e44e9f711d7d7c05b67b84b8c6dd0394b688922a490cfd3fe216db1"
],
"ValidatorFields": [
"0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d",
"0x0100000000000000000000008e35f095545c56b07c942a4f3b055ef1ec4cb148",
"0x0040597307000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xea65010000000000000000000000000000000000000000000000000000000000",
"0xf265010000000000000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000"
],
"WithdrawalFields": [
"0x45cee50000000000000000000000000000000000000000000000000000000000",
"0x419f040000000000000000000000000000000000000000000000000000000000",
"0x59b0d71688da01057c08e4c1baa8faa629819c2a000000000000000000000000",
"0xe5015b7307000000000000000000000000000000000000000000000000000000"
],
"StateRootAgainstLatestBlockHeaderProof": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"
],
"HistoricalSummaryProof": [
"0x050b5923fe2e470849a7d467e4cbed4803d3a46d516b84552567976960ff4ccc",
"0x6ab9b5fc357c793cc5339398016c28ea21f0be4d56a68b554a29766dd312eeeb",
"0xb8c4c9f1dec537f4c4652e6bf519bac768e85b2590b7683e392aab698b50d529",
"0x1cae4e7facb6883024359eb1e9212d36e68a106a7733dc1c099017a74e5f465a",
"0x616439c1379e10fc6ad2fa192a2e49c2d5a7155fdde95911f37fcfb75952fcb2",
"0x301ab0d5d5ada4bd2e859658fc31743b5a502b26bc9b8b162e5a161e21218048",
"0x9d2bc97fffd61659313009e67a4c729a10274f2af26914a53bc7af6717da211e",
"0x4bdcbe543f9ef7348855aac43d6b6286f9c0c7be53de8a1300bea1ba5ba0758e",
"0xb6631640d626ea9523ae619a42633072614326cc9220462dffdeb63e804ef05f",
"0xf19a76e33ca189a8682ece523c2afda138db575955b7af31a427c9b8adb41e15",
"0x221b43ad87d7410624842cad296fc48360b5bf4e835f6ff610db736774d2f2d3",
"0x297c51f4ff236db943bebeb35538e207c8de6330d26aa8138a9ca206f42154bf",
"0x129a0644f33b9ee4e9a36a11dd59d1dedc64012fbb7a79263d07f82d647ffba8",
"0x763794c2042b9c8381ac7c4d7f4d38b0abb38069b2810b522f87951f25d9d2be",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xee0639a2ada97368e8b3493f9d2141e16c3cd9fe54e13691bb7a2c376c56c7c8",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c",
"0xba2bc704559c23541f0c9efa0b522454e8cd06cd504d0e45724709cf5672640f",
"0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30",
"0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1",
"0xff857f4c17c9fb2e544e496685ebd8e2258c761e4636cfb031ba73a4430061c7",
"0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193",
"0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1",
"0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b",
"0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220",
"0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f",
"0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e",
"0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784",
"0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x9300000000000000000000000000000000000000000000000000000000000000",
"0xd9ed050000000000000000000000000000000000000000000000000000000000",
"0xd824a89a9d5dd329a069b394ddc6618c70ed784982061959ac77d58d48e9d7c8",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0x8dc0b81ebe27fb91663a74713252d2eae5deb3b983374afc3c2d2f6b254128f1",
"0x09c7a863a253b4163a10916db2781eb35f58c016f0a24b3331ed84af3822c341"
]
}
````
## File: src/test/test-data/operators.json
````json
{
"numOperators": 15,
"operators": [
{
"Address": "0x364ea4241059a1880a289ccf6f3e730371e399c2",
"SecretKey": "26538123046512735896321680912735809321651",
"SField": "11819029207127053157019202363815717308230004305264870932201236865394129798528",
"RPoint": {
"X": "4151816577563894587001429602861434330710880584628236143409913584674951374486",
"Y": "14755168631248666267406561486877346692276498564690931377857101897349212004983"
},
"PubkeyG1": {
"X": "18260007818883133054078754218619977578772505796600400998181738095793040006897",
"Y": "3432351341799135763167709827653955074218841517684851694584291831827675065899"
},
"PubkeyG2": {
"X": {
"A0": "7873360006442301391509331169041141152067986239770361289399829888515098511089",
"A1": "4819287168576083437987399094513915926762558557241451705488044107651777333471"
},
"Y": {
"A0": "20134059591429338312012269666175785157377156721420899775744488735642013266015",
"A1": "5180163866630806529018451776671042574814236436776522234046604410442923698614"
}
}
},
{
"Address": "0x435a2d0799fd5b4f6576d43ddedbfb21eeffd2ed",
"SecretKey": "26538123046512735896321680912735809321652",
"SField": "6841827072565597045205937795412322299165267769171326199068981979305129605253",
"RPoint": {
"X": "12076439550529905259390228862926613756534481189623148993240145246002590976299",
"Y": "974155300103671376671957621510050067124810818694470470430352429136065997968"
},
"PubkeyG1": {
"X": "20885635058167472198503803626695880018252841373181906566024991517994827983735",
"Y": "11837569400448747146619236953090341530939723755586681794040848163257307322015"
},
"PubkeyG2": {
"X": {
"A0": "3533647070879242563419359582151690893240073815886344269869120254242419508633",
"A1": "9536422671122585613677052246081291259644057320757694709671580313420208694081"
},
"Y": {
"A0": "12166486073145345231362662138062912830359628001285426518990520911876160470477",
"A1": "14962020261456080798629037722818062638639517674733500632912397408761625050802"
}
}
},
{
"Address": "0x523e0a2d296e6bc969296a0541289077d2c43246",
"SecretKey": "26538123046512735896321680912735809321653",
"SField": "6251323578278497984001410170708026455838289583399486055694150917867937589622",
"RPoint": {
"X": "17659335174238407859449322441269022710538670095928217612186082625191038704952",
"Y": "4030658631342963783788534863066598739941334059525264488801360888493204491332"
},
"PubkeyG1": {
"X": "2470846831436625524441718675125184646728308675024947867896488186792002263384",
"Y": "19398539713444487906856323737585450565673486119324566923739712072838048985181"
},
"PubkeyG2": {
"X": {
"A0": "18996698705977687383601958641379064195173448823300612508806962257981430138470",
"A1": "11390594476086232146697028608851603084418668227205561974107759331535935107531"
},
"Y": {
"A0": "10549563568039161023384174670373065438958654243218116252293848148484855972510",
"A1": "8392609726364984764069457200587817879819857139319066962985233014328835122137"
}
}
},
{
"Address": "0x618a412f2446921d3d942c8bf073000b97728f0f",
"SecretKey": "26538123046512735896321680912735809321654",
"SField": "12061860258274605985652162007024956030804911220114181564759813442108765736942",
"RPoint": {
"X": "7145152916403894254971893757626878750640430645346094737434568772482788991869",
"Y": "5571984232385193687234211544970704499791824743626734155736946702800034693686"
},
"PubkeyG1": {
"X": "12541368988415564232975583436986866293473603658145394466756402945376729450760",
"Y": "8511301244073806918313939726706790376744658017957805706622009852760184010910"
},
"PubkeyG2": {
"X": {
"A0": "12496796444461928801423998788871427472219126045995885876471223766691196263411",
"A1": "20900414356950550650632823116879453189806004631615934725359633772870431548864"
},
"Y": {
"A0": "1850282599163866918562002476972618618179924465313771842342246796868912815924",
"A1": "16267494054236543151546820684638619209010244733864385924658421853804576220788"
}
}
},
{
"Address": "0x70130e87da895ef840a9f8c41c6ebf0e5b48e824",
"SecretKey": "26538123046512735896321680912735809321655",
"SField": "18246936953360371302513647065102080917080836724122762820676034131128284534276",
"RPoint": {
"X": "14485348008548227125800493276491803315994326328485845085024767746874209352205",
"Y": "8354185564983387451413630826006944059627584517454500268893837380556264839426"
},
"PubkeyG1": {
"X": "12250988429106632987382001675218698439738318866960678150521924430133077466824",
"Y": "19721708441253375174926671177088389244933199376153285627796664290007532195052"
},
"PubkeyG2": {
"X": {
"A0": "15382764368631216635417307340007437259725116431665265535534236291634253279005",
"A1": "551113638605800186370795409845956026924016364384585878450724593710757152783"
},
"Y": {
"A0": "15798745520528389465130300542056950632504309474787848602575280595042835040946",
"A1": "15121681895121404713757090493706231060528428365493560188175592305488154660061"
}
}
},
{
"Address": "0x7290f60d6666d7be7a41de178216502342562996",
"SecretKey": "26538123046512735896321680912735809321656",
"SField": "18341272017139583935816150534955335402336824558387991233830658246285519689014",
"RPoint": {
"X": "18174419042266772640587019436747343741444240929289718199821728520031148396624",
"Y": "12944489167977623909526722715453960134323253038442885147323841789720380485266"
},
"PubkeyG1": {
"X": "21781696011906991536752467363456044926552139424863730238878701223169542126809",
"Y": "12365056838489612884478362448314510672391017658276612399537174676963477032436"
},
"PubkeyG2": {
"X": {
"A0": "7602713822854229266858239462850154602893490834036446886716499673780465081437",
"A1": "6755904671274545514612841103024355730484086518739594420309025281657878144069"
},
"Y": {
"A0": "19169973646980957540631709981893659820049084634788201359252419558688034251538",
"A1": "3617096885662895416635037761438216245027220053803903379077414864234110502059"
}
}
},
{
"Address": "0x8d8e96b3bb658c5830aef90760f79e6a2a3bba41",
"SecretKey": "26538123046512735896321680912735809321657",
"SField": "12685816762097293544054119736761928362052971499853660676176419762479313483888",
"RPoint": {
"X": "1845886565323083117137480765392173031492903457678364169791939158881472188429",
"Y": "13621394807982841470261073338436747381963203491878788814653645637014444568601"
},
"PubkeyG1": {
"X": "3212013732044844428966711160610869391929177932647992063562413834101261373977",
"Y": "15448314027915072279969465354772467136408027411697025021594849451692520530947"
},
"PubkeyG2": {
"X": {
"A0": "19758878765410158466580323802894117259849215806855698140995410808900358199475",
"A1": "6427725156456990179277796366756163887223625521472017190816516318004039813324"
},
"Y": {
"A0": "20996191524580276458284414610581896434480007527247384059381015445157582556532",
"A1": "14948621227766242224388430084744009250457802204821770762677931043984889219820"
}
}
},
{
"Address": "0xa71e43be339f9791235641f457c1ba2da86b9eb3",
"SecretKey": "26538123046512735896321680912735809321658",
"SField": "6042114504025969519448864857463886717993192687698758790871627541311475539318",
"RPoint": {
"X": "2992138105299859744840964524791160337229571931324293630805658245249757407389",
"Y": "15694736019044294046047702836262370586371888280839741425891501314654607706201"
},
"PubkeyG1": {
"X": "3667070400945503213289817934609709506016134089772708044008927730500827726299",
"Y": "201965492781810222622603023497133228304861253112953784295749598350720860919"
},
"PubkeyG2": {
"X": {
"A0": "18480475432118436773114589869805528330114616937670136874673576408735271448220",
"A1": "13748787029936230407330641361791178403853002576716058275674223802004495813919"
},
"Y": {
"A0": "9393505688889091760306178659573951040934743290746925629508348356081460722985",
"A1": "8911229656394169335558484911670452902836715586054327693497766770926601776864"
}
}
},
{
"Address": "0xca9827a614f6c910c33f7668823f9d079515a6b2",
"SecretKey": "26538123046512735896321680912735809321659",
"SField": "4089036516179531739857184481801462737502679331172598056405360493929192933822",
"RPoint": {
"X": "11344456565890051191982811922569789285426097564838757420328232921661131972594",
"Y": "5214697287561608797059987511134212052053543960787141328755773567687921715591"
},
"PubkeyG1": {
"X": "10603532593425799461116428547718401984650331452231439217583617357908466102307",
"Y": "9038166977605721429955182587619307974711670991162762169137768185433368026502"
},
"PubkeyG2": {
"X": {
"A0": "2665943891698121544094051819956376909984132954889653928721123177413544545101",
"A1": "5067301718610021746809469955529953474259097833119436258748751618113087224295"
},
"Y": {
"A0": "14618865954382816111109898027326987227550002588645252687730089534591097604721",
"A1": "17668847581520475585017930293964643097348730464667967915604118496715335354459"
}
}
},
{
"Address": "0xdda9a89ddf845c9ee5d2810e0833b4a71d66179d",
"SecretKey": "265381230465127358963216809127358093216510",
"SField": "3494626234115151042064050025070425206800280651375775826757871600988046500493",
"RPoint": {
"X": "2823775573240572793382269280921054559755380883233834426161454792596668870337",
"Y": "2116482807847046775754020075448309148535809630987701014560424524130609194680"
},
"PubkeyG1": {
"X": "9580940219508129828900474539321198495544615416706116943528153675304087866935",
"Y": "19817948430483568579408524674460630359968264374260052173849319927990972424409"
},
"PubkeyG2": {
"X": {
"A0": "14146902041950849992065142134350889699589625292784530382058790130731064171060",
"A1": "9643173139567873327873151464253356980578492901466026171946322545605998333728"
},
"Y": {
"A0": "14418997116441129782787372775003831815699951211438552978754436263436048789176",
"A1": "7555011997614448462339727638657930440787456831044184868102463346992814218544"
}
}
},
{
"Address": "0xdde7a5dbb45d0f48d74d40772c2dda8acd89a304",
"SecretKey": "265381230465127358963216809127358093216511",
"SField": "19331236222084214974477469493395354532285940046992732403697375380156732141073",
"RPoint": {
"X": "18634736052925172105067492946541508040225080612748659668517401756117768987613",
"Y": "5852722109239017950047732236711408776617783536462429224588732414658414009409"
},
"PubkeyG1": {
"X": "3369815046108753886283376619953242018939084016540366266511463175341760902943",
"Y": "17472522035733702939562892574753748100276735367509783930154400656979558647072"
},
"PubkeyG2": {
"X": {
"A0": "12828974775248479668694052242644004695064721299497890085252948770085462880539",
"A1": "7272600703570139069540236030139742611695513123908572460354778844049179248665"
},
"Y": {
"A0": "6187997634621416381645973736825134854588734099638054268199535544372796244575",
"A1": "3168611351805563509285790274061051556477979031759511765104889273283967743401"
}
}
},
{
"Address": "0xe3fe83a83f3c7dc625382f062c159809551c580b",
"SecretKey": "265381230465127358963216809127358093216512",
"SField": "5508482612796623641475745691306343682701589066262691055533968657240534344204",
"RPoint": {
"X": "21685944819341458246191809126376074155122173268146956524878025759785902438148",
"Y": "14479737406316720196422920923227363552037206020376032776793814881581188057870"
},
"PubkeyG1": {
"X": "8070743655895930899886894874393947088184525509008178391331533769906913292208",
"Y": "6166041079211541340267466423045787629866667719971895616570343665150421540281"
},
"PubkeyG2": {
"X": {
"A0": "2678990229866359665311344452542118509908675024979420599004546926969138331011",
"A1": "2049191090024360768595356041854174484396277833846595743282145840232552333594"
},
"Y": {
"A0": "3201866794780851807317614975829248389859676087140523014123159053900573945553",
"A1": "6264970183638030024425633356945995283617627028846078571269515762464600802251"
}
}
},
{
"Address": "0x7e32bdd5ba50eb8d5e5bd7d3eef1e8bb7f0177f0",
"SecretKey": "265381230465127358963216809127358093216513",
"SField": "8232703131218555943214969386347851175399186201805966768750414520371803575150",
"RPoint": {
"X": "19242125080553338551396473264815502757575266047308561121276377861187981762008",
"Y": "6218373850521459529400329711936878862028424392534186108921910846882020330028"
},
"PubkeyG1": {
"X": "3330687773238963481566047234114671341321386178661830488834900006935732354709",
"Y": "1586247746639542071922023676451514656089805747478597321110273768112339015566"
},
"PubkeyG2": {
"X": {
"A0": "17613195447478151801754685924971955231494961653094677584006576833833181220337",
"A1": "8248149727711895523076824904570372119241450169122068550957488380013460422052"
},
"Y": {
"A0": "4921860114926042695859196707091513960894135032516354392120636579531786968732",
"A1": "6497674699404552189948059719808314151701318782389032881121361153353612847519"
}
}
},
{
"Address": "0x90a89f27673e9b99001d25c5328ea7b248341aee",
"SecretKey": "265381230465127358963216809127358093216514",
"SField": "4235478359213410732582392105113225989686712574423827420889537931357288127804",
"RPoint": {
"X": "20654778647025427886329231590146081813406215959318690452465157767589910386387",
"Y": "16434595237030862998502345611891947093280350242989172641995260786504200724952"
},
"PubkeyG1": {
"X": "20354234858398388849816932240956270026965879865370684564771582015555136553997",
"Y": "19793685024108212849441227006186442648468124186906966567382667532342323504884"
},
"PubkeyG2": {
"X": {
"A0": "2406250413451469352424770691776336657864578839352351617783440321552775726788",
"A1": "17191001754715829242845993906554922262830769871842065626630924785145928353563"
},
"Y": {
"A0": "9681366579801251558910069516165884865963240333390287489273262916032696422128",
"A1": "16915387601971985830991163871960501726831560513559637189932888959407309364443"
}
}
},
{
"Address": "0x8d26afa6847f837d05c1bad6e9f87c7ac1be1566",
"SecretKey": "265381230465127358963216809127358093216515",
"SField": "11035850218432072216042138989371348095984008884653111551948778896223502226130",
"RPoint": {
"X": "17221699912650368882632599262801462631856236460154101376095314228983311857640",
"Y": "16920232814901492682413294000319977483527313089896426400855455024070752739456"
},
"PubkeyG1": {
"X": "3168995773972892355858790410620399201792411842283506602925392425152373993765",
"Y": "3166501588898491583880629288124229291720446936681364855672031758818970837581"
},
"PubkeyG2": {
"X": {
"A0": "1061540865081686461127583443728867079739536618424919508446137160337220400469",
"A1": "14314019642545413347565949557134146221646346593211321744274793627527703796333"
},
"Y": {
"A0": "5285717062790019556696539193484956838225490023146378618961849297769407066983",
"A1": "14567857613796427090761122799009706481422522677880753883893727561643094726547"
}
}
}
]
}
````
## File: src/test/test-data/owners.json
````json
{
"numOwners": 10,
"owners": [
{
"Address": "0x364ea4241059a1880a289ccf6f3e730371e399c2"
},
{
"Address": "0x164dfa4241059a1880a289ccf6f3e730371e399c2"
},
{
"Address": "0x264ea4241059a1880a289ccf6f3e730371e399c4"
},
{
"Address": "0x464ea4241059a1880a289ccf6f3e730371e399c5"
},
{
"Address": "0x564ea4241059a1880a289ccf6f3e730371e399c6"
},
{
"Address": "0x864ea4241059a1880a289ccf6f3e730371e399c7"
},
{
"Address": "0x344ea4241059a1880a289ccf6f3e730371e399c8"
},
{
"Address": "0x304ea4241059a1880a289ccf6f3e730371e399c9"
},
{
"Address": "0x364ea4241059a1880a289ccf6f3e730371e399d4"
},
{
"Address": "0x364ea4241059a1880a289ccf6f3e730371e39955"
}
]
}
````
## File: src/test/test-data/partialWithdrawalProof_Latest.json
````json
{
"slot": 6397852,
"validatorIndex": 302913,
"historicalSummaryIndex": 146,
"withdrawalIndex": 0,
"blockHeaderRootIndex": 8092,
"beaconStateRoot": "0xc4ea7f435356d2de29784712dc1fb5597d7d36ec705ddebcbef8bdc4cb4ecaf0",
"slotRoot": "0x9c9f610000000000000000000000000000000000000000000000000000000000",
"timestampRoot": "0xb06fed6400000000000000000000000000000000000000000000000000000000",
"blockHeaderRoot": "0x8d9698a822c4f57d890f9465c046ae8799301ff01daca55fb2e8e347d547ec6d",
"executionPayloadRoot": "0xf6e0315572785590a6ee69adab31e5c1e9879264aeead5b7762a384ae7d938d0",
"latestBlockHeaderRoot": "0x34e6f6ae9370c1eacc38aad8c5a887983979b79a35c57f09570afd80a879fd69",
"SlotProof": [
"0x89c5010000000000000000000000000000000000000000000000000000000000",
"0xab4a015ca78ff722e478d047b19650dc6fc92a4270c6cd34401523d3d6a1d9f2",
"0xe01b2b111680b6b01268c23ee3d7d4d145e98809dd5894cc8d1944bdc19e346e"
],
"WithdrawalProof": [
"0xa3d843f57c18ee3dac0eb263e446fe5d0110059137807d3cae4a2e60ccca013f",
"0x87441da495942a4af734cbca4dbcf0b96b2d83137ce595c9f29495aae6a8d99e",
"0xae0dc609ecbfb26abc191227a76efb332aaea29725253756f2cad136ef5837a6",
"0x765bcd075991ecad96203020d1576fdb9b45b41dad3b5adde11263ab9f6f56b8",
"0x1000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x9d9b56c23faa6a8abf0a49105eb12bbdfdf198a9c6c616b8f24f6e91ad79de92",
"0xac5e32ea973e990d191039e91c7d9fd9830599b8208675f159c3df128228e729",
"0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"
],
"ValidatorProof": [
"0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e",
"0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee",
"0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1",
"0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1",
"0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac",
"0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537",
"0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38",
"0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748",
"0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a",
"0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c",
"0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b",
"0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e",
"0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee",
"0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9",
"0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74",
"0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3",
"0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f",
"0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6",
"0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528",
"0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x846b080000000000000000000000000000000000000000000000000000000000",
"0x5a6c050000000000000000000000000000000000000000000000000000000000",
"0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e",
"0xce3d1b002aa817e3718132b7ffe6cea677f81b1b3b690b8052732d8e1a70d06b",
"0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b",
"0x673e299c82563e700195d7d66981bdf521b9609a3b6d22be78f069b359e553e5"
],
"TimestampProof": [
"0x28a2c80000000000000000000000000000000000000000000000000000000000",
"0xa749df3368741198702798435eea361b1b1946aa9456587a2be377c8472ea2df",
"0x568268c732e7471701bcaedcae302de5fe87b60cf9e6518696719f9732078b97",
"0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"
],
"ExecutionPayloadProof": [
"0xb6a435ffd17014d1dad214ba466aaa7fba5aa247945d2c29fd53e90d554f4474",
"0x336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0e",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0x5ec9aaf0a3571602f4704d4471b9af564caf17e4d22a7c31017293cb95949053",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0xc1f7cb289e44e9f711d7d7c05b67b84b8c6dd0394b688922a490cfd3fe216db1"
],
"ValidatorFields": [
"0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d",
"0x0100000000000000000000008e35f095545c56b07c942a4f3b055ef1ec4cb148",
"0x0040597307000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xea65010000000000000000000000000000000000000000000000000000000000",
"0xf265010000000000000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000"
],
"WithdrawalFields": [
"0x45cee50000000000000000000000000000000000000000000000000000000000",
"0x419f040000000000000000000000000000000000000000000000000000000000",
"0x59b0d71688da01057c08e4c1baa8faa629819c2a000000000000000000000000",
"0xbd56200000000000000000000000000000000000000000000000000000000000"
],
"StateRootAgainstLatestBlockHeaderProof": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"
],
"HistoricalSummaryProof": [
"0x050b5923fe2e470849a7d467e4cbed4803d3a46d516b84552567976960ff4ccc",
"0x6ab9b5fc357c793cc5339398016c28ea21f0be4d56a68b554a29766dd312eeeb",
"0xb8c4c9f1dec537f4c4652e6bf519bac768e85b2590b7683e392aab698b50d529",
"0x1cae4e7facb6883024359eb1e9212d36e68a106a7733dc1c099017a74e5f465a",
"0x616439c1379e10fc6ad2fa192a2e49c2d5a7155fdde95911f37fcfb75952fcb2",
"0x301ab0d5d5ada4bd2e859658fc31743b5a502b26bc9b8b162e5a161e21218048",
"0x9d2bc97fffd61659313009e67a4c729a10274f2af26914a53bc7af6717da211e",
"0x4bdcbe543f9ef7348855aac43d6b6286f9c0c7be53de8a1300bea1ba5ba0758e",
"0xb6631640d626ea9523ae619a42633072614326cc9220462dffdeb63e804ef05f",
"0xf19a76e33ca189a8682ece523c2afda138db575955b7af31a427c9b8adb41e15",
"0x221b43ad87d7410624842cad296fc48360b5bf4e835f6ff610db736774d2f2d3",
"0x297c51f4ff236db943bebeb35538e207c8de6330d26aa8138a9ca206f42154bf",
"0x129a0644f33b9ee4e9a36a11dd59d1dedc64012fbb7a79263d07f82d647ffba8",
"0x763794c2042b9c8381ac7c4d7f4d38b0abb38069b2810b522f87951f25d9d2be",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xee0639a2ada97368e8b3493f9d2141e16c3cd9fe54e13691bb7a2c376c56c7c8",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c",
"0xba2bc704559c23541f0c9efa0b522454e8cd06cd504d0e45724709cf5672640f",
"0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30",
"0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1",
"0xff857f4c17c9fb2e544e496685ebd8e2258c761e4636cfb031ba73a4430061c7",
"0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193",
"0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1",
"0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b",
"0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220",
"0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f",
"0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e",
"0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784",
"0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x9300000000000000000000000000000000000000000000000000000000000000",
"0xd9ed050000000000000000000000000000000000000000000000000000000000",
"0xd824a89a9d5dd329a069b394ddc6618c70ed784982061959ac77d58d48e9d7c8",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71",
"0x8dc0b81ebe27fb91663a74713252d2eae5deb3b983374afc3c2d2f6b254128f1",
"0xe237bc62b6b5269da5f4093c292d0f3bf2cf4d2eb93b4f366dba675c4df9cc62"
]
}
````
## File: src/test/test-data/reputedOwners.json
````json
{
"numOwners": 10,
"owners": [
{
"Address": "0x364ea4241059a1880a289ccf6f3e730371e399c2"
},
{
"Address": "0x164dfa4241059a1880a289ccf6f3e730371e399c2"
},
{
"Address": "0x264ea4241059a1880a289ccf6f3e730371e399c4"
},
{
"Address": "0x464ea4241059a1880a289ccf6f3e730371e399c5"
},
{
"Address": "0x564ea4241059a1880a289ccf6f3e730371e399c6"
},
{
"Address": "0x864ea4241059a1880a289ccf6f3e730371e399c7"
},
{
"Address": "0x344ea4241059a1880a289ccf6f3e730371e399c8"
},
{
"Address": "0x304ea4241059a1880a289ccf6f3e730371e399c9"
},
{
"Address": "0x364ea4241059a1880a289ccf6f3e730371e399d4"
},
{
"Address": "0x364ea4241059a1880a289ccf6f3e730371e39955"
}
]
}
````
## File: src/test/test-data/withdrawal_credential_proof_302913_30ETHBalance.json
````json
{"validatorIndex":302913,"beaconStateRoot":"0x9761c69195aeb853adc72f41b00232e0dccdfaaf5828bc1db56416eb1d4396df","balanceRoot":"0x6cba5d7307000000e56d25fc0600000008bd5d7307000000239e5d7307000000","latestBlockHeaderRoot":"0xbc2f3b78cf1d4d5b47d6cc987864860035088de6316b4eb1ccbe8677f31e98e1","ValidatorBalanceProof":["0x26805d73070000002aa55d7307000000218e5d730700000056d35d7307000000","0x24846a54724508d8394a9e30f819bc19fe6727745be4f19e1e3924b985b4b8e9","0x48fb4849c31d88b413f650c17bd55bb11fe38bcef5d234dba8532737ae78f776","0x0d7d38f90a04ef49d15ca10af0e818e227134e4b46c7b22dd10e149f2a2bc612","0xdd345235f28f92719c39c9a5042b91f814d535ae5df574c0f6cd38f77791fc2b","0x0855928643f4210d9d8d0986847674139be32ebf3db984b6d81f3ed136475b6d","0x28460d51c1ce75299f8c0dff8bca9f498fef0edc57c343bbd76592b3d6b984a3","0x068a141709053afb48698290176068f448867cba9c6ff10f986b1d02691f6e70","0x9f0958275fe6ea80b1825bcd11907d1076600719ce574ae2a609e51a975dde54","0xd43fbb18b30449f5c81148848fd6579b4f6cebfe66187bd1c36a293b22085fc2","0x7bf0aac502053da7dfd5356960db63ab48d3cc6ee5f36f41639eadacd34ac272","0x866e3b213d02f1cb86149c2472789ab9d7fb3fb52371a893dc8b6f8936ebd0c1","0xcec7cc4b82fb8dd889257128af3e4433700fcc0fb0505b431aa5f592111e785b","0xdd08667067baa43e2b41b793464a591a29643b5582c8fc9c2c045f984c5848d7","0x842ec80062a4f2f7e9d80ab408374ae42817acf5036aff40b3ade345ab196090","0xfe1ae22b8ba21fce84b25f5da3e37a91db49158b7025d6371d5a54c03adba125","0x8c039a81d68952077db2cc11713ae6f5a9ecc5ebe859c0612ff7215c0ccd7aba","0x5415d3199f1da31287d646d03c2691728396d87f7ef52c142596301e27a343ca","0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4","0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0x846b080000000000000000000000000000000000000000000000000000000000","0x7d46d32db9a97974916a2114aeffc11fc031a954b2110742267054a438055c1a","0x81e1234eb27473f180f890c98019ecb7655a3e58105c7182d8daf726d75aaf8d","0x738a1fb07361c872216ae655df9a1690d465e6698acd1c9230ebc3fd89412c69","0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b","0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"],"WithdrawalCredentialProof":["0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e","0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee","0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1","0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1","0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac","0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537","0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38","0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748","0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a","0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c","0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b","0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e","0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee","0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9","0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74","0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3","0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f","0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6","0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528","0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76","0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f","0x846b080000000000000000000000000000000000000000000000000000000000","0x5a6c050000000000000000000000000000000000000000000000000000000000","0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e","0x8efea8a7f00e6411ae6187a6d9a2c22ad033c0ad749ce0940e5bf2fd76ac35c7","0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b","0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"],"ValidatorFields":["0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d","0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443","0xe56d25fc06000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0xea65010000000000000000000000000000000000000000000000000000000000","0xf265010000000000000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000"],"StateRootAgainstLatestBlockHeaderProof":["0x0000000000000000000000000000000000000000000000000000000000000000","0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"]}
````
## File: src/test/test-data/withdrawal_credential_proof_302913_exited.json
````json
{
"validatorIndex": 302913,
"beaconStateRoot": "0xd6c32aa93c58fd5ca1f36246234878ec6b0c36b2c218d8bd2521b4c4f2f8cd23",
"balanceRoot": "0x6cba5d7307000000000000000000000008bd5d7307000000239e5d7307000000",
"latestBlockHeaderRoot": "0xc8fd8d84d59b2fe5e331e72466e237ba5014d9f078b0d65271aa00d2689fec9b",
"ValidatorBalanceProof": [
"0x26805d73070000002aa55d7307000000218e5d730700000056d35d7307000000",
"0x24846a54724508d8394a9e30f819bc19fe6727745be4f19e1e3924b985b4b8e9",
"0x48fb4849c31d88b413f650c17bd55bb11fe38bcef5d234dba8532737ae78f776",
"0x0d7d38f90a04ef49d15ca10af0e818e227134e4b46c7b22dd10e149f2a2bc612",
"0xdd345235f28f92719c39c9a5042b91f814d535ae5df574c0f6cd38f77791fc2b",
"0x0855928643f4210d9d8d0986847674139be32ebf3db984b6d81f3ed136475b6d",
"0x28460d51c1ce75299f8c0dff8bca9f498fef0edc57c343bbd76592b3d6b984a3",
"0x068a141709053afb48698290176068f448867cba9c6ff10f986b1d02691f6e70",
"0x9f0958275fe6ea80b1825bcd11907d1076600719ce574ae2a609e51a975dde54",
"0xd43fbb18b30449f5c81148848fd6579b4f6cebfe66187bd1c36a293b22085fc2",
"0x7bf0aac502053da7dfd5356960db63ab48d3cc6ee5f36f41639eadacd34ac272",
"0x866e3b213d02f1cb86149c2472789ab9d7fb3fb52371a893dc8b6f8936ebd0c1",
"0xcec7cc4b82fb8dd889257128af3e4433700fcc0fb0505b431aa5f592111e785b",
"0xdd08667067baa43e2b41b793464a591a29643b5582c8fc9c2c045f984c5848d7",
"0x842ec80062a4f2f7e9d80ab408374ae42817acf5036aff40b3ade345ab196090",
"0xfe1ae22b8ba21fce84b25f5da3e37a91db49158b7025d6371d5a54c03adba125",
"0x8c039a81d68952077db2cc11713ae6f5a9ecc5ebe859c0612ff7215c0ccd7aba",
"0x5415d3199f1da31287d646d03c2691728396d87f7ef52c142596301e27a343ca",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0x846b080000000000000000000000000000000000000000000000000000000000",
"0x7d46d32db9a97974916a2114aeffc11fc031a954b2110742267054a438055c1a",
"0x81e1234eb27473f180f890c98019ecb7655a3e58105c7182d8daf726d75aaf8d",
"0xa015eda3f293610cf5763cc878497fe3fbdb20309c1ba004f384b467a11db327",
"0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b",
"0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"
],
"WithdrawalCredentialProof": [
"0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e",
"0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee",
"0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1",
"0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1",
"0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac",
"0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537",
"0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38",
"0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748",
"0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a",
"0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c",
"0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b",
"0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e",
"0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee",
"0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9",
"0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74",
"0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3",
"0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f",
"0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6",
"0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528",
"0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x846b080000000000000000000000000000000000000000000000000000000000",
"0x5a6c050000000000000000000000000000000000000000000000000000000000",
"0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e",
"0xd740083592ddcf0930ad47cc8e7758d567c2dc63767624bda4d50cafcbfe7093",
"0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b",
"0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"
],
"ValidatorFields": [
"0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d",
"0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xea65010000000000000000000000000000000000000000000000000000000000",
"0xf265010000000000000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000"
],
"StateRootAgainstLatestBlockHeaderProof": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"
]
}
````
## File: src/test/test-data/withdrawal_credential_proof_302913.json
````json
{
"validatorIndex": 302913,
"beaconStateRoot": "0x4678e21381463879490925e8911492b890c0ce7ba347cce57fd8d4e0bb27f04a",
"balanceRoot": "0x6cba5d7307000000e5015b730700000008bd5d7307000000239e5d7307000000",
"latestBlockHeaderRoot": "0x859816e76b0944c3110a31f26acc301718122e1acfe8fb161df9c0e684515593",
"ValidatorBalanceProof": [
"0x26805d73070000002aa55d7307000000218e5d730700000056d35d7307000000",
"0x24846a54724508d8394a9e30f819bc19fe6727745be4f19e1e3924b985b4b8e9",
"0x48fb4849c31d88b413f650c17bd55bb11fe38bcef5d234dba8532737ae78f776",
"0x0d7d38f90a04ef49d15ca10af0e818e227134e4b46c7b22dd10e149f2a2bc612",
"0xdd345235f28f92719c39c9a5042b91f814d535ae5df574c0f6cd38f77791fc2b",
"0x0855928643f4210d9d8d0986847674139be32ebf3db984b6d81f3ed136475b6d",
"0x28460d51c1ce75299f8c0dff8bca9f498fef0edc57c343bbd76592b3d6b984a3",
"0x068a141709053afb48698290176068f448867cba9c6ff10f986b1d02691f6e70",
"0x9f0958275fe6ea80b1825bcd11907d1076600719ce574ae2a609e51a975dde54",
"0xd43fbb18b30449f5c81148848fd6579b4f6cebfe66187bd1c36a293b22085fc2",
"0x7bf0aac502053da7dfd5356960db63ab48d3cc6ee5f36f41639eadacd34ac272",
"0x866e3b213d02f1cb86149c2472789ab9d7fb3fb52371a893dc8b6f8936ebd0c1",
"0xcec7cc4b82fb8dd889257128af3e4433700fcc0fb0505b431aa5f592111e785b",
"0xdd08667067baa43e2b41b793464a591a29643b5582c8fc9c2c045f984c5848d7",
"0x842ec80062a4f2f7e9d80ab408374ae42817acf5036aff40b3ade345ab196090",
"0xfe1ae22b8ba21fce84b25f5da3e37a91db49158b7025d6371d5a54c03adba125",
"0x8c039a81d68952077db2cc11713ae6f5a9ecc5ebe859c0612ff7215c0ccd7aba",
"0x5415d3199f1da31287d646d03c2691728396d87f7ef52c142596301e27a343ca",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0x846b080000000000000000000000000000000000000000000000000000000000",
"0x7d46d32db9a97974916a2114aeffc11fc031a954b2110742267054a438055c1a",
"0x81e1234eb27473f180f890c98019ecb7655a3e58105c7182d8daf726d75aaf8d",
"0xa158c9a519bf3c8d2296ebb6a2df54283c7014e7b84524da0574c95fe600a1fd",
"0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b",
"0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"
],
"WithdrawalCredentialProof": [
"0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e",
"0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee",
"0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1",
"0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1",
"0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac",
"0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537",
"0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38",
"0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748",
"0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a",
"0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c",
"0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b",
"0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e",
"0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee",
"0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9",
"0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74",
"0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3",
"0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f",
"0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6",
"0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528",
"0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x846b080000000000000000000000000000000000000000000000000000000000",
"0x5a6c050000000000000000000000000000000000000000000000000000000000",
"0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e",
"0x3f918c09ea95d520538a81f767de0f1be3f30fc9d599805c68048ef5c23a85e2",
"0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b",
"0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"
],
"ValidatorFields": [
"0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d",
"0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443",
"0xe5015b7307000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xea65010000000000000000000000000000000000000000000000000000000000",
"0xf265010000000000000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000"
],
"StateRootAgainstLatestBlockHeaderProof": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"
]
}
````
## File: src/test/test-data/withdrawal_credential_proof_510257.json
````json
{
"validatorIndex": 510257,
"beaconStateRoot": "0xffa8d20fba11c926a5b8d9ea4b0738c1fe22d1981177b39cd6a86ed3669702fe",
"balanceRoot": "0x0000000000000000e5015b730700000000000000000000000000000000000000",
"latestBlockHeaderRoot": "0x359b8fc7ea96256c42c38ec047e0052217c7889bbb46cf928fa75fbe2a718e0b",
"ValidatorBalanceProof": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b",
"0x6dd4ffb4ca5aa522f9dbe4fc9a0e07d26c61f8d309590c36f6ba2f2e55144e3a",
"0x46af8bc21a2d3401a9d413f03c93ecb0b29d5746307f71dc7118bad7dd46ca3f",
"0x07b3dda614c6ceb1364c74c9128707e339167bc2875217db87c3169083e60937",
"0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30",
"0x0368e4efe7928a54c6ef32ec3447b168e69db5ab597f6f5d9ee09528a85e36cb",
"0xd0073758e6864c0b3e519a78a765091a5b42652268f7c7146d3c6bdf33676601",
"0xddc34931fd2c88c485af67a69b31fe118013f42b48ce81a349f47fb329b2b381",
"0x8905afda3b52ca75fed67a6fd8342b2bf7bb1f3bfb8246b5b8730097ca547b1c",
"0x9ac6965d7809be3640d5bae20323a5c726a181dd50f05406c547f2392ce3e25a",
"0x2a397932c520e69909074a21d4e367678c47b34d5c132c0b868b51df7d4b0b53",
"0xb896ba3596c19cb4f1b98b79fe8234299f164762055b7114968b480c4cb0f9bb",
"0x36adfe560682ca3335a39ba90eb021c0daf74b32a908203bc66bc9d680819914",
"0x5537955b317ce653fc15a7d7a8e0b4e2733d6e2f630f3404437751251aa51bea",
"0x6dece44e9d5964318fd5c7b458afe405bbfbb2dc79eb12fe87e65d101da20eb5",
"0x8c039a81d68952077db2cc11713ae6f5a9ecc5ebe859c0612ff7215c0ccd7aba",
"0x5415d3199f1da31287d646d03c2691728396d87f7ef52c142596301e27a343ca",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0x846b080000000000000000000000000000000000000000000000000000000000",
"0x7d46d32db9a97974916a2114aeffc11fc031a954b2110742267054a438055c1a",
"0x81e1234eb27473f180f890c98019ecb7655a3e58105c7182d8daf726d75aaf8d",
"0xa383de4eac63dd7869d3ef08c5aca0ed832a5fac0603bcc8b5db37297e617ec5",
"0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b",
"0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"
],
"WithdrawalCredentialProof": [
"0x1a7c6cc33e5d44fd12622c350a89467af1dde63b878aada6d92122e7af62be18",
"0xeb50c3b01c1ec2a90e7ac51a1472633125fb9cfa1c516dc3a10fa61d10d16726",
"0x25da66081ff6b20267ab5cfb4301bc4935b03c09138a0ce56b9e6a476292a64f",
"0xe73b742215086105ee3509e12da84334de7c522c8fcfbe0bd506ce9f037c49aa",
"0x9951384987ef65d5135e31779d1752ca0a0a907b49f9c040d967b82eac01824a",
"0x8f03ed90c033494704345311ee036253524e6a63d90de1320abe06a1dc097d6c",
"0x3de8f72a828bf9a4e0105c96b705b983b6acf2a36d18f016089ce2ac2e912377",
"0x96cad999eeb82a853ebc7b0240b613c4ad3dc1e4d24bf53820bc99a42fe1b323",
"0x2d9c9feea67401140c6361f569491a9a34a67ac4579c48b27ae1f91692daa9ff",
"0x55c251dcae98b63839b0986e3724c9840529b10af62f1256b2f124380008035d",
"0xe3a61aed717bccf4def4d98d1e548e62bd6a1199d26b7fd90149738e2e91c82f",
"0x265ae7041099b690f20ffe0ead1172a213740bf99c6dbd82d239a82ef0d5e97d",
"0x2e5ac328c7b8dfd5f0be9ed5311a060a3daa85f0f2b9ac750981233bfc69c1b6",
"0xd53273ba4b65d46cefdcf4042a55aba80a1d68bad67dffceb26c1074597e84c9",
"0x1257580608fe43f8118c73bcb20bc4556e5034249fd68035b9a84e0e18c09010",
"0x71d6e5c378dfd648434c3cd6426d35b5751ae595ff66e76de95b38f4e2c5c08d",
"0xbce1ff41e2ec18651cd3dec46adefc600f19fc2237853cbcc9f9e8cba4767d65",
"0xfb2b2dc8116aec79c89d6a3bf4631e1d34a531c81e24af8dca50652a39af0fa6",
"0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528",
"0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0x846b080000000000000000000000000000000000000000000000000000000000",
"0x5a6c050000000000000000000000000000000000000000000000000000000000",
"0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e",
"0xca8a23905f2088e7d8a9a6d631d265b45c2e839137ac66c86bf80456bbebddba",
"0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b",
"0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"
],
"ValidatorFields": [
"0xcc3045318f8e5b6ec9441a54183cacde7f180509086f92868ba8942c92a79ca4",
"0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443",
"0xe5015b7307000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x6cde020000000000000000000000000000000000000000000000000000000000",
"0x74de020000000000000000000000000000000000000000000000000000000000",
"0xf708030000000000000000000000000000000000000000000000000000000000",
"0xf709030000000000000000000000000000000000000000000000000000000000"
],
"StateRootAgainstLatestBlockHeaderProof": [
"0x5e83fd9f12a10bd0aa14d9300f245cdc5ffaabec780453c1638eaf6177dd64f1",
"0x336f092b490e7794f3f013536c0fb0b40195fff09689811c1a0887b3bfbc10ae",
"0xde1c709ab0e3d3fb72c4e5805bb81fae2b2d55c5da32ed1c0b995e7b5793b930"
]
}
````
## File: src/test/test-data/withdrawalCredentialAndBalanceProof_61068.json
````json
{
"validatorIndex": 61068,
"beaconStateRoot": "0x9022cd2f5102866cce67dc86f3190719cbf0ff0aff080cab6e975bfa823cc04b",
"balanceRoot": "0xe5015b7307000000e5015b7307000000e5015b7307000000e5015b7307000000",
"latestBlockHeaderRoot": "0xb70c51b37b01fe8c02ab81152374651196b471a0ef71ffefe858ea2b19ea7d06",
"ValidatorBalanceProof": [
"0xe5015b73070000003972637307000000e5015b7307000000e5015b7307000000",
"0x0e633cd1b96df1d8901b2a4499fdde621b96a8d34aac9a21048cb23cd26accb1",
"0xe30fb84408d69db1528b91db81ae4cfa0af2e8c67db3ff3887f13df381909240",
"0x718981def04d64a67b1d26315dfa303c92256ad272085a8b1e80a9df83474a9d",
"0xf67e708bb7d3936a1997e5e5c68a386a22b4bfe1906f00e9a545f0334867fb97",
"0x9a5c59195425b461a16df891952f6da6d37b05b9283117c414476e56e10def3a",
"0x9f78503c130eefa10ab9e4a56b114add6b5f755fe736306e169af8be8f1a48ad",
"0x78c3b05556e88eedf00401d25b1e320a2c2fbe8acc872dbab422fb1a8d44355a",
"0xf7ae7fc6688d03a0b16b38010e3d30b80fcfcdbdc479e7ac663b77458f978fe9",
"0x27bac8758154fa8ebcbfa158c2f85947738f8143ecebdf4fcb62eb03a8f2ae17",
"0x08fd4f5d0a1221d230b03e76ba37ff155b87e73082e22d65022cd81854e1c40d",
"0x42fe25019f71e593e06bdc1553d9b7e925aef3922863181029068f75c196d019",
"0x73c806b68d570e08e2a113a93eeac34f71dbb2e99c6469a2172254e4fd3b239a",
"0xb0ff98aa3c254d96da2ed42c3e951f47683679d980759e7f9e5dacd26c95dacb",
"0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784",
"0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xc0f1000000000000000000000000000000000000000000000000000000000000",
"0xa2b940dcdbbb8e6a9940e94ac5cc90a8ee18d7549882d4f3b26406a8770db1df",
"0x1c9f4ae53f01f807ea9c3e70da4793f791e170473ecf849d8eabf644323370c9",
"0x2aef1c6b5ca45b9b7b2ea972fee379327de7e35e10e41d3f1756787d1824b29f",
"0x760dd5d4f9e7552bb44ba3cabfc37f7736406b83787878afc9c1788e7b7bbf6c",
"0xe7467e5a471e40f7969ed8b81a4f5d77af173320e89f2ba7379bd2db7a63cfd6"
],
"WithdrawalCredentialProof": [
"0xba087de35dc4b429ea39111a751664750e61e65c2c036c3ebf4d86e7e5de9340",
"0xf3bfbc0caa1f8cb89df8a1a04bc18af14714102748965d52e1542b33c94b8ba2",
"0x0c5a3c12daec8ad96ad6b635dd816b4c4252853260fc5dac3fce29f012f347d8",
"0x2fd3aa6c99cc05e9fa3473f7cb0b7d737f051134e96be2cebd206825ce636ea2",
"0x01da838685987c0be55ff085d903f8676048139f2e0aec20d426e336af670449",
"0x4e4e743ed6cf9c4028e37808bdc202b6cc2f4eecb0f330002c4e41ce181f1def",
"0xe25e6e55a2bcc935cfc225be988e42253421dc184b1384ceac85fa92228e9782",
"0xf63336cf1d3b1172dad5513e209d459da726843898d7fcc8b4d2d99e502f2866",
"0xae999ce50a65a88755ae12cba3616b368eba8f246c263d8154aa0aaa530c9064",
"0x34706d3d48c0d6e24af619c8acd756da09af05236982caee98aef440242d08c1",
"0xa61ed5b762a431e9641811d11fd9aa7219d47d4bfd803b7087e95390852f214a",
"0xbd5c30fa2319470f928f91102b7fcf53bba4bb8ca4a38de53429efe7d00b0cde",
"0xc655c6a25140072224c98e8b8963fab37d8eb51655265018f62d0860a4d58ad3",
"0xf00f90e9671a1decbdf18b1ad2247fd9192f1be793818c2c303ee607f43876f0",
"0xe5ae53bbdf410ecdecbbf58c7b3a4f9ee0885535ef95a3d5a34b69b32da31c07",
"0x6c2d2ac0c0cbe5b9a09cfaf42b1f2ea0a23d2a46e40473a837bc6725fb94c432",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0xc0f1000000000000000000000000000000000000000000000000000000000000",
"0x8f03000000000000000000000000000000000000000000000000000000000000",
"0x655ba28a360c2ba8b28ec4ab0f0cc329d0be63fc010e624850f4eb4c6e4e5f2c",
"0x4257d5a9fa3f4382614a49c599fcf10a01a18d4fd9b81a0af588a2e38a1611af",
"0x760dd5d4f9e7552bb44ba3cabfc37f7736406b83787878afc9c1788e7b7bbf6c",
"0xe7467e5a471e40f7969ed8b81a4f5d77af173320e89f2ba7379bd2db7a63cfd6"
],
"ValidatorFields": [
"0xe827e0052f9bbd6b5eec2bc2bc5b1e83208b30feca87ae04388819c83321c183",
"0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443",
"0x0040597307000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xb201000000000000000000000000000000000000000000000000000000000000",
"0xc901000000000000000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000",
"0xffffffffffffffff000000000000000000000000000000000000000000000000"
],
"LatestBlockHeaderProof": [
"0xa787e90f294d84c8f175b8decee03b95aef0bd5c25ff0dc306500fdb537affd5",
"0x81ee8c21a9d7b67ee31ff8b2f4aeb8c08e02758a6270e5f52445eb1cc5bc12f1",
"0x0616a07f6e3bf6aa5e42faaf52627f4ff40a4b8e300b2cec71ed5c0fff0f643e",
"0x740363449ce964e54c36c8ace68b3d3e1b37f7a4099643255db1e8b4bb9e69a0",
"0xe7467e5a471e40f7969ed8b81a4f5d77af173320e89f2ba7379bd2db7a63cfd6"
]
}
````
## File: src/test/test-data/withdrawalCredentialAndBalanceProof_61336.json
````json
{
"validatorIndex": 61336,
"beaconStateRoot": "0x040086cbfc077b7c4461001a5d59e9cc62603db076a63b9a746d5e8320accf4f",
"balanceRoot": "0xe5015b7307000000000000000000000000000000000000000000000000000000",
"latestBlockHeaderRoot": "0xb70c51b37b01fe8c02ab81152374651196b471a0ef71ffefe858ea2b19ea7d06",
"ValidatorBalanceProof": [
"0xc834937207000000c834937207000000c83493720700000048c4927207000000",
"0xb78a61531f51cb2ec7dd495212bd2d6c7ef34d2e7e58d3561bef92901f179715",
"0x3b21ec6a4c8b5b904a453f6f7477095895fe64bc6941a3cc69ec9f60bfb8585e",
"0x635421906b61e81b6b8c9be44cfaf116892c1b4f141cbd6dbbe0a265ac76566f",
"0xced97d3fa19fa6b43cf3e214e63ec8d6b27027ac89129073416b86abd1887330",
"0x343d88e386646d5f9c6bfed46c27e224b3f536e56dbb580cf643b49dc45580d5",
"0xe335f3934d72a2f6e79b32a2b1695a76c8c037cfc46b8e81b2a77d730de62427",
"0x78c3b05556e88eedf00401d25b1e320a2c2fbe8acc872dbab422fb1a8d44355a",
"0xf7ae7fc6688d03a0b16b38010e3d30b80fcfcdbdc479e7ac663b77458f978fe9",
"0x27bac8758154fa8ebcbfa158c2f85947738f8143ecebdf4fcb62eb03a8f2ae17",
"0x08fd4f5d0a1221d230b03e76ba37ff155b87e73082e22d65022cd81854e1c40d",
"0x42fe25019f71e593e06bdc1553d9b7e925aef3922863181029068f75c196d019",
"0x73c806b68d570e08e2a113a93eeac34f71dbb2e99c6469a2172254e4fd3b239a",
"0xb0ff98aa3c254d96da2ed42c3e951f47683679d980759e7f9e5dacd26c95dacb",
"0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784",
"0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xc0f1000000000000000000000000000000000000000000000000000000000000",
"0xa2b940dcdbbb8e6a9940e94ac5cc90a8ee18d7549882d4f3b26406a8770db1df",
"0x1c9f4ae53f01f807ea9c3e70da4793f791e170473ecf849d8eabf644323370c9",
"0xfc4c6267c598928f75ed9a6093b17f3d4a803520ae75403dfe052ae422d0dd75",
"0x760dd5d4f9e7552bb44ba3cabfc37f7736406b83787878afc9c1788e7b7bbf6c",
"0xe7467e5a471e40f7969ed8b81a4f5d77af173320e89f2ba7379bd2db7a63cfd6"
],
"WithdrawalCredentialProof": [
"0x7b0e44f08bd17b0d0b03dc34533e4a32a2b97104a0fbec66260b629b7b7782fc",
"0xfd4057403be1b96a41b63c9ac4a06b4c64954ad128a8c8b3a52b3dcb96d122a8",
"0xcb9960e19d11ac9070baf26a31f1873e0d37385f27e552714f0cf3b4b216fe46",
"0xce8a034702a0986ac89f1fff7167f615950a83abf321842d424594537f4fd274",
"0x0990c5bf1d6f66b49861d8bdb6b68aec978b11f58fe98f6dbfa9809230717a2a",
"0xf0bf5c561e6f031cf01b6790362ae58746c1f31be4db59269d14de4806505d79",
"0xbd02a0782b3639b6b6a0f012fad51f9c63c46e4e5d8456e8e847c1affa1052f9",
"0x430dd7834cf49b572dc5245c2c120037477a43f9b410675acfa7fc32a7d4649f",
"0xfec85caf431b0850a8c2fb58f522c43670ca089e65bc5c4ae38f59487bd98880",
"0x34706d3d48c0d6e24af619c8acd756da09af05236982caee98aef440242d08c1",
"0xa61ed5b762a431e9641811d11fd9aa7219d47d4bfd803b7087e95390852f214a",
"0xbd5c30fa2319470f928f91102b7fcf53bba4bb8ca4a38de53429efe7d00b0cde",
"0xc655c6a25140072224c98e8b8963fab37d8eb51655265018f62d0860a4d58ad3",
"0xf00f90e9671a1decbdf18b1ad2247fd9192f1be793818c2c303ee607f43876f0",
"0xe5ae53bbdf410ecdecbbf58c7b3a4f9ee0885535ef95a3d5a34b69b32da31c07",
"0x6c2d2ac0c0cbe5b9a09cfaf42b1f2ea0a23d2a46e40473a837bc6725fb94c432",
"0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb",
"0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab",
"0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4",
"0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f",
"0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa",
"0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c",
"0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167",
"0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7",
"0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0",
"0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544",
"0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765",
"0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4",
"0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1",
"0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636",
"0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c",
"0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7",
"0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff",
"0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5",
"0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d",
"0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c",
"0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327",
"0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74",
"0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76",
"0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f",
"0xc0f1000000000000000000000000000000000000000000000000000000000000",
"0x8f03000000000000000000000000000000000000000000000000000000000000",
"0x655ba28a360c2ba8b28ec4ab0f0cc329d0be63fc010e624850f4eb4c6e4e5f2c",
"0x1dbd2f10de6e10eaa0f945d5c106e1c0352e752f28165f04e831711772043f8d",
"0x760dd5d4f9e7552bb44ba3cabfc37f7736406b83787878afc9c1788e7b7bbf6c",
"0xe7467e5a471e40f7969ed8b81a4f5d77af173320e89f2ba7379bd2db7a63cfd6"
],
"ValidatorFields": [
"0x2c58c7f513dab2de353f008ddaf054749e80709b8ec1f397011773c7b29cd950",
"0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443",
"0xe5015b7307000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xb301000000000000000000000000000000000000000000000000000000000000",
"0x0c02000000000000000000000000000000000000000000000000000000000000",
"0x6603000000000000000000000000000000000000000000000000000000000000",
"0x6604000000000000000000000000000000000000000000000000000000000000"
],
"LatestBlockHeaderProof": [
"0xa787e90f294d84c8f175b8decee03b95aef0bd5c25ff0dc306500fdb537affd5",
"0x81ee8c21a9d7b67ee31ff8b2f4aeb8c08e02758a6270e5f52445eb1cc5bc12f1",
"0x0616a07f6e3bf6aa5e42faaf52627f4ff40a4b8e300b2cec71ed5c0fff0f643e",
"0x2df72afcad3355b9ea6965e94642ab99a49f8ba3e8abea7bf49bc594c2c48a90",
"0xe7467e5a471e40f7969ed8b81a4f5d77af173320e89f2ba7379bd2db7a63cfd6"
]
}
````
## File: src/test/token/bEIGEN.t.sol
````
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "../harnesses/EigenHarness.sol";
import "../../contracts/token/BackingEigen.sol";
contract bEIGENTest is Test {
mapping(address => bool) fuzzedOutAddresses;
address initialOwner = 0xbb00DDa2832850a43840A3A86515E3Fe226865F2;
address minterToSet = address(500);
address mintTo = address(12_345);
ProxyAdmin proxyAdmin;
EigenHarness eigenImpl;
Eigen eigen;
BackingEigen bEIGENImpl;
BackingEigen bEIGEN;
function setUp() public {
vm.startPrank(initialOwner);
proxyAdmin = new ProxyAdmin();
// deploy proxies
eigen = Eigen(address(new TransparentUpgradeableProxy(address(proxyAdmin), address(proxyAdmin), "")));
bEIGEN = BackingEigen(address(new TransparentUpgradeableProxy(address(proxyAdmin), address(proxyAdmin), "")));
// deploy impls
eigenImpl = new EigenHarness(IERC20(address(bEIGEN)));
bEIGENImpl = new BackingEigen(IERC20(address(eigen)));
// upgrade proxies
proxyAdmin.upgrade(ITransparentUpgradeableProxy(payable(address(eigen))), address(eigenImpl));
proxyAdmin.upgrade(ITransparentUpgradeableProxy(payable(address(bEIGEN))), address(bEIGENImpl));
vm.stopPrank();
}
function test_Initialize() public {
bEIGEN.initialize(initialOwner);
// check that the owner is initialOwner
assertEq(bEIGEN.owner(), initialOwner);
// check the transfer restrictions are disabled after one year in the future
assertEq(bEIGEN.transferRestrictionsDisabledAfter(), type(uint).max);
}
function testFuzz_CanBackTheEigenToken(uint eigenSupply) public {
StdCheats.deal(address(eigen), address(this), eigenSupply);
bEIGEN.initialize(initialOwner);
// check that the total supply of bEIGEN is equal to the total supply of EIGEN
assertEq(bEIGEN.totalSupply(), eigen.totalSupply());
assertEq(bEIGEN.balanceOf(address(eigen)), bEIGEN.totalSupply());
}
function test_setIsMinterAndMint() public {
bEIGEN.initialize(initialOwner);
vm.prank(initialOwner);
bEIGEN.setIsMinter(minterToSet, true);
require(bEIGEN.isMinter(minterToSet), "minter not set correctly");
uint amountToMint = 5e25;
uint balanceBefore = bEIGEN.balanceOf(mintTo);
vm.prank(minterToSet);
bEIGEN.mint(mintTo, amountToMint);
uint balanceAfter = bEIGEN.balanceOf(mintTo);
uint balanceDiff = balanceAfter - balanceBefore;
assertEq(balanceDiff, amountToMint, "mint not working correctly");
}
function test_setIsMinter_revertsWhenNotCalledByOwner() public {
bEIGEN.initialize(initialOwner);
vm.prank(mintTo);
vm.expectRevert("Ownable: caller is not the owner");
bEIGEN.setIsMinter(minterToSet, true);
}
function test_burn() public {
test_setIsMinterAndMint();
vm.prank(initialOwner);
bEIGEN.setAllowedFrom(mintTo, true);
uint amountToBurn = 1005e18;
uint balanceBefore = bEIGEN.balanceOf(mintTo);
vm.prank(mintTo);
bEIGEN.burn(amountToBurn);
uint balanceAfter = bEIGEN.balanceOf(mintTo);
uint balanceDiff = balanceBefore - balanceAfter;
assertEq(balanceDiff, amountToBurn, "mint not working correctly");
}
function test_mint_revertsWhenNotCalledByMinter() public {
test_setIsMinterAndMint();
uint amountToMint = 5e25;
vm.expectRevert("BackingEigen.mint: caller is not a minter");
bEIGEN.mint(mintTo, amountToMint);
}
}
````
## File: src/test/token/EigenTransferRestrictions.t.sol
````
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol";
import "../harnesses/EigenHarness.sol";
contract EigenTransferRestrictionsTest is Test {
mapping(address => bool) fuzzedOutAddresses;
address minter1 = 0xbb00DDa2832850a43840A3A86515E3Fe226865F2;
address minter2 = 0x87787389BB2Eb2EC8Fe4aA6a2e33D671d925A60f;
ProxyAdmin proxyAdmin;
EigenHarness eigenImpl;
Eigen eigen;
uint totalSupply = 1.67e9 ether;
// EVENTS FROM EIGEN.sol
/// @notice event emitted when the allowedFrom status of an address is set
event SetAllowedFrom(address indexed from, bool isAllowedFrom);
/// @notice event emitted when the allowedTo status of an address is set
event SetAllowedTo(address indexed to, bool isAllowedTo);
/// @notice event emitted when a minter mints
event Mint(address indexed minter, uint amount);
/// @notice event emitted when the transfer restrictions are disabled
event TransferRestrictionsDisabled();
modifier filterAddress(address fuzzedAddress) {
vm.assume(!fuzzedOutAddresses[fuzzedAddress]);
_;
}
function setUp() public {
vm.startPrank(minter1);
proxyAdmin = new ProxyAdmin();
// initialize with dummy BackingEigen address
eigenImpl =
new EigenHarness(new ERC20PresetFixedSupply({name: "bEIGEN", symbol: "bEIGEN", initialSupply: totalSupply, owner: minter1}));
eigen = Eigen(address(new TransparentUpgradeableProxy(address(eigenImpl), address(proxyAdmin), "")));
eigen.bEIGEN().transfer(address(eigen), totalSupply);
vm.stopPrank();
fuzzedOutAddresses[minter1] = true;
fuzzedOutAddresses[minter2] = true;
fuzzedOutAddresses[address(proxyAdmin)] = true;
fuzzedOutAddresses[address(eigen)] = true;
fuzzedOutAddresses[address(0)] = true;
}
function test_AllowedFromCanSendAnywhere(address to) public filterAddress(to) {
_simulateMint();
// minter1 and eigenminter2 are already allowedFrom
vm.startPrank(minter1);
eigen.transfer(to, eigen.balanceOf(minter1) / 2);
vm.startPrank(minter2);
eigen.transfer(to, eigen.balanceOf(minter2) / 2);
}
function test_CanSetAllowedFrom(address from, address to) public filterAddress(from) filterAddress(to) {
_simulateMint();
// set allowedFrom[from] = true
vm.startPrank(minter1);
vm.expectEmit(true, true, true, true, address(eigen));
emit SetAllowedFrom(from, true);
eigen.setAllowedFrom(from, true);
assertEq(eigen.allowedFrom(from), true, "EigenTest.test_CanSetAllowedFrom: allowedFrom was not set correctly");
// new allowedFrom can send
vm.startPrank(from);
eigen.transfer(to, eigen.balanceOf(from) / 2);
// set allowedFrom[from] = false
vm.startPrank(minter1);
vm.expectEmit(true, true, true, true, address(eigen));
emit SetAllowedFrom(from, false);
eigen.setAllowedFrom(from, false);
assertEq(eigen.allowedFrom(from), false, "EigenTest.test_CanSetAllowedFrom: allowedFrom was not set correctly");
}
function test_OnlyOwnerCanSetAllowedFrom(address notOwner) public filterAddress(notOwner) {
_simulateMint();
// set allowedFrom[from] = true
vm.startPrank(notOwner);
vm.expectRevert("Ownable: caller is not the owner");
eigen.setAllowedFrom(notOwner, true);
}
function test_NotAllowedFromCannotSendIfNoAllowedTos(address from, address to) public filterAddress(from) filterAddress(to) {
_simulateMint();
// send other tokens from minter1
vm.startPrank(minter1);
eigen.transfer(from, eigen.balanceOf(minter1) / 2);
// sending from other will revert
vm.startPrank(from);
uint fromBalance = eigen.balanceOf(from);
vm.expectRevert("Eigen._beforeTokenTransfer: from or to must be whitelisted");
eigen.transfer(to, fromBalance / 2);
}
function test_CanSetAllowedTo(address from, address to) public filterAddress(from) filterAddress(to) {
_simulateMint();
// set allowedFrom[from] = true
vm.startPrank(minter1);
vm.expectEmit(true, true, true, true, address(eigen));
emit SetAllowedTo(to, true);
eigen.setAllowedTo(to, true);
assertEq(eigen.allowedTo(to), true, "EigenTest.test_CanSetAllowedTo: allowedTo was not set correctly");
// new allowedFrom can send
vm.startPrank(from);
eigen.transfer(to, eigen.balanceOf(to) / 2);
// set allowedFrom[from] = false
vm.startPrank(minter1);
vm.expectEmit(true, true, true, true, address(eigen));
emit SetAllowedTo(to, false);
eigen.setAllowedTo(to, false);
assertEq(eigen.allowedTo(to), false, "EigenTest.test_CanSetAllowedTo: allowedTo was not set correctly");
}
function test_OnlyOwnerCanSetAllowedTo(address notOwner) public filterAddress(notOwner) {
_simulateMint();
// set allowedFrom[from] = true
vm.startPrank(notOwner);
vm.expectRevert("Ownable: caller is not the owner");
eigen.setAllowedTo(notOwner, true);
}
function test_disableTransferRestrictions(address from, address to) public filterAddress(from) filterAddress(to) {
_simulateMint();
vm.startPrank(minter1);
eigen.transfer(from, eigen.balanceOf(minter1) / 2);
// transfer will revert
vm.startPrank(from);
uint fromBalance = eigen.balanceOf(from);
vm.expectRevert("Eigen._beforeTokenTransfer: from or to must be whitelisted");
eigen.transfer(to, fromBalance / 2);
assertEq(eigen.transferRestrictionsDisabledAfter(), type(uint).max, "invalid test setup");
// set transfer restrictions to be disabled after one year in the future
vm.startPrank(minter1);
vm.expectEmit(true, true, true, true, address(eigen));
emit TransferRestrictionsDisabled();
eigen.disableTransferRestrictions();
assertEq(
eigen.transferRestrictionsDisabledAfter(),
0,
"EigenTest.test_disableTransferRestrictions: transfer restrictions were not disabled correctly"
);
vm.startPrank(from);
// transfer restrictions are disabled
assertTrue(eigen.transfer(to, fromBalance / 2), "transfer returned false");
}
function _simulateMint() internal {
// dummy mint
EigenHarness(address(eigen)).mint(minter1, totalSupply / 2);
EigenHarness(address(eigen)).mint(minter2, totalSupply / 2);
// set allowed froms
EigenHarness(address(eigen)).setAllowedFromPermissionless(minter1, true);
EigenHarness(address(eigen)).setAllowedFromPermissionless(minter2, true);
// set transfer restrictions to be disabled after to max
EigenHarness(address(eigen)).setTransferRestrictionsDisabledAfterToMax();
// set owner to minter1
EigenHarness(address(eigen)).transferOwnershipPermissionless(minter1);
}
}
````
## File: src/test/token/EigenWrapping.t.sol
````
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "../harnesses/EigenHarness.sol";
import "../../contracts/token/BackingEigen.sol";
contract EigenWrappingTests is Test {
mapping(address => bool) fuzzedOutAddresses;
address minter1 = 0xbb00DDa2832850a43840A3A86515E3Fe226865F2;
address minter2 = 0x87787389BB2Eb2EC8Fe4aA6a2e33D671d925A60f;
ProxyAdmin proxyAdmin;
EigenHarness eigenImpl;
Eigen eigen;
BackingEigen bEIGENImpl;
BackingEigen bEIGEN;
uint totalSupply = 1.67e9 ether;
// EVENTS FROM EIGEN.sol
/// @notice event emitted when the allowedFrom status of an address is set
event SetAllowedFrom(address indexed from, bool isAllowedFrom);
/// @notice event emitted when the allowedTo status of an address is set
event SetAllowedTo(address indexed to, bool isAllowedTo);
/// @notice event emitted when a minter mints
event Mint(address indexed minter, uint amount);
/// @notice event emitted when the transfer restrictions are disabled
event TransferRestrictionsDisabled();
modifier filterAddress(address fuzzedAddress) {
vm.assume(!fuzzedOutAddresses[fuzzedAddress]);
_;
}
function setUp() public {
vm.startPrank(minter1);
proxyAdmin = new ProxyAdmin();
// deploy proxies
eigen = Eigen(address(new TransparentUpgradeableProxy(address(proxyAdmin), address(proxyAdmin), "")));
bEIGEN = BackingEigen(address(new TransparentUpgradeableProxy(address(proxyAdmin), address(proxyAdmin), "")));
// deploy impls
eigenImpl = new EigenHarness(IERC20(address(bEIGEN)));
bEIGENImpl = new BackingEigen(IERC20(address(eigen)));
// upgrade proxies
proxyAdmin.upgrade(ITransparentUpgradeableProxy(payable(address(eigen))), address(eigenImpl));
proxyAdmin.upgrade(ITransparentUpgradeableProxy(payable(address(bEIGEN))), address(bEIGENImpl));
vm.stopPrank();
fuzzedOutAddresses[minter1] = true;
fuzzedOutAddresses[minter2] = true;
fuzzedOutAddresses[address(proxyAdmin)] = true;
fuzzedOutAddresses[address(eigen)] = true;
fuzzedOutAddresses[address(bEIGEN)] = true;
fuzzedOutAddresses[address(0)] = true;
}
function test_AnyoneCanUnwrap(address unwrapper, uint unwrapAmount) public filterAddress(unwrapper) {
vm.assume(unwrapper != address(0));
_simulateMint();
_simulateBackingAndSetTransferRestrictions();
// minter1 balance
uint minter1Balance = eigen.balanceOf(minter1);
// send EIGEN to unwrapper
vm.prank(minter1);
eigen.transfer(unwrapper, minter1Balance);
// initial bEIGEN balance
uint initialBEIGENBalanceOfEigenToken = bEIGEN.balanceOf(address(eigen));
// initial EIGEN token supply
assertEq(eigen.totalSupply(), bEIGEN.totalSupply(), "eigen totalSupply changed incorrectly");
// unwrap
// unwrap amount should be less than minter1 balance
unwrapAmount = unwrapAmount % minter1Balance;
vm.prank(unwrapper);
eigen.unwrap(unwrapAmount);
// check total supply and balance changes
assertEq(eigen.totalSupply(), bEIGEN.totalSupply(), "eigen totalSupply changed incorrectly");
assertEq(
bEIGEN.balanceOf(address(eigen)),
initialBEIGENBalanceOfEigenToken - unwrapAmount,
"beigen balance of EIGEN tokens changed incorrectly"
);
assertEq(eigen.balanceOf(address(unwrapper)), minter1Balance - unwrapAmount);
assertEq(bEIGEN.balanceOf(address(unwrapper)), unwrapAmount);
}
function test_AnyoneCanWrap(address wrapper, uint wrapAmount) public filterAddress(wrapper) {
vm.assume(wrapper != address(0));
_simulateMint();
_simulateBackingAndSetTransferRestrictions();
// initial bEIGEN balance
uint initialBEIGENBalanceOfEigenToken = bEIGEN.balanceOf(address(eigen));
// minter1 balance
uint minter1Balance = eigen.balanceOf(minter1);
// unwrap
vm.startPrank(minter1);
eigen.unwrap(minter1Balance);
// send bEIGEN to wrapper
bEIGEN.transfer(wrapper, minter1Balance);
vm.stopPrank();
// initial EIGEN token supply
assertEq(eigen.totalSupply(), bEIGEN.totalSupply(), "eigen totalSupply changed incorrectly");
// wrap
// wrap amount should be less than minter1 balance
wrapAmount = wrapAmount % minter1Balance;
vm.startPrank(wrapper);
// approve bEIGEN
bEIGEN.approve(address(eigen), wrapAmount);
// wrap
eigen.wrap(wrapAmount);
vm.stopPrank();
// check total supply and balance changes
assertEq(eigen.totalSupply(), bEIGEN.totalSupply(), "eigen totalSupply changed incorrectly");
assertEq(bEIGEN.balanceOf(address(eigen)), initialBEIGENBalanceOfEigenToken - minter1Balance + wrapAmount);
assertEq(eigen.balanceOf(address(wrapper)), wrapAmount);
assertEq(bEIGEN.balanceOf(address(wrapper)), minter1Balance - wrapAmount);
}
function test_CannotUnwrapMoreThanBalance(address unwrapper, uint unwrapAmount) public filterAddress(unwrapper) {
_simulateMint();
_simulateBackingAndSetTransferRestrictions();
// unwrap amount should be less than minter1 balance
unwrapAmount = unwrapAmount % eigen.balanceOf(minter1);
// send EIGEN to unwrapper
vm.prank(minter1);
eigen.transfer(unwrapper, unwrapAmount);
// unwrap
vm.prank(unwrapper);
vm.expectRevert("ERC20: burn amount exceeds balance");
eigen.unwrap(unwrapAmount + 1);
}
function test_CannotWrapMoreThanBalance(address wrapper, uint wrapAmount) public filterAddress(wrapper) {
_simulateMint();
_simulateBackingAndSetTransferRestrictions();
// wrap amount should be less than minter1 balance
wrapAmount = wrapAmount % eigen.balanceOf(minter1);
// unwrap
vm.startPrank(minter1);
eigen.unwrap(wrapAmount);
bEIGEN.transfer(wrapper, wrapAmount);
vm.stopPrank();
// wrap
vm.startPrank(wrapper);
// approve bEIGEN
bEIGEN.approve(address(eigen), type(uint).max);
// send bEIGEN to wrapper
vm.expectRevert("ERC20: transfer amount exceeds balance");
eigen.wrap(wrapAmount + 1);
vm.stopPrank();
}
function _simulateMint() internal {
// dummy mint
EigenHarness(address(eigen)).mint(minter1, totalSupply / 2);
EigenHarness(address(eigen)).mint(minter2, totalSupply / 2);
// set allowed froms
EigenHarness(address(eigen)).setAllowedFromPermissionless(minter1, true);
EigenHarness(address(eigen)).setAllowedFromPermissionless(minter2, true);
// set transfer restrictions to be disabled after to max
EigenHarness(address(eigen)).setTransferRestrictionsDisabledAfterToMax();
// set owner to minter1
EigenHarness(address(eigen)).transferOwnershipPermissionless(minter1);
}
function _simulateBackingAndSetTransferRestrictions() internal {
bEIGEN.initialize(minter1);
vm.startPrank(minter1);
bEIGEN.setAllowedFrom(minter1, true);
vm.stopPrank();
}
}
````
## File: src/test/tree/AllocationManagerUnit.tree
````
.
├── AllocationManager Tree (**** denotes that integration tests are needed to fully validate path)
├── when setAllocationDelay is called by the operator
│ ├── given that the caller is not an operator in the delegationManager
│ │ └── it should revert
│ ├── given that the delay is set to 0
│ │ └── it should revert
│ ├── given that a previous delay is set and has passed
│ │ └── it should set the new delay to the previous delay
│ └── given the caller provides a valid delay
│ ├── given that a previous delay is set and has passed
│ │ └── it should set the new delay to the previous delay delay
│ ├── given that a previous delay is set and has not passed
│ │ └── it should should overwrite the previous pending delay with the new delay
│ └── it should set the pendingDelay, update the effectBlock, and emit an `AllocationDelaySetEvent`
├── when setAllocationDelay is called by the delegationManager
│ ├── given that the caller is not the delegationManager
│ │ └── it should revert
│ ├── given that the delay is set to 0
│ │ └── it should revert
│ ├── given that a previous delay is set and has passed
│ │ └── it should set the new delay to the previous delay
│ └── given the caller provides a valid delay
│ ├── given that a previous delay is set and has passed
│ │ └── it should set the new delay to the previous delay delay
│ ├── given that a previous delay is set and has not passed
│ │ └── it should should overwrite the previous pending delay with the new delay
│ └── it should set the pendingDelay, update the effectBlock, and emit an `AllocationDelaySetEvent`
├── when clearModificationQueue is called
│ ├── given that the length of the strategies and numToClear are not equal
│ │ └── it should revert
│ ├── given that the operator is registered in the delegationManager
│ │ └── it should revert
│ ├── given that there are no modifications OR numToClear is 0
│ │ └── no modifications should be cleared
│ └── it should loop through the modification queue and the numToClear
│ ├── given that the latest effect block has not been reached
│ │ └── it should break the loop
│ └── given that the latest effect block has been reached
│ ├── it should update the magnitude info to the currentMagnitude, with a pendingDiff of 0 and effectBlock of 0
│ ├── it should change the encumbered magnitude if the pendingDiff was less than 0
│ ├── it should emit an EncumberedmagnitudeUpdated event
│ ├── it should remove the modification from the queue
│ └── it should continue to pop off the modification queue if the size is greater than 0 and numToClear is less than numCompleted
├── given that modifyAllocations is called
│ ├── given that the allocation delay is not set for the msg.sender
│ │ └── it should revert
│ └── it should loop through the list of allocations
│ ├── given that the length of operator sets and magnitudes does not match
│ │ └── it should revert
│ ├── given that the operatorSets to allocate mags to do not exist in the AVSD
│ │ └── it should revert
│ ├── it should clear the modification queue for the given strategy for the type(uint16).max number of modifications
│ ├── given that the maximum magnitude does not equal the expected maximum magnitude
│ │ └── it should revert
│ └── it should loop through the list of operator sets for the allocation
│ ├── given that the pendingDiff is nonZero
│ │ └── it should revert
│ ├── given that that the magnitude delta is the same as the pendingDiff
│ │ └── it should revert
│ ├── given that the pendingDiff is less than 0 (this is a deallocation)
│ │ └── it should update the effect block in memory to be the operator deallocation delay
│ ├── given that the pendingDiff is greater than 0 (this is an allocation)
│ │ ├── it should update the effect timestamp in memory to be the operator allocation delay
│ │ └── it should increase the encumberedMagnitude in memory by the pendingDiff
│ ├── it should push to the modification queue
│ ├── it should update the magnitude info, encumbered magnitude, emit an event for encumbered magnitude
│ └── it should emit an AllocationUpdated Event
└── when slashOperator is called
├── given that the wads to slash is 0
│ └── it should revert
├── given that the wads to slash is greater than WAD
│ └── it should revert
├── given that the operator is not registered in the delegationManager
│ └── it should revert
├── given that the operator is deregistered from AVS for the given block
│ └── it should revert
└── it should loop through all slashing params
├── it should slash the current magnitude by wads to slash
├── given that there is a pending deallocation
│ ├── it should slash the pending diff
│ └── it should emit an event for AllocationUpdated with the original deallocation's effect timestamp
├── it should update the magnitude info, encumbered magnitude, emit an event for encumbered magnitude
├── it should decrease the operator's max magnitude
├── it should decrease the operators shares in the delegation manager
└── It should emit an AllocationUpdated event
````
## File: src/test/tree/DelegationManagerUnit.tree
````
.
├── DelegationManager Tree (*** denotes that integration tests are needed to validate path)
├── when registerAsOperator is called
│ ├── given that the caller has already delegated to an operator
│ │ └── it should revert
│ ├── it should call `_setOperatorDetails`
│ │ └── it should emit an OperatorDetailsModified event
│ └── it should call `_delegate`
│ ├── given that delegation is paused
│ │ └── it should revert
│ ├── it should set the operator allocation delay in the AllocationManager contract
│ ├── it should set the operator delegated to itself and emit a StakerDelegated event
│ ├── given the caller has delegateable shares
│ │ └── it should increase the operator's shares and and emit an OperatorSharesIncreased event
│ └── it should push an operator stake update
│ └── it should emit an OperatorRegistered event and OperatorMetadataURIUpdated event
├── when modifyOperatorDetails is called
│ ├── given caller is not an operator
│ │ └── it should revert
│ └── given caller is an operator
│ └── it should update the operatorDetails and emit an OperatorDetailsModified event
├── when updateOperatorMetadataURI is called
│ ├── given caller is not an operator
│ │ └── it should revert
│ └── given caller is an operator
│ └── it should emit an OperatorMetadataURIUpdated event
├── when delegateTo is called
│ ├── given operator is not registered
│ │ └── it should revert
│ ├── given staker is already delegated to an operator
│ │ └── it should revert
│ └── it should call `_delegate` (internal function) with msg.sender as the staker
├── when delegateToBySignature is called
│ ├── given block timestamp is > staker signature expiry
│ │ └── it should revert
│ ├── given operator is not registered
│ │ └── it should revert
│ ├── given staker is already delegated to an operator
│ │ └── it should revert
│ ├── given staker signature verification fails
│ │ └── it should revert
│ └── given staker signature verification succeeds
│ └── it should call _delegate() (internal function)
├── when _delegate() is called
│ ├── given that new delegations are paused
│ │ └── it should revert
│ ├── given operator's delegationApprover is set to zero address OR given caller is the delegationApprover
│ │ └── it should check delegatable shares and update accordingly (**below logic tree reused elsewhere**)
│ │ ├── given staker doesn't have delegatable shares
│ │ │ └── it should set staker delegated to operator, call the StakeRegistry, and emit events
│ │ └── given staker has delegatable shares
│ │ ├── it should get the maxMagnitudes for each strategy from the staker's delegated operator
│ │ └── it should call `_increaseDelegation` with the maxMagnitude of the staker's operator
│ └── given operator's delegationApprover is set to nonzero address AND the caller is not the delegationApprover
│ ├── given the delegationApprover is an EOA
│ │ ├── given the block timestamp is past the expiry timestamp
│ │ │ └── it should revert
│ │ ├── given the delegationApprove salt has already been used
│ │ │ └── it should revert
│ │ ├── given the signature verification fails
│ │ │ └── it should revert
│ │ └── given the signature verification succeeds
│ │ └── it should set salt as spent, check delegatable shares and update accordingly (**logic tree reused from above**)
│ └── given the delegationApprover is a contract
│ ├── given the block timestamp is past the expiry timestamp
│ │ └── it should revert
│ ├── given the delegationApprove salt has already been used
│ │ └── it should revert
│ ├── given the contract isn't EIP1271 compliant
│ │ └── it should revert
│ ├── given the signature verification fails, isValidSignature() does not return EIP1271_MAGICVALUE
│ │ └── it should revert
│ └── given the signature verification succeeds, isValidSignature() returns EIP1271_MAGICVALUE
│ └── it should set salt as spent, check delegatable shares and update accordingly (**logic tree reused from above**)
├── when undelegate is called
│ ├── given staker is not delegated to an operator
│ │ └── it should revert
│ ├── given that the staker is registered as operator
│ │ └── it should revert
│ ├── given that staker param is zero address
│ │ └── it should revert
│ ├── given msg.sender is neither the staker, operator, or delegationApprover (if applicable)
│ │ └── it should revert
│ ├── given the msg.sender is the operator or delegationApprover
│ │ └── it should emit a StakerForceUndelegated event
│ └── it should emit a StakerUndelegatedEvent and undelegate the staker
│ └── it should loop through each strategy that the staker has non-zero depositShares
│ ├── given `_hasNonZeroScalingFactors` returns false (the strategy has a maxMagnitude of 0 OR the strategy is the beacon chain strategy and the beaconChainScalingFactor is 0)
│ │ └── it should decrement all operator depositShares with a respective withdrawalRoot of 0x0
│ └── given `_hasNonZeroScalingFactors` returns true
│ └── it should convert depositShares to sharesToWithdraw to factor in Slashed amounts and call _removeSharesAndQueueWithdrawal and return a withdrawalRoot
├── when queueWithdrawals is called ***
│ ├── given that entering the withdrawal queue is paused
│ │ └── it should revert
│ ├── given for a withdrawal, the withdrawer address is not the msg.sender
│ │ └── it should revert
│ ├── given for a withdrawal, the strategies and shares arrays are not equal length
│ │ └── it should revert
│ └── it should loop through each withdrawal and call `_removeSharesAndQueueWithdrawal`
├── when _removeSharesAndQueueWithdrawal is called
│ ├── given that the staker is a zero address
│ │ └── it should revert
│ ├── given that the length of strategies is 0
│ │ └── it should revert
│ └── it should loop through each strategy
│ ├── given `_hasNonZeroScalingFactors` returns false (the strategy has a maxMagnitude of 0 OR the strategy is the beacon chain strategy and the beaconChainScalingFactor is 0)
│ │ └── it should revert
│ ├── given that the sharesToWithdraw is greater than the staker's withdrawable shares (accounting for slashing)
│ │ └── it should revert
│ ├── given that the staker is delegated to (not zero address)
│ │ └── it should decrease the operator's shares and emit OperatorSharesDecreased
│ ├── given that the strategy is the beacon chain strategy
│ │ └── it should remove shares from the eigen pod manager
│ ├── given that the strategy is not the beacon chain eth strategy
│ │ └── it should remove shares from the strategy manager
│ ├── it should increment the staker's cumulativeWithdrawalsQueued
│ ├── it should calculate the scaled shares for queued withdrawal and set the withdrawal root as pending
│ └── it should emit a SlashingWithdrawalQueued event and return the withdrawal root
├── when completeQueuedWithdrawal OR completeQueuedWithdrawals is called ***
│ ├── given that the exiting the withdrawal queue is paused
│ │ └── it should revert
│ ├── given that the function is reentered
│ │ └── it should revert
│ └── it should call _completeQueuedWithdrawal (internal function) for each withdrawal
├── when _completeQueuedWithdrawal is called ***
│ ├── given that the tokens and strategies arrays are not equal length
│ │ └── it should revert
│ ├── given that the caller is not the withdrawer
│ │ └── it should revert
│ ├── given that the withdrawal root is not pending
│ │ └── it should revert
│ ├── given that the withdrawal is a legacy pre-slashing upgrade withdrawal and LEGACY_MIN_WITHDRAWAL_DELAY_BLOCKS have not passed
│ │ └── it should revert
│ ├── given that the withdrawal is a post slashing upgrade withdrawal and MIN_WITHDRAWAL_DELAY seconds have not passed
│ │ └── it should revert
│ └── given that the above conditions are satisfied
│ ├── given that receiveAsTokens is false
│ │ └── for each strategy, it should call addShares on the shareManager to withdraw from
│ │ ├── given the shareManager is the EigenPodManager
│ │ │ └── it should call `EigenPodManager.addShares` which may callback `increaseDelegatedShares` to the DelegationManager
│ │ └── given the shareManager is the StrategyManager
│ │ └── it should call `StrategyManager.addShares` which will callback `increaseDelegatedShares` to the DelegationManager
│ ├── given that receiveAsTokens is true
│ │ └── for each strategy, it should call withdrawSharesAsTokens on the shareManager to withdraw from
│ │ ├── given the shareManager is the EigenPodManager
│ │ │ └── it should call `EigenPodManager.withdrawSharesAsTokens` which will withdraw ETH from the staker EigenPod if EPM shares not in deficit
│ │ └── given the shareManager is the StrategyManager
│ │ └── it should call `StrategyManager.withdrawSharesAsTokens` which will withdraw the strategy's underlying token
│ ├── it should delete the withdrawal root from pending withdrawals
│ └── it should emit a WithdrawalCompleted event
├── when increaseDelegatedShares is called
│ ├── if the caller is not the strategy manager or eigen pod manager
│ │ └── it should revert
│ └── given that the staker is delegated
│ ├── it should get the maxMagnitudes for each strategy from the staker's delegated operator
│ └── it should call `_increaseDelegation` with the maxMagnitude of the staker's operator
├── when _increaseDelegation is called
│ ├── given `_hasNonZeroScalingFactors` returns false (the strategy has a maxMagnitude of 0 OR the strategy is the beacon chain strategy and the beaconChainScalingFactor is 0)
│ │ └── it should revert
│ └── it should increase the operator's shares by addedShares amount, update the staker's depositScalingFactor, and emit OperatorSharesIncreased, DepositScalingFactorUpdated
├── when decreaseBeaconChainScalingFactor is called
│ ├── if the caller is not the eigen pod manager
│ │ └── it should revert
│ ├── it should decrease the beacon chain scaling factor and emit a BeaconChainScalingFactorDecreased event
│ └── given that the staker is delegated
│ └── it should decrease operator shares with the beacon chain shares slashed and emit OperatorSharesDecreased
├── when _decreaseDelegation is called
│ ├── given `_hasNonZeroScalingFactors` returns false (the strategy has a maxMagnitude of 0 OR the strategy is the beacon chain strategy and the beaconChainScalingFactor is 0)
│ │ └── it should revert
│ └── it should decrease the operator's shares by slashed shares amount, update the staker's depositScalingFactor, and emit OperatorSharesDecreased, DepositScalingFactorUpdated
└── when decreaseOperatorShares is called
├── given not called by AllocationManager
│ └── it should revert
└── it should decrease the operator's shares by slashed shares amount and emit OperatorSharesDecreased
````
## File: src/test/tree/EigenPodManagerUnit.tree
````
├── EigenPodManager Tree (*** denotes that integration tests are needed to validate path)
├── when contract is deployed and initialized
│ └── it should properly set storage
├── when initialize called again
│ └── it should revert
├── when createPod called
│ ├── given the user has already created a pod
│ │ └── it should revert
│ └── given the user has not created a pod
│ └── it should deploy a pod
├── when stake is called
│ ├── given the user has not created a pod
│ │ └── it should deploy a pod
│ └── given the user has already created a pod
│ └── it should call stake on the eigenPod
├── when updateBeaconChainOracle is called
│ ├── given the user is not the owner
│ │ └── it should revert
│ └── given the user is the owner
│ └── it should set the beacon chain oracle
├── when addShares is called
│ ├── given that the caller is not the delegationManager
│ │ └── it should revert
│ ├── given that the podOwner address is 0
│ │ └── it should revert
│ ├── given that the shares amount is negative
│ │ └── it should revert
│ ├── given that the shares is not a whole gwei amount
│ │ └── it should revert
│ └── given that all of the above conditions are satisfied
│ └── it should update the podOwnerShares
├── when removeShares is called
│ ├── given that the caller is not the delegationManager
│ │ └── it should revert
│ ├── given that the shares amount is negative
│ │ └── it should revert
│ ├── given that the shares is not a whole gwei amount
│ │ └── it should revert
│ ├── given that removing shares results in the pod owner having negative shares
│ │ └── it should revert
│ └── given that all of the above conditions are satisfied
│ └── it should update the podOwnerShares
├── when withdrawSharesAsTokens is called
│ ├── given that the podOwner is address 0
│ │ └── it should revert
│ ├── given that the destination is address 0
│ │ └── it should revert
│ ├── given that the shares amount is negative
│ │ └── it should revert
│ ├── given that the shares is not a whole gwei amount
│ │ └── it should revert
│ ├── given that the current podOwner shares are negative
│ │ ├── given that the shares to withdraw are larger in magnitude than the shares of the podOwner
│ │ │ └── it should set the podOwnerShares to 0 and decrement shares to withdraw by the share deficit
│ │ └── given that the shares to withdraw are smaller in magnitude than shares of the podOwner
│ │ └── it should increment the podOwner shares by the shares to withdraw
│ └── given that the pod owner shares are positive
│ └── it should withdraw restaked ETH from the eigenPod
├── when shares are adjusted
│ ├── given that sharesBefore is negative or 0
│ │ ├── given that sharesAfter is negative or zero
│ │ │ └── the change in delegateable shares should be 0
│ │ └── given that sharesAfter is positive
│ │ └── the change in delegateable shares should be positive
│ └── given that sharesBefore is positive
│ ├── given that sharesAfter is negative or zero
│ │ └── the change in delegateable shares is negative sharesBefore
│ └── given that sharesAfter is positive
│ └── the change in delegateable shares is the difference between sharesAfter and sharesBefore
└── when recordBeaconChainETHBalanceUpdate is called
├── given that the podOwner's eigenPod is not the caller
│ └── it should revert
├── given that the podOwner is a zero address
│ └── it should revert
├── given that sharesDelta is not a whole gwei amount
│ ├── it should revert
│ └── given that the shares delta is valid
│ └── it should update the podOwnerShares
├── given that the change in delegateable shares is positive ***
│ └── it should increase delegated shares on the delegationManager
├── given that the change in delegateable shares is negative ***
│ └── it should decrease delegated shares on the delegationManager
├── given that the change in delegateable shares is 0 ***
│ └── it should only update the podOwnerShares
└── given that the function is reentered ***
└── it should revert
````
## File: src/test/tree/EigenPodUnit.tree
````
.
├── EigenPod Tree (*** denotes that integration tests are needed to validate path)
├── when the contract is deployed and initialized
│ └── it should properly set storage
├── when initialize called again
│ └── it should revert
├── // EigenPodManager Caller Tree
├── when stake is called
│ ├── given the caller is not the EigenPodManager
│ │ └── it should revert
│ ├── given the value staked is not 32 ETH
│ │ └── it should revert
│ └── given that all of the above conditions are satisfied
│ └── it should stake ETH in the beacon chain deposit contract
├── when withdrawRestakedBeaconChainETH is called - function only relevant when `withdrawableRestakedExecutionLayerGwei` is incremented after a full withdrawal
│ ├── given that the caller is not the EigenPodManager
│ │ └── it should revert
│ ├── given that the amount to withdraw is not a whole Gwei amount
│ │ └── it should revert
│ ├── given that the amount to withdraw is greater than the withdrawable restaked execution layer amount
│ │ └── it should revert
│ └── given the above conditions are satisfied
│ └── it should send eth from the pod to the recipient
├── // EigenPodOwner Caller Tree
├── when verifyWithdrawalCredentials is called ***
│ ├── given that the caller is not the eigen pod Owner
│ │ └── it should revert
│ ├── given that verify credentials is paused
│ │ └── it should revert
│ ├── given that the proof is not valid for the timestamp
│ │ └── it should revert
│ ├── given that restaking is not enabled
│ │ └── it should revert
│ ├── given that the validator indices, proofs, and validator fields are different lengths
│ │ └── it should revert
│ ├── given that the withdrawal credential proof is stale
│ │ └── it should revert
│ ├── given that the beacon state root proof is invalid
│ │ └── it should revert
│ ├── it should call _verifyWithdrawalCredentials for each validator
│ └── it should record a beaconChainETH balance update in the EPM
├── when _verifyWithdrawalCredentials is called (internal function)
│ ├── given that the validators status is not INACTIVE
│ │ └── it should revert
│ ├── given that the validator is currently in the process of fully exiting
│ │ └── it should revert
│ ├── given that validator's withdrawal credentials does not correspond to the pod withdrawal credentials
│ │ └── it should revert
│ ├── given that the validator fields proof is not valid
│ │ └── it should revert
│ └── given that all the above conditions are satisfied
│ ├── it should set the validator's restaked balance to their effective balance
│ ├── it should update the _validatorPubkeyHashToInfo mapping with an active validator, restaked balance in gwei, and lastCheckpointedAt timestamp
│ ├── it should emit ValidatorRestaked and ValidatorBalanceUpdated Events
│ └── it should return the validator's restakedBalance in wei
├── when recoverTokens is called
│ ├── given that the caller is not the eigen pod owner
│ │ └── it should revert
│ ├── given that non proof withdrawals are paused
│ │ └── it should revert
│ ├── given that the tokens and amounts to withdraw are different lengths
│ │ └── it should revert
│ └── given that the above conditions pass
│ └── it should transfer tokens to the recipient
├── // Checkpointing Tree
├── when startCheckpoint is called
│ ├── given that the caller is not the eigen pod owner
│ │ └── it should revert
│ ├── given that start checkpoints is not paused
│ │ └── it should revert
│ ├── it should call _startCheckpoint
│ └── given _startCheckpoint does not revert
│ └── given hasRestaked is false
│ └── it should set hasRestaked to true and emit RestakingActivated
├── when _startCheckpoint is called
│ ├── given a current checkpoint is in progress, currentCheckpointTimestamp != 0
│ │ └── it should revert
│ ├── given the last checkpoint occurred in the same block, lastCheckpointTimestamp == block.timestamp
│ │ └── it should revert
│ ├── given revertIfNoBalance is true and the pod has no increase balance in gwei
│ │ └── it should revert
│ └── given that the above conditions pass
│ ├── it should set the currentCheckpointTimestamp to the current block timestamp
│ ├── it should set the currentCheckpoint with the parentBlockRoot at block.timestamp and with the current activeValidatorCount
│ └── it should emit CheckpointCreated
├── when verifyCheckpointProofs is called
│ ├── given that verify checkpoint proofs is paused
│ │ └── it should revert
│ ├── given there is no currently active checkpoint, currentCheckpointTimestamp == 0
│ │ └── it should revert
│ ├── given the balanceContainerProof does not match with the current checkpoint beaconBlockRoot
│ │ └── it should revert
│ ├── for each balanceProof, it should process the respective validator accordingly
│ │ ├── given the validator is not active
│ │ │ └── it should continue to next validator proof
│ │ ├── given the validator last checkpointed timestamp is >= currentCheckpointTimestamp
│ │ │ └── it should continue to next validator proof
│ │ └── given _verifyCheckpointProof does not revert
│ │ └── it should decrement proofsRemaining, add to balanceDeltasGwei, add to exitedBalancesGwei
│ └── given that all above checkpoint proofs pass
│ └── it should update checkpointBalanceExitedGwei at the checkpoint timestamp and call _updateCheckpoint
├── when _verifyCheckpointProof is called
│ ├── given verifyValidatorBalance does not match with balanceContainerRoot
│ │ └── it should revert
│ └── it should return the balanceDeltaGwei and exitedBalanceGwei if the validator did a full exit
├── when _updateCheckpoint is called
│ ├── given there are still proofs remaining for the checkpoint
│ │ └── it should update the current checkpoint
│ └── given there are 0 proofs remaining for the checkpoint
│ ├── it should update the lastCheckpointTimestamp with currentCheckpointTimestamp
│ ├── it should delete currentCheckpointTimestamp resetting it to 0
│ ├── it should delete the currentCheckpoint
│ ├── it should recordBeaconChainBalanceUpdate on the EPM with the total delta of shares in wei
│ └── it should emit CheckpointFinalized
├── when _getParentBlockRoot is called
│ ├── given the provided timestamp is out of range, HISTORY_BUFFER_LENGTH * 12seconds
│ │ └── it should revert
│ └── given the slot at the provided timestamp was skipped
│ └── it should revert
└── when verifyStaleBalance is called
├── given that verify stale balance is paused
│ └── it should revert
├── given that the validator last checkpointed timestamp is not stale enough (2 weeks)
│ └── it should revert
├── given that the validator status is not ACTIVE
│ └── it should revert
├── given that the validator has not been slashed
│ └── it should revert
├── given that the beacon state root proof does not match the beaconBlockRoot at the given beaconTimestamp
│ └── it should revert
├── given the validator container proof does not match the beacon state root
│ └── it should revert
└── given that all the above conditions pass
└── it should call _startCheckpoint with revertIfNoBalance set to false
````
## File: src/test/tree/PermissionControllerUnit.tree
````
.
└── PermissionController (**** denotes that integration tests are needed to fully validate path)
├── when setAdmin is called
│ ├── given that the current admin it not set
│ │ └── given that the caller is not the account
│ │ └── it should revert
│ ├── given that the current admin is set
│ │ └── given that the msg.sender is not the current admin
│ │ └── it should revert
│ ├── given that the new admin is the zero address
│ │ └── it should revert
│ └── given that a valid caller sets a valid admin
│ └── it should update the permissions of the account & emit an AdminSet event
├── when setAppointee is called
│ ├── given that the caller is not the admin
│ │ └── it should revert
│ ├── given that the appointee already has permissions
│ │ └── it should revert
│ └── given that proper permissions are set
│ └── it should emit a DelegateSet event, and update the `appointeePermissions` and `permissionAppointee` mappings for the account
└── when removeAppointee is called
├── given that the caller is not the admin
│ └── it should revert
├── given that the appointee does not have permissions
│ └── it should revert
└── given that proper permissions are set
└── it should emit a DelegateRemoved event, and update the `appointeePermissions` and `permissionAppointee` mappings for the account
````
## File: src/test/tree/StrategyManagerUnit.tree
````
├── StrategyManagerUnit.t.sol (*** denotes that integration tests are needed to validate path)
├── initialize
| ├── given that initialized is only called once
│ │ └── it should set the storage variables correctly (owner, strategyWhitelister, pauserRegistry)
│ └── given that initialize is called again
│ └── it should revert
├── depositIntoStrategy()
│ ├── given that deposits paused
│ │ └── it should revert
│ ├── given the function is re-entered
│ │ └── it should revert
│ ├── given that the strategy is not whitelisted
│ │ └── it should revert
│ ├── given the token safeTransferFrom() reverts
│ │ └── it should revert
│ └── given that token safeTransferFrom() succeeds
│ ├── given the staker has existing shares in strategy (not first deposit)
│ │ └── it should increase shares, nonce. while stakerStrategyListLength is unchanged
│ ├── given the staker has no existing shares in strategy (first deposit)
│ │ └── stakerStrategyListLength increases by 1 and shares increase
│ ├── given the staker has delegated to a operator ***
│ │ └── it should deposit successfully with shares increase, including delegated shares
│ └── given the staker is not delegated
│ └── it should deposit successfully with shares increase
├── depositIntoStrategyWithSignature()
│ ├── given that deposits paused
│ │ └── it should revert
│ ├── given the function is re-entered
│ │ └── it should revert
│ ├── given the signature expired
│ │ └── it should revert
│ ├── given that deposits paused and strategy not whitelisted
│ │ └── it should revert
│ ├── given the staker is a EOA
│ │ ├── given the signature verification fails
│ │ │ └── it should revert
│ │ └── given the signature verification succeeds
│ │ ├── given the token safeTransferFrom reverts
│ │ │ └── it should revert
│ │ └── given the token safeTransferFrom succeeds
│ │ ├── given that the staker has delegated to a operator ***
│ │ │ └── it should deposit successfully with shares and nonce increase, including delegated shares
│ │ └── given that the staker is not delegated
│ │ └── it should deposit successfully with shares and nonce increase
│ └── given the staker is a contract
│ ├── given the contract isn't EIP1271 compliant
│ │ └── it should revert
│ ├── given the signature verification fails, isValidSignature() return != EIP1271_MAGICVALUE
│ │ └── it should revert
│ └── given the signature verification succeeds, isValidSignature() returns EIP1271_MAGICVALUE
│ ├── given the token safeTransferFrom reverts
│ │ └── it should revert
│ └── given the token safeTransferFrom succeeds
│ ├── given the staker has delegated to a operator ***
│ │ └── it should deposit successfully with shares and nonce increase, including delegated shares
│ └── given the staker is not delegated
│ └── it should deposit successfully with shares and nonce increase
├── removeShares()
│ ├── given not called by DelegationManager
│ │ └── it should revert
│ ├── given the share amount is 0
│ │ └── it should revert
│ ├── given the share amount is too high, higher than deposited amount
│ │ └── it should revert
│ ├── given the share amount is equal to the deposited amount
│ │ └── staker shares should be 0 with decremented stakerStrategyListLength
│ └── given the share amount is less than the deposited amount
│ └── staker shares should now be deposited - shares amount, unchanged stakerStrategyListLength
├── addShares()
│ ├── given not called by DelegationManager
│ │ └── it should revert
│ ├── given the share amount is 0
│ │ └── it should revert
│ ├── given the staker is 0 address
│ │ └── it should revert
│ ├── given adding shares with 0 existing shares
│ │ └── it should increase shares and increment stakerStrategyListLength
│ ├── given adding shares with 0 existing shares and staker has MAX_STAKER_STRATEGY_LIST_LENGTH
│ │ └── it should revert
│ └── given the adding shares with > 0 existing shares
│ └── it should increase shares, unchanged stakerStrategyListLength
├── withdrawSharesAsTokens()
│ ├── given not called by DelegationManager
│ │ └── it should revert
│ └── given that deposited strategy is called
│ │ └── it should withdraw tokens from strategy with token balanceOf() update
├── setStrategyWhitelister()
│ ├── given not called by owner
│ │ └── it should revert
│ └── given called by owner address
│ └── it should update strategyWhitelister address
├── addStrategiesToDepositWhitelist()
│ ├── given not called by strategyWhitelister address
│ │ └── it should revert
│ └── given the strategyWhitelister address is called
│ ├── given adding one single strategy that is already whitelisted
│ │ └── it should not emit StrategyAddedToDepositWhitelist with mapping still true
│ ├── given adding one single strategy
│ │ └── it should whitelist the new strategy with mapping set to true
│ └── given adding multiple strategies to whitelist
│ └── it should whitelist all new strategies with mappings set to true
└── removeStrategiesFromDepositWhitelist()
├── given not called by strategyWhitelister address
│ └── it should revert
└── given called by strategyWhitelister address
├── given removing one single strategy that is not whitelisted
│ └── it shouldn't emit StrategyRemovedFromDepositWhitelist with mapping still false
├── given removing one single strategy
│ └── it should de-whitelist the new strategy with mapping set to false
└── given removing multiple strategies to whitelist
└── it should de-whitelist all specified strategies with mappings set to false
````
## File: src/test/unit/libraries/BytesLibUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "src/contracts/libraries/BytesLib.sol";
contract BytesLibHarness {
function concat(bytes memory a, bytes memory b) public pure returns (bytes memory) {
return BytesLib.concat(a, b);
}
function slice(bytes memory data, uint start, uint length) public pure returns (bytes memory) {
return BytesLib.slice(data, start, length);
}
function toAddress(bytes memory data, uint offset) public pure returns (address) {
return BytesLib.toAddress(data, offset);
}
function toUint256(bytes memory data, uint offset) public pure returns (uint) {
return BytesLib.toUint256(data, offset);
}
function toUint128(bytes memory data, uint offset) public pure returns (uint128) {
return BytesLib.toUint128(data, offset);
}
function toUint96(bytes memory data, uint offset) public pure returns (uint96) {
return BytesLib.toUint96(data, offset);
}
function toUint64(bytes memory data, uint offset) public pure returns (uint64) {
return BytesLib.toUint64(data, offset);
}
function toUint32(bytes memory data, uint offset) public pure returns (uint32) {
return BytesLib.toUint32(data, offset);
}
function toUint16(bytes memory data, uint offset) public pure returns (uint16) {
return BytesLib.toUint16(data, offset);
}
function toUint8(bytes memory data, uint offset) public pure returns (uint8) {
return BytesLib.toUint8(data, offset);
}
function equal(bytes memory a, bytes memory b) public pure returns (bool) {
return BytesLib.equal(a, b);
}
}
contract BytesLibUnitTests is Test {
BytesLibHarness harness;
function setUp() public {
harness = new BytesLibHarness();
}
function test_Concat_Basic() public view {
bytes memory a = hex"1234";
bytes memory b = hex"5678";
bytes memory expected = hex"12345678";
bytes memory result = harness.concat(a, b);
assertTrue(harness.equal(result, expected));
}
function test_Concat_EmptyInputs() public view {
bytes memory empty = hex"";
bytes memory data = hex"1234";
assertTrue(harness.equal(harness.concat(empty, empty), empty));
assertTrue(harness.equal(harness.concat(data, empty), data));
assertTrue(harness.equal(harness.concat(empty, data), data));
}
function test_Slice_Basic() public view {
bytes memory data = hex"0123456789";
bytes memory expected = hex"234567";
bytes memory result = harness.slice(data, 1, 3);
assertTrue(harness.equal(result, expected));
}
function test_Revert_SliceOutOfBounds() public {
bytes memory data = hex"0123456789";
// Test start beyond length
vm.expectRevert(BytesLib.OutOfBounds.selector);
harness.slice(data, 10, 1);
// Test length beyond data bounds
vm.expectRevert(BytesLib.OutOfBounds.selector);
harness.slice(data, 0, 11);
// Test start + length beyond bounds
vm.expectRevert(BytesLib.OutOfBounds.selector);
harness.slice(data, 5, 6);
}
function test_ToAddress() public view {
bytes memory data = hex"000000000000000000000000A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
address expected = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
assertEq(harness.toAddress(data, 12), expected);
}
function test_ToUint() public view {
bytes memory data = hex"000000000000000000000000000000000000000000000000000000000000002A";
assertEq(harness.toUint256(data, 0), 42);
assertEq(harness.toUint8(data, 31), 42);
assertEq(harness.toUint16(data, 30), 42);
assertEq(harness.toUint32(data, 28), 42);
assertEq(harness.toUint64(data, 24), 42);
assertEq(harness.toUint96(data, 20), 42);
assertEq(harness.toUint128(data, 16), 42);
}
function test_Equal() public view {
bytes memory a = hex"1234567890";
bytes memory b = hex"1234567890";
bytes memory c = hex"1234567891";
assertTrue(harness.equal(a, b));
assertFalse(harness.equal(a, c));
}
function test_Revert_ToTypesOutOfBounds() public {
bytes memory tooShort = hex"1234";
vm.expectRevert(BytesLib.OutOfBounds.selector);
harness.toAddress(tooShort, 0);
vm.expectRevert(BytesLib.OutOfBounds.selector);
harness.toUint256(tooShort, 0);
}
}
````
## File: src/test/unit/libraries/SnapshotsUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "src/contracts/libraries/Snapshots.sol";
contract SnapshotsHarness {
using Snapshots for Snapshots.DefaultWadHistory;
using Snapshots for Snapshots.DefaultZeroHistory;
Snapshots.DefaultWadHistory internal wadHistory;
Snapshots.DefaultZeroHistory internal zeroHistory;
function pushWad(uint32 key, uint64 value) public {
wadHistory.push(key, value);
}
function upperLookupWad(uint32 key) public view returns (uint64) {
return wadHistory.upperLookup(key);
}
function latestWad() public view returns (uint64) {
return wadHistory.latest();
}
function lengthWad() public view returns (uint) {
return wadHistory.length();
}
function pushZero(uint32 key, uint value) public {
zeroHistory.push(key, value);
}
function upperLookupZero(uint32 key) public view returns (uint) {
return zeroHistory.upperLookup(key);
}
function latestZero() public view returns (uint) {
return zeroHistory.latest();
}
function lengthZero() public view returns (uint) {
return zeroHistory.length();
}
}
contract SnapshotsUnitTests is Test {
SnapshotsHarness harness;
function setUp() public {
harness = new SnapshotsHarness();
}
/// forge-config: default.allow_internal_expect_revert = true
function test_Revert_InvalidSnapshotOrdering(uint r) public {
uint32 key = uint32(bound(r, 1, type(uint32).max));
uint32 smallerKey = uint32(bound(r, 0, key - 1));
harness.pushWad(key, 1);
vm.expectRevert(Snapshots.InvalidSnapshotOrdering.selector);
harness.pushWad(smallerKey, 2);
}
function test_Push_Correctness(uint r) public {
uint32 key = uint32(bound(r, 0, type(uint32).max));
uint64 value = uint32(bound(r, 0, type(uint64).max));
harness.pushWad(key, value);
assertEq(harness.upperLookupWad(key), value);
assertEq(harness.latestWad(), value);
assertEq(harness.lengthWad(), 1);
}
function test_UpperLookup_InitiallyWad(uint32 r) public view {
assertEq(harness.upperLookupWad(r), 1e18);
}
function test_Latest_InitiallyWad() public view {
assertEq(harness.latestWad(), 1e18);
}
function test_Length_InitiallyZero() public view {
assertEq(harness.lengthWad(), 0);
}
function test_Revert_InvalidSnapshotOrdering_ZeroHistory(uint r) public {
uint32 key = uint32(bound(r, 1, type(uint32).max));
uint32 smallerKey = uint32(bound(r, 0, key - 1));
harness.pushZero(key, 1);
vm.expectRevert(Snapshots.InvalidSnapshotOrdering.selector);
harness.pushZero(smallerKey, 2);
}
function test_Push_Correctness_ZeroHistory(uint r) public {
uint32 key = uint32(bound(r, 0, type(uint32).max));
uint value = bound(r, 0, type(uint224).max);
harness.pushZero(key, value);
assertEq(harness.upperLookupZero(key), value);
assertEq(harness.latestZero(), value);
assertEq(harness.lengthZero(), 1);
}
function test_UpperLookup_InitiallyZero(uint32 r) public view {
assertEq(harness.upperLookupZero(r), 0);
}
function test_Latest_InitiallyZero() public view {
assertEq(harness.latestZero(), 0);
}
function test_Length_InitiallyZero_ZeroHistory() public view {
assertEq(harness.lengthZero(), 0);
}
}
````
## File: src/test/unit/mixins/SemVerMixin.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import {Test} from "forge-std/Test.sol";
import {SemVerMixin} from "src/contracts/mixins/SemVerMixin.sol";
// Helper contract to test the abstract SemVerMixin
contract SemVerMixinMock is SemVerMixin {
constructor(string memory version) SemVerMixin(version) {}
// Expose internal function for testing
function majorVersion() public view returns (string memory) {
return _majorVersion();
}
}
contract SemVerMixinTest is Test {
SemVerMixinMock public semVer;
function test_version_returnsCorrectVersion() public {
semVer = new SemVerMixinMock("v1.2.3");
assertEq(semVer.version(), "v1.2.3");
}
function test_majorVersion_returnsCorrectMajorVersion() public {
semVer = new SemVerMixinMock("v1.2.3");
assertEq(semVer.majorVersion(), "v1");
}
}
````
## File: src/test/unit/mixins/SignatureUtilsUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "src/contracts/mixins/SignatureUtilsMixin.sol";
contract MockSigner {
mapping(bytes32 => mapping(bytes => bool)) public validSignatures;
function setValidSignature(bytes32 digest, bytes memory signature, bool valid) public {
validSignatures[digest][signature] = valid;
}
function isValidSignatureNow(bytes32 digest, bytes memory signature) public view returns (bool) {
return validSignatures[digest][signature];
}
}
contract SignatureUtilsMixinUnit is Test, SignatureUtilsMixin("v0.0.0") {
uint signerPk;
address signer;
bytes32 hash;
bytes32 digest;
bytes32 expectedDomainSeparator;
function setUp() public {
vm.chainId(1);
signerPk = 1;
signer = vm.addr(signerPk);
hash = keccak256("");
digest = _calculateSignableDigest(hash);
expectedDomainSeparator = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("EigenLayer")),
keccak256(bytes(_majorVersion())),
block.chainid,
address(this)
)
);
}
function test_domainSeparator_NonZero() public view {
assertTrue(domainSeparator() != 0, "The domain separator should be non-zero");
assertTrue(domainSeparator() == expectedDomainSeparator, "The domain separator should be as expected");
}
function test_domainSeparator_NewChainId() public {
bytes32 initialDomainSeparator = domainSeparator();
// Change the chain ID
vm.chainId(9999);
bytes32 newDomainSeparator = domainSeparator();
assertTrue(newDomainSeparator != 0, "The new domain separator should be non-zero");
assertTrue(initialDomainSeparator != newDomainSeparator, "The domain separator should change when the chain ID changes");
}
/// forge-config: default.allow_internal_expect_revert = true
function test_checkIsValidSignatureNow_Expired() public {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, digest);
vm.expectRevert(ISignatureUtilsMixinErrors.SignatureExpired.selector);
_checkIsValidSignatureNow(signer, digest, abi.encode(r, s, v), block.timestamp - 1);
}
// function testFail_checkIsValidSignatureNow_InvalidSignature() public {
// _checkIsValidSignatureNow(signer, digest, "", block.timestamp);
// }
}
````
## File: src/test/unit/AllocationManagerUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/harnesses/AllocationManagerHarness.sol";
import "src/test/utils/EigenLayerUnitTestSetup.sol";
import "src/test/mocks/MockAVSRegistrar.sol";
contract AllocationManagerUnitTests is EigenLayerUnitTestSetup, IAllocationManagerErrors, IAllocationManagerEvents {
using StdStyle for *;
using ArrayLib for *;
/// -----------------------------------------------------------------------
/// Constants
/// -----------------------------------------------------------------------
/// NOTE: Raising these values directly increases cpu time for tests.
uint internal constant FUZZ_MAX_ALLOCATIONS = 8;
uint internal constant FUZZ_MAX_STRATS = 8;
uint internal constant FUZZ_MAX_OP_SETS = 8;
uint8 internal constant PAUSED_MODIFY_ALLOCATIONS = 0;
uint8 internal constant PAUSED_OPERATOR_SLASHING = 1;
uint8 internal constant PAUSED_OPERATOR_SET_REGISTRATION_AND_DEREGISTRATION = 2;
uint32 constant ASSUMED_BLOCK_TIME = 12 seconds;
uint32 constant DEALLOCATION_DELAY = 14 days / ASSUMED_BLOCK_TIME;
uint32 constant ALLOCATION_CONFIGURATION_DELAY = 17.5 days / ASSUMED_BLOCK_TIME;
uint32 constant DEFAULT_OPERATOR_ALLOCATION_DELAY = 1 days / ASSUMED_BLOCK_TIME;
uint constant DEFAULT_OPERATOR_SHARES = 1e18;
/// -----------------------------------------------------------------------
/// Mocks
/// -----------------------------------------------------------------------
AllocationManagerHarness allocationManager;
ERC20PresetFixedSupply tokenMock;
StrategyBase strategyMock;
/// -----------------------------------------------------------------------
/// Defaults
/// -----------------------------------------------------------------------
OperatorSet defaultOperatorSet;
IStrategy[] defaultStrategies;
address defaultOperator = address(this);
address defaultAVS = address(new MockAVSRegistrar());
/// -----------------------------------------------------------------------
/// Internal Storage Helpers
/// -----------------------------------------------------------------------
mapping(IStrategy => uint64) _encumberedMagnitudes;
/// -----------------------------------------------------------------------
/// Setup
/// -----------------------------------------------------------------------
function setUp() public virtual override {
EigenLayerUnitTestSetup.setUp();
_initializeAllocationManager(address(this), pauserRegistry, 0);
tokenMock = new ERC20PresetFixedSupply("Mock Token", "MOCK", type(uint).max, address(this));
strategyMock = StrategyBase(
address(
new TransparentUpgradeableProxy(
address(new StrategyBase(IStrategyManager(address(strategyManagerMock)), pauserRegistry, "v9.9.9")),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(StrategyBase.initialize.selector, tokenMock)
)
)
);
defaultStrategies = strategyMock.toArray();
defaultOperatorSet = OperatorSet(defaultAVS, 0);
cheats.prank(defaultAVS);
allocationManager.updateAVSMetadataURI(defaultAVS, "https://example.com");
_createOperatorSet(defaultOperatorSet, defaultStrategies);
_registerOperator(defaultOperator);
_setAllocationDelay(defaultOperator, DEFAULT_OPERATOR_ALLOCATION_DELAY);
_registerForOperatorSet(defaultOperator, defaultOperatorSet);
_grantDelegatedStake(defaultOperator, defaultOperatorSet, DEFAULT_OPERATOR_SHARES);
}
/// -----------------------------------------------------------------------
/// Internal Helpers
/// -----------------------------------------------------------------------
function _initializeAllocationManager(address _initialOwner, IPauserRegistry _pauserRegistry, uint _initialPausedStatus)
internal
returns (AllocationManagerHarness)
{
return allocationManager = AllocationManagerHarness(
address(
new TransparentUpgradeableProxy(
address(
new AllocationManagerHarness(
IDelegationManager(address(delegationManagerMock)),
_pauserRegistry,
IPermissionController(address(permissionController)),
DEALLOCATION_DELAY,
ALLOCATION_CONFIGURATION_DELAY
)
),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(AllocationManager.initialize.selector, _initialOwner, _initialPausedStatus)
)
)
);
}
function _registerOperator(address operator) internal {
delegationManagerMock.setIsOperator(operator, true);
}
function _setAllocationDelay(address operator, uint32 delay) internal {
cheats.prank(operator);
allocationManager.setAllocationDelay(operator, delay);
cheats.roll(block.number + ALLOCATION_CONFIGURATION_DELAY + 1);
}
function _createOperatorSet(OperatorSet memory operatorSet, IStrategy[] memory strategies) internal returns (OperatorSet memory) {
cheats.prank(operatorSet.avs);
allocationManager.createOperatorSets(
operatorSet.avs, CreateSetParams({operatorSetId: operatorSet.id, strategies: strategies}).toArray()
);
return operatorSet;
}
function _createOperatorSets(OperatorSet[] memory operatorSets, IStrategy[] memory strategies) internal {
CreateSetParams[] memory createSetParams = new CreateSetParams[](operatorSets.length);
for (uint i; i < operatorSets.length; ++i) {
createSetParams[i] = CreateSetParams({operatorSetId: operatorSets[i].id, strategies: strategies});
}
cheats.prank(operatorSets[0].avs);
allocationManager.createOperatorSets(operatorSets[0].avs, createSetParams);
}
function _registerForOperatorSet(address operator, OperatorSet memory operatorSet) internal {
cheats.prank(operator);
allocationManager.registerForOperatorSets(
operator, RegisterParams({avs: operatorSet.avs, operatorSetIds: operatorSet.id.toArrayU32(), data: ""})
);
}
function _grantDelegatedStake(address operator, OperatorSet memory operatorSet, uint stake) internal {
IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet);
delegationManagerMock.setOperatorsShares(operator, strategies, stake);
}
function _registerForOperatorSets(address operator, OperatorSet[] memory operatorSets) internal {
cheats.startPrank(operator);
for (uint i; i < operatorSets.length; ++i) {
allocationManager.registerForOperatorSets(
operator, RegisterParams({avs: operatorSets[i].avs, operatorSetIds: operatorSets[i].id.toArrayU32(), data: ""})
);
}
cheats.stopPrank();
}
struct Magnitudes {
uint encumbered;
uint max;
uint allocatable;
}
/**
* Get expected post slash storage values
* Assumes that:
* 1. WAD is max before slash
* 2. encumbered is equal to magnitude before slash
*/
function _getExpectedSlashVals(uint wadToSlash, uint64 magBeforeSlash)
internal
pure
returns (uint wadSlashed, uint64 newCurrentMag, uint64 newMaxMag, uint64 newEncumberedMag)
{
return _getExpectedSlashVals(wadToSlash, magBeforeSlash, magBeforeSlash);
}
/**
* Get expected post slash storage values
* Assumes that:
* 1. WAD is max before slash
*/
function _getExpectedSlashVals(uint wadToSlash, uint64 magBeforeSlash, uint64 encumberedMagBeforeSlash)
internal
pure
returns (uint wadSlashed, uint64 newCurrentMag, uint64 newMaxMag, uint64 newEncumberedMag)
{
// Get slippage to apply to returned values - we basically recreate mulWadRoundUp here
uint64 slippage = _calculateSlippage(magBeforeSlash, wadToSlash);
// Get the magnitude to slash - this value is rounded UP in the implementation
uint64 slashedMag = uint64((uint(magBeforeSlash) * wadToSlash / WAD + slippage));
wadSlashed = slashedMag;
newCurrentMag = magBeforeSlash - slashedMag;
newMaxMag = WAD - slashedMag;
newEncumberedMag = encumberedMagBeforeSlash - slashedMag;
}
/// @dev Returns 0 or 1, depending on the remainder of the division
function _calculateSlippage(uint64 magnitude, uint wadToSlash) internal pure returns (uint64) {
return mulmod(magnitude, wadToSlash, WAD) > 0 ? 1 : 0;
}
function _checkAllocationStorage(
address operator,
OperatorSet memory operatorSet,
IStrategy strategy,
Allocation memory expectedAllocation,
Magnitudes memory expectedMagnitudes
) internal view {
Allocation memory allocation = allocationManager.getAllocation(operator, operatorSet, strategy);
console.log("\nChecking Allocation Storage:".yellow());
console.log(" currentMagnitude: %d", allocation.currentMagnitude);
console.log(" pendingDiff: %d", allocation.pendingDiff);
console.log(" effectBlock: %d", allocation.effectBlock);
assertEq(expectedAllocation.currentMagnitude, allocation.currentMagnitude, "currentMagnitude != expected");
assertEq(expectedAllocation.pendingDiff, allocation.pendingDiff, "pendingDiff != expected");
assertEq(expectedAllocation.effectBlock, allocation.effectBlock, "effectBlock != expected");
uint encumberedMagnitude = allocationManager.getEncumberedMagnitude(operator, strategy);
uint maxMagnitude = allocationManager.getMaxMagnitudes(operator, strategy.toArray())[0];
uint allocatableMagnitude = allocationManager.getAllocatableMagnitude(operator, strategy);
console.log(" encumberedMagnitude: %d", encumberedMagnitude);
console.log(" maxMagnitude: %d", maxMagnitude);
console.log(" allocatableMagnitude: %d", allocatableMagnitude);
assertEq(expectedMagnitudes.encumbered, encumberedMagnitude, "encumberedMagnitude != expected");
assertEq(expectedMagnitudes.max, maxMagnitude, "maxMagnitude != expected");
assertEq(expectedMagnitudes.allocatable, allocatableMagnitude, "allocatableMagnitude != expected");
// Check `getMaxMagnitudes` alias for coverage.
assertEq(expectedMagnitudes.max, allocationManager.getMaxMagnitudes(operator.toArray(), strategy)[0], "maxMagnitude != expected");
// Check `getAllocations` alias for coverage.
Allocation memory getAllocations = allocationManager.getAllocations(operator.toArray(), operatorSet, strategy)[0];
assertEq(expectedAllocation.currentMagnitude, getAllocations.currentMagnitude, "currentMagnitude != expected");
assertEq(expectedAllocation.pendingDiff, getAllocations.pendingDiff, "pendingDiff != expected");
assertEq(expectedAllocation.effectBlock, getAllocations.effectBlock, "effectBlock != expected");
console.log("Success!".green().bold());
}
/// @dev Check that the deallocation queue is in ascending order of effectBlocks
function _checkDeallocationQueueOrder(address operator, IStrategy strategy, uint numDeallocations) internal view {
uint32 lastEffectBlock = 0;
for (uint i = 0; i < numDeallocations; ++i) {
bytes32 operatorSetKey = allocationManager.deallocationQueueAtIndex(operator, strategy, i);
Allocation memory allocation = allocationManager.getAllocation(operator, OperatorSetLib.decode(operatorSetKey), strategy);
assertTrue(lastEffectBlock <= allocation.effectBlock, "Deallocation queue is not in ascending order of effectBlocks");
lastEffectBlock = allocation.effectBlock;
}
}
function _checkSlashableStake(
OperatorSet memory operatorSet,
address operator,
IStrategy[] memory strategies,
uint expectedSlashableStake
) internal view {
_checkSlashableStake(operatorSet, operator, strategies, expectedSlashableStake, block.number);
}
function _checkSlashableStake(
OperatorSet memory operatorSet,
address operator,
IStrategy[] memory strategies,
uint expectedSlashableStake,
uint futureBlock
) internal view {
uint[][] memory slashableStake = allocationManager.getMinimumSlashableStake({
operatorSet: operatorSet,
operators: operator.toArray(),
strategies: strategies,
futureBlock: uint32(futureBlock)
});
for (uint i = 0; i < strategies.length; i++) {
console.log("\nChecking Slashable Stake:".yellow());
console.log(" slashableStake[%d] = %d", i, slashableStake[0][i]);
assertEq(slashableStake[0][i], expectedSlashableStake, "slashableStake != expected");
}
console.log("Success!".green().bold());
}
function _checkAllocationEvents(
address operator,
OperatorSet memory operatorSet,
IStrategy strategy,
uint64 magnitude,
uint64 encumberedMagnitude,
uint32 effectBlock
) internal {
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit EncumberedMagnitudeUpdated(operator, strategy, encumberedMagnitude);
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit AllocationUpdated(operator, operatorSet, strategy, magnitude, effectBlock);
}
function _checkDeallocationEvent(
address operator,
OperatorSet memory operatorSet,
IStrategy strategy,
uint64 magnitude,
uint32 effectBlock
) internal {
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit AllocationUpdated(operator, operatorSet, strategy, magnitude, effectBlock);
}
function _checkClearDeallocationQueueEvents(address operator, IStrategy strategy, uint64 encumberedMagnitude) internal {
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit EncumberedMagnitudeUpdated(operator, strategy, encumberedMagnitude);
}
function _checkSlashEvents(
address operator,
OperatorSet memory operatorSet,
IStrategy strategy,
uint wadToSlash,
string memory description,
uint64 currentMag,
uint64 maxMag,
uint64 encumberedMag
) internal {
return _checkSlashEvents(
operator,
operatorSet,
strategy.toArray(),
wadToSlash.toArrayU256(),
description,
currentMag.toArrayU64(),
maxMag.toArrayU64(),
encumberedMag.toArrayU64()
);
}
function _checkSlashEvents(
address operator,
OperatorSet memory operatorSet,
IStrategy[] memory strategies,
uint[] memory wadToSlash,
string memory description,
uint64[] memory currentMags,
uint64[] memory maxMags,
uint64[] memory encumberedMags
) internal {
for (uint i = 0; i < strategies.length; i++) {
// If there is nothing slashed, we don't emit events for encumbered magnitude
if (wadToSlash[i] == 0) continue;
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit EncumberedMagnitudeUpdated(operator, strategies[i], encumberedMags[i]);
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit AllocationUpdated(operator, operatorSet, strategies[i], currentMags[i], uint32(block.number));
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit MaxMagnitudeUpdated(operator, strategies[i], maxMags[i]);
}
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit OperatorSlashed(operator, operatorSet, strategies, wadToSlash, description);
}
/// -----------------------------------------------------------------------
/// Allocate/deallocate params
/// -----------------------------------------------------------------------
/// @dev Create allocate params, allocating `magnitude` to each strategy in the set
function _newAllocateParams(OperatorSet memory operatorSet, uint64 magnitude) internal view returns (AllocateParams[] memory) {
IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet);
uint64[] memory newMagnitudes = new uint64[](strategies.length);
for (uint i; i < strategies.length; ++i) {
newMagnitudes[i] = magnitude;
}
return AllocateParams({operatorSet: operatorSet, strategies: strategies, newMagnitudes: newMagnitudes}).toArray();
}
/// @dev Create allocate params for multiple operator sets
function _newAllocateParams(OperatorSet[] memory operatorSets, uint64 magnitude) internal view returns (AllocateParams[] memory) {
AllocateParams[] memory allocateParams = new AllocateParams[](operatorSets.length);
for (uint i; i < operatorSets.length; ++i) {
allocateParams[i] = _newAllocateParams(operatorSets[i], magnitude)[0];
}
return allocateParams;
}
/// @dev Create random allocation params to the default operator set and strategy
function _randAllocateParams_DefaultOpSet() internal returns (AllocateParams[] memory) {
return _randAllocateParams_SingleMockStrategy(defaultOperatorSet.toArray());
}
/// @dev Create allocate params for random magnitudes to the same default strategy across multiple operator sets
function _randAllocateParams_SingleMockStrategy(OperatorSet[] memory operatorSets) internal returns (AllocateParams[] memory) {
// Give each set a minimum of 1 magnitude
uint64[] memory magnitudes = new uint64[](operatorSets.length);
uint64 usedMagnitude;
for (uint8 i = 0; i < magnitudes.length; ++i) {
magnitudes[i] = 1;
usedMagnitude++;
}
// Distribute remaining magnitude
uint64 maxMagnitude = WAD;
for (uint8 i = 0; i < magnitudes.length; ++i) {
uint64 remainingMagnitude = maxMagnitude - usedMagnitude;
if (remainingMagnitude > 0) {
magnitudes[i] += uint64(random().Uint256(0, remainingMagnitude));
usedMagnitude += magnitudes[i] - 1;
}
}
AllocateParams[] memory params = new AllocateParams[](magnitudes.length);
for (uint i; i < params.length; ++i) {
params[i] =
AllocateParams({operatorSet: operatorSets[i], strategies: defaultStrategies, newMagnitudes: magnitudes[i].toArrayU64()});
}
return params;
}
/// @dev Create allocate params for random magnitudes to the same default strategy across multiple operator sets
/// NOTE: this variant allocates ALL magnitude (1 WAD)
function _randAllocateParams_SingleMockStrategy_AllocAll(OperatorSet[] memory operatorSets)
internal
returns (AllocateParams[] memory)
{
// Give each set a minimum of 1 magnitude
uint64[] memory magnitudes = new uint64[](operatorSets.length);
uint64 usedMagnitude;
for (uint8 i = 0; i < magnitudes.length; ++i) {
magnitudes[i] = 1;
usedMagnitude++;
}
// Distribute remaining magnitude
uint64 maxMagnitude = WAD;
for (uint8 i = 0; i < magnitudes.length; ++i) {
uint64 remainingMagnitude = maxMagnitude - usedMagnitude;
if (remainingMagnitude > 0) {
magnitudes[i] += uint64(random().Uint64(0, remainingMagnitude));
usedMagnitude += magnitudes[i] - 1;
}
}
// If there's any left, dump it on a random set
uint64 magnitudeLeft = maxMagnitude - usedMagnitude;
if (magnitudeLeft > 0) {
uint randIdx = random().Uint256(0, magnitudes.length - 1);
magnitudes[randIdx] += magnitudeLeft;
usedMagnitude += magnitudeLeft;
}
AllocateParams[] memory params = new AllocateParams[](magnitudes.length);
for (uint i; i < params.length; ++i) {
params[i] =
AllocateParams({operatorSet: operatorSets[i], strategies: defaultStrategies, newMagnitudes: magnitudes[i].toArrayU64()});
}
return params;
}
/// @dev Create allocate/deallocate params to the same default strategy across multiple sets
function _randAllocAndDeallocParams_SingleMockStrategy(OperatorSet[] memory operatorSets)
internal
returns (AllocateParams[] memory, AllocateParams[] memory)
{
AllocateParams[] memory allocateParams = _randAllocateParams_SingleMockStrategy(operatorSets);
AllocateParams[] memory deallocateParams = new AllocateParams[](allocateParams.length);
// Generate a random deallocation for each operator set
for (uint i; i < deallocateParams.length; ++i) {
deallocateParams[i] = AllocateParams({
operatorSet: allocateParams[i].operatorSet,
strategies: allocateParams[i].strategies,
newMagnitudes: uint64(random().Uint256({min: 0, max: allocateParams[i].newMagnitudes[0] - 1})).toArrayU64()
});
}
return (allocateParams, deallocateParams);
}
/// -----------------------------------------------------------------------
/// Utils
/// -----------------------------------------------------------------------
function _maxNumToClear() internal pure returns (uint16[] memory) {
uint16[] memory numToClear = new uint16[](1);
numToClear[0] = type(uint16).max;
return numToClear;
}
function _defaultAllocEffectBlock() internal view returns (uint32) {
return uint32(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
}
}
contract AllocationManagerUnitTests_Initialization_Setters is AllocationManagerUnitTests {
/// -----------------------------------------------------------------------
/// initialize()
/// -----------------------------------------------------------------------
/// @dev Asserts the following:
/// 1. The fn can only be called once, during deployment.
/// 2. The fn initializes the contract state correctly (owner, pauserRegistry, and initialPausedStatus).
function testFuzz_Initialize(Randomness r) public rand(r) {
// Generate random values for the expected initial state of the contract.
address expectedInitialOwner = r.Address();
IPauserRegistry expectedPauserRegistry = IPauserRegistry(r.Address());
// Deploy the contract with the expected initial state.
uint initialPausedStatus = r.Uint256();
AllocationManager alm = _initializeAllocationManager(expectedInitialOwner, expectedPauserRegistry, initialPausedStatus);
// Assert that the contract can only be initialized once.
vm.expectRevert("Initializable: contract is already initialized");
alm.initialize(expectedInitialOwner, initialPausedStatus);
// Assert immutable state
assertEq(address(alm.delegation()), address(delegationManagerMock));
assertEq(alm.DEALLOCATION_DELAY(), DEALLOCATION_DELAY);
assertEq(alm.ALLOCATION_CONFIGURATION_DELAY(), ALLOCATION_CONFIGURATION_DELAY);
// Assert initialization state
assertEq(alm.owner(), expectedInitialOwner);
assertEq(alm.paused(), initialPausedStatus);
}
}
contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests {
using ArrayLib for *;
using SlashingLib for *;
/// -----------------------------------------------------------------------
/// slashOperator()
/// -----------------------------------------------------------------------
function _randSlashingParams(address operator, uint32 operatorSetId) internal returns (SlashingParams memory) {
return SlashingParams({
operator: operator,
operatorSetId: operatorSetId,
strategies: defaultStrategies,
wadsToSlash: random().Uint256(1, WAD).toArrayU256(),
description: "test"
});
}
function test_revert_paused() public {
allocationManager.pause(2 ** PAUSED_OPERATOR_SLASHING);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
allocationManager.slashOperator(defaultAVS, _randSlashingParams(defaultOperator, 0));
}
function test_revert_slashZero() public {
SlashingParams memory slashingParams = _randSlashingParams(defaultOperator, 0);
slashingParams.wadsToSlash[0] = 0;
cheats.prank(defaultAVS);
cheats.expectRevert(InvalidWadToSlash.selector);
allocationManager.slashOperator(defaultAVS, slashingParams);
}
function test_revert_slashGreaterThanWAD() public {
SlashingParams memory slashingParams = _randSlashingParams(defaultOperator, 0);
slashingParams.wadsToSlash[0] = WAD + 1;
cheats.prank(defaultAVS);
cheats.expectRevert(InvalidWadToSlash.selector);
allocationManager.slashOperator(defaultAVS, slashingParams);
}
function test_revert_NotRegisteredToSet() public {
cheats.prank(defaultAVS);
cheats.expectRevert(OperatorNotSlashable.selector);
allocationManager.slashOperator(defaultAVS, _randSlashingParams(random().Address(), 0));
}
function test_revert_NotMemberOfSet() public {
cheats.prank(defaultAVS);
cheats.expectRevert(OperatorNotSlashable.selector);
allocationManager.slashOperator(defaultAVS, _randSlashingParams(random().Address(), 0));
}
function test_revert_InputArrayLengthMismatch() public {
SlashingParams memory slashingParams = _randSlashingParams(defaultOperator, 0);
slashingParams.strategies = slashingParams.strategies.setLength(2);
cheats.prank(defaultAVS);
cheats.expectRevert(InputArrayLengthMismatch.selector);
allocationManager.slashOperator(defaultAVS, slashingParams);
}
function test_revert_StrategiesMustBeInAscendingOrder() public {
IStrategy[] memory strategies = new IStrategy[](3);
strategies[0] = IStrategy(address(3));
strategies[1] = IStrategy(address(2));
strategies[2] = IStrategy(address(1));
_createOperatorSet(OperatorSet(defaultAVS, 1), strategies);
_registerForOperatorSet(defaultOperator, OperatorSet(defaultAVS, 1));
cheats.prank(defaultAVS);
cheats.expectRevert(StrategiesMustBeInAscendingOrder.selector);
allocationManager.slashOperator(
defaultAVS,
SlashingParams({
operator: defaultOperator,
operatorSetId: 1,
strategies: strategies,
wadsToSlash: uint(0.5 ether).toArrayU256(3),
description: "test"
})
);
}
function test_revert_StrategyNotInOperatorSet() public {
IStrategy[] memory strategies = new IStrategy[](3);
strategies[0] = IStrategy(address(1));
strategies[1] = IStrategy(address(2));
strategies[2] = IStrategy(address(3));
_createOperatorSet(OperatorSet(defaultAVS, 1), strategies);
_registerForOperatorSet(defaultOperator, OperatorSet(defaultAVS, 1));
strategies = strategies.setLength(4);
strategies[3] = IStrategy(address(4));
cheats.prank(defaultAVS);
cheats.expectRevert(StrategyNotInOperatorSet.selector);
allocationManager.slashOperator(
defaultAVS,
SlashingParams({
operator: defaultOperator,
operatorSetId: 1,
strategies: defaultStrategies,
wadsToSlash: uint(0.5 ether).toArrayU256(),
description: "test"
})
);
}
/**
* Attempts to slash an operator before the allocation is active
* Validates:
* 1. The events of the slash indicate nothing was slashed
* 2. Storage is not mutated post slash
* 3. The operator's allocation takes effect as normal post slash
*/
function test_operatorAllocated_notActive() public {
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, WAD);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
uint64 encumberedMagnitudeBefore = allocationManager.getEncumberedMagnitude(defaultOperator, strategyMock);
uint64 maxMagnitudeBefore = allocationManager.getMaxMagnitudes(defaultOperator, strategyMock.toArray())[0];
// The only slash event we expect is the OperatorSlashed. Validate the number
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit OperatorSlashed(defaultOperator, defaultOperatorSet, defaultStrategies, uint(0).toArrayU256(), "test");
uint numLogsBefore = cheats.getRecordedLogs().length;
cheats.prank(defaultAVS);
allocationManager.slashOperator(
defaultAVS,
SlashingParams({
operator: defaultOperator,
operatorSetId: allocateParams[0].operatorSet.id,
strategies: defaultStrategies,
wadsToSlash: WAD.toArrayU256(),
description: "test"
})
);
uint numLogsAfter = cheats.getRecordedLogs().length;
// Assert only 1 log was emitted
assertEq(numLogsAfter, numLogsBefore + 1, "Incorrect number of logs emitted");
// Assert encumberedMagnitude and maxMagnitude are unchanged
assertEq(
encumberedMagnitudeBefore,
allocationManager.getEncumberedMagnitude(defaultOperator, strategyMock),
"encumberedMagnitude mutated"
);
assertEq(maxMagnitudeBefore, allocationManager.getMaxMagnitudes(defaultOperator, strategyMock.toArray())[0], "maxMagnitude mutated");
// Roll to effect block and validate allocation
uint32 effectBlock = uint32(block.number) + DEFAULT_OPERATOR_ALLOCATION_DELAY;
uint64 pendingIncrease = allocateParams[0].newMagnitudes[0];
cheats.roll(effectBlock);
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: pendingIncrease, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({encumbered: pendingIncrease, max: WAD, allocatable: WAD - pendingIncrease})
});
}
/**
* Allocates all magnitude to for a single strategy to an operatorSet. Slashes 25%
* Validates:
* 1. Events are emitted
* 2. Allocation & info introspection
* 3. Slashable stake introspection
*/
function test_slashPostAllocation() public {
// Generate allocation for this operator set, we allocate max
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, WAD);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
_checkSlashEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
wadToSlash: uint(25e16),
description: "test",
currentMag: uint64(75e16),
maxMag: uint64(75e16),
encumberedMag: uint64(75e16)
});
// Slash operator for 25%
cheats.prank(defaultAVS);
allocationManager.slashOperator(
defaultAVS,
SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: 25e16.toArrayU256(),
description: "test"
})
);
// Check storage
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: 75e16, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({encumbered: 75e16, max: 75e16, allocatable: 0})
});
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: DEFAULT_OPERATOR_SHARES.mulWad(75e16)
});
}
/// @notice Same test as above, but fuzzes the allocation
function testFuzz_slashPostAllocation(Randomness r) public rand(r) {
AllocateParams[] memory allocateParams = _randAllocateParams_DefaultOpSet();
// Allocate magnitude and roll forward to completable block
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
SlashingParams memory slashingParams = _randSlashingParams(defaultOperator, defaultOperatorSet.id);
(uint expectedWadSlashed, uint64 expectedCurrentMag, uint64 expectedMaxMag, uint64 expectedEncumberedMag) =
_getExpectedSlashVals({magBeforeSlash: allocateParams[0].newMagnitudes[0], wadToSlash: slashingParams.wadsToSlash[0]});
_checkSlashEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
wadToSlash: expectedWadSlashed,
description: "test",
currentMag: expectedCurrentMag,
maxMag: expectedMaxMag,
encumberedMag: expectedEncumberedMag
});
// Slash Operator
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams);
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: expectedCurrentMag, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: expectedEncumberedMag,
max: expectedMaxMag,
allocatable: expectedMaxMag - expectedEncumberedMag
})
});
uint slashedStake = DEFAULT_OPERATOR_SHARES.mulWad(expectedWadSlashed); // Wad is same as slashed mag since we start with max mag
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: (DEFAULT_OPERATOR_SHARES - slashedStake).mulWad(expectedCurrentMag.divWad(expectedMaxMag))
});
}
/**
* Allocates half of magnitude for a single strategy to an operatorSet. Then allocates again. Slashes 50%
* Asserts that:
* 1. Events are emitted
* 2. Encumbered mag is updated
* 3. Max mag is updated
* 4. Calculations for `getAllocatableMagnitude` and `getAllocation` are correct
* 5. The second allocation is not slashed from
*/
function testFuzz_slash_oneCompletedAlloc_onePendingAlloc(Randomness r) public rand(r) {
uint wadToSlash = r.Uint256(0.01 ether, WAD);
// Generate allocation for `strategyMock`, we allocate half
{
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, 5e17);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
}
// Allocate the other half
AllocateParams[] memory allocateParams2 = _newAllocateParams(defaultOperatorSet, WAD);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams2);
uint32 secondAllocEffectBlock = _defaultAllocEffectBlock();
// Slash operator for 50%
SlashingParams memory slashingParams = SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: wadToSlash.toArrayU256(),
description: "test"
});
(uint expectedWadSlashed, uint64 expectedCurrentMag, uint64 expectedMaxMag, uint64 expectedEncumberedMag) =
_getExpectedSlashVals({magBeforeSlash: 5e17, encumberedMagBeforeSlash: WAD, wadToSlash: wadToSlash});
uint slashedStake = DEFAULT_OPERATOR_SHARES.mulWad(expectedWadSlashed); // Wad is same as slashed mag since we start with max mag
uint newTotalStake = DEFAULT_OPERATOR_SHARES - slashedStake;
_checkSlashEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
wadToSlash: expectedWadSlashed,
description: "test",
currentMag: expectedCurrentMag,
maxMag: expectedMaxMag,
encumberedMag: expectedEncumberedMag
});
// Slash Operator
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams);
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: expectedCurrentMag, pendingDiff: 5e17, effectBlock: secondAllocEffectBlock}),
expectedMagnitudes: Magnitudes({encumbered: expectedEncumberedMag, max: expectedMaxMag, allocatable: 0})
});
// Slashable stake should include first allocation and slashed magnitude
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: newTotalStake.mulWad(expectedCurrentMag.divWad(expectedMaxMag))
});
cheats.roll(secondAllocEffectBlock);
uint64 newSlashableMagnitude = expectedCurrentMag + 5e17;
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: newSlashableMagnitude, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({encumbered: expectedEncumberedMag, max: expectedMaxMag, allocatable: 0})
});
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: newTotalStake.mulWad(newSlashableMagnitude.divWad(expectedMaxMag))
});
}
/**
* Allocates 100% magnitude for a single strategy to an operatorSet.
* First slashes 99% from the operatorSet, slashes 99.99% a second time, and on the third slash, slashes
* 99.9999999999999% which should get rounded up to 100% or WAD wadSlashed leaving the operator with no magnitude
* in the operatorSet, 0 encumbered magnitude, and 0 max magnitude.
*
* Asserts that:
* 1. Events are emitted
* 2. Storage properly updated after each slash
* 3. Slashed amounts are rounded up to ensure magnitude is always slashed
*/
function test_repeatUntilFullSlash() public {
// Generate allocation for `strategyMock`, we allocate 100% to opSet 0
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, WAD);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// 1. Slash operator for 99% in opSet 0 bringing their magnitude to 1e16
SlashingParams memory slashingParams = SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: 99e16.toArrayU256(),
description: "test"
});
uint64 expectedEncumberedMagnitude = 1e16; // After slashing 99%, only 1% expected encumberedMagnitude
uint64 magnitudeAfterSlash = 1e16;
uint64 maxMagnitudeAfterSlash = 1e16; // 1e15 is maxMagnitude
_checkSlashEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
wadToSlash: uint(99e16),
description: "test",
currentMag: magnitudeAfterSlash,
maxMag: maxMagnitudeAfterSlash,
encumberedMag: expectedEncumberedMagnitude
});
// Slash Operator
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams);
// Check storage
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: magnitudeAfterSlash, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: expectedEncumberedMagnitude,
max: maxMagnitudeAfterSlash,
allocatable: maxMagnitudeAfterSlash - expectedEncumberedMagnitude
})
});
// 2. Slash operator again for 99.99% in opSet 0 bringing their magnitude to 1e14
slashingParams = SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: 9999e14.toArrayU256(),
description: "test"
});
expectedEncumberedMagnitude = 1e12; // After slashing 99.99%, only 0.01% expected encumberedMagnitude
magnitudeAfterSlash = 1e12;
maxMagnitudeAfterSlash = 1e12;
_checkSlashEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
wadToSlash: uint(9999e14),
description: "test",
currentMag: magnitudeAfterSlash,
maxMag: maxMagnitudeAfterSlash,
encumberedMag: expectedEncumberedMagnitude
});
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams);
// Check storage
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: magnitudeAfterSlash, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: expectedEncumberedMagnitude,
max: maxMagnitudeAfterSlash,
allocatable: maxMagnitudeAfterSlash - expectedEncumberedMagnitude
})
});
// 3. Slash operator again for 99.9999999999999% in opSet 0
slashingParams = SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: uint(WAD - 1e3).toArrayU256(),
description: "test"
});
// Should technically be 1e3 remaining but with rounding error and rounding up slashed amounts
// the remaining magnitude is 0
expectedEncumberedMagnitude = 0; // Should technically be 1e3 remaining but with rounding error and rounding up slashed amounts.
magnitudeAfterSlash = 0;
maxMagnitudeAfterSlash = 0;
_checkSlashEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
wadToSlash: WAD,
description: "test",
currentMag: magnitudeAfterSlash,
maxMag: maxMagnitudeAfterSlash,
encumberedMag: expectedEncumberedMagnitude
});
// Slash Operator
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams);
// Check storage
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: 0, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({encumbered: 0, max: 0, allocatable: 0})
});
// Check slashable amount after final slash
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: 0
});
}
/**
* Allocates all of magnitude to a single strategy to an operatorSet. Deallocate half. Finally, slash while deallocation is pending
* Asserts that:
* 1. Events are emitted, including for deallocation
* 2. Encumbered mag is updated
* 3. Max mag is updated
* 4. Calculations for `getAllocatableMagnitude` and `getAllocation` are correct
* 5. The deallocation is slashed from
* 6. Pending magnitude updates post deallocation are valid
*/
function testFuzz_SlashWhileDeallocationPending(Randomness r) public rand(r) {
// Initialize state
AllocateParams[] memory allocateParams = r.AllocateParams(defaultAVS, 1, 1);
AllocateParams[] memory deallocateParams = r.DeallocateParams(allocateParams);
CreateSetParams[] memory createSetParams = r.CreateSetParams(allocateParams);
RegisterParams memory registerParams = r.RegisterParams(allocateParams);
SlashingParams memory slashingParams = r.SlashingParams(defaultOperator, allocateParams[0]);
delegationManagerMock.setOperatorShares(defaultOperator, allocateParams[0].strategies[0], DEFAULT_OPERATOR_SHARES);
cheats.prank(defaultAVS);
allocationManager.createOperatorSets(defaultAVS, createSetParams);
cheats.startPrank(defaultOperator);
allocationManager.registerForOperatorSets(defaultOperator, registerParams);
// Allocate
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate
allocationManager.modifyAllocations(defaultOperator, deallocateParams);
uint32 deallocationEffectBlock = uint32(block.number + DEALLOCATION_DELAY + 1);
cheats.stopPrank();
uint magnitudeAllocated = allocateParams[0].newMagnitudes[0];
uint magnitudeDeallocated = magnitudeAllocated - deallocateParams[0].newMagnitudes[0];
uint magnitudeSlashed = (magnitudeAllocated * slashingParams.wadsToSlash[0] / WAD)
+ _calculateSlippage(uint64(magnitudeAllocated), slashingParams.wadsToSlash[0]);
uint expectedCurrentMagnitude = magnitudeAllocated - magnitudeSlashed;
int128 expectedPendingDiff =
-int128(uint128(magnitudeDeallocated - magnitudeDeallocated.mulWadRoundUp(slashingParams.wadsToSlash[0])));
// Manually check slash events since we have a deallocation pending
// Deallocation update is emitted first
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit AllocationUpdated(
defaultOperator,
allocateParams[0].operatorSet,
allocateParams[0].strategies[0],
uint64(uint128(int128(uint128(expectedCurrentMagnitude)) + expectedPendingDiff)),
deallocationEffectBlock
);
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit EncumberedMagnitudeUpdated(defaultOperator, allocateParams[0].strategies[0], uint64(magnitudeAllocated - magnitudeSlashed));
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit AllocationUpdated(
defaultOperator,
allocateParams[0].operatorSet,
allocateParams[0].strategies[0],
uint64(expectedCurrentMagnitude),
uint32(block.number)
);
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit MaxMagnitudeUpdated(defaultOperator, allocateParams[0].strategies[0], uint64(WAD - magnitudeSlashed));
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit OperatorSlashed(
defaultOperator, allocateParams[0].operatorSet, allocateParams[0].strategies, magnitudeSlashed.toArrayU256(), ""
);
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams);
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: allocateParams[0].operatorSet,
strategy: allocateParams[0].strategies[0],
expectedAllocation: Allocation({
currentMagnitude: uint64(expectedCurrentMagnitude),
pendingDiff: expectedPendingDiff,
effectBlock: deallocationEffectBlock
}),
expectedMagnitudes: Magnitudes({encumbered: expectedCurrentMagnitude, max: uint64(WAD - magnitudeSlashed), allocatable: 0})
});
cheats.roll(deallocationEffectBlock);
allocationManager.clearDeallocationQueue(defaultOperator, allocateParams[0].strategies, _maxNumToClear());
uint64 newMag = uint64(uint128(int128(uint128(expectedCurrentMagnitude)) + expectedPendingDiff));
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: allocateParams[0].operatorSet,
strategy: allocateParams[0].strategies[0],
expectedAllocation: Allocation({currentMagnitude: newMag, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: newMag,
max: uint64(WAD - magnitudeSlashed),
allocatable: uint128(-expectedPendingDiff) // This works because we allocated all in the randomization allocation helper
})
});
}
/**
* Allocates all magnitude to a single opSet. Then slashes the entire magnitude
* Validates:
* 1. Storage post slash
* 2. The operator cannot allocate again
*/
function testRevert_allocateAfterSlashedEntirely() public {
// Allocate all magnitude
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, WAD);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
_checkSlashEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
wadToSlash: WAD,
description: "test",
currentMag: 0,
maxMag: 0,
encumberedMag: 0
});
// Slash operator for 100%
cheats.prank(defaultAVS);
allocationManager.slashOperator(
defaultAVS,
SlashingParams({
operator: defaultOperator,
operatorSetId: allocateParams[0].operatorSet.id,
strategies: defaultStrategies,
wadsToSlash: WAD.toArrayU256(),
description: "test"
})
);
// Validate storage post slash
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: 0, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({encumbered: 0, max: 0, allocatable: 0})
});
OperatorSet memory operatorSet = _createOperatorSet(OperatorSet(defaultAVS, random().Uint32()), defaultStrategies);
AllocateParams[] memory allocateParams2 = _newAllocateParams(operatorSet, 1);
// Attempt to allocate
cheats.prank(defaultOperator);
cheats.expectRevert(InsufficientMagnitude.selector);
allocationManager.modifyAllocations(defaultOperator, allocateParams2);
}
/**
* Allocates all magnitude to a single opSet. Deallocates magnitude. Slashes all
* Asserts that:
* 1. The Allocation is 0 after slash
* 2. Them storage post slash for encumbered and maxMags is zero
*/
function test_slash_allocateAll_deallocateAll() public {
// Allocate all magnitude
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, _newAllocateParams(defaultOperatorSet, WAD));
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate all
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, _newAllocateParams(defaultOperatorSet, 0));
// Validate event for the deallocation
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit AllocationUpdated(defaultOperator, defaultOperatorSet, strategyMock, 0, uint32(block.number + DEALLOCATION_DELAY + 1));
// Slash operator for 100%
cheats.prank(defaultAVS);
allocationManager.slashOperator(
defaultAVS,
SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: WAD.toArrayU256(),
description: "test"
})
);
// forgefmt: disable-next-item
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: 0, pendingDiff: 0, effectBlock: uint32(block.number) + DEALLOCATION_DELAY + 1}),
expectedMagnitudes: Magnitudes({encumbered: 0, max: 0, allocatable: 0})
});
// Complete deallocation
cheats.roll(uint32(block.number) + DEALLOCATION_DELAY + 1);
allocationManager.clearDeallocationQueue(defaultOperator, defaultStrategies, _maxNumToClear());
// Validate allocatable amount is 0
assertEq(0, allocationManager.getAllocatableMagnitude(defaultOperator, strategyMock), "Allocatable magnitude should be 0");
}
/**
* Slashes the operator after deallocation, even if the deallocation has not been cleared.
* Validates that:
* 1. Even if we do not clear deallocation queue, the deallocation is NOT slashed from since we're passed the deallocationEffectBlock
* 2. Validates storage post slash & post clearing deallocation queue
* 3. Max magnitude only decreased proportionally by the magnitude set after deallocation
*/
function test_allocate_deallocate_slashAfterDeallocation() public {
// Allocate all magnitude
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, WAD);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate half
AllocateParams[] memory deallocateParams = _newAllocateParams(defaultOperatorSet, 5e17);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, deallocateParams);
uint32 deallocationEffectBlock = uint32(block.number + DEALLOCATION_DELAY + 1);
// Warp to deallocation effect block
cheats.roll(deallocationEffectBlock);
// Slash operator for 25%
SlashingParams memory slashingParams = SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: 25e16.toArrayU256(),
description: "test"
});
uint64 expectedEncumberedMagnitude = 375e15; // 25e16 is slashed. 5e17 was previously
uint64 magnitudeAfterSlash = 375e15;
uint64 maxMagnitudeAfterSlash = 875e15; // Operator can only allocate up to 75e16 magnitude since 25% is slashed
uint expectedSlashedMagnitude = SlashingLib.mulWadRoundUp(5e17, 25e16);
// Slash Operator, only emit events assuming that there is no deallocation
_checkSlashEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
wadToSlash: expectedSlashedMagnitude,
description: "test",
currentMag: magnitudeAfterSlash,
maxMag: maxMagnitudeAfterSlash,
encumberedMag: expectedEncumberedMagnitude
});
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams);
allocationManager.clearDeallocationQueue(defaultOperator, defaultStrategies, _maxNumToClear());
uint64 allocatableMagnitudeAfterSlash = allocationManager.getAllocatableMagnitude(defaultOperator, strategyMock);
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: magnitudeAfterSlash, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: expectedEncumberedMagnitude,
max: maxMagnitudeAfterSlash,
allocatable: allocatableMagnitudeAfterSlash
})
});
}
/**
* Allocates to multiple operatorSets for a strategy. Only slashes from one operatorSet.
* Validates:
* 1. The first operatorSet has less slashable shares post slash
* 2. The second operatorSet has the same number slashable shares post slash (within slippage)
* 3. The PROPORTION that is slashable for opSet 2 has increased
*/
function testFuzz_allocateMultipleOpsets_slashSingleOpset(Randomness r) public rand(r) {
// Get magnitude to allocate
uint64 magnitudeToAllocate = r.Uint64(1, 5e17);
uint wadToSlash = r.Uint256(1, 1e18);
OperatorSet memory operatorSet = OperatorSet(defaultAVS, 1);
OperatorSet memory operatorSet2 = OperatorSet(defaultAVS, 2);
// Allocate 40% to firstOperatorSet, 40% to secondOperatorSet
AllocateParams[] memory allocateParams = new AllocateParams[](2);
allocateParams[0] = _newAllocateParams(_createOperatorSet(OperatorSet(defaultAVS, 1), defaultStrategies), magnitudeToAllocate)[0];
allocateParams[1] = _newAllocateParams(_createOperatorSet(OperatorSet(defaultAVS, 2), defaultStrategies), magnitudeToAllocate)[0];
// Register operator for both operatorSets
_registerForOperatorSet(defaultOperator, operatorSet);
_registerForOperatorSet(defaultOperator, operatorSet2);
// Modify allocations
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Get slashable shares for each operatorSet
uint[][] memory opset2SlashableSharesBefore =
allocationManager.getMinimumSlashableStake(operatorSet2, defaultOperator.toArray(), defaultStrategies, uint32(block.number));
// Slash operator on operatorSet1 for 50%
SlashingParams memory slashingParams = SlashingParams({
operator: defaultOperator,
operatorSetId: allocateParams[0].operatorSet.id,
strategies: defaultStrategies,
wadsToSlash: wadToSlash.toArrayU256(),
description: "test"
});
(, uint64 expectedCurrentMag, uint64 expectedMaxMag, uint64 expectedEncumberedMag) = _getExpectedSlashVals({
magBeforeSlash: allocateParams[0].newMagnitudes[0],
wadToSlash: slashingParams.wadsToSlash[0],
encumberedMagBeforeSlash: allocateParams[0].newMagnitudes[0] * 2
});
// Slash Operator
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams);
// Validate storage operatorSet1
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: operatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: expectedCurrentMag, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: expectedEncumberedMag,
max: expectedMaxMag,
allocatable: expectedMaxMag - expectedEncumberedMag
})
});
// Validate storage for operatorSet2
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: operatorSet2,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: magnitudeToAllocate, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: expectedEncumberedMag,
max: expectedMaxMag,
allocatable: expectedMaxMag - expectedEncumberedMag
})
});
// Check proportion after slash
uint opSet2PortionOfMaxMagnitudeAfterSlash = uint(magnitudeToAllocate) * WAD / expectedMaxMag;
assertGt(
opSet2PortionOfMaxMagnitudeAfterSlash,
magnitudeToAllocate, // This is the same as proportion before slash
"opSet2 should have a greater proportion to slash from previous"
);
// Assert that slashable stake is the same - we add slippage here due to rounding error from the slash itself
uint[][] memory minSlashableStake =
allocationManager.getMinimumSlashableStake(operatorSet2, defaultOperator.toArray(), defaultStrategies, uint32(block.number));
assertEq(opset2SlashableSharesBefore[0][0], minSlashableStake[0][0] + 1, "opSet2 slashable shares should be the same");
}
/**
* Allocates to multiple strategies for the given operatorSetKey. Slashes from both strategies Validates a slash propagates to both strategies.
* Validates that
* 1. Proper events are emitted for each strategy slashed
* 2. Each strategy is slashed proportional to its allocation
* 3. Storage is updated for each strategy, opSet
*/
function testFuzz_allocateMultipleStrategies_slashMultiple(Randomness r) public rand(r) {
// Initialize random params
uint64 strategy1Magnitude = r.Uint64(1, 1e18);
uint64 strategy2Magnitude = r.Uint64(1, 1e18);
uint wadToSlash = r.Uint256(1, 1e18);
// Crate and allocate to operatorSets
OperatorSet memory operatorSet = OperatorSet(defaultAVS, random().Uint32());
_createOperatorSet(operatorSet, random().StrategyArray(2));
_registerForOperatorSet(defaultOperator, operatorSet);
IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet);
uint[] memory wadsToSlash = new uint[](strategies.length);
{
if (strategies[1] < strategies[0]) {
IStrategy temp = strategies[0];
strategies[0] = strategies[1];
strategies[1] = temp;
}
AllocateParams memory allocateParams =
AllocateParams({operatorSet: operatorSet, strategies: strategies, newMagnitudes: new uint64[](2)});
allocateParams.newMagnitudes[0] = strategy1Magnitude;
allocateParams.newMagnitudes[1] = strategy2Magnitude;
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams.toArray());
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
for (uint i = 0; i < strategies.length; i++) {
wadsToSlash[i] = wadToSlash;
}
}
// Store post-slash vars to check against
uint64[] memory expectedEncumberedMags = new uint64[](2);
uint[] memory expectedSlashedMagnitude = new uint[](2);
uint64[] memory expectedMagnitudeAfterSlash = new uint64[](2);
uint64[] memory expectedMaxMagnitudeAfterSlash = new uint64[](2);
{
(
uint strat1ExpectedWadSlashed,
uint64 strat1ExpectedCurrentMag,
uint64 strat1ExpectedMaxMag,
uint64 strat1ExpectedEncumberedMag
) = _getExpectedSlashVals({magBeforeSlash: strategy1Magnitude, wadToSlash: wadToSlash});
expectedEncumberedMags[0] = strat1ExpectedEncumberedMag;
expectedSlashedMagnitude[0] = strat1ExpectedWadSlashed;
expectedMagnitudeAfterSlash[0] = strat1ExpectedCurrentMag;
expectedMaxMagnitudeAfterSlash[0] = strat1ExpectedMaxMag;
}
{
(
uint strat2ExpectedWadSlashed,
uint64 strat2ExpectedCurrentMag,
uint64 strat2ExpectedMaxMag,
uint64 strat2ExpectedEncumberedMag
) = _getExpectedSlashVals({magBeforeSlash: strategy2Magnitude, wadToSlash: wadToSlash});
expectedEncumberedMags[1] = strat2ExpectedEncumberedMag;
expectedSlashedMagnitude[1] = strat2ExpectedWadSlashed;
expectedMagnitudeAfterSlash[1] = strat2ExpectedCurrentMag;
expectedMaxMagnitudeAfterSlash[1] = strat2ExpectedMaxMag;
}
_checkSlashEvents({
operator: defaultOperator,
operatorSet: operatorSet,
strategies: strategies,
wadToSlash: expectedSlashedMagnitude,
description: "test",
currentMags: expectedMagnitudeAfterSlash,
maxMags: expectedMaxMagnitudeAfterSlash,
encumberedMags: expectedEncumberedMags
});
// Slash Operator
{
SlashingParams memory slashingParams = SlashingParams({
operator: defaultOperator,
operatorSetId: operatorSet.id,
strategies: strategies,
wadsToSlash: wadsToSlash,
description: "test"
});
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams);
}
// Check storage
for (uint i; i < strategies.length; ++i) {
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: operatorSet,
strategy: strategies[i],
expectedAllocation: Allocation({currentMagnitude: expectedMagnitudeAfterSlash[i], pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: expectedEncumberedMags[i],
max: expectedMaxMagnitudeAfterSlash[i],
allocatable: expectedMaxMagnitudeAfterSlash[i] - expectedEncumberedMags[i]
})
});
}
}
/// @dev Allocates magnitude. Deallocates some. Slashes a portion, and then allocates up to the max available magnitude
function testFuzz_allocate_deallocate_allocateMax(Randomness r) public rand(r) {
AllocateParams[] memory allocateParams = r.AllocateParams({avs: defaultAVS, numAllocations: 1, numStrats: 1});
AllocateParams[] memory deallocateParams = r.DeallocateParams(allocateParams);
CreateSetParams[] memory createSetParams = r.CreateSetParams(allocateParams);
OperatorSet memory operatorSet = allocateParams[0].operatorSet;
IStrategy strategy = allocateParams[0].strategies[0];
cheats.prank(defaultAVS);
allocationManager.createOperatorSets(defaultAVS, createSetParams);
_registerForOperatorSet(defaultOperator, operatorSet);
// Allocate some magnitude, then deallocate some.
cheats.startPrank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
allocationManager.modifyAllocations(defaultOperator, deallocateParams);
cheats.roll(block.number + DEALLOCATION_DELAY + 1);
cheats.stopPrank();
// Slash operator for some random amount (1% -> 99%).
SlashingParams memory slashingParams = SlashingParams({
operator: defaultOperator,
operatorSetId: operatorSet.id,
strategies: strategy.toArray(),
wadsToSlash: (r.Uint256(0.01 ether, 0.99 ether)).toArrayU256(),
description: "test"
});
(uint expectedWadSlashed, uint64 expectedCurrentMag, uint64 expectedMaxMag, uint64 expectedEncumberedMag) =
_getExpectedSlashVals({magBeforeSlash: deallocateParams[0].newMagnitudes[0], wadToSlash: slashingParams.wadsToSlash[0]});
_checkSlashEvents({
operator: defaultOperator,
operatorSet: operatorSet,
strategy: allocateParams[0].strategies[0],
wadToSlash: expectedWadSlashed,
description: "test",
currentMag: expectedCurrentMag,
maxMag: expectedMaxMag,
encumberedMag: expectedEncumberedMag
});
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams);
// Clear deallocation queue.
allocationManager.clearDeallocationQueue(defaultOperator, strategy.toArray(), _maxNumToClear());
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: operatorSet,
strategy: strategy,
expectedAllocation: Allocation({currentMagnitude: expectedCurrentMag, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: expectedEncumberedMag,
max: expectedMaxMag,
allocatable: expectedMaxMag - expectedEncumberedMag
})
});
// Allocate up to max magnitude
AllocateParams[] memory allocateParams2 = _newAllocateParams(operatorSet, expectedMaxMag);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams2);
int128 pendingDiff = int128(uint128(expectedMaxMag - expectedCurrentMag));
// Check storage
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: operatorSet,
strategy: strategy,
expectedAllocation: Allocation({
currentMagnitude: expectedCurrentMag,
pendingDiff: pendingDiff,
effectBlock: _defaultAllocEffectBlock()
}),
expectedMagnitudes: Magnitudes({encumbered: expectedMaxMag, max: expectedMaxMag, allocatable: 0})
});
}
/**
* Allocates magnitude to an operator, deallocates all, warps to deallocation effect block and attempts to slash
* Asserts that the operator is not slashed
*/
function test_noFundsSlashedAfterDeallocationDelay() public {
// Allocate all magnitude
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, _newAllocateParams(defaultOperatorSet, WAD));
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate all
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, _newAllocateParams(defaultOperatorSet, 0));
// Warp to deallocation effect block
cheats.roll(block.number + DEALLOCATION_DELAY + 1);
// Slash operator for all wad
cheats.prank(defaultAVS);
allocationManager.slashOperator(
defaultAVS,
SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: WAD.toArrayU256(),
description: "test"
})
);
// Assert that the operator's max magnitude and allocatable magnitude are still WAD
assertEq(WAD, allocationManager.getAllocatableMagnitude(defaultOperator, strategyMock), "Allocatable magnitude should be WAD");
assertEq(WAD, allocationManager.getMaxMagnitude(defaultOperator, strategyMock), "Max magnitude should be WAD");
}
function testRevert_noFundsSlashedAfterDeregistration() public {
// Allocate all magnitude
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, _newAllocateParams(defaultOperatorSet, WAD));
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deregister operator
DeregisterParams memory deregisterParams =
DeregisterParams({operator: defaultOperator, avs: defaultAVS, operatorSetIds: defaultOperatorSet.id.toArrayU32()});
cheats.prank(defaultOperator);
allocationManager.deregisterFromOperatorSets(deregisterParams);
// Warp to deallocation effect block
cheats.roll(block.number + DEALLOCATION_DELAY + 1);
// Slash operator for all wad
cheats.expectRevert(OperatorNotSlashable.selector);
cheats.prank(defaultAVS);
allocationManager.slashOperator(
defaultAVS,
SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: WAD.toArrayU256(),
description: "test"
})
);
}
function test_deallocationSlashedJustBeforeEffectBlock() public {
// Allocate all magnitude
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, _newAllocateParams(defaultOperatorSet, WAD));
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate all
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, _newAllocateParams(defaultOperatorSet, 0));
// Warp to just before deallocation effect block
cheats.roll(block.number + DEALLOCATION_DELAY);
// Slash operator for all wad
cheats.prank(defaultAVS);
allocationManager.slashOperator(
defaultAVS,
SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: WAD.toArrayU256(),
description: "test"
})
);
// Assert that the operator has no max magnitude or allocatable magnitude
assertEq(0, allocationManager.getAllocatableMagnitude(defaultOperator, strategyMock), "Allocatable magnitude should be 0");
assertEq(0, allocationManager.getMaxMagnitude(defaultOperator, strategyMock), "Max magnitude should be 0");
}
function test_deregisteredOperatorSlashableBeforeDelay() public {
// Allocate all magnitude
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, _newAllocateParams(defaultOperatorSet, WAD));
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deregister operator
DeregisterParams memory deregisterParams =
DeregisterParams({operator: defaultOperator, avs: defaultAVS, operatorSetIds: defaultOperatorSet.id.toArrayU32()});
cheats.prank(defaultOperator);
allocationManager.deregisterFromOperatorSets(deregisterParams);
// Roll to the slashableUntil at block
cheats.roll(block.number + DEALLOCATION_DELAY);
// Slash operator for all wad
cheats.prank(defaultAVS);
allocationManager.slashOperator(
defaultAVS,
SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: WAD.toArrayU256(),
description: "test"
})
);
// Assert that the operator has no max magnitude or allocatable magnitude
assertEq(0, allocationManager.getAllocatableMagnitude(defaultOperator, strategyMock), "Allocatable magnitude should be 0");
assertEq(0, allocationManager.getMaxMagnitude(defaultOperator, strategyMock), "Max magnitude should be 0");
}
}
contract AllocationManagerUnitTests_ModifyAllocations is AllocationManagerUnitTests {
using ArrayLib for *;
using OperatorSetLib for *;
using SlashingLib for *;
function test_revert_paused() public {
allocationManager.pause(2 ** PAUSED_MODIFY_ALLOCATIONS);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
allocationManager.modifyAllocations(address(this), new AllocateParams[](0));
}
function test_revert_invalidCaller() public {
address invalidOperator = address(0x2);
cheats.expectRevert(InvalidCaller.selector);
allocationManager.modifyAllocations(invalidOperator, new AllocateParams[](0));
}
function test_revert_allocationDelayNotSet() public {
address invalidOperator = address(0x2);
cheats.prank(invalidOperator);
cheats.expectRevert(UninitializedAllocationDelay.selector);
allocationManager.modifyAllocations(invalidOperator, new AllocateParams[](0));
}
function test_revert_allocationDelayNotInEffect() public {
address operator = address(0x2);
_registerOperator(operator);
cheats.startPrank(operator);
allocationManager.setAllocationDelay(operator, 5);
// even though the operator has an allocation delay set, it is not in effect
// and modifyAllocations should still be blocked
cheats.expectRevert(UninitializedAllocationDelay.selector);
allocationManager.modifyAllocations(operator, new AllocateParams[](0));
}
function test_revert_lengthMismatch() public {
AllocateParams[] memory allocateParams = _randAllocateParams_DefaultOpSet();
allocateParams[0].newMagnitudes = new uint64[](0);
cheats.expectRevert(InputArrayLengthMismatch.selector);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
}
function test_revert_invalidOperatorSet() public {
AllocateParams[] memory allocateParams = AllocateParams({
operatorSet: OperatorSet(random().Address(), 0),
strategies: defaultStrategies,
newMagnitudes: uint64(0.5 ether).toArrayU64()
}).toArray();
cheats.expectRevert(InvalidOperatorSet.selector);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
}
function test_revert_multiAlloc_modificationAlreadyPending_diffTx() public {
// Allocate magnitude
AllocateParams[] memory allocateParams = _randAllocateParams_DefaultOpSet();
cheats.startPrank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Warp to just before allocation complete block
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY - 1);
// Attempt to allocate magnitude again
cheats.expectRevert(ModificationAlreadyPending.selector);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.stopPrank();
}
function test_revert_multiAlloc_modificationAlreadyPending_sameTx() public {
// Allocate magnitude
AllocateParams[] memory allocateParams = new AllocateParams[](2);
allocateParams[0] = _randAllocateParams_DefaultOpSet()[0];
allocateParams[1] = allocateParams[0];
cheats.expectRevert(ModificationAlreadyPending.selector);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
}
/**
* @notice Regression tests for the bugfix where pending modifications were checked by
* require(allocation.pendingDiff == 0, ModificationAlreadyPending());
* which would overwrite the effectBlock, pendingDiff if a pendingDiff
* of a deallocation was slashed to become 0.
*
* This test checks that the effectBlock, pendingDiff are not overwritten even if the pendingDiff is 0
* when attempting to modify allocations again
*/
function test_modifyAllocations_PendingDiffZero() public {
// Step 1: Allocate to the operator set
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, 501);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Step 2: Roll blocks forward until the allocation effectBlock
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Step 3: Deallocate from the operator set
AllocateParams[] memory deallocateParams = _newAllocateParams(defaultOperatorSet, 500);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, deallocateParams);
Allocation memory allocation = allocationManager.getAllocation(defaultOperator, defaultOperatorSet, strategyMock);
uint32 originalEffectBlock = allocation.effectBlock;
// Step 4: Slash the operator to adjust pendingDiff to 0, slashing rounds up the amount of magnitude to slash
// so with an existing deallocation/pendingDiff of 1, it should result in a pendingDiff of 0
SlashingParams memory slashingParams = SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: 5e17.toArrayU256(),
description: "Test slashing"
});
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams);
allocation = allocationManager.getAllocation(defaultOperator, defaultOperatorSet, strategyMock);
assertEq(allocation.pendingDiff, 0, "Pending diff should be 0");
assertEq(allocation.effectBlock, originalEffectBlock, "Effect block should not have changed");
// Step 5: Modify allocations again (Should not be called)
AllocateParams[] memory newAllocateParams = _newAllocateParams(defaultOperatorSet, 1000);
cheats.prank(defaultOperator);
cheats.expectRevert(ModificationAlreadyPending.selector);
allocationManager.modifyAllocations(defaultOperator, newAllocateParams);
// Assert that the allocation was modified without reverting
allocation = allocationManager.getAllocation(defaultOperator, defaultOperatorSet, strategyMock);
assertEq(allocation.currentMagnitude, 250, "Allocation should be updated to 250 after slashing 50%");
// Note: These 2 assertions fail prior to the bugfix and if we kept the same
// require(allocation.pendingDiff == 0, ModificationAlreadyPending());
// in the code. The effectBlock, pendingDiff would get overwritten with the new modification
// but the deallocationQueue would now be unordered(in terms of effectBlocks) with this overwrite.
assertEq(allocation.effectBlock, originalEffectBlock, "Effect block should not have changed");
assertEq(allocation.pendingDiff, 0, "Pending diff should still be 0");
}
/**
* @notice Regression tests for the bugfix where pending modifications were checked by
* require(allocation.pendingDiff == 0, ModificationAlreadyPending());
* which would overwrite the effectBlock, pendingDiff if a pendingDiff
* of a deallocation was slashed to become 0.
*
* This test checks that the deallocationQueue is ascending ordered by effectBlocks
*/
function test_modifyAllocations_PendingDiffZero_CheckOrderedDeallocationQueue() public {
// Step 1: Register the operator to multiple operator sets
OperatorSet memory operatorSet1 = OperatorSet(defaultAVS, 1);
OperatorSet memory operatorSet2 = OperatorSet(defaultAVS, 2);
_createOperatorSet(operatorSet1, defaultStrategies);
_createOperatorSet(operatorSet2, defaultStrategies);
_registerForOperatorSet(defaultOperator, operatorSet1);
_registerForOperatorSet(defaultOperator, operatorSet2);
// Step 2: Allocate to both operator sets
AllocateParams[] memory allocateParams1 = _newAllocateParams(operatorSet1, 1001);
AllocateParams[] memory allocateParams2 = _newAllocateParams(operatorSet2, 1000);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams1);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams2);
// Step 3: Roll blocks forward until the allocation effectBlock
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Step 4: Deallocate from both operator sets
AllocateParams[] memory deallocateParams1 = _newAllocateParams(operatorSet1, 1000);
AllocateParams[] memory deallocateParams2 = _newAllocateParams(operatorSet2, 0);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, deallocateParams1);
// roll blocks forward so that deallocations have different effectBlocks
cheats.roll(block.number + 100);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, deallocateParams2);
// Step 5: Slash the first deallocation to adjust pendingDiff to 0
SlashingParams memory slashingParams1 = SlashingParams({
operator: defaultOperator,
operatorSetId: operatorSet1.id,
strategies: defaultStrategies,
wadsToSlash: 5e17.toArrayU256(),
description: "Test slashing"
});
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams1);
// Step 6: Modify allocations again for operatorSet1 making another deallocation and
// overwriting/increasing the effectBlock
// roll blocks forward so that deallocations have different effectBlocks
cheats.roll(block.number + 100);
// Note: this should revert but previously it would not prior to the bugfix
AllocateParams[] memory newAllocateParams1 = _newAllocateParams(operatorSet1, 400);
cheats.prank(defaultOperator);
cheats.expectRevert(ModificationAlreadyPending.selector);
allocationManager.modifyAllocations(defaultOperator, newAllocateParams1);
// Assert that the deallocationQueue is unordered for the 2 deallocations in queue
_checkDeallocationQueueOrder(defaultOperator, defaultStrategies[0], 2);
}
/**
* @notice Regression tests for the bugfix where pending modifications were checked by
* require(allocation.pendingDiff == 0, ModificationAlreadyPending());
* which would overwrite the effectBlock, pendingDiff if a pendingDiff
* of a deallocation was slashed to become 0.
*
* This test checks that the deallocationQueue is ascending ordered by effectBlocks
* In this case the new modifyAllocations call is an allocation
* where the effectBlock is increased and the deallocationQueue is unordered as well because the operator
* allocationDelay configured to be long enough.
*/
function test_modifyAllocations_PendingDiffZero_Allocation() public {
// Step 1: Register the operator to multiple operator sets
OperatorSet memory operatorSet1 = OperatorSet(defaultAVS, 1);
OperatorSet memory operatorSet2 = OperatorSet(defaultAVS, 2);
_createOperatorSet(operatorSet1, defaultStrategies);
_createOperatorSet(operatorSet2, defaultStrategies);
_registerForOperatorSet(defaultOperator, operatorSet1);
_registerForOperatorSet(defaultOperator, operatorSet2);
// Step 2: Allocate to both operator sets
AllocateParams[] memory allocateParams1 = _newAllocateParams(operatorSet1, 1001);
AllocateParams[] memory allocateParams2 = _newAllocateParams(operatorSet2, 1000);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams1);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams2);
// Step 3: Update operator allocation delay
cheats.prank(defaultOperator);
allocationManager.setAllocationDelay(defaultOperator, DEALLOCATION_DELAY + 10 days);
cheats.roll(block.number + ALLOCATION_CONFIGURATION_DELAY);
// Step 4: Deallocate from both operator sets
AllocateParams[] memory deallocateParams1 = _newAllocateParams(operatorSet1, 1000);
AllocateParams[] memory deallocateParams2 = _newAllocateParams(operatorSet2, 0);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, deallocateParams1);
// roll blocks forward so that deallocations have different effectBlocks
cheats.roll(block.number + 100);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, deallocateParams2);
// Step 5: Slash the first deallocation to adjust pendingDiff to 0
SlashingParams memory slashingParams1 = SlashingParams({
operator: defaultOperator,
operatorSetId: operatorSet1.id,
strategies: defaultStrategies,
wadsToSlash: 5e17.toArrayU256(),
description: "Test slashing"
});
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams1);
// Step 6: Modify allocations again for operatorSet1 making an allocation and
// overwriting/increasing the effectBlock
// Note: this should revert but previously it would not prior to the bugfix
AllocateParams[] memory newAllocateParams1 = _newAllocateParams(operatorSet1, 5000);
cheats.prank(defaultOperator);
cheats.expectRevert(ModificationAlreadyPending.selector);
allocationManager.modifyAllocations(defaultOperator, newAllocateParams1);
// Assert that the deallocationQueue is unordered for the 2 deallocations in queue
_checkDeallocationQueueOrder(defaultOperator, defaultStrategies[0], 2);
}
function test_revert_allocateZeroMagnitude() public {
// Allocate exact same magnitude as initial allocation (0)
AllocateParams[] memory allocateParams = _randAllocateParams_DefaultOpSet();
allocateParams[0].newMagnitudes[0] = 0;
cheats.expectRevert(SameMagnitude.selector);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
}
function test_revert_allocateSameMagnitude() public {
// Allocate nonzero magnitude
AllocateParams[] memory allocateParams = _randAllocateParams_DefaultOpSet();
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Warp to allocation complete block
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Attempt to allocate no magnitude (ie. same magnitude)
cheats.expectRevert(SameMagnitude.selector);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
}
function testFuzz_revert_insufficientAllocatableMagnitude(Randomness r) public rand(r) {
// Allocate some magnitude
AllocateParams[] memory allocateParams = _randAllocateParams_DefaultOpSet();
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Warp to allocation complete block
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Attempt to allocate more magnitude than the operator has
// uint64 allocatedMag = allocateParams[0].newMagnitudes[0];
allocateParams[0].newMagnitudes[0] = WAD + 1;
cheats.expectRevert(InsufficientMagnitude.selector);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
}
function test_revert_allocateDeallocate_modificationPending() public {
// Allocate
AllocateParams[] memory allocateParams = _randAllocateParams_DefaultOpSet();
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Deallocate
allocateParams[0].newMagnitudes[0] -= 1;
cheats.expectRevert(ModificationAlreadyPending.selector);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
}
function test_revert_deallocateTwice_modificationPending() public {
// Allocate
AllocateParams[] memory allocateParams = _randAllocateParams_DefaultOpSet();
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Warp past allocation complete timestsamp
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate
allocateParams[0].newMagnitudes[0] -= 1;
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Deallocate again -> expect revert
cheats.expectRevert(ModificationAlreadyPending.selector);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
}
function test_revert_safeCastOverflow() public {
// setup additional operatorSets for tests
OperatorSet memory opSet1 = OperatorSet(defaultAVS, 1);
_createOperatorSet(opSet1, defaultStrategies);
_registerOperator(defaultOperator);
_setAllocationDelay(defaultOperator, DEFAULT_OPERATOR_ALLOCATION_DELAY);
_registerForOperatorSet(defaultOperator, opSet1);
OperatorSet memory opSet2 = OperatorSet(defaultAVS, 2);
_createOperatorSet(opSet2, defaultStrategies);
_registerOperator(defaultOperator);
_setAllocationDelay(defaultOperator, DEFAULT_OPERATOR_ALLOCATION_DELAY);
_registerForOperatorSet(defaultOperator, opSet2);
// 1. Allocate all available magnitude for the strategy (WAD)
AllocateParams[] memory allocateParams = _randAllocateParams_DefaultOpSet();
allocateParams[0].newMagnitudes[0] = WAD;
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
assertEq(allocationManager.getAllocatableMagnitude(defaultOperator, strategyMock), 0, "Allocatable magnitude should be 0");
assertEq(allocationManager.getEncumberedMagnitude(defaultOperator, strategyMock), WAD, "Encumbered magnitude should be WAD");
// 2. allocate to another operatorSet for the same strategy to reset encumberedMagnitude back to 0
allocateParams[0].operatorSet = opSet1;
allocateParams[0].newMagnitudes[0] = type(uint64).max - WAD + 1;
cheats.prank(defaultOperator);
cheats.expectRevert("SafeCast: value doesn't fit in 64 bits");
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// 3. after resetting encumberedMagnitude, attempt to allocate to opSet2 with WAD
allocateParams[0].operatorSet = opSet2;
allocateParams[0].newMagnitudes[0] = WAD;
cheats.prank(defaultOperator);
cheats.expectRevert(InsufficientMagnitude.selector);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// 4. after resetting encumberedMagnitude, attempt to allocate to opSet2 with 1
allocateParams[0].operatorSet = opSet2;
allocateParams[0].newMagnitudes[0] = 1;
cheats.prank(defaultOperator);
cheats.expectRevert(InsufficientMagnitude.selector);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
}
/**
* @notice Tests edge cases around allocation delay:
* 1. Set allocation delay to a value greater than ALLOCATION_CONFIGURATION_DELAY
* 2. Allocate magnitude before the configured delay is hit
* 3. Set allocation delay to a value less than ALLOCATION_CONFIGURATION_DELAY
* 4. Allocate magnitude after allocation in step 2 takes effect, but before the new delay is hit
* Validates that you should be able to allocate in step 4 since there is no other pending modifications
*/
function testFuzz_ShouldBeAbleToAllocateSoonerThanLastDelay(Randomness r) public rand(r) {
uint32 firstDelay = r.Uint32(ALLOCATION_CONFIGURATION_DELAY, type(uint24).max);
uint32 secondDelay = r.Uint32(1, ALLOCATION_CONFIGURATION_DELAY - 1);
uint64 half = 0.5 ether;
cheats.prank(defaultAVS);
allocationManager.createOperatorSets(defaultAVS, CreateSetParams(1, defaultStrategies).toArray());
cheats.startPrank(defaultOperator);
allocationManager.setAllocationDelay(defaultOperator, firstDelay);
allocationManager.modifyAllocations(defaultOperator, _newAllocateParams(defaultOperatorSet, half));
// Validate storage - the `firstDelay` should not be applied yet
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: 0, pendingDiff: int64(half), effectBlock: _defaultAllocEffectBlock()}),
expectedMagnitudes: Magnitudes({encumbered: half, max: WAD, allocatable: WAD - half})
});
allocationManager.setAllocationDelay(defaultOperator, secondDelay);
cheats.roll(block.number + ALLOCATION_CONFIGURATION_DELAY + 1);
allocationManager.modifyAllocations(defaultOperator, _newAllocateParams(defaultOperatorSet, half + 1));
cheats.stopPrank();
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: half, pendingDiff: int64(1), effectBlock: uint32(block.number + secondDelay)}),
expectedMagnitudes: Magnitudes({encumbered: half + 1, max: WAD, allocatable: WAD - (half + 1)})
});
}
/**
* @notice Allocates a random magnitude to the default operatorSet.
* Validates:
* 1. Storage is clear prior to allocation
* 2. Events are emitted
* 3. Allocation storage/introspection after allocation
* 4. Allocation storage/introspection after roll to allocation effect block
*/
function testFuzz_allocate_singleStrat_singleOperatorSet(Randomness r) public rand(r) {
// Create allocation
AllocateParams[] memory allocateParams = _randAllocateParams_DefaultOpSet();
// Save vars to check against
uint64 magnitude = allocateParams[0].newMagnitudes[0];
uint32 effectBlock = _defaultAllocEffectBlock();
// Check that the operator has no allocated sets/strats before allocation
OperatorSet[] memory allocatedSets = allocationManager.getAllocatedSets(defaultOperator);
IStrategy[] memory allocatedStrats = allocationManager.getAllocatedStrategies(defaultOperator, defaultOperatorSet);
assertEq(allocatedSets.length, 0, "should not have any allocated sets before allocation");
assertEq(allocatedStrats.length, 0, "should not have any allocated strats before allocation");
_checkAllocationEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
magnitude: magnitude,
encumberedMagnitude: magnitude,
effectBlock: effectBlock
});
// Allocate magnitude
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Check storage Prior to Completion
// 1. Validate allocated sets and strategies
allocatedSets = allocationManager.getAllocatedSets(defaultOperator);
allocatedStrats = allocationManager.getAllocatedStrategies(defaultOperator, defaultOperatorSet);
assertEq(allocatedSets.length, 1, "should have a single allocated set");
assertEq(allocatedSets[0].key(), defaultOperatorSet.key(), "should be allocated to default set");
assertEq(allocatedStrats.length, 1, "should have a single allocated strategy to default set");
assertEq(address(allocatedStrats[0]), address(strategyMock), "should have allocated default strat");
// 2. Validate allocation + info
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: 0, pendingDiff: int128(uint128(magnitude)), effectBlock: effectBlock}),
expectedMagnitudes: Magnitudes({encumbered: magnitude, max: WAD, allocatable: WAD - magnitude})
});
// 3. Check allocation and info after roll to completion
cheats.roll(effectBlock);
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: magnitude, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({encumbered: magnitude, max: WAD, allocatable: WAD - magnitude})
});
}
/**
* @notice Allocates magnitude for a single strategy to multiple operatorSets
* Validates:
* 1. Events
* 2. Allocation storage/introspection after allocation
* 3. Allocation storage/introspection after roll to allocation effect block
*/
function testFuzz_allocate_singleStrat_multipleSets(Randomness r) public rand(r) {
uint8 numOpSets = uint8(r.Uint256(1, FUZZ_MAX_OP_SETS));
// Create and register for operator sets, each with a single default strategy
OperatorSet[] memory operatorSets = r.OperatorSetArray(defaultAVS, numOpSets);
AllocateParams[] memory allocateParams = _randAllocateParams_SingleMockStrategy(operatorSets);
_createOperatorSets(operatorSets, defaultStrategies);
_registerForOperatorSets(defaultOperator, operatorSets);
// Save vars to check against
uint32 effectBlock = _defaultAllocEffectBlock();
uint64 usedMagnitude;
for (uint i; i < allocateParams.length; ++i) {
usedMagnitude += allocateParams[i].newMagnitudes[0];
}
// Validate events
for (uint i; i < allocateParams.length; ++i) {
// There is only one strategy in each allocateParams, so we don't need a nested for loop
_checkAllocationEvents({
operator: defaultOperator,
operatorSet: operatorSets[i],
strategy: strategyMock,
magnitude: allocateParams[i].newMagnitudes[0],
encumberedMagnitude: _encumberedMagnitudes[strategyMock] + allocateParams[i].newMagnitudes[0],
effectBlock: effectBlock
});
_encumberedMagnitudes[strategyMock] += allocateParams[i].newMagnitudes[0];
}
// Allocate magnitude
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Check storage
// 1. Sanity check number of allocated sets
OperatorSet[] memory allocatedSets = allocationManager.getAllocatedSets(defaultOperator);
assertEq(allocatedSets.length, numOpSets, "should have multiple allocated sets");
// 2. Check storage after allocation
for (uint i; i < allocateParams.length; ++i) {
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: operatorSets[i],
strategy: strategyMock,
expectedAllocation: Allocation({
currentMagnitude: 0,
pendingDiff: int128(uint128(allocateParams[i].newMagnitudes[0])),
effectBlock: effectBlock
}),
expectedMagnitudes: Magnitudes({
encumbered: _encumberedMagnitudes[strategyMock],
max: WAD,
allocatable: WAD - _encumberedMagnitudes[strategyMock]
})
});
IStrategy[] memory allocatedStrats = allocationManager.getAllocatedStrategies(defaultOperator, operatorSets[i]);
assertEq(allocatedStrats.length, 1, "should have a single allocated strategy to each set");
assertEq(address(allocatedStrats[0]), address(strategyMock), "should have allocated default strat");
assertEq(allocatedSets[i].key(), operatorSets[i].key(), "should be allocated to expected set");
}
// 3. Check storage after roll to completion
cheats.roll(effectBlock);
for (uint i; i < allocateParams.length; ++i) {
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: operatorSets[i],
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: allocateParams[i].newMagnitudes[0], pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: _encumberedMagnitudes[strategyMock],
max: WAD,
allocatable: WAD - _encumberedMagnitudes[strategyMock]
})
});
}
}
/**
* @notice Allocates once, warps to allocation effect block, then allocates again
* Validates:
* 1. Events for each allocation
* 2. Allocation storage/introspection immediately after each allocation
*/
function testFuzz_allocateMultipleTimes(Randomness r) public rand(r) {
uint64 firstAlloc = r.Uint64(1, WAD - 1);
uint64 secondAlloc = r.Uint64(firstAlloc + 1, WAD);
// Validate events
_checkAllocationEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
magnitude: firstAlloc,
encumberedMagnitude: firstAlloc,
effectBlock: _defaultAllocEffectBlock()
});
// Allocate magnitude
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, firstAlloc);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: 0, pendingDiff: int64(firstAlloc), effectBlock: _defaultAllocEffectBlock()}),
expectedMagnitudes: Magnitudes({encumbered: firstAlloc, max: WAD, allocatable: WAD - firstAlloc})
});
// Warp to allocation complete block
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Allocate magnitude again
allocateParams = _newAllocateParams(defaultOperatorSet, secondAlloc);
_checkAllocationEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
magnitude: secondAlloc,
encumberedMagnitude: secondAlloc,
effectBlock: _defaultAllocEffectBlock()
});
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Check storage
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({
currentMagnitude: firstAlloc,
pendingDiff: int64(secondAlloc - firstAlloc),
effectBlock: _defaultAllocEffectBlock()
}),
expectedMagnitudes: Magnitudes({encumbered: secondAlloc, max: WAD, allocatable: WAD - secondAlloc})
});
}
/**
* Allocates maximum magnitude to multiple strategies for the same operatorSet
* Validates that encumbered magnitude is max for each strategy
*/
function testFuzz_allocateMaxToMultipleStrategies(Randomness r) public rand(r) {
uint numStrats = r.Uint256(2, FUZZ_MAX_STRATS);
OperatorSet memory operatorSet = OperatorSet(defaultAVS, r.Uint32());
IStrategy[] memory strategies = r.StrategyArray(numStrats);
_createOperatorSet(operatorSet, strategies);
_registerForOperatorSet(defaultOperator, operatorSet);
for (uint i; i < numStrats; ++i) {
_checkAllocationEvents({
operator: defaultOperator,
operatorSet: operatorSet,
strategy: strategies[i],
magnitude: WAD,
encumberedMagnitude: WAD,
effectBlock: _defaultAllocEffectBlock()
});
}
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(
defaultOperator,
AllocateParams({operatorSet: operatorSet, strategies: strategies, newMagnitudes: WAD.toArrayU64(numStrats)}).toArray()
);
for (uint i; i < numStrats; ++i) {
assertEq(WAD, allocationManager.getEncumberedMagnitude(defaultOperator, strategies[i]), "encumberedMagnitude not max");
}
}
/**
* Allocates to `firstMod` magnitude and then deallocate to `secondMod` magnitude
* Validates:
* 1. Events are valid for the allocation and deallocation
* 2. Storage after the allocation is made
* 3. Storage after the deallocation is made
* 4. Storage after the deallocation effect block is hit
* 5. Storage after the deallocation queue is cleared (specifically encumbered mag is decreased)
*/
function testFuzz_allocate_deallocate_whenRegistered(Randomness r) public rand(r) {
// Bound allocation and deallocation
uint64 firstMod = r.Uint64(1, WAD);
uint64 secondMod = r.Uint64(0, firstMod - 1);
// Allocate magnitude to default registered set
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, firstMod);
_checkAllocationEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
magnitude: firstMod,
encumberedMagnitude: firstMod,
effectBlock: _defaultAllocEffectBlock()
});
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Warp to allocation complete block
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate
allocateParams = _newAllocateParams(defaultOperatorSet, secondMod);
_checkDeallocationEvent({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
magnitude: secondMod,
effectBlock: uint32(block.number + DEALLOCATION_DELAY + 1)
});
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Check storage after dealloc
uint32 effectBlock = uint32(block.number + DEALLOCATION_DELAY + 1);
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({
currentMagnitude: firstMod,
pendingDiff: -int128(uint128(firstMod - secondMod)),
effectBlock: effectBlock
}),
expectedMagnitudes: Magnitudes({encumbered: firstMod, max: WAD, allocatable: WAD - firstMod})
});
// Check storage after roll to completion
cheats.roll(effectBlock);
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: secondMod, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({encumbered: secondMod, max: WAD, allocatable: WAD - secondMod})
});
// Check storage after clearing deallocation queue
allocationManager.clearDeallocationQueue(defaultOperator, strategyMock.toArray(), uint16(1).toArrayU16());
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: secondMod, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({encumbered: secondMod, max: WAD, allocatable: WAD - secondMod})
});
}
/**
* Allocates to an operatorSet, then fully deallocates after the strategy is removed from the set.
* Validates that the deallocation takes effect immediately after the strategy is removed
*/
function test_allocate_removeStrategyFromSet_fullyDeallocate() public {
// Allocate
AllocateParams[] memory allocateParams = _randAllocateParams_SingleMockStrategy(defaultOperatorSet.toArray());
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Remove strategy from operatorSet
cheats.prank(defaultAVS);
allocationManager.removeStrategiesFromOperatorSet(defaultAVS, defaultOperatorSet.id, defaultStrategies);
// Deallocate All Instantly
AllocateParams[] memory deallocateParams = allocateParams;
deallocateParams[0].newMagnitudes[0] = 0;
// We check the allocation event and not the deallocation event since the encumbered mag is updated too!
_checkAllocationEvents({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
magnitude: 0,
encumberedMagnitude: 0,
effectBlock: uint32(block.number)
});
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, deallocateParams);
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: 0, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({encumbered: 0, max: WAD, allocatable: WAD})
});
}
/**
* Allocates to an operatorSet, deallocates, then removes a strategy from the operatorSet
* Validates that:
* 1. The deallocation still completes at its expected time
*/
function testFuzz_allocate_deallocate_removeStrategyFromSet(Randomness r) public {
// Allocate
AllocateParams[] memory allocateParams = _randAllocateParams_SingleMockStrategy(defaultOperatorSet.toArray());
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate
AllocateParams[] memory deallocateParams = r.DeallocateParams(allocateParams);
uint32 deallocEffectBlock = uint32(block.number + DEALLOCATION_DELAY + 1);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, deallocateParams);
// Remove strategy from operatorSet
cheats.prank(defaultAVS);
allocationManager.removeStrategiesFromOperatorSet(defaultAVS, defaultOperatorSet.id, defaultStrategies);
// Roll to just before deallocation complete block & clear deallocation queue for sanity
cheats.roll(deallocEffectBlock - 1);
allocationManager.clearDeallocationQueue(defaultOperator, strategyMock.toArray(), _maxNumToClear());
int128 expectedDiff = -int128(uint128(allocateParams[0].newMagnitudes[0] - deallocateParams[0].newMagnitudes[0]));
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({
currentMagnitude: allocateParams[0].newMagnitudes[0],
pendingDiff: expectedDiff,
effectBlock: deallocEffectBlock
}),
expectedMagnitudes: Magnitudes({
encumbered: allocateParams[0].newMagnitudes[0],
max: WAD,
allocatable: WAD - allocateParams[0].newMagnitudes[0]
})
});
// Roll to deallocation complete block
cheats.roll(deallocEffectBlock);
// Note that the encumbered mag hasn't been updated since we haven't cleared the deallocaction queue!
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: deallocateParams[0].newMagnitudes[0], pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: deallocateParams[0].newMagnitudes[0],
max: WAD,
allocatable: WAD - deallocateParams[0].newMagnitudes[0]
})
});
}
/**
* Allocates to an operator set, then fully deallocates when not registered to the set.
* Validates that:
* 1. Events are properly emitted post instantaneous deallocation
* 2. The deallocation is instant & can be reallocated immediately
* 3. Storage/introspection post combined deallocation/allocation
*/
function testFuzz_allocate_fullyDeallocate_reallocate_WhenNotRegistered(Randomness r) public rand(r) {
// Bound allocation and deallocation
uint64 firstMod = r.Uint64(1, WAD);
// Create new operator sets that the operator is not registered for
OperatorSet memory operatorSetA = _createOperatorSet(OperatorSet(defaultAVS, r.Uint32()), defaultStrategies);
OperatorSet memory operatorSetB = _createOperatorSet(OperatorSet(defaultAVS, r.Uint32()), defaultStrategies);
// Allocate magnitude to operator set
AllocateParams[] memory allocateParams = _newAllocateParams(operatorSetA, firstMod);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Warp to allocation complete block
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate instantly and reallocate all magnitude to second operator set
allocateParams = new AllocateParams[](2);
allocateParams[0] = _newAllocateParams(operatorSetA, 0)[0];
allocateParams[1] = _newAllocateParams(operatorSetB, firstMod)[0];
// We check the allocation event and not the deallocation event since
// encumbered magnitude is also updated here
_checkAllocationEvents({
operator: defaultOperator,
operatorSet: operatorSetA,
strategy: strategyMock,
magnitude: 0,
encumberedMagnitude: 0,
effectBlock: uint32(block.number) // Instant deallocation
});
_checkAllocationEvents({
operator: defaultOperator,
operatorSet: operatorSetB,
strategy: strategyMock,
magnitude: firstMod,
encumberedMagnitude: firstMod,
effectBlock: _defaultAllocEffectBlock()
});
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Check storage after deallocation
// Check operator set A
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: operatorSetA,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: 0, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({encumbered: firstMod, max: WAD, allocatable: WAD - firstMod}) // This is from opsetB
});
// Check operator set B
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: operatorSetB,
strategy: strategyMock,
expectedAllocation: Allocation({
currentMagnitude: 0,
pendingDiff: int128(uint128(firstMod)),
effectBlock: _defaultAllocEffectBlock()
}),
expectedMagnitudes: Magnitudes({encumbered: firstMod, max: WAD, allocatable: WAD - firstMod})
});
}
/**
* Allocates all magnitude to a single strategy across multiple operatorSets. Deallocates fully, and then reallocates
* Validates:
* 1. Events are emitted for the allocation, deallocation, and reallocation (including the deallocation queue clear)
* 2. Stake is fully allocated & encumbered mag used up
* 3. Stake can be reallocated after the deallocation delay
*/
function testFuzz_allocate_fromClearedDeallocQueue(Randomness r) public rand(r) {
uint numOpSets = r.Uint256(1, FUZZ_MAX_OP_SETS);
// Create multiple operator sets, register, and allocate to each. Ensure all magnitude is fully allocated.
OperatorSet[] memory deallocSets = r.OperatorSetArray(defaultAVS, numOpSets);
_createOperatorSets(deallocSets, defaultStrategies);
_registerForOperatorSets(defaultOperator, deallocSets);
AllocateParams[] memory allocateParams = _randAllocateParams_SingleMockStrategy_AllocAll(deallocSets);
for (uint i; i < allocateParams.length; ++i) {
// There is only one strategy each allocateParams, so we don't need a nested for loop
_checkAllocationEvents({
operator: defaultOperator,
operatorSet: allocateParams[i].operatorSet,
strategy: strategyMock,
magnitude: allocateParams[i].newMagnitudes[0],
encumberedMagnitude: _encumberedMagnitudes[strategyMock] + allocateParams[i].newMagnitudes[0],
effectBlock: _defaultAllocEffectBlock()
});
_encumberedMagnitudes[strategyMock] += allocateParams[i].newMagnitudes[0];
}
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
assertEq(
allocationManager.getAllocatableMagnitude(defaultOperator, strategyMock),
0,
"operator should not have any remaining allocatable magnitude"
);
// Move forward to allocation completion
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate fully from each operator set
AllocateParams[] memory deallocateParams = _newAllocateParams(deallocSets, 0);
for (uint i; i < numOpSets; ++i) {
_checkDeallocationEvent({
operator: defaultOperator,
operatorSet: deallocSets[i],
strategy: strategyMock,
magnitude: 0,
effectBlock: uint32(block.number + DEALLOCATION_DELAY + 1)
});
}
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, deallocateParams);
assertEq(
allocationManager.getAllocatableMagnitude(defaultOperator, strategyMock),
0,
"operator should still not have any allocatable magnitude"
);
// Move forward to deallocation completion
cheats.roll(block.number + DEALLOCATION_DELAY + 1);
// Check that we now have sufficient allocatable magnitude
assertEq(
allocationManager.getAllocatableMagnitude(defaultOperator, strategyMock), WAD, "operator should have all magnitude allocatable"
);
// Create and register for a new operator set with the same default strategy.
// If we try to allocate to this new set, it should clear the deallocation queue,
// allowing all magnitude to be allocated
OperatorSet memory finalOpSet = _createOperatorSet(OperatorSet(defaultAVS, r.Uint32()), defaultStrategies);
_registerForOperatorSet(defaultOperator, finalOpSet);
AllocateParams[] memory finalAllocParams = _newAllocateParams(finalOpSet, WAD);
_checkClearDeallocationQueueEvents({operator: defaultOperator, strategy: strategyMock, encumberedMagnitude: 0});
_checkAllocationEvents({
operator: defaultOperator,
operatorSet: finalOpSet,
strategy: strategyMock,
magnitude: WAD,
encumberedMagnitude: WAD,
effectBlock: _defaultAllocEffectBlock()
});
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, finalAllocParams);
// Check that all magnitude will be allocated to the new set, and each prior set
// has a zeroed-out allocation
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: finalOpSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: 0, pendingDiff: int128(uint128(WAD)), effectBlock: _defaultAllocEffectBlock()}),
expectedMagnitudes: Magnitudes({encumbered: WAD, max: WAD, allocatable: 0})
});
}
/**
* Allocates all mag and then deallocates all mag
* Validates
* 1. Events for the deallocation
* 2. Storage after deallocation
* 3. Storage after clearing the deallocation queue
*/
function test_deallocate_all() public {
// Allocate all
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, WAD);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Warp to allocation complete block
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate all
allocateParams[0].newMagnitudes[0] = 0;
_checkDeallocationEvent({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
magnitude: 0,
effectBlock: uint32(block.number + DEALLOCATION_DELAY + 1)
});
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Warp to completion and clear deallocation queue
cheats.roll(block.number + DEALLOCATION_DELAY + 1);
allocationManager.clearDeallocationQueue(defaultOperator, defaultStrategies, uint16(1).toArrayU16());
// Check storage
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: 0, pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({encumbered: 0, max: WAD, allocatable: WAD})
});
}
/**
* Allocates, deallocates, and then clears the deallocation queue. Multiple strategies & sets in a single operatorSet
* Validates:
* 1. Events for allocation, deallocation, and deallocation queue clear
* 2. Storage after allocation & after allocation effect block
* 3. Storage after deallocation & after deallocation effect block
*/
function testFuzz_lifecycle_allocate_deallocate_MultipleSetsAndStrats(Randomness r) public rand(r) {
uint numAllocations = r.Uint256(2, FUZZ_MAX_ALLOCATIONS);
uint numStrats = r.Uint256(2, FUZZ_MAX_STRATS);
AllocateParams[] memory allocateParams = r.AllocateParams(defaultAVS, numAllocations, numStrats);
AllocateParams[] memory deallocateParams = r.DeallocateParams(allocateParams);
CreateSetParams[] memory createSetParams = r.CreateSetParams(allocateParams);
cheats.prank(defaultAVS);
allocationManager.createOperatorSets(defaultAVS, createSetParams);
for (uint i = 0; i < allocateParams.length; i++) {
_registerForOperatorSet(defaultOperator, allocateParams[i].operatorSet);
}
// Allocate
for (uint i; i < allocateParams.length; ++i) {
for (uint j; j < allocateParams[i].strategies.length; ++j) {
_checkAllocationEvents({
operator: defaultOperator,
operatorSet: allocateParams[i].operatorSet,
strategy: allocateParams[i].strategies[j],
magnitude: allocateParams[i].newMagnitudes[j],
encumberedMagnitude: allocateParams[i].newMagnitudes[j],
effectBlock: _defaultAllocEffectBlock()
});
}
}
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Check storage after allocation
for (uint i; i < allocateParams.length; ++i) {
for (uint j = 0; j < allocateParams[i].strategies.length; j++) {
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: allocateParams[i].operatorSet,
strategy: allocateParams[i].strategies[j],
expectedAllocation: Allocation({
currentMagnitude: 0,
pendingDiff: int128(uint128(allocateParams[i].newMagnitudes[j])),
effectBlock: _defaultAllocEffectBlock()
}),
expectedMagnitudes: Magnitudes({
encumbered: allocateParams[i].newMagnitudes[j],
max: WAD,
allocatable: WAD - allocateParams[i].newMagnitudes[j]
})
});
}
}
// Warp to allocation complete block
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Check storage after roll to completion
for (uint i; i < allocateParams.length; ++i) {
for (uint j = 0; j < allocateParams[i].strategies.length; j++) {
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: allocateParams[i].operatorSet,
strategy: allocateParams[i].strategies[j],
expectedAllocation: Allocation({currentMagnitude: allocateParams[i].newMagnitudes[j], pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: allocateParams[i].newMagnitudes[j],
max: WAD,
allocatable: WAD - allocateParams[i].newMagnitudes[j]
})
});
}
}
// Deallocate
for (uint i; i < deallocateParams.length; ++i) {
for (uint j = 0; j < deallocateParams[i].strategies.length; j++) {
_checkDeallocationEvent({
operator: defaultOperator,
operatorSet: deallocateParams[i].operatorSet,
strategy: deallocateParams[i].strategies[j],
magnitude: deallocateParams[i].newMagnitudes[j],
effectBlock: uint32(block.number + DEALLOCATION_DELAY + 1)
});
}
}
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, deallocateParams);
for (uint i; i < allocateParams.length; ++i) {
for (uint j = 0; j < allocateParams[i].strategies.length; j++) {
int128 expectedDiff = -int128(uint128(allocateParams[i].newMagnitudes[j] - deallocateParams[i].newMagnitudes[j]));
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: deallocateParams[i].operatorSet,
strategy: deallocateParams[i].strategies[j],
expectedAllocation: Allocation({
currentMagnitude: allocateParams[i].newMagnitudes[j],
pendingDiff: expectedDiff,
effectBlock: uint32(block.number + DEALLOCATION_DELAY + 1)
}),
expectedMagnitudes: Magnitudes({
encumbered: allocateParams[i].newMagnitudes[j],
max: WAD,
allocatable: WAD - allocateParams[i].newMagnitudes[j]
})
});
}
}
// Warp to deallocation complete block
cheats.roll(block.number + DEALLOCATION_DELAY + 1);
for (uint i; i < allocateParams.length; ++i) {
for (uint j = 0; j < allocateParams[i].strategies.length; j++) {
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: deallocateParams[i].operatorSet,
strategy: allocateParams[i].strategies[j],
expectedAllocation: Allocation({currentMagnitude: deallocateParams[i].newMagnitudes[j], pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: deallocateParams[i].newMagnitudes[j],
max: WAD,
allocatable: WAD - deallocateParams[i].newMagnitudes[j]
})
});
}
}
}
}
contract AllocationManagerUnitTests_ClearDeallocationQueue is AllocationManagerUnitTests {
using ArrayLib for *;
/// -----------------------------------------------------------------------
/// clearDeallocationQueue()
/// -----------------------------------------------------------------------
function test_revert_paused() public {
allocationManager.pause(2 ** PAUSED_MODIFY_ALLOCATIONS);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
allocationManager.clearDeallocationQueue(defaultOperator, new IStrategy[](0), new uint16[](0));
}
function test_revert_arrayMismatch() public {
IStrategy[] memory strategies = new IStrategy[](1);
uint16[] memory numToClear = new uint16[](2);
cheats.expectRevert(InputArrayLengthMismatch.selector);
allocationManager.clearDeallocationQueue(defaultOperator, strategies, numToClear);
}
/**
* @notice Allocates magnitude to an operator and then
* - Clears deallocation queue when only an allocation exists
* - Clears deallocation queue when the alloc can be completed - asserts emit has been emitted
* - Validates storage after the second clear
*/
function testFuzz_allocate(Randomness r) public rand(r) {
AllocateParams[] memory allocateParams = _randAllocateParams_DefaultOpSet();
// Allocate magnitude
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Attempt to clear queue, assert no events emitted
allocationManager.clearDeallocationQueue(defaultOperator, defaultStrategies, _maxNumToClear());
Vm.Log[] memory entries = vm.getRecordedLogs();
assertEq(0, entries.length, "should not have emitted any events");
// Warp to allocation complete block
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Clear queue - this is a noop
allocationManager.clearDeallocationQueue(defaultOperator, defaultStrategies, _maxNumToClear());
entries = vm.getRecordedLogs();
assertEq(0, entries.length, "should not have emitted any events 2");
// Validate allocation is no longer pending
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: allocateParams[0].newMagnitudes[0], pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: allocateParams[0].newMagnitudes[0],
max: WAD,
allocatable: WAD - allocateParams[0].newMagnitudes[0]
})
});
}
/**
* @notice Allocates magnitude to an operator registered for some operator sets, and then
* - Clears deallocation queue when nothing can be completed
* - After the first clear, asserts the allocation info takes into account the deallocation
* - Clears deallocation queue when the dealloc can be completed
* - Assert events & validates storage after the deallocateParams are completed
*/
function testFuzz_allocate_deallocate(Randomness r) public rand(r) {
// Generate a random allocation and subsequent deallocation from the default operator set
(AllocateParams[] memory allocateParams, AllocateParams[] memory deallocateParams) =
_randAllocAndDeallocParams_SingleMockStrategy(defaultOperatorSet.toArray());
// Allocate
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// Roll to allocation complete block
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate
_checkDeallocationEvent({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
magnitude: deallocateParams[0].newMagnitudes[0],
effectBlock: uint32(block.number + DEALLOCATION_DELAY + 1)
});
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, deallocateParams);
// Clear queue - since we have not rolled forward, this should be a no-op
allocationManager.clearDeallocationQueue(defaultOperator, defaultStrategies, _maxNumToClear());
// Validate storage - encumbered magnitude should just be allocateParams (we only have 1 allocation)
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({
currentMagnitude: allocateParams[0].newMagnitudes[0],
pendingDiff: -int128(uint128(allocateParams[0].newMagnitudes[0] - deallocateParams[0].newMagnitudes[0])),
effectBlock: uint32(block.number + DEALLOCATION_DELAY + 1)
}),
expectedMagnitudes: Magnitudes({
encumbered: allocateParams[0].newMagnitudes[0],
max: WAD,
allocatable: WAD - allocateParams[0].newMagnitudes[0]
})
});
// Warp to deallocation complete block
cheats.roll(block.number + DEALLOCATION_DELAY + 1);
// Clear queue
_checkClearDeallocationQueueEvents({
operator: defaultOperator,
strategy: strategyMock,
encumberedMagnitude: deallocateParams[0].newMagnitudes[0]
});
allocationManager.clearDeallocationQueue(defaultOperator, defaultStrategies, _maxNumToClear());
// Validate storage - encumbered magnitude should just be deallocateParams (we only have 1 deallocation)
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: defaultOperatorSet,
strategy: strategyMock,
expectedAllocation: Allocation({currentMagnitude: deallocateParams[0].newMagnitudes[0], pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: deallocateParams[0].newMagnitudes[0],
max: WAD,
allocatable: WAD - deallocateParams[0].newMagnitudes[0]
})
});
}
/**
* Allocates, deallocates, and then allocates again. Asserts that
* - The deallocation does not block state updates from the second allocation, even though the allocation has an earlier
* effect block
*/
function test_allocate_deallocate_allocate() public {
// Allocate half of mag to default operator set
AllocateParams[] memory firstAllocation = _newAllocateParams(defaultOperatorSet, 5e17);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, firstAllocation);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate half from default operator set
uint32 deallocationEffectBlock = uint32(block.number + DEALLOCATION_DELAY + 1);
AllocateParams[] memory firstDeallocation = _newAllocateParams(defaultOperatorSet, 25e16);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, firstDeallocation);
Allocation memory allocation = allocationManager.getAllocation(defaultOperator, defaultOperatorSet, strategyMock);
assertEq(deallocationEffectBlock, allocation.effectBlock, "effect block not correct");
// Create and register for a new operator set
OperatorSet memory newOperatorSet = _createOperatorSet(OperatorSet(defaultAVS, random().Uint32()), defaultStrategies);
_registerForOperatorSet(defaultOperator, newOperatorSet);
// Allocate 33e16 mag to new operator set
uint32 allocationEffectBlock = _defaultAllocEffectBlock();
AllocateParams[] memory secondAllocation = _newAllocateParams(newOperatorSet, 33e16);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, secondAllocation);
allocation = allocationManager.getAllocation(defaultOperator, newOperatorSet, strategyMock);
assertEq(allocationEffectBlock, allocation.effectBlock, "effect block not correct");
// Warp to allocation effect block & clear the queue - clearing is a noop here
cheats.roll(allocationEffectBlock);
allocationManager.clearDeallocationQueue(defaultOperator, defaultStrategies, _maxNumToClear());
// Validate `getAllocatableMagnitude`. Allocatable magnitude should be the difference between the max magnitude and the encumbered magnitude
uint64 allocatableMagnitude = allocationManager.getAllocatableMagnitude(defaultOperator, strategyMock);
assertEq(WAD - 33e16 - 5e17, allocatableMagnitude, "allocatableMagnitude not correct");
// Validate that we can allocate again for opset2. This should not revert
AllocateParams[] memory thirdAllocation = _newAllocateParams(newOperatorSet, 10e16);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, thirdAllocation);
// Warp & validate deallocation
cheats.roll(deallocationEffectBlock);
_checkClearDeallocationQueueEvents({operator: defaultOperator, strategy: strategyMock, encumberedMagnitude: 58e16});
allocationManager.clearDeallocationQueue(defaultOperator, defaultStrategies, _maxNumToClear());
}
/**
* Allocates to opset1, allocates to opset2, deallocates from opset1. Asserts that the allocation, which has a higher
* effect block is not blocking the deallocation.
* The allocs/deallocs looks like
* 1. (allocation, opSet2, mag: 5e17, effectBlock: 50th day)
* 2. (deallocation, opSet1, mag: 0, effectBlock: 42.5 day)
*
* The deallocation queue looks like
* 1. (deallocation, opSet1, mag: 0, effectBlock: 42.5 day)
*/
function test_regression_deallocationNotBlocked() public {
// Set allocation delay to be longer than the deallocation delay
uint32 allocationDelay = DEALLOCATION_DELAY * 2;
cheats.prank(defaultOperator);
allocationManager.setAllocationDelay(defaultOperator, allocationDelay);
cheats.roll(block.number + ALLOCATION_CONFIGURATION_DELAY + 1);
(, uint32 storedDelay) = allocationManager.getAllocationDelay(defaultOperator);
assertEq(allocationDelay, storedDelay, "allocation delay not valid");
// Allocate half of mag to default operator set
AllocateParams[] memory firstAllocation = _newAllocateParams(defaultOperatorSet, 5e17);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, firstAllocation);
cheats.roll(block.number + allocationDelay);
// Create and register for a second operator set
OperatorSet memory newOperatorSet = _createOperatorSet(OperatorSet(defaultAVS, random().Uint32()), defaultStrategies);
_registerForOperatorSet(defaultOperator, newOperatorSet);
// Allocate half of mag to opset2
AllocateParams[] memory secondAllocation = _newAllocateParams(newOperatorSet, 5e17);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, secondAllocation);
uint32 allocationEffectBlock = uint32(block.number + allocationDelay);
Allocation memory allocation = allocationManager.getAllocation(defaultOperator, newOperatorSet, strategyMock);
assertEq(allocationEffectBlock, allocation.effectBlock, "effect block not correct");
// Deallocate all from opSet1
uint32 deallocationEffectBlock = uint32(block.number + DEALLOCATION_DELAY + 1);
AllocateParams[] memory firstDeallocation = _newAllocateParams(defaultOperatorSet, 0);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, firstDeallocation);
allocation = allocationManager.getAllocation(defaultOperator, defaultOperatorSet, strategyMock);
assertEq(deallocationEffectBlock, allocation.effectBlock, "effect block not correct");
// Warp to deallocation effect block & clear the queue
cheats.roll(deallocationEffectBlock);
allocationManager.clearDeallocationQueue(defaultOperator, defaultStrategies, _maxNumToClear());
// At this point, we should be able to allocate again to opSet1 AND have only 5e17 encumbered magnitude
assertEq(5e17, allocationManager.getEncumberedMagnitude(defaultOperator, strategyMock), "encumbered magnitude not correct");
AllocateParams[] memory thirdAllocation = _newAllocateParams(defaultOperatorSet, 5e17);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, thirdAllocation);
}
}
contract AllocationManagerUnitTests_SetAllocationDelay is AllocationManagerUnitTests {
/// -----------------------------------------------------------------------
/// setAllocationDelay() + getAllocationDelay()
/// -----------------------------------------------------------------------
address operatorToSet = address(0x1);
function setUp() public override {
AllocationManagerUnitTests.setUp();
_registerOperator(operatorToSet);
}
function test_revert_callerNotOperator() public {
delegationManagerMock.setIsOperator(operatorToSet, false);
cheats.prank(operatorToSet);
cheats.expectRevert(InvalidOperator.selector);
allocationManager.setAllocationDelay(operatorToSet, 1);
}
function test_revert_callerNotAuthorized() public {
cheats.expectRevert(InvalidCaller.selector);
allocationManager.setAllocationDelay(operatorToSet, 1);
}
function test_regression_boundary() public {
cheats.prank(operatorToSet);
allocationManager.setAllocationDelay(operatorToSet, 0);
// Warp to the allocation config delay - we should not be set yet
cheats.roll(block.number + ALLOCATION_CONFIGURATION_DELAY);
(bool isSet, uint32 delay) = allocationManager.getAllocationDelay(operatorToSet);
assertFalse(isSet, "isSet should not be set");
// Warp to the next block - we should be set now
cheats.roll(block.number + 1);
(isSet, delay) = allocationManager.getAllocationDelay(operatorToSet);
assertTrue(isSet, "isSet should be set");
}
function testFuzz_setDelay(Randomness r) public rand(r) {
uint32 delay = r.Uint32(0, type(uint32).max);
// Set delay
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit AllocationDelaySet(operatorToSet, delay, uint32(block.number + ALLOCATION_CONFIGURATION_DELAY + 1));
cheats.prank(operatorToSet);
allocationManager.setAllocationDelay(operatorToSet, delay);
// Check values after set
(bool isSet, uint32 returnedDelay) = allocationManager.getAllocationDelay(operatorToSet);
assertFalse(isSet, "isSet should not be set");
assertEq(0, returnedDelay, "returned delay should be 0");
// Warp to effect block
cheats.roll(block.number + ALLOCATION_CONFIGURATION_DELAY + 1);
// Check values after config delay
(isSet, returnedDelay) = allocationManager.getAllocationDelay(operatorToSet);
assertTrue(isSet, "isSet should be set");
assertEq(delay, returnedDelay, "delay not set");
}
function test_fuzz_setDelay_multipleTimesWithinConfigurationDelay(Randomness r) public rand(r) {
uint32 firstDelay = r.Uint32(1, type(uint32).max);
uint32 secondDelay = r.Uint32(1, type(uint32).max);
// Set delay
cheats.prank(operatorToSet);
allocationManager.setAllocationDelay(operatorToSet, firstDelay);
// Warp just before effect block
cheats.roll(block.number + ALLOCATION_CONFIGURATION_DELAY);
// Set delay again
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit AllocationDelaySet(operatorToSet, secondDelay, uint32(block.number + ALLOCATION_CONFIGURATION_DELAY + 1));
cheats.prank(operatorToSet);
allocationManager.setAllocationDelay(operatorToSet, secondDelay);
// Warp to effect block of first delay
cheats.roll(block.number + 1);
// Assert that the delay is still not set
(bool isSet, uint32 returnedDelay) = allocationManager.getAllocationDelay(operatorToSet);
assertFalse(isSet, "isSet should not be set");
assertEq(0, returnedDelay, "returned delay should be 0");
// Warp to effect block of second delay
cheats.roll(block.number + ALLOCATION_CONFIGURATION_DELAY + 1);
(isSet, returnedDelay) = allocationManager.getAllocationDelay(operatorToSet);
assertTrue(isSet, "isSet should be set");
assertEq(secondDelay, returnedDelay, "delay not set");
}
function testFuzz_multipleDelays(Randomness r) public rand(r) {
uint32 firstDelay = r.Uint32(1, type(uint32).max);
uint32 secondDelay = r.Uint32(1, type(uint32).max);
// Set delay
cheats.prank(operatorToSet);
allocationManager.setAllocationDelay(operatorToSet, firstDelay);
// Warp to effect block of first delay
cheats.roll(block.number + ALLOCATION_CONFIGURATION_DELAY + 1);
// Set delay again
cheats.prank(operatorToSet);
allocationManager.setAllocationDelay(operatorToSet, secondDelay);
// Assert that first delay is set
(bool isSet, uint32 returnedDelay) = allocationManager.getAllocationDelay(operatorToSet);
assertTrue(isSet, "isSet should be set");
assertEq(firstDelay, returnedDelay, "delay not set");
// Warp to effect block of second delay
cheats.roll(block.number + ALLOCATION_CONFIGURATION_DELAY + 1);
// Check values after second delay
(isSet, returnedDelay) = allocationManager.getAllocationDelay(operatorToSet);
assertTrue(isSet, "isSet should be set");
assertEq(secondDelay, returnedDelay, "delay not set");
}
function testFuzz_setDelay_DMCaller(Randomness r) public rand(r) {
uint32 delay = r.Uint32(1, type(uint32).max);
cheats.prank(address(delegationManagerMock));
allocationManager.setAllocationDelay(operatorToSet, delay);
// Warp to effect block
cheats.roll(block.number + ALLOCATION_CONFIGURATION_DELAY + 1);
(bool isSet, uint32 returnedDelay) = allocationManager.getAllocationDelay(operatorToSet);
assertTrue(isSet, "isSet should be set");
assertEq(delay, returnedDelay, "delay not set");
}
}
contract AllocationManagerUnitTests_registerForOperatorSets is AllocationManagerUnitTests {
using ArrayLib for *;
RegisterParams defaultRegisterParams;
function setUp() public override {
AllocationManagerUnitTests.setUp();
defaultRegisterParams = RegisterParams(defaultAVS, defaultOperatorSet.id.toArrayU32(), "");
}
function test_registerForOperatorSets_Paused() public {
allocationManager.pause(2 ** PAUSED_OPERATOR_SET_REGISTRATION_AND_DEREGISTRATION);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
allocationManager.registerForOperatorSets(defaultOperator, defaultRegisterParams);
}
function testFuzz_registerForOperatorSets_InvalidOperator_x(Randomness r) public rand(r) {
address operator = r.Address();
cheats.prank(operator);
cheats.expectRevert(InvalidOperator.selector);
allocationManager.registerForOperatorSets(operator, defaultRegisterParams);
}
function testFuzz_registerForOperatorSets_InvalidOperatorSet(Randomness r) public rand(r) {
cheats.prank(defaultOperator);
cheats.expectRevert(InvalidOperatorSet.selector);
defaultRegisterParams.operatorSetIds[0] = 1; // invalid id
allocationManager.registerForOperatorSets(defaultOperator, defaultRegisterParams); // invalid id
}
function testFuzz_registerForOperatorSets_AlreadyMemberOfSet(Randomness r) public rand(r) {
cheats.prank(defaultOperator);
cheats.expectRevert(AlreadyMemberOfSet.selector);
allocationManager.registerForOperatorSets(defaultOperator, defaultRegisterParams);
}
function testFuzz_registerForOperatorSets_Correctness(Randomness r) public rand(r) {
address operator = r.Address();
uint numOpSets = r.Uint256(1, FUZZ_MAX_OP_SETS);
uint32[] memory operatorSetIds = new uint32[](numOpSets);
CreateSetParams[] memory createSetParams = new CreateSetParams[](numOpSets);
_registerOperator(operator);
for (uint i; i < numOpSets; ++i) {
operatorSetIds[i] = r.Uint32(1, type(uint32).max);
createSetParams[i].operatorSetId = operatorSetIds[i];
createSetParams[i].strategies = defaultStrategies;
}
cheats.prank(defaultAVS);
allocationManager.createOperatorSets(defaultAVS, createSetParams);
for (uint j; j < numOpSets; ++j) {
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit OperatorAddedToOperatorSet(operator, OperatorSet(defaultAVS, operatorSetIds[j]));
}
cheats.expectCall(
defaultAVS, abi.encodeWithSelector(IAVSRegistrar.registerOperator.selector, operator, defaultAVS, operatorSetIds, "")
);
cheats.prank(operator);
allocationManager.registerForOperatorSets(operator, RegisterParams(defaultAVS, operatorSetIds, ""));
assertEq(allocationManager.getRegisteredSets(operator).length, numOpSets, "should be registered for all sets");
for (uint k; k < numOpSets; ++k) {
OperatorSet memory operatorSet = OperatorSet(defaultAVS, operatorSetIds[k]);
assertTrue(allocationManager.isMemberOfOperatorSet(operator, operatorSet), "should be member of set");
assertEq(allocationManager.getMembers(OperatorSet(defaultAVS, operatorSetIds[k]))[0], operator, "should be member of set");
}
}
}
contract AllocationManagerUnitTests_deregisterFromOperatorSets is AllocationManagerUnitTests {
using ArrayLib for *;
DeregisterParams defaultDeregisterParams;
function setUp() public override {
AllocationManagerUnitTests.setUp();
defaultDeregisterParams = DeregisterParams(defaultOperator, defaultAVS, defaultOperatorSet.id.toArrayU32());
}
function test_deregisterFromOperatorSets_Paused() public {
allocationManager.pause(2 ** PAUSED_OPERATOR_SET_REGISTRATION_AND_DEREGISTRATION);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
allocationManager.deregisterFromOperatorSets(defaultDeregisterParams);
}
function test_deregisterFromOperatorSets_revert_invalidCaller_notOperator() public {
address randomOperator = address(0x1);
defaultDeregisterParams.operator = randomOperator;
cheats.expectRevert(InvalidCaller.selector);
allocationManager.deregisterFromOperatorSets(defaultDeregisterParams);
}
function test_deregisterFromOperatorSets_revert_invalidCaller_notAVS() public {
address randomAVS = address(0x1);
cheats.prank(randomAVS);
cheats.expectRevert(InvalidCaller.selector);
allocationManager.deregisterFromOperatorSets(defaultDeregisterParams);
}
function testFuzz_deregisterFromOperatorSets_InvalidOperatorSet(Randomness r) public rand(r) {
defaultDeregisterParams.operatorSetIds = uint32(1).toArrayU32(); // invalid id
cheats.prank(defaultOperator);
cheats.expectRevert(InvalidOperatorSet.selector);
allocationManager.deregisterFromOperatorSets(defaultDeregisterParams);
}
function testFuzz_deregisterFromOperatorSets_NotMemberOfSet(Randomness r) public rand(r) {
defaultDeregisterParams.operator = r.Address();
cheats.prank(defaultDeregisterParams.operator);
cheats.expectRevert(NotMemberOfSet.selector);
allocationManager.deregisterFromOperatorSets(defaultDeregisterParams);
}
function testFuzz_deregisterFromOperatorSets_Correctness(Randomness r) public rand(r) {
uint numOpSets = r.Uint256(1, FUZZ_MAX_OP_SETS);
uint32[] memory operatorSetIds = new uint32[](numOpSets);
CreateSetParams[] memory createSetParams = new CreateSetParams[](numOpSets);
for (uint i; i < numOpSets; ++i) {
operatorSetIds[i] = r.Uint32(1, type(uint32).max);
createSetParams[i].operatorSetId = operatorSetIds[i];
createSetParams[i].strategies = defaultStrategies;
}
cheats.prank(defaultAVS);
allocationManager.createOperatorSets(defaultAVS, createSetParams);
address operator = r.Address();
_registerOperator(operator);
cheats.prank(operator);
allocationManager.registerForOperatorSets(operator, RegisterParams(defaultAVS, operatorSetIds, ""));
for (uint j; j < numOpSets; ++j) {
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit OperatorRemovedFromOperatorSet(operator, OperatorSet(defaultAVS, operatorSetIds[j]));
}
cheats.expectCall(
defaultAVS, abi.encodeWithSelector(IAVSRegistrar.deregisterOperator.selector, operator, defaultAVS, operatorSetIds)
);
bool callFromAVS = r.Boolean();
if (callFromAVS) cheats.prank(defaultAVS);
else cheats.prank(operator);
allocationManager.deregisterFromOperatorSets(DeregisterParams(operator, defaultAVS, operatorSetIds));
assertEq(allocationManager.getRegisteredSets(operator).length, 0, "should not be registered for any sets");
for (uint k; k < numOpSets; ++k) {
assertFalse(
allocationManager.isMemberOfOperatorSet(operator, OperatorSet(defaultAVS, operatorSetIds[k])), "should not be member of set"
);
assertEq(allocationManager.getMemberCount(OperatorSet(defaultAVS, operatorSetIds[k])), 0, "should not be member of set");
}
}
}
contract AllocationManagerUnitTests_addStrategiesToOperatorSet is AllocationManagerUnitTests {
using ArrayLib for *;
function test_addStrategiesToOperatorSet_InvalidOperatorSet() public {
cheats.prank(defaultAVS);
cheats.expectRevert(InvalidOperatorSet.selector);
allocationManager.addStrategiesToOperatorSet(defaultAVS, 1, defaultStrategies);
}
function test_addStrategiesToOperatorSet_StrategyAlreadyInOperatorSet() public {
cheats.prank(defaultAVS);
cheats.expectRevert(StrategyAlreadyInOperatorSet.selector);
allocationManager.addStrategiesToOperatorSet(defaultAVS, defaultOperatorSet.id, defaultStrategies);
}
function testFuzz_addStrategiesToOperatorSet_Correctness(Randomness r) public rand(r) {
uint numStrategies = r.Uint256(1, FUZZ_MAX_STRATS);
IStrategy[] memory strategies = new IStrategy[](numStrategies);
for (uint i; i < numStrategies; ++i) {
strategies[i] = IStrategy(r.Address());
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit StrategyAddedToOperatorSet(defaultOperatorSet, strategies[i]);
}
cheats.prank(defaultAVS);
allocationManager.addStrategiesToOperatorSet(defaultAVS, defaultOperatorSet.id, strategies);
IStrategy[] memory strategiesInSet = allocationManager.getStrategiesInOperatorSet(defaultOperatorSet);
for (uint j; j < numStrategies; ++j) {
assertTrue(strategiesInSet[j + 1] == strategies[j], "should be strat of set");
}
}
}
contract AllocationManagerUnitTests_removeStrategiesFromOperatorSet is AllocationManagerUnitTests {
using ArrayLib for *;
function test_removeStrategiesFromOperatorSet_InvalidOperatorSet() public {
cheats.prank(defaultAVS);
cheats.expectRevert(InvalidOperatorSet.selector);
allocationManager.removeStrategiesFromOperatorSet(defaultAVS, 1, defaultStrategies);
}
function test_removeStrategiesFromOperatorSet_StrategyNotInOperatorSet() public {
cheats.prank(defaultAVS);
cheats.expectRevert(StrategyNotInOperatorSet.selector);
allocationManager.removeStrategiesFromOperatorSet(defaultAVS, defaultOperatorSet.id, IStrategy(random().Address()).toArray());
}
function testFuzz_removeStrategiesFromOperatorSet_Correctness(Randomness r) public rand(r) {
uint numStrategies = r.Uint256(1, FUZZ_MAX_STRATS);
IStrategy[] memory strategies = r.StrategyArray(numStrategies);
cheats.prank(defaultAVS);
allocationManager.addStrategiesToOperatorSet(defaultAVS, defaultOperatorSet.id, strategies);
for (uint i; i < numStrategies; ++i) {
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit StrategyRemovedFromOperatorSet(defaultOperatorSet, strategies[i]);
}
assertEq(allocationManager.getStrategiesInOperatorSet(defaultOperatorSet).length, numStrategies + 1, "sanity check");
cheats.prank(defaultAVS);
allocationManager.removeStrategiesFromOperatorSet(defaultAVS, defaultOperatorSet.id, strategies);
// The original strategy should still be in the operator set.
assertEq(allocationManager.getStrategiesInOperatorSet(defaultOperatorSet).length, 1, "should not be strat of set");
}
}
contract AllocationManagerUnitTests_createOperatorSets is AllocationManagerUnitTests {
using ArrayLib for *;
function testRevert_createOperatorSets_InvalidOperatorSet() public {
cheats.prank(defaultAVS);
cheats.expectRevert(InvalidOperatorSet.selector);
allocationManager.createOperatorSets(defaultAVS, CreateSetParams(defaultOperatorSet.id, defaultStrategies).toArray());
}
function testRevert_createOperatorSets_NonexistentAVSMetadata(Randomness r) public rand(r) {
address avs = r.Address();
cheats.expectRevert(NonexistentAVSMetadata.selector);
cheats.prank(avs);
allocationManager.createOperatorSets(avs, CreateSetParams(defaultOperatorSet.id, defaultStrategies).toArray());
}
function testFuzz_createOperatorSets_Correctness(Randomness r) public rand(r) {
address avs = r.Address();
uint numOpSets = r.Uint256(1, FUZZ_MAX_OP_SETS);
uint numStrategies = r.Uint256(1, FUZZ_MAX_STRATS);
cheats.prank(avs);
allocationManager.updateAVSMetadataURI(avs, "https://example.com");
CreateSetParams[] memory createSetParams = new CreateSetParams[](numOpSets);
for (uint i; i < numOpSets; ++i) {
createSetParams[i].operatorSetId = r.Uint32(1, type(uint32).max);
createSetParams[i].strategies = r.StrategyArray(numStrategies);
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit OperatorSetCreated(OperatorSet(avs, createSetParams[i].operatorSetId));
for (uint j; j < numStrategies; ++j) {
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit StrategyAddedToOperatorSet(OperatorSet(avs, createSetParams[i].operatorSetId), createSetParams[i].strategies[j]);
}
}
cheats.prank(avs);
allocationManager.createOperatorSets(avs, createSetParams);
for (uint k; k < numOpSets; ++k) {
OperatorSet memory opSet = OperatorSet(avs, createSetParams[k].operatorSetId);
assertTrue(allocationManager.isOperatorSet(opSet), "should be operator set");
IStrategy[] memory strategiesInSet = allocationManager.getStrategiesInOperatorSet(opSet);
assertEq(strategiesInSet.length, numStrategies, "strategiesInSet length should be numStrategies");
for (uint l; l < numStrategies; ++l) {
assertTrue(
allocationManager.getStrategiesInOperatorSet(opSet)[l] == createSetParams[k].strategies[l], "should be strat of set"
);
}
}
assertEq(createSetParams.length, allocationManager.getOperatorSetCount(avs), "should be correct number of sets");
}
}
contract AllocationManagerUnitTests_setAVSRegistrar is AllocationManagerUnitTests {
function test_getAVSRegistrar() public {
address randomAVS = random().Address();
IAVSRegistrar avsRegistrar = allocationManager.getAVSRegistrar(randomAVS);
assertEq(address(avsRegistrar), address(randomAVS), "AVS registrar should return default");
}
function testFuzz_setAVSRegistrar_Correctness(Randomness r) public rand(r) {
address avs = r.Address();
IAVSRegistrar avsRegistrar = IAVSRegistrar(defaultAVS);
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit AVSRegistrarSet(avs, avsRegistrar);
cheats.prank(avs);
allocationManager.setAVSRegistrar(avs, avsRegistrar);
assertTrue(avsRegistrar == allocationManager.getAVSRegistrar(avs), "should be set");
}
}
contract AllocationManagerUnitTests_updateAVSMetadataURI is AllocationManagerUnitTests {
function test_updateAVSMetadataURI_Correctness() public {
string memory newURI = "test";
cheats.expectEmit(true, true, true, true, address(allocationManager));
emit AVSMetadataURIUpdated(defaultAVS, newURI);
cheats.prank(defaultAVS);
allocationManager.updateAVSMetadataURI(defaultAVS, newURI);
}
}
contract AllocationManagerUnitTests_getStrategyAllocations is AllocationManagerUnitTests {
using ArrayLib for *;
function testFuzz_getStrategyAllocations_Correctness(Randomness r) public rand(r) {
AllocateParams[] memory allocateParams = r.AllocateParams(defaultAVS, 1, 1);
CreateSetParams[] memory createSetParams = r.CreateSetParams(allocateParams);
cheats.prank(defaultAVS);
allocationManager.createOperatorSets(defaultAVS, createSetParams);
cheats.startPrank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.stopPrank();
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
(OperatorSet[] memory operatorSets,) = allocationManager.getStrategyAllocations(defaultOperator, allocateParams[0].strategies[0]);
assertEq(operatorSets[0].avs, allocateParams[0].operatorSet.avs, "should be defaultAVS");
assertEq(operatorSets[0].id, allocateParams[0].operatorSet.id, "should be defaultOperatorSet");
_checkAllocationStorage({
operator: defaultOperator,
operatorSet: operatorSets[0],
strategy: createSetParams[0].strategies[0],
expectedAllocation: Allocation({currentMagnitude: allocateParams[0].newMagnitudes[0], pendingDiff: 0, effectBlock: 0}),
expectedMagnitudes: Magnitudes({
encumbered: allocateParams[0].newMagnitudes[0],
max: WAD,
allocatable: WAD - allocateParams[0].newMagnitudes[0]
})
});
}
}
contract AllocationManagerUnitTests_getSlashableStake is AllocationManagerUnitTests {
using SlashingLib for *;
using ArrayLib for *;
/**
* Allocates half of magnitude for a single strategy to an operatorSet. Then allocates again. Slashes 50%
* of the first allocation. Validates slashable stake at each step.
*/
function test_allocate_onePendingAllocation(Randomness r) public rand(r) {
// Generate allocation for `strategyMock`, we allocate half
{
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, 5e17);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
}
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: DEFAULT_OPERATOR_SHARES.mulWad(5e17)
});
// Allocate the other half
AllocateParams[] memory allocateParams2 = _newAllocateParams(defaultOperatorSet, WAD);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams2);
uint32 secondAllocEffectBlock = _defaultAllocEffectBlock();
// Check minimum slashable stake remains the same
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: DEFAULT_OPERATOR_SHARES.mulWad(5e17)
});
// Check minimum slashable stake would not change even after the second allocation becomes effective
// This is because the allocation is not effective yet & we're getting a MINIMUM
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: DEFAULT_OPERATOR_SHARES.mulWad(5e17),
futureBlock: secondAllocEffectBlock + 1
});
// Check minimum slashable stake after the second allocation becomes effective
cheats.roll(secondAllocEffectBlock);
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: DEFAULT_OPERATOR_SHARES
});
}
/**
* Allocates to `firstMod` magnitude and then deallocate to `secondMod` magnitude
* Validates slashable stake at each step after allocation and deallocation
*/
function testFuzz_allocate_deallocate_validateSlashableStake(Randomness r) public rand(r) {
// Bound allocation and deallocation
uint64 firstMod = r.Uint64(1, WAD);
uint64 secondMod = r.Uint64(0, firstMod - 1);
// Allocate magnitude to default registered set
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, firstMod);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// 1. Validate slashable stake.
// This value should be 0 even at the effectBlock since its minimal slashable stake
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: 0,
futureBlock: _defaultAllocEffectBlock()
});
// Warp to allocation complete block
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// 2. Check slashable stake after allocation effect block
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: DEFAULT_OPERATOR_SHARES.mulWad(firstMod)
});
// Deallocate
allocateParams = _newAllocateParams(defaultOperatorSet, secondMod);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// 3. Check slashable stake after deallocation - should be same at current block
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: DEFAULT_OPERATOR_SHARES.mulWad(firstMod),
futureBlock: uint32(block.number)
});
// 4. Check slashable stake at the deallocation effect block
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: DEFAULT_OPERATOR_SHARES.mulWad(secondMod),
futureBlock: uint32(block.number + DEALLOCATION_DELAY + 1)
});
// Warp to deallocation effect block
cheats.roll(block.number + DEALLOCATION_DELAY + 1);
// 5. Check slashable stake at the deallocation effect block
_checkSlashableStake({
operatorSet: defaultOperatorSet,
operator: defaultOperator,
strategies: defaultStrategies,
expectedSlashableStake: DEFAULT_OPERATOR_SHARES.mulWad(secondMod)
});
}
/**
* Allocates all of magnitude to a single strategy to an operatorSet.
* Deallocate some portion. Finally, slash while deallocation is pending
*/
function testFuzz_SlashWhileDeallocationPending(Randomness r) public rand(r) {
// Initialize state
AllocateParams[] memory allocateParams = r.AllocateParams(defaultAVS, 1, 1);
AllocateParams[] memory deallocateParams = r.DeallocateParams(allocateParams);
CreateSetParams[] memory createSetParams = r.CreateSetParams(allocateParams);
RegisterParams memory registerParams = r.RegisterParams(allocateParams);
SlashingParams memory slashingParams = r.SlashingParams(defaultOperator, allocateParams[0]);
delegationManagerMock.setOperatorShares(defaultOperator, allocateParams[0].strategies[0], DEFAULT_OPERATOR_SHARES);
cheats.prank(defaultAVS);
allocationManager.createOperatorSets(defaultAVS, createSetParams);
cheats.startPrank(defaultOperator);
allocationManager.registerForOperatorSets(defaultOperator, registerParams);
// Allocate
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Deallocate
allocationManager.modifyAllocations(defaultOperator, deallocateParams);
uint32 deallocationEffectBlock = uint32(block.number + DEALLOCATION_DELAY + 1);
cheats.stopPrank();
// Check slashable stake after deallocation (still pending; no change)
_checkSlashableStake({
operatorSet: allocateParams[0].operatorSet,
operator: defaultOperator,
strategies: allocateParams[0].strategies,
expectedSlashableStake: allocateParams[0].newMagnitudes[0]
});
// Check slashable stake after deallocation takes effect, before slashing
_checkSlashableStake({
operatorSet: allocateParams[0].operatorSet,
operator: defaultOperator,
strategies: allocateParams[0].strategies,
expectedSlashableStake: deallocateParams[0].newMagnitudes[0],
futureBlock: deallocationEffectBlock
});
uint magnitudeAllocated = allocateParams[0].newMagnitudes[0];
uint magnitudeDeallocated = magnitudeAllocated - deallocateParams[0].newMagnitudes[0];
uint magnitudeSlashed = magnitudeAllocated.mulWad(slashingParams.wadsToSlash[0]);
uint expectedCurrentMagnitude = magnitudeAllocated - magnitudeSlashed;
int128 expectedPendingDiff =
-int128(uint128(magnitudeDeallocated - magnitudeDeallocated.mulWadRoundUp(slashingParams.wadsToSlash[0])));
// Slash
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashingParams);
// Check slashable stake after slash
_checkSlashableStake({
operatorSet: allocateParams[0].operatorSet,
operator: defaultOperator,
strategies: allocateParams[0].strategies,
expectedSlashableStake: expectedCurrentMagnitude
});
// Check slashable stake after deallocation takes effect
// Add 1 slippage for rounding down slashable stake
_checkSlashableStake({
operatorSet: allocateParams[0].operatorSet,
operator: defaultOperator,
strategies: allocateParams[0].strategies,
expectedSlashableStake: expectedCurrentMagnitude - uint128(-expectedPendingDiff) - 1,
futureBlock: deallocationEffectBlock
});
cheats.roll(deallocationEffectBlock);
allocationManager.clearDeallocationQueue(defaultOperator, allocateParams[0].strategies, _maxNumToClear());
// Check slashable stake after slash and deallocation
// Add 1 slippage for rounding down slashable stake
_checkSlashableStake({
operatorSet: allocateParams[0].operatorSet,
operator: defaultOperator,
strategies: allocateParams[0].strategies,
expectedSlashableStake: expectedCurrentMagnitude - uint128(-expectedPendingDiff) - 1
});
}
function testFuzz_allocate_deregister(Randomness r) public rand(r) {
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, 5e17);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
cheats.prank(defaultOperator);
allocationManager.deregisterFromOperatorSets(
DeregisterParams(defaultOperator, defaultOperatorSet.avs, defaultOperatorSet.id.toArrayU32())
);
// Check slashable stake right after deregistration
_checkSlashableStake({
operatorSet: allocateParams[0].operatorSet,
operator: defaultOperator,
strategies: allocateParams[0].strategies,
expectedSlashableStake: DEFAULT_OPERATOR_SHARES.mulWad(5e17)
});
// Assert slashable stake after deregistration delay is 0
cheats.roll(block.number + DEALLOCATION_DELAY + 1);
_checkSlashableStake({
operatorSet: allocateParams[0].operatorSet,
operator: defaultOperator,
strategies: allocateParams[0].strategies,
expectedSlashableStake: 0
});
}
}
contract AllocationManagerUnitTests_isOperatorSlashable is AllocationManagerUnitTests {
using SlashingLib for *;
using ArrayLib for *;
function test_registeredOperator() public view {
assertEq(
allocationManager.isOperatorSlashable(defaultOperator, defaultOperatorSet), true, "registered operator should be slashable"
);
assertEq(allocationManager.isMemberOfOperatorSet(defaultOperator, defaultOperatorSet), true, "operator should be member of set");
}
function test_deregisteredOperatorAndSlashable() public {
// 1. deregister defaultOperator from defaultOperator set
uint32 deregisterBlock = uint32(block.number);
cheats.prank(defaultOperator);
allocationManager.deregisterFromOperatorSets(
DeregisterParams(defaultOperator, defaultOperatorSet.avs, defaultOperatorSet.id.toArrayU32())
);
assertEq(
allocationManager.isMemberOfOperatorSet(defaultOperator, defaultOperatorSet), false, "operator should not be member of set"
);
// 2. assert operator is still slashable
assertEq(
allocationManager.isOperatorSlashable(defaultOperator, defaultOperatorSet), true, "deregistered operator should be slashable"
);
// 3. roll blocks forward to slashableUntil block assert still slashable
cheats.roll(deregisterBlock + DEALLOCATION_DELAY);
assertEq(
allocationManager.isOperatorSlashable(defaultOperator, defaultOperatorSet), true, "deregistered operator should be slashable"
);
// 4. roll 1 block forward and assert not slashable
cheats.roll(deregisterBlock + DEALLOCATION_DELAY + 1);
assertEq(
allocationManager.isOperatorSlashable(defaultOperator, defaultOperatorSet),
false,
"deregistered operator should not be slashable"
);
}
}
contract AllocationManagerUnitTests_getMaxMagnitudesAtBlock is AllocationManagerUnitTests {
using ArrayLib for *;
function testFuzz_correctness(Randomness r) public rand(r) {
// Randomly allocate
AllocateParams[] memory allocateParams = _randAllocateParams_DefaultOpSet();
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// Slash first time
SlashingParams memory slashParams = SlashingParams({
operator: defaultOperator,
operatorSetId: defaultOperatorSet.id,
strategies: defaultStrategies,
wadsToSlash: r.Uint256(0.1 ether, 0.99 ether).toArrayU256(),
description: "test"
});
uint32 firstSlashBlock = uint32(block.number);
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashParams);
uint64 maxMagnitudeAfterFirstSlash = allocationManager.getMaxMagnitude(defaultOperator, strategyMock);
// Warp to random block
uint32 secondSlashBlock = uint32(block.number + r.Uint32());
cheats.roll(secondSlashBlock);
// Slash second time
slashParams.wadsToSlash = (r.Uint64(0.1 ether, 0.99 ether)).toArrayU256();
cheats.prank(defaultAVS);
allocationManager.slashOperator(defaultAVS, slashParams);
uint64 maxMagnitudeAfterSecondSlash = allocationManager.getMaxMagnitude(defaultOperator, strategyMock);
// Warp to a block after the second slash
cheats.roll(block.number + r.Uint32());
// Validate get max magnitudes at block
assertEq(
allocationManager.getMaxMagnitudesAtBlock(defaultOperator, defaultStrategies, firstSlashBlock)[0],
maxMagnitudeAfterFirstSlash,
"max magnitude after first slash not correct"
);
assertEq(
allocationManager.getMaxMagnitudesAtBlock(defaultOperator, defaultStrategies, secondSlashBlock)[0],
maxMagnitudeAfterSecondSlash,
"max magnitude after second slash not correct"
);
}
}
contract AllocationManagerUnitTests_getAllocatedStake is AllocationManagerUnitTests {
using ArrayLib for *;
using SlashingLib for *;
/**
* Allocates to `firstMod` magnitude and validates allocated stake is correct
*/
function testFuzz_allocate(Randomness r) public rand(r) {
// Generate allocation for `strategyMock`, we allocate half
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, 5e17);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
uint[][] memory allocatedStake =
allocationManager.getAllocatedStake(defaultOperatorSet, defaultOperator.toArray(), defaultStrategies);
assertEq(allocatedStake[0][0], DEFAULT_OPERATOR_SHARES.mulWad(5e17), "allocated stake not correct");
}
/**
* Allocates to `firstMod` magnitude and then deallocate to `secondMod` magnitude
* Validates allocated stake is updated even after deallocation is cleared in storage
*/
function testFuzz_allocate_deallocate(Randomness r) public rand(r) {
// Bound allocation and deallocation
uint64 firstMod = r.Uint64(1, WAD);
uint64 secondMod = r.Uint64(0, firstMod - 1);
// 1. Allocate magnitude to default registered set & warp to allocation complete block
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, firstMod);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// 2. Deallocate
allocateParams = _newAllocateParams(defaultOperatorSet, secondMod);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
// 3. Check allocated stake right after deallocation - shouldn't be updated
uint[][] memory allocatedStake =
allocationManager.getAllocatedStake(defaultOperatorSet, defaultOperator.toArray(), defaultStrategies);
assertEq(allocatedStake[0][0], DEFAULT_OPERATOR_SHARES.mulWad(firstMod), "allocated stake not correct");
// 4. Check slashable stake at the deallocation effect block
// Warp to deallocation effect block
cheats.roll(block.number + DEALLOCATION_DELAY + 1);
allocatedStake = allocationManager.getAllocatedStake(defaultOperatorSet, defaultOperator.toArray(), defaultStrategies);
assertEq(allocatedStake[0][0], DEFAULT_OPERATOR_SHARES.mulWad(secondMod), "allocated stake not correct");
}
/**
* Allocates to `firstMod` magnitude and then deregisters the operator.
* Validates allocated stake is nonzero even after deregistration delay
*/
function testFuzz_allocate_deregister(Randomness r) public rand(r) {
// 1. Generate allocation for `strategyMock`, we allocate half
AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, 5e17);
cheats.prank(defaultOperator);
allocationManager.modifyAllocations(defaultOperator, allocateParams);
cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY);
// 2. Deregister from operator set & warp to deregistration effect block
cheats.prank(defaultOperator);
allocationManager.deregisterFromOperatorSets(
DeregisterParams(defaultOperator, defaultOperatorSet.avs, defaultOperatorSet.id.toArrayU32())
);
cheats.roll(block.number + DEALLOCATION_DELAY + 1);
// 3. Check allocated stake
uint[][] memory allocatedStake =
allocationManager.getAllocatedStake(defaultOperatorSet, defaultOperator.toArray(), defaultStrategies);
assertEq(allocatedStake[0][0], DEFAULT_OPERATOR_SHARES.mulWad(5e17), "allocated stake should remain same after deregistration");
}
}
````
## File: src/test/unit/AVSDirectoryUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/contracts/core/AVSDirectory.sol";
import "src/test/utils/EigenLayerUnitTestSetup.sol";
contract AVSDirectoryUnitTests is EigenLayerUnitTestSetup, IAVSDirectoryEvents, IAVSDirectoryErrors, ISignatureUtilsMixinTypes {
uint8 constant PAUSED_OPERATOR_REGISTER_DEREGISTER_TO_AVS = 0;
AVSDirectory avsDirectory;
address defaultAVS;
address defaultOperator;
uint defaultOperatorPk;
SignatureWithSaltAndExpiry defaultOperatorSignature;
function setUp() public virtual override {
EigenLayerUnitTestSetup.setUp();
avsDirectory = _deployAVSD(address(delegationManagerMock), pauserRegistry);
defaultAVS = cheats.randomAddress();
defaultOperatorPk = cheats.randomUint(1, MAX_PRIVATE_KEY);
defaultOperator = cheats.addr(defaultOperatorPk);
defaultOperatorSignature = _newOperatorRegistrationSignature({
operatorPk: defaultOperatorPk,
avs: defaultAVS,
salt: bytes32(cheats.randomUint()),
expiry: type(uint).max
});
delegationManagerMock.setIsOperator(defaultOperator, true);
}
function _deployAVSD(address delegationManager, IPauserRegistry pauserRegistry) internal returns (AVSDirectory avsd) {
avsd = AVSDirectory(
address(
new TransparentUpgradeableProxy(
address(new AVSDirectory(IDelegationManager(delegationManager), pauserRegistry, "v9.9.9")),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(
AVSDirectory.initialize.selector,
address(this),
0 // 0 is initialPausedStatus
)
)
)
);
isExcludedFuzzAddress[address(avsd)] = true;
bytes memory v = bytes(avsd.version());
bytes32 expectedDomainSeparator = keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH, keccak256(bytes("EigenLayer")), keccak256(bytes.concat(v[0], v[1])), block.chainid, address(avsd)
)
);
assertEq(avsd.domainSeparator(), expectedDomainSeparator, "sanity check");
}
function _newOperatorRegistrationSignature(uint operatorPk, address avs, bytes32 salt, uint expiry)
internal
view
returns (SignatureWithSaltAndExpiry memory)
{
(uint8 v, bytes32 r, bytes32 s) =
cheats.sign(operatorPk, avsDirectory.calculateOperatorAVSRegistrationDigestHash(cheats.addr(operatorPk), avs, salt, expiry));
return SignatureWithSaltAndExpiry({signature: abi.encodePacked(r, s, v), salt: salt, expiry: expiry});
}
/// -----------------------------------------------------------------------
/// initialize()
/// -----------------------------------------------------------------------
function test_initialize_Correctness() public {
assertEq(address(avsDirectory.delegation()), address(delegationManagerMock));
cheats.expectRevert("Initializable: contract is already initialized");
avsDirectory.initialize(address(this), 0);
}
/// -----------------------------------------------------------------------
/// updateAVSMetadataURI()
/// -----------------------------------------------------------------------
function test_updateAVSMetadataURI_Correctness() public {
cheats.expectEmit(true, true, true, true, address(avsDirectory));
emit AVSMetadataURIUpdated(address(this), "test");
avsDirectory.updateAVSMetadataURI("test");
}
/// -----------------------------------------------------------------------
/// cancelSalt()
/// -----------------------------------------------------------------------
function test_cancelSalt_Correctness() public {
bytes32 salt = bytes32(cheats.randomUint());
cheats.prank(defaultAVS);
avsDirectory.cancelSalt(salt);
assertTrue(avsDirectory.operatorSaltIsSpent(defaultAVS, salt));
}
/// -----------------------------------------------------------------------
/// registerOperatorToAVS()
/// -----------------------------------------------------------------------
function test_registerOperatorToAVS_Paused() public {
cheats.prank(pauser);
avsDirectory.pause(2 ** PAUSED_OPERATOR_REGISTER_DEREGISTER_TO_AVS);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
avsDirectory.registerOperatorToAVS(defaultOperator, defaultOperatorSignature);
}
function test_registerOperatorToAVS_SignatureExpired() public {
defaultOperatorSignature.expiry = block.timestamp - 1;
cheats.expectRevert(ISignatureUtilsMixinErrors.SignatureExpired.selector);
avsDirectory.registerOperatorToAVS(defaultOperator, defaultOperatorSignature);
}
function test_registerOperatorToAVS_OperatorAlreadyRegistered() public {
cheats.startPrank(defaultAVS);
avsDirectory.registerOperatorToAVS(defaultOperator, defaultOperatorSignature);
cheats.expectRevert(OperatorAlreadyRegisteredToAVS.selector);
avsDirectory.registerOperatorToAVS(defaultOperator, defaultOperatorSignature);
cheats.stopPrank();
}
function test_registerOperatorToAVS_SaltSpent() public {
cheats.prank(defaultOperator);
avsDirectory.cancelSalt(defaultOperatorSignature.salt);
cheats.prank(defaultAVS);
cheats.expectRevert(SaltSpent.selector);
avsDirectory.registerOperatorToAVS(defaultOperator, defaultOperatorSignature);
}
function test_registerOperatorToAVS_OperatorNotRegisteredToEigenLayer() public {
delegationManagerMock.setIsOperator(defaultOperator, false);
cheats.expectRevert(OperatorNotRegisteredToEigenLayer.selector);
avsDirectory.registerOperatorToAVS(defaultOperator, defaultOperatorSignature);
}
function test_registerOperatorToAVS_Correctness() public {
cheats.expectEmit(true, true, true, false, address(avsDirectory));
emit OperatorAVSRegistrationStatusUpdated(defaultOperator, defaultAVS, OperatorAVSRegistrationStatus.REGISTERED);
cheats.prank(defaultAVS);
avsDirectory.registerOperatorToAVS(defaultOperator, defaultOperatorSignature);
assertTrue(avsDirectory.avsOperatorStatus(defaultAVS, defaultOperator) == OperatorAVSRegistrationStatus.REGISTERED);
assertTrue(avsDirectory.operatorSaltIsSpent(defaultOperator, defaultOperatorSignature.salt));
}
/// -----------------------------------------------------------------------
/// deregisterOperatorFromAVS()
/// -----------------------------------------------------------------------
function test_deregisterOperatorFromAVS_Paused() public {
cheats.prank(pauser);
avsDirectory.pause(2 ** PAUSED_OPERATOR_REGISTER_DEREGISTER_TO_AVS);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
avsDirectory.deregisterOperatorFromAVS(defaultOperator);
}
function test_deregisterOperatorFromAVS_OperatorNotRegisteredToAVS() public {
cheats.expectRevert(OperatorNotRegisteredToAVS.selector);
avsDirectory.deregisterOperatorFromAVS(defaultOperator);
}
function test_deregisterOperatorFromAVS_Correctness() public {
cheats.startPrank(defaultAVS);
avsDirectory.registerOperatorToAVS(defaultOperator, defaultOperatorSignature);
cheats.expectEmit(true, true, true, false, address(avsDirectory));
emit OperatorAVSRegistrationStatusUpdated(defaultOperator, defaultAVS, OperatorAVSRegistrationStatus.UNREGISTERED);
avsDirectory.deregisterOperatorFromAVS(defaultOperator);
cheats.stopPrank();
assertTrue(avsDirectory.avsOperatorStatus(defaultAVS, defaultOperator) == OperatorAVSRegistrationStatus.UNREGISTERED);
}
}
````
## File: src/test/unit/DelegationUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/mocks/ERC1271WalletMock.sol";
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol";
import "src/contracts/core/DelegationManager.sol";
import "src/contracts/strategies/StrategyBase.sol";
import "src/test/utils/EigenLayerUnitTestSetup.sol";
import "src/contracts/libraries/SlashingLib.sol";
import "src/test/utils/ArrayLib.sol";
import "src/test/harnesses/DelegationManagerHarness.sol";
/**
* @notice Unit testing of the DelegationManager contract. Withdrawals are tightly coupled
* with EigenPodManager and StrategyManager and are part of integration tests.
* Contracts tested: DelegationManager
* Contracts not mocked: StrategyBase, PauserRegistry
*/
contract DelegationManagerUnitTests is EigenLayerUnitTestSetup, IDelegationManagerEvents, IDelegationManagerErrors {
using SlashingLib for *;
using ArrayLib for *;
using Math for *;
/// -----------------------------------------------------------------------
/// Contracts and Mocks
/// -----------------------------------------------------------------------
DelegationManagerHarness delegationManager;
DelegationManagerHarness delegationManagerImplementation;
StrategyBase strategyImplementation;
StrategyBase strategyMock;
IERC20 tokenMock;
uint tokenMockInitialSupply = 10e50;
/// -----------------------------------------------------------------------
/// Constants
/// -----------------------------------------------------------------------
uint32 constant MIN_WITHDRAWAL_DELAY_BLOCKS = 126_000; // 17.5 days in blocks
IStrategy public constant beaconChainETHStrategy = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);
uint8 internal constant PAUSED_NEW_DELEGATION = 0;
uint8 internal constant PAUSED_ENTER_WITHDRAWAL_QUEUE = 1;
uint8 internal constant PAUSED_EXIT_WITHDRAWAL_QUEUE = 2;
// Fuzz bound checks
uint constant MIN_FUZZ_SHARES = 10_000;
uint constant MIN_FUZZ_MAGNITUDE = 10_000;
uint constant APPROX_REL_DIFF = 1e8; // 0.0.0000000100000000% relative difference for assertion checks. Needed due to rounding errors
// Max shares in a strategy, see StrategyBase.sol
uint constant MAX_STRATEGY_SHARES = 1e38 - 1;
uint constant MAX_ETH_SUPPLY = 120_400_000 ether;
/// -----------------------------------------------------------------------
/// Defaults & Mappings for Stack too deep errors
/// -----------------------------------------------------------------------
// Delegation signer
uint delegationSignerPrivateKey = uint(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80);
address defaultApprover = cheats.addr(delegationSignerPrivateKey);
uint stakerPrivateKey = uint(123_456_789);
uint staker2PrivateKey = uint(234_567_891);
address defaultStaker = cheats.addr(stakerPrivateKey);
address defaultStaker2 = cheats.addr(staker2PrivateKey);
address defaultOperator = address(this);
address defaultOperator2 = address(0x123);
address defaultAVS = address(this);
string emptyStringForMetadataURI;
ISignatureUtilsMixinTypes.SignatureWithExpiry emptyApproverSignatureAndExpiry;
bytes32 emptySalt;
// Helper to use in storage
DepositScalingFactor dsf;
uint stakerDSF;
/// @notice mappings used to handle duplicate entries in fuzzed address array input
mapping(address => uint) public totalSharesForStrategyInArray;
mapping(IStrategy => uint) public totalSharesDecreasedForStrategy;
mapping(IStrategy => uint) public delegatedSharesBefore;
mapping(address => uint) public stakerDepositShares;
// Keep track of queued withdrawals
mapping(address => Withdrawal[]) public stakerQueuedWithdrawals;
function setUp() public virtual override {
// Setup
EigenLayerUnitTestSetup.setUp();
delegationManager =
DelegationManagerHarness(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")));
// Redeploy StrategyManagerMock with DM
strategyManagerMock = StrategyManagerMock(payable(address(new StrategyManagerMock(delegationManager))));
// Deploy DelegationManager implementation and upgrade proxy
delegationManagerImplementation = new DelegationManagerHarness(
IStrategyManager(address(strategyManagerMock)),
IEigenPodManager(address(eigenPodManagerMock)),
IAllocationManager(address(allocationManagerMock)),
pauserRegistry,
IPermissionController(address(permissionController)),
MIN_WITHDRAWAL_DELAY_BLOCKS
);
eigenLayerProxyAdmin.upgradeAndCall(
ITransparentUpgradeableProxy(payable(address(delegationManager))),
address(delegationManagerImplementation),
abi.encodeWithSelector(
DelegationManager.initialize.selector,
address(this),
0 // 0 is initial paused status
)
);
// Deploy mock token and strategy
tokenMock = new ERC20PresetFixedSupply("Mock Token", "MOCK", tokenMockInitialSupply, address(this));
strategyImplementation = new StrategyBase(IStrategyManager(address(strategyManagerMock)), pauserRegistry, "v9.9.9");
strategyMock = StrategyBase(
address(
new TransparentUpgradeableProxy(
address(strategyImplementation),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(StrategyBase.initialize.selector, tokenMock)
)
)
);
// Roll blocks forward so that block.number - MIN_WITHDRAWAL_DELAY_BLOCKS doesn't revert
// in _getSlashableSharesInQueue
cheats.roll(MIN_WITHDRAWAL_DELAY_BLOCKS + 1);
// Exclude delegation manager from fuzzed tests
isExcludedFuzzAddress[address(delegationManager)] = true;
isExcludedFuzzAddress[address(strategyManagerMock)] = true;
isExcludedFuzzAddress[defaultApprover] = true;
}
/**
* INTERNAL / HELPER FUNCTIONS
*/
/**
* @notice internal function to deploy mock tokens and strategies and have the staker deposit into them.
* Since we are mocking the strategyManager we call strategyManagerMock.setDeposits so that when
* DelegationManager calls getDeposits, we can have these share amounts returned.
*/
function _deployAndDepositIntoStrategies(address staker, uint[] memory sharesAmounts, bool depositBeaconChainShares)
internal
returns (IStrategy[] memory)
{
uint numStrats = sharesAmounts.length;
IStrategy[] memory strategies = new IStrategy[](numStrats);
for (uint8 i = 0; i < numStrats; i++) {
// If depositing beaconShares, then for last index of shareAmount, set shares into EPM instead
if (depositBeaconChainShares && i == numStrats - 1) {
strategies[i] = beaconChainETHStrategy;
eigenPodManagerMock.setPodOwnerShares(staker, int(sharesAmounts[numStrats - 1]));
break;
}
ERC20PresetFixedSupply token = new ERC20PresetFixedSupply(
string(abi.encodePacked("Mock Token ", i)), string(abi.encodePacked("MOCK", i)), tokenMockInitialSupply, address(this)
);
strategies[i] = StrategyBase(
address(
new TransparentUpgradeableProxy(
address(strategyImplementation),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(StrategyBase.initialize.selector, token)
)
)
);
strategyManagerMock.addDeposit(staker, strategies[i], sharesAmounts[i]);
}
return strategies;
}
/**
* @notice internal function to deploy mock tokens and strategies and have the staker deposit into them.
* Since we are mocking the strategyManager we call strategyManagerMock.setDeposits so that when
* DelegationManager calls getDeposits, we can have these share amounts returned.
*/
function _depositIntoStrategies(address staker, IStrategy[] memory strategies, uint[] memory sharesAmounts) internal {
uint numStrats = strategies.length;
require(numStrats == sharesAmounts.length, "DelegationManagerUnitTests: length mismatch");
for (uint8 i = 0; i < numStrats; i++) {
// If depositing beaconShares, then for last index of shareAmount, set shares into EPM instead
if (strategies[i] == beaconChainETHStrategy) eigenPodManagerMock.setPodOwnerShares(staker, int(sharesAmounts[i]));
else strategyManagerMock.addDeposit(staker, strategies[i], sharesAmounts[i]);
}
}
/**
* @notice internal function for calculating a signature from the delegationSigner corresponding to `_delegationSignerPrivateKey`, approving
* the `staker` to delegate to `operator`, with the specified `salt`, and expiring at `expiry`.
*/
function _getApproverSignature(uint _delegationSignerPrivateKey, address staker, address operator, bytes32 salt, uint expiry)
internal
view
returns (ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry)
{
approverSignatureAndExpiry.expiry = expiry;
{
bytes32 digestHash = delegationManager.calculateDelegationApprovalDigestHash(
staker, operator, delegationManager.delegationApprover(operator), salt, expiry
);
(uint8 v, bytes32 r, bytes32 s) = cheats.sign(_delegationSignerPrivateKey, digestHash);
approverSignatureAndExpiry.signature = abi.encodePacked(r, s, v);
}
return approverSignatureAndExpiry;
}
// @notice Assumes operator does not have a delegation approver & staker != approver
function _delegateToOperatorWhoAcceptsAllStakers(address staker, address operator) internal {
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry;
cheats.prank(staker);
delegationManager.delegateTo(operator, approverSignatureAndExpiry, emptySalt);
}
function _delegateToOperatorWhoRequiresSig(address staker, address operator, bytes32 salt) internal {
uint expiry = type(uint).max;
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry =
_getApproverSignature(delegationSignerPrivateKey, staker, operator, salt, expiry);
cheats.prank(staker);
delegationManager.delegateTo(operator, approverSignatureAndExpiry, salt);
}
function _delegateToOperatorWhoRequiresSig(address staker, address operator) internal {
_delegateToOperatorWhoRequiresSig(staker, operator, emptySalt);
}
function _registerOperatorWithBaseDetails(address operator) internal {
_registerOperator(operator, address(0), emptyStringForMetadataURI);
}
function _registerOperatorWithDelegationApprover(address operator) internal {
_registerOperator(operator, defaultApprover, emptyStringForMetadataURI);
}
function _registerOperatorWith1271DelegationApprover(address operator) internal returns (ERC1271WalletMock) {
address delegationSigner = defaultApprover;
/**
* deploy a ERC1271WalletMock contract with the `delegationSigner` address as the owner,
* so that we can create valid signatures from the `delegationSigner` for the contract to check when called
*/
ERC1271WalletMock wallet = new ERC1271WalletMock(delegationSigner);
_registerOperator(operator, address(wallet), emptyStringForMetadataURI);
return wallet;
}
function _registerOperator(address operator, address delegationApprover, string memory metadataURI)
internal
filterFuzzedAddressInputs(operator)
{
cheats.prank(operator);
delegationManager.registerAsOperator(delegationApprover, 0, metadataURI);
}
/**
* @notice Using this helper function to fuzz withdrawalAmounts since fuzzing two dynamic sized arrays of equal lengths
* reject too many inputs.
*/
function _fuzzDepositWithdrawalAmounts(Randomness r, uint32 numStrategies)
internal
returns (
uint[] memory depositAmounts,
uint[] memory withdrawalAmounts,
uint64[] memory prevMagnitudes,
uint64[] memory newMagnitudes
)
{
withdrawalAmounts = new uint[](numStrategies);
depositAmounts = new uint[](numStrategies);
prevMagnitudes = new uint64[](numStrategies);
newMagnitudes = new uint64[](numStrategies);
for (uint i = 0; i < numStrategies; i++) {
depositAmounts[i] = r.Uint256(1, MAX_STRATEGY_SHARES);
// generate withdrawal amount within range s.t withdrawAmount <= depositAmount
withdrawalAmounts[i] = r.Uint256(1, depositAmounts[i]);
prevMagnitudes[i] = r.Uint64(2, WAD);
newMagnitudes[i] = r.Uint64(1, prevMagnitudes[i]);
}
return (depositAmounts, withdrawalAmounts, prevMagnitudes, newMagnitudes);
}
function _setUpQueueWithdrawalsSingleStrat(address staker, IStrategy strategy, uint depositSharesToWithdraw)
internal
view
returns (QueuedWithdrawalParams[] memory, Withdrawal memory, bytes32)
{
IStrategy[] memory strategyArray = strategy.toArray();
QueuedWithdrawalParams[] memory queuedWithdrawalParams = new QueuedWithdrawalParams[](1);
{
uint[] memory withdrawalAmounts = depositSharesToWithdraw.toArrayU256();
queuedWithdrawalParams[0] =
QueuedWithdrawalParams({strategies: strategyArray, depositShares: withdrawalAmounts, __deprecated_withdrawer: address(0)});
}
// Get scaled shares to withdraw
uint[] memory scaledSharesArray = _getScaledShares(staker, strategy, depositSharesToWithdraw).toArrayU256();
Withdrawal memory withdrawal = Withdrawal({
staker: staker,
delegatedTo: delegationManager.delegatedTo(staker),
withdrawer: staker,
nonce: delegationManager.cumulativeWithdrawalsQueued(staker),
startBlock: uint32(block.number),
strategies: strategyArray,
scaledShares: scaledSharesArray
});
bytes32 withdrawalRoot = delegationManager.calculateWithdrawalRoot(withdrawal);
return (queuedWithdrawalParams, withdrawal, withdrawalRoot);
}
function _setUpQueueWithdrawals(address staker, IStrategy[] memory strategies, uint[] memory depositWithdrawalAmounts)
internal
view
returns (QueuedWithdrawalParams[] memory, Withdrawal memory, bytes32)
{
QueuedWithdrawalParams[] memory queuedWithdrawalParams = new QueuedWithdrawalParams[](1);
{
queuedWithdrawalParams[0] = QueuedWithdrawalParams({
strategies: strategies,
depositShares: depositWithdrawalAmounts,
__deprecated_withdrawer: address(0)
});
}
// Get scaled shares to withdraw
uint[] memory scaledSharesArray = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
scaledSharesArray[i] = _getScaledShares(staker, strategies[i], depositWithdrawalAmounts[i]);
}
Withdrawal memory withdrawal = Withdrawal({
staker: staker,
delegatedTo: delegationManager.delegatedTo(staker),
withdrawer: staker,
nonce: delegationManager.cumulativeWithdrawalsQueued(staker),
startBlock: uint32(block.number),
strategies: strategies,
scaledShares: scaledSharesArray
});
bytes32 withdrawalRoot = delegationManager.calculateWithdrawalRoot(withdrawal);
return (queuedWithdrawalParams, withdrawal, withdrawalRoot);
}
/// @notice Call queue withdrawals and push the Withdrawal to storage for testing purposes and
/// later assertions
function _queueWithdrawals(address staker, QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal)
internal
{
stakerQueuedWithdrawals[staker].push(withdrawal);
cheats.prank(staker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
}
function _getScaledShares(address staker, IStrategy strategy, uint depositSharesToWithdraw) internal view returns (uint) {
DepositScalingFactor memory _dsf = DepositScalingFactor(delegationManager.depositScalingFactor(staker, strategy));
return _dsf.scaleForQueueWithdrawal(depositSharesToWithdraw);
}
/// @notice get the shares expected to be withdrawn given the staker, strategy, maxMagnitude, and depositSharesToWithdraw
function _getWithdrawableShares(
address staker,
IStrategy[] memory strategies,
uint64[] memory maxMagnitudes,
uint[] memory depositSharesToWithdraw
) internal view returns (uint[] memory) {
require(strategies.length == depositSharesToWithdraw.length, "DelegationManagerUnitTests: length mismatch");
uint[] memory withdrawnShares = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
uint slashingFactor = _getSlashingFactor(staker, strategies[i], maxMagnitudes[i]);
withdrawnShares[i] = _calcWithdrawableShares(
depositSharesToWithdraw[i], delegationManager.depositScalingFactor(staker, strategies[i]), slashingFactor
);
}
return withdrawnShares;
}
function _getSlashingFactor(address staker, IStrategy strategy, uint64 operatorMaxMagnitude) internal view returns (uint) {
if (strategy == beaconChainETHStrategy) {
uint64 beaconChainSlashingFactor = eigenPodManagerMock.beaconChainSlashingFactor(staker);
return operatorMaxMagnitude.mulWad(beaconChainSlashingFactor);
}
return operatorMaxMagnitude;
}
/**
* Deploy and deposit staker into a single strategy, then set up a queued withdrawal for the staker
* Assumptions:
* - operator is already a registered operator.
* - withdrawalAmount <= depositAmount
*/
function _setUpCompleteQueuedWithdrawalSingleStrat(
address staker,
uint depositAmount,
uint withdrawalAmount,
bool isBeaconChainStrategy
) internal returns (Withdrawal memory, IERC20[] memory, bytes32) {
uint[] memory depositAmounts = new uint[](1);
depositAmounts[0] = depositAmount;
IStrategy[] memory strategies = _deployAndDepositIntoStrategies(staker, depositAmounts, isBeaconChainStrategy);
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: staker, strategy: strategies[0], depositSharesToWithdraw: withdrawalAmount});
cheats.prank(staker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
// Set the current deposits to be the depositAmount - withdrawalAmount
uint[] memory currentAmounts = uint(depositAmount - withdrawalAmount).toArrayU256();
strategyManagerMock.setDeposits(staker, strategies, currentAmounts);
IERC20[] memory tokens = new IERC20[](strategies.length);
for (uint i = 0; i < tokens.length; i++) {
tokens[i] = strategies[i].underlyingToken();
}
return (withdrawal, tokens, withdrawalRoot);
}
/**
* Deploy and deposit staker into a single strategy, then set up multiple queued withdrawals for the staker
* Assumptions:
* - operator is already a registered operator.
* - total deposit amount = depositAmount * numWithdrawals
* - this will fully withdraw from the single strategy
*/
function _setUpCompleteQueuedWithdrawalsSingleStrat(address staker, uint depositAmount, uint numWithdrawals)
internal
returns (Withdrawal[] memory withdrawals, IERC20[][] memory tokens, bytes32[] memory withdrawalRoots)
{
uint[] memory depositAmounts = new uint[](1);
depositAmounts[0] = depositAmount * numWithdrawals;
IStrategy[] memory strategies = _deployAndDepositIntoStrategies(staker, depositAmounts, false);
withdrawals = new Withdrawal[](numWithdrawals);
tokens = new IERC20[][](numWithdrawals);
withdrawalRoots = new bytes32[](numWithdrawals);
for (uint i = 0; i < numWithdrawals; i++) {
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: staker, strategy: strategies[0], depositSharesToWithdraw: depositAmount});
cheats.prank(staker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
withdrawals[i] = withdrawal;
tokens[i] = new IERC20[](1);
tokens[i][0] = strategies[0].underlyingToken();
withdrawalRoots[i] = withdrawalRoot;
}
{
// Set the current deposits to be 0
uint[] memory currentAmounts = new uint[](1);
currentAmounts[0] = 0;
strategyManagerMock.setDeposits(staker, strategies, currentAmounts);
}
return (withdrawals, tokens, withdrawalRoots);
}
/**
* Deploy and deposit staker into strategies, then set up a queued withdrawal for the staker
* Assumptions:
* - operator is already a registered operator.
* - for each i, withdrawalAmount[i] <= depositAmount[i] (see filterFuzzedDepositWithdrawInputs above)
*/
function _setUpCompleteQueuedWithdrawal(
address staker,
uint[] memory depositAmounts,
uint[] memory withdrawalAmounts,
bool depositBeaconChainShares
) internal returns (Withdrawal memory, IERC20[] memory, bytes32) {
IStrategy[] memory strategies = _deployAndDepositIntoStrategies(staker, depositAmounts, depositBeaconChainShares);
IERC20[] memory tokens = new IERC20[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
tokens[i] = strategies[i].underlyingToken();
}
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawals({staker: staker, strategies: strategies, depositWithdrawalAmounts: withdrawalAmounts});
cheats.prank(staker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
return (withdrawal, tokens, withdrawalRoot);
}
function _setOperatorMagnitude(address operator, IStrategy strategy, uint64 magnitude) internal {
allocationManagerMock.setMaxMagnitude(operator, strategy, magnitude);
}
function _setNewBeaconChainSlashingFactor(address staker, int beaconShares, uint sharesDecrease)
internal
returns (uint64 prevBeaconSlashingFactor, uint64 newBeaconSlashingFactor)
{
uint newRestakedBalanceWei = uint(beaconShares) - sharesDecrease;
prevBeaconSlashingFactor = eigenPodManagerMock.beaconChainSlashingFactor(staker);
newBeaconSlashingFactor = uint64(prevBeaconSlashingFactor.mulDiv(newRestakedBalanceWei, uint(beaconShares)));
eigenPodManagerMock.setBeaconChainSlashingFactor(staker, newBeaconSlashingFactor);
}
function _decreaseBeaconChainShares(address staker, int beaconShares, uint sharesDecrease)
internal
returns (uint64 prevBeaconSlashingFactor, uint64 newBeaconSlashingFactor)
{
(prevBeaconSlashingFactor, newBeaconSlashingFactor) = _setNewBeaconChainSlashingFactor(staker, beaconShares, sharesDecrease);
cheats.prank(address(eigenPodManagerMock));
delegationManager.decreaseDelegatedShares({
staker: staker,
curDepositShares: uint(beaconShares),
beaconChainSlashingFactorDecrease: prevBeaconSlashingFactor - newBeaconSlashingFactor
});
}
/// -----------------------------------------------------------------------
/// Event helpers
/// -----------------------------------------------------------------------
struct RegisterAsOperatorEmitStruct {
address operator;
address delegationApprover;
string metadataURI;
}
function _registerOperator_expectEmit(RegisterAsOperatorEmitStruct memory params) internal {
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit DelegationApproverUpdated(params.operator, params.delegationApprover);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit StakerDelegated(params.operator, params.operator);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorRegistered(params.operator, params.delegationApprover);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorMetadataURIUpdated(params.operator, params.metadataURI);
}
struct DelegateToEmitStruct {
address staker;
address operator;
IStrategy[] strategies;
uint[] depositShares;
uint[] depositScalingFactors;
}
function _delegateTo_expectEmit(DelegateToEmitStruct memory params) internal {
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit StakerDelegated(params.staker, params.operator);
for (uint i = 0; i < params.strategies.length; i++) {
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit DepositScalingFactorUpdated(params.staker, params.strategies[i], params.depositScalingFactors[i]);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesIncreased(params.operator, params.staker, params.strategies[i], params.depositShares[i]);
}
}
struct DelegateToSingleStratEmitStruct {
address staker;
address operator;
IStrategy strategy;
uint depositShares;
uint depositScalingFactor;
}
function _delegateTo_expectEmit_singleStrat(DelegateToSingleStratEmitStruct memory params) internal {
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit StakerDelegated(params.staker, params.operator);
if (params.depositShares > 0) {
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit DepositScalingFactorUpdated(params.staker, params.strategy, params.depositScalingFactor);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesIncreased(params.operator, params.staker, params.strategy, params.depositShares);
}
}
struct UndelegateEmitStruct {
address staker;
address operator;
IStrategy strategy;
uint depositSharesQueued;
uint operatorSharesDecreased;
Withdrawal withdrawal;
bytes32 withdrawalRoot;
uint depositScalingFactor;
bool forceUndelegated;
}
/// @notice Assumes only single strategy for staker being withdrawn, only checks for single strategy if
/// param.strategy address is not 0x0
function _undelegate_expectEmit_singleStrat(UndelegateEmitStruct memory params) internal {
if (params.forceUndelegated) {
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit StakerForceUndelegated(params.staker, params.operator);
}
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit StakerUndelegated(params.staker, params.operator);
if (address(params.strategy) != address(0)) {
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesDecreased(params.operator, params.staker, params.strategy, params.operatorSharesDecreased);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit SlashingWithdrawalQueued(params.withdrawalRoot, params.withdrawal, params.operatorSharesDecreased.toArrayU256());
}
}
struct IncreaseDelegatedSharesEmitStruct {
address staker;
address operator;
IStrategy strategy;
uint sharesToIncrease;
uint depositScalingFactor;
}
function _increaseDelegatedShares_expectEmit(IncreaseDelegatedSharesEmitStruct memory params) internal {
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit DepositScalingFactorUpdated(params.staker, params.strategy, params.depositScalingFactor);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesIncreased(params.operator, params.staker, params.strategy, params.sharesToIncrease);
}
struct DecreaseDelegatedSharesEmitStruct {
address staker;
address operator;
uint sharesToDecrease;
}
function _decreaseDelegatedShares_expectEmit(DecreaseDelegatedSharesEmitStruct memory params) internal {
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesDecreased(params.operator, params.staker, beaconChainETHStrategy, params.sharesToDecrease);
}
struct QueueWithdrawalsEmitStruct {
address staker;
address operator;
QueuedWithdrawalParams[] queuedWithdrawalParams;
Withdrawal withdrawal;
bytes32 withdrawalRoot;
}
function _queueWithdrawals_expectEmit(QueueWithdrawalsEmitStruct memory params) internal {
for (uint i = 0; i < params.queuedWithdrawalParams.length; i++) {
uint[] memory sharesToWithdraw = new uint[](params.queuedWithdrawalParams[i].strategies.length);
for (uint j = 0; j < params.queuedWithdrawalParams[i].strategies.length; j++) {
uint depositScalingFactor =
delegationManager.depositScalingFactor(defaultStaker, params.queuedWithdrawalParams[i].strategies[j]);
uint newMaxMagnitude =
allocationManagerMock.getMaxMagnitudes(params.operator, params.queuedWithdrawalParams[i].strategies)[j];
sharesToWithdraw[j] =
_calcWithdrawableShares(params.queuedWithdrawalParams[i].depositShares[j], depositScalingFactor, newMaxMagnitude);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesDecreased(
params.operator, params.staker, params.queuedWithdrawalParams[i].strategies[j], sharesToWithdraw[j]
);
}
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit SlashingWithdrawalQueued(params.withdrawalRoot, params.withdrawal, sharesToWithdraw);
}
}
struct CompleteQueuedWithdrawalEmitStruct {
Withdrawal withdrawal;
IERC20[] tokens;
bool receiveAsTokens;
}
function _completeQueuedWithdrawal_expectEmit(CompleteQueuedWithdrawalEmitStruct memory params) internal {
if (!params.receiveAsTokens) {
address operator = delegationManager.delegatedTo(params.withdrawal.staker);
uint64[] memory slashingFactors = new uint64[](params.withdrawal.strategies.length);
slashingFactors = allocationManagerMock.getMaxMagnitudes(operator, params.withdrawal.strategies);
// receiving as shares so check for OperatorSharesIncrease and DepositScalingFactor updated
for (uint i = 0; i < params.withdrawal.strategies.length; i++) {
// Get updated deposit scaling factor
uint curDepositShares;
if (params.withdrawal.strategies[i] == beaconChainETHStrategy) {
curDepositShares = uint(eigenPodManagerMock.stakerDepositShares(params.withdrawal.staker, address(0)));
slashingFactors[i] =
uint64(slashingFactors[i].mulWad(eigenPodManagerMock.beaconChainSlashingFactor(params.withdrawal.staker)));
} else {
curDepositShares = strategyManagerMock.stakerDepositShares(params.withdrawal.staker, params.withdrawal.strategies[i]);
}
uint sharesToWithdraw = _calcCompletedWithdrawnShares(params.withdrawal.scaledShares[i], slashingFactors[i]);
uint expectedDepositScalingFactor = _calcDepositScalingFactor({
prevDsf: delegationManager.depositScalingFactor(params.withdrawal.staker, params.withdrawal.strategies[i]),
prevDepositShares: curDepositShares,
addedDepositShares: sharesToWithdraw,
slashingFactor: slashingFactors[i]
});
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit DepositScalingFactorUpdated(params.withdrawal.staker, params.withdrawal.strategies[i], expectedDepositScalingFactor);
if (operator != address(0)) {
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesIncreased(operator, params.withdrawal.staker, params.withdrawal.strategies[i], sharesToWithdraw);
}
}
}
emit SlashingWithdrawalCompleted(delegationManager.calculateWithdrawalRoot(params.withdrawal));
}
struct CompleteQueuedWithdrawalsEmitStruct {
Withdrawal[] withdrawals;
IERC20[][] tokens;
bool[] receiveAsTokens;
}
function _completeQueuedWithdrawals_expectEmit(CompleteQueuedWithdrawalsEmitStruct memory params) internal {
for (uint i = 0; i < params.withdrawals.length; i++) {
_completeQueuedWithdrawal_expectEmit(
CompleteQueuedWithdrawalEmitStruct({
withdrawal: params.withdrawals[i],
tokens: params.tokens[i],
receiveAsTokens: params.receiveAsTokens[i]
})
);
}
}
struct SlashOperatorSharesEmitStruct {
address operator;
IStrategy strategy;
uint sharesToDecrease;
uint sharesToBurn;
}
function _slashOperatorShares_expectEmit(SlashOperatorSharesEmitStruct memory params) internal {
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesDecreased(params.operator, address(0), params.strategy, params.sharesToDecrease);
}
/// -----------------------------------------------------------------------
/// Slashing Lib helpers
/// Logic is essentially copied from SlashingLib to test the calculations
/// and to avoid using the same library in the tests
/// -----------------------------------------------------------------------
/// @notice Calculates the exact withdrawable shares
function _calcWithdrawableShares(uint depositShares, uint depositScalingFactor, uint slashingFactor) internal pure returns (uint) {
return depositShares.mulWad(depositScalingFactor).mulWad(slashingFactor);
}
function _calcCompletedWithdrawnShares(uint scaledShares, uint slashingFactor) internal pure returns (uint) {
return scaledShares.mulWad(slashingFactor);
}
/// @notice Calculates the new deposit scaling factor after a deposit
function _calcDepositScalingFactor(uint prevDsf, uint prevDepositShares, uint addedDepositShares, uint slashingFactor)
internal
pure
returns (uint)
{
if (prevDepositShares == 0) return uint(WAD).divWad(slashingFactor);
uint currWithdrawableShares = _calcWithdrawableShares(prevDepositShares, prevDsf, slashingFactor);
uint newWithdrawableShares = currWithdrawableShares + addedDepositShares;
uint newDsf = newWithdrawableShares.divWad(prevDepositShares + addedDepositShares).divWad(slashingFactor);
return newDsf;
}
function _calcSlashedAmount(uint operatorShares, uint64 prevMaxMagnitude, uint64 newMaxMagnitude)
internal
pure
returns (uint slashedAmount, uint operatorSharesAfterSlash)
{
operatorSharesAfterSlash = operatorShares.mulDiv(newMaxMagnitude, prevMaxMagnitude, Math.Rounding.Up);
slashedAmount = operatorShares - operatorSharesAfterSlash;
}
/// -----------------------------------------------------------------------
/// Helper Assertions
/// -----------------------------------------------------------------------
/// @notice Asserts for depositShares, withdrawableShares, and depositScalingFactor after a deposit
function _assertDeposit(
address staker,
address operator,
IStrategy strategy,
uint operatorSharesBefore,
uint withdrawableSharesBefore,
uint depositSharesBefore,
uint prevDsf,
uint depositAmount
) internal view {
(uint[] memory withdrawableShares, uint[] memory depositShares) =
delegationManager.getWithdrawableShares(staker, strategy.toArray());
// Check deposit shares added correctly
assertEq(
depositShares[0], depositSharesBefore + depositAmount, "depositShares should be equal to depositSharesBefore + depositAmount"
);
// Check withdrawable shares are increased, with rounding error
assertApproxEqRel(
withdrawableShares[0],
withdrawableSharesBefore + depositAmount,
APPROX_REL_DIFF,
"withdrawableShares should be equal to existingDepositShares - depositShares"
);
// Check the new dsf is accurate
uint expectedWithdrawableShares;
uint expectedDsf;
{
uint64 maxMagnitude = allocationManagerMock.getMaxMagnitude(operator, strategy);
uint slashingFactor = _getSlashingFactor(staker, strategy, maxMagnitude);
expectedDsf = _calcDepositScalingFactor(prevDsf, depositSharesBefore, depositAmount, slashingFactor);
expectedWithdrawableShares = _calcWithdrawableShares(depositSharesBefore + depositAmount, expectedDsf, slashingFactor);
}
// Check the new dsf is accurate
assertEq(
expectedDsf, delegationManager.depositScalingFactor(staker, strategy), "depositScalingFactor should be equal to expectedDsf"
);
// Check new operatorShares increased correctly
if (operator != address(0)) {
assertEq(
operatorSharesBefore + depositAmount,
delegationManager.operatorShares(operator, strategy),
"OperatorShares not increased correctly"
);
}
// Check the newly calculated withdrawable shares are correct
assertEq(withdrawableShares[0], expectedWithdrawableShares, "withdrawableShares should be equal to expectedWithdrawableShares");
}
/// @notice Asserts for depositShares, withdrawableShares, and depositScalingFactor after a delegation
function _assertDelegation(
address staker,
address operator,
IStrategy strategy,
uint operatorSharesBefore,
uint withdrawableSharesBefore,
uint depositSharesBefore,
uint prevDsf
) internal view {
(uint[] memory withdrawableShares, uint[] memory depositShares) =
delegationManager.getWithdrawableShares(staker, strategy.toArray());
// Check deposit shares don't change
assertEq(depositShares[0], depositSharesBefore, "depositShares should be equal to depositSharesBefore");
// Check withdrawable shares don't change
assertApproxEqRel(
withdrawableShares[0],
withdrawableSharesBefore,
APPROX_REL_DIFF,
"withdrawableShares should be equal to withdrawableSharesBefore"
);
// Check the new dsf is accurate
uint expectedWithdrawableShares;
uint expectedDsf;
{
uint64 maxMagnitude = allocationManagerMock.getMaxMagnitude(operator, strategy);
expectedDsf = prevDsf.divWad(maxMagnitude);
uint slashingFactor = _getSlashingFactor(staker, strategy, maxMagnitude);
expectedWithdrawableShares = _calcWithdrawableShares(depositSharesBefore, expectedDsf, slashingFactor);
}
// Check the new dsf is accurate
assertEq(
expectedDsf, delegationManager.depositScalingFactor(staker, strategy), "depositScalingFactor should be equal to expectedDsf"
);
// Check new operatorShares increased correctly
if (operator != address(0)) {
assertEq(
operatorSharesBefore + withdrawableSharesBefore,
delegationManager.operatorShares(operator, strategy),
"OperatorShares not increased correctly"
);
}
// Check the newly calculated withdrawable shares are correct
assertEq(withdrawableShares[0], expectedWithdrawableShares, "withdrawableShares should be equal to expectedWithdrawableShares");
}
/// @notice Asserts for depositShares, and operatorShares decremented properly after a withdrawal
function _assertWithdrawal(
address staker,
address operator,
IStrategy strategy,
uint operatorSharesBefore,
uint depositSharesBefore,
uint depositSharesWithdrawn,
uint depositScalingFactor,
uint slashingFactor
) internal view {
(uint[] memory withdrawableShares, uint[] memory depositShares) =
delegationManager.getWithdrawableShares(staker, strategy.toArray());
// Check deposit shares decreased correctly
assertEq(
depositShares[0],
depositSharesBefore - depositSharesWithdrawn,
"depositShares should be equal to depositSharesBefore - depositSharesWithdrawn"
);
// Check withdrawable shares are decreased, with rounding error
uint expectedWithdrawableShares =
_calcWithdrawableShares(depositSharesBefore - depositSharesWithdrawn, depositScalingFactor, slashingFactor);
assertEq(withdrawableShares[0], expectedWithdrawableShares, "withdrawableShares should be equal to expectedWithdrawableShares");
// Check operatorShares decreased properly
uint expectedWithdrawnShares = _calcWithdrawableShares(depositSharesWithdrawn, depositScalingFactor, slashingFactor);
assertEq(
operatorSharesBefore - expectedWithdrawnShares,
delegationManager.operatorShares(operator, strategy),
"OperatorShares not decreased correctly"
);
}
struct AssertCompletedWithdrawalStruct {
address staker;
address currentOperator;
Withdrawal withdrawal;
bool receiveAsTokens;
uint[] operatorSharesBefore;
uint[] withdrawableSharesBefore;
uint[] depositSharesBefore;
uint[] prevDepositScalingFactors;
uint[] slashingFactors;
uint64 beaconChainSlashingFactor;
}
/**
* @notice Asserts for a queuedWithdrawal that its root is no longer pending and the withdrawal no longer exists
* Also checks if the withdrawal is completed as shares that the current operator shares are increased appropriately
* with the staker's depositScalingFactor updated.
* NOTE: assumes no duplicate strategies in the withdrawal
*/
function _assertCompletedWithdrawal(AssertCompletedWithdrawalStruct memory params) internal view {
assertTrue(delegationManager.delegatedTo(params.staker) == params.currentOperator, "staker should be delegated to currentOperator");
_assertWithdrawalRootsComplete(params.staker, params.withdrawal.toArray());
// Check operator and staker shares if receiving as shares
if (params.receiveAsTokens) {
for (uint i = 0; i < params.withdrawal.strategies.length; i++) {
{
// assert deposit and withdrawable shares unchanged
(uint[] memory withdrawableShares, uint[] memory depositShares) =
delegationManager.getWithdrawableShares(params.staker, params.withdrawal.strategies[i].toArray());
assertEq(
params.withdrawableSharesBefore[i],
withdrawableShares[0],
"withdrawableShares should be equal to withdrawableSharesBefore"
);
assertEq(params.depositSharesBefore[i], depositShares[0], "depositShares should be equal to depositSharesBefore");
}
// assert operatorShares unchanged
assertEq(
params.operatorSharesBefore[i],
delegationManager.operatorShares(params.currentOperator, params.withdrawal.strategies[i]),
"OperatorShares should be equal to operatorSharesBefore"
);
// assert dsf is unchanged
assertEq(
params.prevDepositScalingFactors[i],
delegationManager.depositScalingFactor(params.staker, params.withdrawal.strategies[i]),
"depositScalingFactor should be equal to prevDepositScalingFactors"
);
}
} else {
for (uint i = 0; i < params.withdrawal.strategies.length; i++) {
// calculate shares to complete withdraw and add back as shares
if (params.withdrawal.strategies[i] == beaconChainETHStrategy) {
params.slashingFactors[i] = uint64(params.slashingFactors[i].mulWad(params.beaconChainSlashingFactor));
}
uint sharesToAddBack = _calcCompletedWithdrawnShares(params.withdrawal.scaledShares[i], params.slashingFactors[i]);
// assert deposit shares, withdrawable shares, and operator shares, and depositScalingFactor
_assertDeposit({
staker: params.staker,
operator: params.currentOperator,
strategy: params.withdrawal.strategies[i],
operatorSharesBefore: params.operatorSharesBefore[i],
withdrawableSharesBefore: params.withdrawableSharesBefore[i],
depositSharesBefore: params.depositSharesBefore[i],
prevDsf: params.prevDepositScalingFactors[i],
depositAmount: sharesToAddBack
});
}
}
}
/// @notice assert withdrawals completed are reflected as completed in storage for the withdrawal root and staker
function _assertWithdrawalRootsComplete(address staker, Withdrawal[] memory withdrawals) internal view {
for (uint i = 0; i < withdrawals.length; ++i) {
// Check the withdrawal root is no longer pending
// and also doesn't exist in storage for the staker
bytes32 withdrawalRootToCheck = delegationManager.calculateWithdrawalRoot(withdrawals[i]);
assertFalse(delegationManager.pendingWithdrawals(withdrawalRootToCheck), "withdrawalRoot not pending");
(Withdrawal[] memory withdrawalsInStorage,) = delegationManager.getQueuedWithdrawals(staker);
for (uint j = 0; j < withdrawalsInStorage.length; ++j) {
assertTrue(
withdrawalRootToCheck != delegationManager.calculateWithdrawalRoot(withdrawalsInStorage[j]),
"withdrawal should not exist in storage"
);
}
}
}
function _assertOperatorSharesAfterSlash(
address operator,
IStrategy strategy,
uint operatorSharesBefore,
uint64 prevMaxMagnitude,
uint64 newMaxMagnitude
) internal view returns (uint sharesToDecrement, uint operatorSharesAfterSlash) {
(sharesToDecrement, operatorSharesAfterSlash) =
_calcSlashedAmount({operatorShares: operatorSharesBefore, prevMaxMagnitude: prevMaxMagnitude, newMaxMagnitude: newMaxMagnitude});
assertEq(
operatorSharesAfterSlash,
delegationManager.operatorShares(operator, strategy),
"OperatorShares should be equal to operatorSharesAfterSlash"
);
assertEq(
delegationManager.operatorShares(operator, strategy) + sharesToDecrement,
operatorSharesBefore,
"OperatorShares + sharesToDecrement should be equal to operatorSharesBefore"
);
}
function _assertSharesAfterSlash(
address staker,
IStrategy strategy,
uint withdrawableSharesBefore,
uint expectedWithdrawableShares,
uint prevMaxMagnitude,
uint currMaxMagnitude
) internal view {
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, strategy.toArray());
assertApproxEqRel(
uint(withdrawableSharesBefore).mulDiv(currMaxMagnitude, prevMaxMagnitude),
withdrawableShares[0],
APPROX_REL_DIFF,
"withdrawableShares should be equal to withdrawableSharesBefore * currMaxMagnitude / prevMaxMagnitude"
);
assertEq(withdrawableShares[0], expectedWithdrawableShares, "withdrawableShares should be equal to expectedWithdrawableShares");
}
function _assertSharesAfterBeaconSlash(
address staker,
uint withdrawableSharesBefore,
uint expectedWithdrawableShares,
uint prevBeaconSlashingFactor
) internal view {
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, beaconChainETHStrategy.toArray());
uint currBeaconSlashingFactor = eigenPodManagerMock.beaconChainSlashingFactor(defaultStaker);
assertEq(withdrawableShares[0], expectedWithdrawableShares, "withdrawableShares should be equal to expectedWithdrawableShares");
assertApproxEqRel(
uint(withdrawableSharesBefore).mulDiv(currBeaconSlashingFactor, prevBeaconSlashingFactor),
withdrawableShares[0],
APPROX_REL_DIFF,
"withdrawableShares should be equal to withdrawableSharesBefore * currBeaconSlashingFactor / prevBeaconChainSlashingFactor"
);
}
/// @notice Due to rounding, withdrawable shares and operator shares may not align even if the operator
/// only has the single staker with deposits.
function _assertWithdrawableAndOperatorShares(uint withdrawableShares, uint operatorShares, string memory errorMessage) internal pure {
if (withdrawableShares > 0) {
assertApproxEqRel(withdrawableShares, operatorShares, APPROX_REL_DIFF, errorMessage);
} else {}
assertLe(withdrawableShares, operatorShares, "withdrawableShares should be less than or equal to operatorShares");
}
/**
* @notice Assertion checks after queuing a withdrawal. Reads withdrawals set in storage in test
* - Asserts exact match of Withdrawal struct exists in storage
* - Asserts Withdrawal root is pending
*/
function _assertQueuedWithdrawalExists(address staker) internal view {
for (uint i = 0; i < stakerQueuedWithdrawals[staker].length; ++i) {
Withdrawal memory withdrawal = stakerQueuedWithdrawals[staker][i];
bytes32 withdrawalRootToCheck = delegationManager.calculateWithdrawalRoot(withdrawal);
assertTrue(delegationManager.pendingWithdrawals(withdrawalRootToCheck), "withdrawalRoot not pending");
(Withdrawal[] memory withdrawals,) = delegationManager.getQueuedWithdrawals(staker);
for (uint j = 0; j < withdrawals.length; ++j) {
if (withdrawalRootToCheck == delegationManager.calculateWithdrawalRoot(withdrawals[j])) {
assertEq(withdrawals[j].staker, withdrawal.staker);
assertEq(withdrawals[j].withdrawer, withdrawal.withdrawer);
assertEq(withdrawals[j].delegatedTo, withdrawal.delegatedTo);
assertEq(withdrawals[j].nonce, withdrawal.nonce);
assertEq(withdrawals[j].startBlock, withdrawal.startBlock);
assertEq(withdrawals[j].scaledShares.length, withdrawal.scaledShares.length);
for (uint k = 0; k < withdrawal.scaledShares.length; ++k) {
assertEq(withdrawals[j].scaledShares[k], withdrawal.scaledShares[k]);
assertEq(address(withdrawals[j].strategies[k]), address(withdrawal.strategies[k]));
}
}
}
}
}
/**
* @notice Assertion checks after queuing a withdrawal. Reads withdrawals set in storage in test
* - Asserts exact match of Withdrawal struct exists in storage
* - Asserts Withdrawal root is pending
*/
function _assertQueuedWithdrawalExists(address staker, Withdrawal memory withdrawal) internal view {
bytes32 withdrawalRootToCheck = delegationManager.calculateWithdrawalRoot(withdrawal);
assertTrue(delegationManager.pendingWithdrawals(withdrawalRootToCheck), "withdrawalRoot not pending");
(Withdrawal[] memory withdrawals,) = delegationManager.getQueuedWithdrawals(staker);
for (uint i = 0; i < withdrawals.length; ++i) {
assertEq(withdrawals[i].staker, withdrawal.staker);
assertEq(withdrawals[i].withdrawer, withdrawal.withdrawer);
assertEq(withdrawals[i].delegatedTo, withdrawal.delegatedTo);
assertEq(withdrawals[i].nonce, withdrawal.nonce);
assertEq(withdrawals[i].startBlock, withdrawal.startBlock);
assertEq(withdrawals[i].scaledShares.length, withdrawal.scaledShares.length);
for (uint j = 0; j < withdrawal.scaledShares.length; ++j) {
assertEq(withdrawals[i].scaledShares[j], withdrawal.scaledShares[j]);
assertEq(address(withdrawals[i].strategies[j]), address(withdrawal.strategies[j]));
}
}
}
}
contract DelegationManagerUnitTests_Initialization_Setters is DelegationManagerUnitTests {
function test_initialization() public view {
assertTrue(delegationManager.domainSeparator() != bytes32(0), "sanity check");
assertEq(
address(delegationManager.strategyManager()),
address(strategyManagerMock),
"constructor / initializer incorrect, strategyManager set wrong"
);
assertEq(
address(delegationManager.pauserRegistry()),
address(pauserRegistry),
"constructor / initializer incorrect, pauserRegistry set wrong"
);
assertEq(
address(delegationManager.eigenPodManager()),
address(eigenPodManagerMock),
"constructor / initializer incorrect, eigenPodManager set wrong"
);
assertEq(
address(delegationManager.allocationManager()),
address(allocationManagerMock),
"constructor / initializer incorrect, allocationManager set wrong"
);
assertEq(
delegationManager.minWithdrawalDelayBlocks(),
MIN_WITHDRAWAL_DELAY_BLOCKS,
"constructor / initializer incorrect, MIN_WITHDRAWAL_DELAY set wrong"
);
assertEq(delegationManager.owner(), address(this), "constructor / initializer incorrect, owner set wrong");
assertEq(delegationManager.paused(), 0, "constructor / initializer incorrect, paused status set wrong");
bytes memory v = bytes(delegationManager.version());
bytes32 expectedDomainSeparator = keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(bytes("EigenLayer")),
keccak256(bytes.concat(v[0], v[1])),
block.chainid,
address(delegationManager)
)
);
assertEq(delegationManager.domainSeparator(), expectedDomainSeparator, "sanity check");
}
/// @notice Verifies that the DelegationManager cannot be initialized multiple times
function test_initialize_revert_reinitialization() public {
cheats.expectRevert("Initializable: contract is already initialized");
delegationManager.initialize(address(this), 0);
}
}
contract DelegationManagerUnitTests_RegisterModifyOperator is DelegationManagerUnitTests {
using ArrayLib for *;
function test_registerAsOperator_revert_paused() public {
// set the pausing flag
cheats.prank(pauser);
delegationManager.pause(2 ** PAUSED_NEW_DELEGATION);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
delegationManager.registerAsOperator(address(0), 0, emptyStringForMetadataURI);
}
// @notice Verifies that someone cannot successfully call `DelegationManager.registerAsOperator(delegationApprover)` again after registering for the first time
function testFuzz_registerAsOperator_revert_cannotRegisterMultipleTimes(address delegationApprover) public {
// Register once
cheats.startPrank(defaultOperator);
delegationManager.registerAsOperator(delegationApprover, 0, emptyStringForMetadataURI);
// Expect revert when register again
cheats.expectRevert(ActivelyDelegated.selector);
delegationManager.registerAsOperator(delegationApprover, 0, emptyStringForMetadataURI);
cheats.stopPrank();
}
/**
* @notice `operator` registers via calling `DelegationManager.registerAsOperator(delegationApprover, metadataURI)`
* Should be able to set any parameters, other than too high value for `stakerOptOutWindowBlocks`
* The set parameters should match the desired parameters (correct storage update)
* Operator becomes delegated to themselves
* Properly emits events – especially the `OperatorRegistered` event, but also `StakerDelegated` & `DelegationApproverUpdated` events
* Reverts appropriately if operator was already delegated to someone (including themselves, i.e. they were already an operator)
* @param operator and @param delegationApprover are fuzzed inputs
*/
function testFuzz_registerAsOperator(address operator, address delegationApprover, string memory metadataURI)
public
filterFuzzedAddressInputs(operator)
{
_registerOperator_expectEmit(
RegisterAsOperatorEmitStruct({operator: operator, delegationApprover: delegationApprover, metadataURI: metadataURI})
);
cheats.prank(operator);
delegationManager.registerAsOperator(delegationApprover, 0, metadataURI);
// Storage checks
assertEq(delegationApprover, delegationManager.delegationApprover(operator), "delegationApprover not set correctly");
assertEq(delegationManager.delegatedTo(operator), operator, "operator not delegated to self");
}
/// @notice Register two separate operators shouldn't revert
function testFuzz_registerAsOperator_TwoSeparateOperatorsRegister(
address operator1,
address delegationApprover1,
address operator2,
address delegationApprover2
) public {
cheats.assume(operator1 != operator2);
// register operator1 with expected emits
_registerOperator_expectEmit(
RegisterAsOperatorEmitStruct({
operator: operator1,
delegationApprover: delegationApprover1,
metadataURI: emptyStringForMetadataURI
})
);
_registerOperator(operator1, delegationApprover1, emptyStringForMetadataURI);
// register operator2 with expected emits
_registerOperator_expectEmit(
RegisterAsOperatorEmitStruct({
operator: operator2,
delegationApprover: delegationApprover2,
metadataURI: emptyStringForMetadataURI
})
);
_registerOperator(operator2, delegationApprover2, emptyStringForMetadataURI);
assertTrue(delegationManager.isOperator(operator1), "operator1 not registered");
assertTrue(delegationManager.isOperator(operator2), "operator2 not registered");
}
// @notice Verifies that a staker who is actively delegated to an operator cannot register as an operator (without first undelegating, at least)
function testFuzz_Revert_registerAsOperator_cannotRegisterWhileDelegated(address staker, address delegationApprover)
public
filterFuzzedAddressInputs(staker)
{
cheats.assume(staker != defaultOperator);
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry;
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, emptySalt);
// expect revert if attempt to register as operator
cheats.expectRevert(ActivelyDelegated.selector);
delegationManager.registerAsOperator(delegationApprover, 0, emptyStringForMetadataURI);
cheats.stopPrank();
}
/// @notice Add test for registerAsOperator where the operator has existing deposits in strategies
/// Assert:
/// depositShares == operatorShares == withdrawableShares
/// check operatorDetails hash encode matches the operatorDetails hash stored (call view function)
function testFuzz_registerAsOperator_withDeposits(Randomness r) public rand(r) {
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
// Set staker shares in StrategyManager
IStrategy[] memory strategiesToReturn = strategyMock.toArray();
uint[] memory sharesToReturn = new uint[](1);
sharesToReturn[0] = shares;
strategyManagerMock.setDeposits(defaultOperator, strategiesToReturn, sharesToReturn);
// register operator, their own staker depositShares should increase their operatorShares
_registerOperator_expectEmit(
RegisterAsOperatorEmitStruct({operator: defaultOperator, delegationApprover: address(0), metadataURI: emptyStringForMetadataURI})
);
_registerOperatorWithBaseDetails(defaultOperator);
uint operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock);
// check depositShares == operatorShares == withdrawableShares
assertEq(operatorSharesAfter, shares, "operator shares not set correctly");
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultOperator, strategiesToReturn);
assertEq(withdrawableShares[0], shares, "withdrawable shares not set correctly");
assertEq(strategyManagerMock.stakerDepositShares(defaultOperator, strategyMock), shares, "staker deposit shares not set correctly");
}
/**
* @notice Tests that an operator can modify their OperatorDetails by calling `DelegationManager.modifyOperatorDetails`
* Should be able to set any parameters, other than setting their `earningsReceiver` to the zero address or too high value for `stakerOptOutWindowBlocks`
* The set parameters should match the desired parameters (correct storage update)
* Properly emits an `DelegationApproverUpdated` event
* Reverts appropriately if the caller is not an operator
* Reverts if operator tries to decrease their `stakerOptOutWindowBlocks` parameter
* @param delegationApprover1 and @param delegationApprover2 are fuzzed inputs
*/
function testFuzz_modifyOperatorParameters(address delegationApprover1, address delegationApprover2) public {
_registerOperator_expectEmit(
RegisterAsOperatorEmitStruct({
operator: defaultOperator,
delegationApprover: delegationApprover1,
metadataURI: emptyStringForMetadataURI
})
);
_registerOperator(defaultOperator, delegationApprover1, emptyStringForMetadataURI);
cheats.startPrank(defaultOperator);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit DelegationApproverUpdated(defaultOperator, delegationApprover2);
delegationManager.modifyOperatorDetails(defaultOperator, delegationApprover2);
assertEq(delegationApprover2, delegationManager.delegationApprover(defaultOperator), "delegationApprover not set correctly");
assertEq(delegationManager.delegatedTo(defaultOperator), defaultOperator, "operator not delegated to self");
// or else the transition is disallowed
cheats.stopPrank();
}
// @notice Tests that an address which is not an operator cannot successfully call `updateOperatorMetadataURI`.
function test_Revert_updateOperatorMetadataUri_notRegistered() public {
assertFalse(delegationManager.isOperator(defaultOperator), "bad test setup");
cheats.prank(defaultOperator);
cheats.expectRevert(OperatorNotRegistered.selector);
delegationManager.updateOperatorMetadataURI(defaultOperator, emptyStringForMetadataURI);
}
function test_Revert_updateOperatorMetadataUri_notOperator() public {
cheats.expectRevert(OperatorNotRegistered.selector);
delegationManager.modifyOperatorDetails(defaultOperator, defaultOperator);
}
/**
* @notice Verifies that a staker cannot call cannot modify their `OperatorDetails` without first registering as an operator
* @dev This is an important check to ensure that our definition of 'operator' remains consistent, in particular for preserving the
* invariant that 'operators' are always delegated to themselves
*/
function testFuzz_UpdateOperatorMetadataURI(string memory metadataURI) public {
_registerOperatorWithBaseDetails(defaultOperator);
// call `updateOperatorMetadataURI` and check for event
cheats.prank(defaultOperator);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorMetadataURIUpdated(defaultOperator, metadataURI);
delegationManager.updateOperatorMetadataURI(defaultOperator, metadataURI);
}
function testFuzz_UAM_modifyOperatorDetails(address delegationApprover) public {
// Set admin
cheats.prank(defaultOperator);
permissionController.setAppointee(
defaultOperator, address(this), address(delegationManager), IDelegationManager.modifyOperatorDetails.selector
);
_registerOperatorWithBaseDetails(defaultOperator);
// Modify operator details
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit DelegationApproverUpdated(defaultOperator, delegationApprover);
delegationManager.modifyOperatorDetails(defaultOperator, delegationApprover);
// Check storage
assertEq(delegationApprover, delegationManager.delegationApprover(defaultOperator), "delegationApprover not set correctly");
}
function testFuzz_UAM_updateOperatorMetadataURI(string memory metadataURI) public {
// Set admin
cheats.prank(defaultOperator);
permissionController.setAppointee(
defaultOperator, address(this), address(delegationManager), IDelegationManager.updateOperatorMetadataURI.selector
);
_registerOperatorWithBaseDetails(defaultOperator);
// call `updateOperatorMetadataURI` and check for event
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorMetadataURIUpdated(defaultOperator, metadataURI);
delegationManager.updateOperatorMetadataURI(defaultOperator, metadataURI);
}
}
contract DelegationManagerUnitTests_delegateTo is DelegationManagerUnitTests {
using ArrayLib for *;
using SlashingLib for *;
function test_Revert_WhenPaused() public {
cheats.prank(defaultOperator);
delegationManager.registerAsOperator(address(0), 0, emptyStringForMetadataURI);
// set the pausing flag
cheats.prank(pauser);
delegationManager.pause(2 ** PAUSED_NEW_DELEGATION);
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry;
cheats.prank(defaultStaker);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, emptySalt);
}
/**
* @notice Delegates from `staker` to an operator, then verifies that the `staker` cannot delegate to another `operator` (at least without first undelegating)
*/
function testFuzz_Revert_WhenDelegateWhileDelegated(
Randomness r,
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry
) public rand(r) {
address staker = r.Address();
address operator = r.Address();
bytes32 salt = r.Bytes32();
// delegate from the staker to an operator
_registerOperatorWithBaseDetails(operator);
_delegateToOperatorWhoAcceptsAllStakers(staker, operator);
// try to delegate again and check that the call reverts
cheats.prank(staker);
cheats.expectRevert(ActivelyDelegated.selector);
delegationManager.delegateTo(operator, approverSignatureAndExpiry, salt);
}
/// @notice Verifies that `staker` cannot delegate to an unregistered `operator`
function testFuzz_Revert_WhenDelegateToUnregisteredOperator(Randomness r) public rand(r) {
address staker = r.Address();
address operator = r.Address();
assertFalse(delegationManager.isOperator(operator), "incorrect test input?");
// try to delegate and check that the call reverts
cheats.prank(staker);
cheats.expectRevert(OperatorNotRegistered.selector);
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry;
delegationManager.delegateTo(operator, approverSignatureAndExpiry, emptySalt);
}
/**
* @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address)
* via the `staker` calling `DelegationManager.delegateTo`
* The function should pass with any `operatorSignature` input (since it should be unused)
* Assertion checks
* - Properly emitted events from `delegateTo`
* - depositShares incremented for staker correctly
* - withdrawableShares are correct
* - depositScalingFactor is updated correctly
* - operatorShares increase by depositShares amount
* - defaultOperator is an operator, staker is delegated to defaultOperator, staker is not an operator
*/
function testFuzz_OperatorWhoAcceptsAllStakers_StrategyManagerShares(
Randomness r,
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry
) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
_registerOperatorWithBaseDetails(defaultOperator);
// verify that the salt hasn't been used before
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
// Set staker shares in StrategyManager
strategyManagerMock.addDeposit(staker, strategyMock, shares);
// delegate from the `staker` to the operator
cheats.prank(staker);
_delegateTo_expectEmit_singleStrat(
DelegateToSingleStratEmitStruct({
staker: staker,
operator: defaultOperator,
strategy: strategyMock,
depositShares: shares,
depositScalingFactor: WAD
})
);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
assertTrue(delegationManager.isOperator(defaultOperator), "staker not registered as operator");
assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
}
/**
* @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address)
* via the `staker` calling `DelegationManager.delegateTo`. `staker` holds beaconChainETHStrategy Shares
* The function should pass with any `operatorSignature` input (since it should be unused)
* Assertion Checks
* - Properly emitted events from `delegateTo`
* - depositShares incremented for staker correctly
* - withdrawableShares are correct
* - depositScalingFactor is updated correctly
* - operatorShares increase by depositShares amount
* - defaultOperator is an operator, staker is delegated to defaultOperator, staker is not an operator
* - That the staker withdrawableShares is <= operatorShares (less due to rounding from non-WAD maxMagnitude)
*/
function testFuzz_OperatorWhoAcceptsAllStakers_beaconChainStrategyShares(
Randomness r,
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry
) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
int beaconShares = int(r.Uint256(1 gwei, MAX_ETH_SUPPLY));
_registerOperatorWithBaseDetails(defaultOperator);
// Set the operators magnitude
_setOperatorMagnitude(defaultOperator, beaconChainETHStrategy, WAD);
// verify that the salt hasn't been used before
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
// Set staker shares in BeaconChainStrategy
eigenPodManagerMock.setPodOwnerShares(staker, beaconShares);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
_delegateTo_expectEmit_singleStrat(
DelegateToSingleStratEmitStruct({
staker: staker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
depositShares: uint(beaconShares),
depositScalingFactor: WAD
})
);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: uint(beaconShares)
});
assertTrue(delegationManager.isOperator(defaultOperator), "staker not registered as operator");
assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, beaconChainETHStrategy.toArray());
_assertWithdrawableAndOperatorShares(
withdrawableShares[0],
delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy),
"withdrawableShares not set correctly"
);
}
/**
* @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address)
* but it should revert as the strategy has been fully slashed for the operator.
* Assertion checks
* - staker is not delegated to defaultOperator afterwards
* - staker is not delegated
* - staker is not registered as an operator
* - salt is not spent
*/
function testFuzz_Revert_OperatorWhoAcceptsAllStakers_AlreadySlashed100Percent_StrategyManagerShares(Randomness r) public rand(r) {
address staker = r.Address();
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
_registerOperatorWithBaseDetails(defaultOperator);
// Set staker shares in StrategyManager
IStrategy[] memory strategiesToReturn = strategyMock.toArray();
uint[] memory sharesToReturn = shares.toArrayU256();
strategyManagerMock.setDeposits(staker, strategiesToReturn, sharesToReturn);
// Set the operators magnitude to be 0
_setOperatorMagnitude(defaultOperator, strategyMock, 0);
// delegate from the `staker` to the operator
cheats.prank(staker);
cheats.expectRevert(FullySlashed.selector);
delegationManager.delegateTo(defaultOperator, emptyApproverSignatureAndExpiry, emptySalt);
assertTrue(delegationManager.delegatedTo(staker) != defaultOperator, "staker should not be delegated to the operator");
assertFalse(delegationManager.isDelegated(staker), "staker should not be delegated");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), emptySalt),
"salt somehow spent too early?"
);
}
/**
* @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address)
* but it should revert as the beaconChainStrategy has been fully slashed for the operator.
* The function should pass with any `operatorSignature` input (since it should be unused)
* Assertion checks
* - beaconChainETHStrategy shares are unchanged for the operator
* - staker is not delegated to defaultOperator afterwards
* - staker is not delegated
* - staker is not registered as an operator
* - salt is not spent
*/
function testFuzz_Revert_OperatorWhoAcceptsAllStakers_AlreadySlashed100Percent_BeaconChainStrategyShares(
Randomness r,
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry
) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
int beaconShares = int(r.Uint256(1 gwei, MAX_ETH_SUPPLY));
_registerOperatorWithBaseDetails(defaultOperator);
// verify that the salt hasn't been used before
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
// Set staker shares in BeaconChainStrategy
eigenPodManagerMock.setPodOwnerShares(staker, beaconShares);
uint beaconSharesBefore = delegationManager.operatorShares(staker, beaconChainETHStrategy);
// Set the operators magnitude for native restaking to be 0
_setOperatorMagnitude(defaultOperator, beaconChainETHStrategy, 0);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
cheats.expectRevert(FullySlashed.selector);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
uint beaconSharesAfter = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy);
assertEq(beaconSharesBefore, beaconSharesAfter, "operator beaconchain shares should not have increased with negative shares");
assertTrue(delegationManager.delegatedTo(staker) != defaultOperator, "staker should not be delegated to the operator");
assertFalse(delegationManager.isDelegated(staker), "staker should not be delegated");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
}
/**
* @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address)
* and the strategy has already been slashed for the operator.
* Assertion Checks
* - Properly emitted events from `delegateTo`
* - depositShares incremented for staker correctly
* - withdrawableShares are correct
* - depositScalingFactor is updated correctly
* - operatorShares increase by depositShares amount
* - defaultOperator is an operator, staker is delegated to defaultOperator, staker is not an operator
* - That the staker withdrawableShares is <= operatorShares (less due to rounding from non-WAD maxMagnitude)
*/
function testFuzz_OperatorWhoAcceptsAllStakers_AlreadySlashed_StrategyManagerShares(Randomness r) public rand(r) {
address staker = r.Address();
uint shares = r.Uint256(1 gwei, MAX_STRATEGY_SHARES);
uint64 maxMagnitude = r.Uint64(1, WAD);
_registerOperatorWithBaseDetails(defaultOperator);
strategyManagerMock.addDeposit(staker, strategyMock, shares);
_setOperatorMagnitude(defaultOperator, strategyMock, maxMagnitude);
// Expected staker scaling factor
uint stakerScalingFactor = uint(WAD).divWad(maxMagnitude);
// delegate from the `staker` to the operator
cheats.prank(staker);
_delegateTo_expectEmit_singleStrat(
DelegateToSingleStratEmitStruct({
staker: staker,
operator: defaultOperator,
strategy: strategyMock,
depositShares: shares,
depositScalingFactor: stakerScalingFactor
})
);
delegationManager.delegateTo(defaultOperator, emptyApproverSignatureAndExpiry, emptySalt);
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
assertTrue(delegationManager.isOperator(defaultOperator), "staker not registered as operator");
assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), emptySalt),
"salt somehow spent too early?"
);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, strategyMock.toArray());
_assertWithdrawableAndOperatorShares(
withdrawableShares[0], delegationManager.operatorShares(defaultOperator, strategyMock), "withdrawableShares not set correctly"
);
}
/**
* @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address)
* and the strategy has already been slashed for the operator. `staker` holds beaconChainETHStrategy Shares
* Assertion Checks
* - Properly emitted events from `delegateTo`
* - depositShares incremented for staker correctly
* - withdrawableShares are correct
* - depositScalingFactor is updated correctly
* - operatorShares increase by depositShares amount
* - defaultOperator is an operator, staker is delegated to defaultOperator, staker is not an operator
* - That the staker withdrawableShares is <= operatorShares (less due to rounding from non-WAD maxMagnitude)
*/
function testFuzz_OperatorWhoAcceptsAllStakers_AlreadySlashed_beaconChainStrategyShares(Randomness r) public rand(r) {
address staker = r.Address();
uint64 maxMagnitude = r.Uint64(1, WAD);
int beaconShares = int(r.Uint256(1 gwei, MAX_ETH_SUPPLY));
// Register and set operator's magnitude
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, beaconChainETHStrategy, maxMagnitude);
// Set staker shares in BeaconChainStrategy
eigenPodManagerMock.setPodOwnerShares(staker, beaconShares);
// delegate from the `staker` to the operator, check for events emitted
cheats.startPrank(staker);
_delegateTo_expectEmit_singleStrat(
DelegateToSingleStratEmitStruct({
staker: staker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
depositShares: beaconShares > 0 ? uint(beaconShares) : 0,
depositScalingFactor: uint(WAD).divWad(maxMagnitude)
})
);
delegationManager.delegateTo(defaultOperator, emptyApproverSignatureAndExpiry, emptySalt);
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: uint(beaconShares)
});
assertTrue(delegationManager.isOperator(defaultOperator), "staker not registered as operator");
assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), emptySalt),
"salt somehow spent too early?"
);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, beaconChainETHStrategy.toArray());
_assertWithdrawableAndOperatorShares(
withdrawableShares[0],
delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy),
"withdrawableShares not set correctly"
);
}
/**
* @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address)
* and the strategy has already been slashed for the operator. `staker` holds beaconChainETHStrategy Shares and has been
* slashed on the beaconChain resulting in a non-WAD beaconChainSlashingFactor.
* Assertion Checks
* - Properly emitted events from `delegateTo`
* - depositShares incremented for staker correctly
* - withdrawableShares are correct
* - depositScalingFactor is updated correctly
* - operatorShares increase by withdrawableShares amount
* - defaultOperator is an operator, staker is delegated to defaultOperator, staker is not an operator
* - That the staker withdrawableShares is <= operatorShares (less due to rounding from non-WAD maxMagnitude)
*/
function testFuzz_OperatorWhoAcceptsAllStakers_AlreadySlashedAVSAndBeaconChain_beaconChainStrategyShares(Randomness r) public rand(r) {
address staker = r.Address();
uint64 maxMagnitude = r.Uint64(1, WAD);
uint64 beaconChainSlashingFactor = r.Uint64(1, WAD - 1);
int beaconShares = int(r.Uint256(1 gwei, MAX_ETH_SUPPLY));
// Register and set operator's magnitude
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, beaconChainETHStrategy, maxMagnitude);
eigenPodManagerMock.setBeaconChainSlashingFactor(staker, beaconChainSlashingFactor);
// Set staker shares in BeaconChainStrategy
eigenPodManagerMock.setPodOwnerShares(staker, beaconShares);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, beaconChainETHStrategy.toArray());
// delegate from the `staker` to the operator, check for events emitted
cheats.startPrank(staker);
_delegateTo_expectEmit_singleStrat(
DelegateToSingleStratEmitStruct({
staker: staker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
depositShares: beaconShares > 0 ? withdrawableShares[0] : 0,
depositScalingFactor: uint(WAD).divWad(maxMagnitude)
})
);
delegationManager.delegateTo(defaultOperator, emptyApproverSignatureAndExpiry, emptySalt);
_assertDelegation({
staker: staker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: withdrawableShares[0],
depositSharesBefore: uint(beaconShares),
prevDsf: WAD
});
assertTrue(delegationManager.isOperator(defaultOperator), "staker not registered as operator");
assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), emptySalt),
"salt somehow spent too early?"
);
(uint[] memory withdrawableSharesAfter,) = delegationManager.getWithdrawableShares(staker, beaconChainETHStrategy.toArray());
_assertWithdrawableAndOperatorShares(
withdrawableSharesAfter[0],
delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy),
"withdrawableShares not set correctly"
);
}
/**
* @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address)
* via the `staker` calling `DelegationManager.delegateTo`
* Similar to tests above but now with staker who has both EigenPod and StrategyManager shares.
* Assertion Checks for strategyMock and beaconChainETHStrategy
* - Properly emitted events from `delegateTo`
* - depositShares incremented for staker correctly
* - withdrawableShares are correct
* - depositScalingFactor is updated correctly
* - operatorShares increase by depositShares amount
* - defaultOperator is an operator, staker is delegated to defaultOperator, staker is not an operator
* - That the staker withdrawableShares is <= operatorShares (less due to rounding from non-WAD maxMagnitude)
*/
function testFuzz_OperatorWhoAcceptsAllStakers_BeaconChainAndStrategyManagerShares(
Randomness r,
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry
) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
int beaconShares = int(r.Uint256(1 gwei, MAX_ETH_SUPPLY));
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
_registerOperatorWithBaseDetails(defaultOperator);
// verify that the salt hasn't been used before
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
// Set staker shares in BeaconChainStrategy and StrategyManager
strategyManagerMock.addDeposit(staker, strategyMock, shares);
eigenPodManagerMock.setPodOwnerShares(staker, beaconShares);
(IStrategy[] memory strategiesToReturn, uint[] memory sharesToReturn) = delegationManager.getDepositedShares(staker);
uint[] memory depositScalingFactors = new uint[](2);
depositScalingFactors[0] = uint(WAD);
depositScalingFactors[1] = uint(WAD);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
_delegateTo_expectEmit(
DelegateToEmitStruct({
staker: staker,
operator: defaultOperator,
strategies: strategiesToReturn,
depositShares: sharesToReturn,
depositScalingFactors: depositScalingFactors
})
);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
cheats.stopPrank();
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: uint(beaconShares)
});
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, strategiesToReturn);
_assertWithdrawableAndOperatorShares(
withdrawableShares[0], delegationManager.operatorShares(defaultOperator, strategyMock), "withdrawableShares not set correctly"
);
_assertWithdrawableAndOperatorShares(
withdrawableShares[1],
delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy),
"withdrawableShares not set correctly"
);
assertTrue(delegationManager.isOperator(defaultOperator), "staker not registered as operator");
assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
}
/**
* @notice `defaultStaker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address)
* via the `defaultStaker` calling `DelegationManager.delegateTo`
* Similar to tests above but now with staker who has both EigenPod and StrategyManager shares.
* The operator has been slashed prior to deposit for both strategies.
* Assertion Checks for strategyMock and beaconChainETHStrategy
* - Properly emitted events from `delegateTo`
* - depositShares incremented for staker correctly
* - withdrawableShares are correct
* - depositScalingFactor is updated correctly
* - operatorShares increase by depositShares amount
* - defaultOperator is an operator, defaultStaker is delegated to defaultOperator, defaultStaker is not an operator
* - That the defaultStaker withdrawableShares is <= operatorShares (less due to rounding from non-WAD maxMagnitude)
*/
function testFuzz_OperatorWhoAcceptsAllStakers_AlreadySlashed_BeaconChainAndStrategyManagerShares(Randomness r) public rand(r) {
// 1. register operator and setup values, magnitudes
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
int beaconShares = int(r.Uint256(1 gwei, MAX_ETH_SUPPLY));
uint64 maxMagnitudeBeacon = r.Uint64(1, WAD);
uint64 maxMagnitudeStrategy = r.Uint64(1, WAD);
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, beaconChainETHStrategy, maxMagnitudeBeacon);
_setOperatorMagnitude(defaultOperator, strategyMock, maxMagnitudeStrategy);
// 2. Set staker shares in BeaconChainStrategy and StrategyManager
strategyManagerMock.addDeposit(defaultStaker, strategyMock, shares);
eigenPodManagerMock.setPodOwnerShares(defaultStaker, beaconShares);
(IStrategy[] memory strategiesToReturn, uint[] memory sharesToReturn) = delegationManager.getDepositedShares(defaultStaker);
// 3. delegate from the `staker` to the operator with expected emitted events
cheats.startPrank(defaultStaker);
uint[] memory depositScalingFactors = new uint[](2);
depositScalingFactors[0] = uint(WAD).divWad(maxMagnitudeStrategy);
depositScalingFactors[1] = uint(WAD).divWad(maxMagnitudeBeacon);
_delegateTo_expectEmit(
DelegateToEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
strategies: strategiesToReturn,
depositShares: sharesToReturn,
depositScalingFactors: depositScalingFactors
})
);
delegationManager.delegateTo(defaultOperator, emptyApproverSignatureAndExpiry, emptySalt);
cheats.stopPrank();
// 4. Assert correct end state values
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: uint(beaconShares)
});
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
assertTrue(delegationManager.isOperator(defaultOperator), "defaultStaker not registered as operator");
assertEq(delegationManager.delegatedTo(defaultStaker), defaultOperator, "defaultStaker delegated to the wrong address");
assertFalse(delegationManager.isOperator(defaultStaker), "staker incorrectly registered as operator");
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), emptySalt),
"salt somehow spent too early?"
);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, strategiesToReturn);
_assertWithdrawableAndOperatorShares(
withdrawableShares[0],
delegationManager.operatorShares(defaultOperator, strategyMock),
"withdrawable strategy shares not set correctly"
);
_assertWithdrawableAndOperatorShares(
withdrawableShares[1],
delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy),
"withdrawable beacon shares not set correctly"
);
}
/**
* @notice `staker` delegates to a operator who does not require any signature verification similar to test above.
* In this scenario, staker doesn't have any delegatable shares and operator shares should not increase. Staker
* should still be correctly delegated to the operator after the call.
*/
function testFuzz_OperatorWhoAcceptsAllStakers_ZeroDelegatableShares(
Randomness r,
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry
) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
_registerOperatorWithBaseDetails(defaultOperator);
// verify that the salt hasn't been used before
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit StakerDelegated(staker, defaultOperator);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
cheats.stopPrank();
assertTrue(delegationManager.isOperator(defaultOperator), "staker not registered as operator");
assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
}
/**
* @notice Like `testDelegateToOperatorWhoRequiresECDSASignature` but using an invalid expiry on purpose and checking that reversion occurs
*/
function testFuzz_Revert_WhenOperatorWhoRequiresECDSASignature_ExpiredDelegationApproverSignature(Randomness r) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
uint expiry = r.Uint256(0, block.timestamp - 1);
// roll to a very late timestamp
skip(type(uint).max / 2);
_registerOperatorWithDelegationApprover(defaultOperator);
// calculate the delegationSigner's signature
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry =
_getApproverSignature(delegationSignerPrivateKey, staker, defaultOperator, salt, expiry);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
cheats.expectRevert(ISignatureUtilsMixinErrors.SignatureExpired.selector);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
cheats.stopPrank();
}
/**
* @notice Like `testDelegateToOperatorWhoRequiresECDSASignature` but undelegating after delegating and trying the same approveSignature
* and checking that reversion occurs with the same salt
*/
function testFuzz_Revert_WhenOperatorWhoRequiresECDSASignature_PreviouslyUsedSalt(Randomness r) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
uint expiry = r.Uint256(block.timestamp + 1, type(uint).max);
_registerOperatorWithDelegationApprover(defaultOperator);
// verify that the salt hasn't been used before
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
// calculate the delegationSigner's signature
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry =
_getApproverSignature(delegationSignerPrivateKey, staker, defaultOperator, salt, expiry);
// delegate from the `staker` to the operator, undelegate, and then try to delegate again with same approversalt
// to check that call reverts
cheats.startPrank(staker);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
assertTrue(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent not spent?"
);
delegationManager.undelegate(staker);
cheats.expectRevert(SaltSpent.selector);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
cheats.stopPrank();
}
/**
* @notice Like `testDelegateToOperatorWhoRequiresECDSASignature` but using an incorrect signature on purpose and checking that reversion occurs
*/
function testFuzz_Revert_WhenOperatorWhoRequiresECDSASignature_WithBadSignature(Randomness random) public rand(random) {
address staker = random.Address();
uint expiry = random.Uint256(block.timestamp + 1, type(uint).max);
_registerOperatorWithDelegationApprover(defaultOperator);
// calculate the signature
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry;
approverSignatureAndExpiry.expiry = expiry;
{
bytes32 digestHash = delegationManager.calculateDelegationApprovalDigestHash(
staker, defaultOperator, delegationManager.delegationApprover(defaultOperator), emptySalt, expiry
);
(uint8 v, bytes32 r, bytes32 s) = cheats.sign(delegationSignerPrivateKey, digestHash);
// mess up the signature by flipping v's parity
v = (v == 27 ? 28 : 27);
approverSignatureAndExpiry.signature = abi.encodePacked(r, s, v);
}
// try to delegate from the `staker` to the operator, and check reversion
cheats.startPrank(staker);
cheats.expectRevert(ISignatureUtilsMixinErrors.InvalidSignature.selector);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, emptySalt);
cheats.stopPrank();
}
/**
* @notice `staker` delegates to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA)
* via the `staker` calling `DelegationManager.delegateTo`
* The function should pass *only with a valid ECDSA signature from the `delegationApprover`, OR if called by the operator or their delegationApprover themselves
* Properly emits a `StakerDelegated` event
* Staker is correctly delegated after the call (i.e. correct storage update)
* Reverts if the staker is already delegated (to the operator or to anyone else)
* Reverts if the ‘operator’ is not actually registered as an operator
*/
function testFuzz_OperatorWhoRequiresECDSASignature(Randomness r) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
uint expiry = r.Uint256(block.timestamp, type(uint).max);
_registerOperatorWithDelegationApprover(defaultOperator);
// verify that the salt hasn't been used before
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
// calculate the delegationSigner's signature
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry =
_getApproverSignature(delegationSignerPrivateKey, staker, defaultOperator, salt, expiry);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit StakerDelegated(staker, defaultOperator);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
cheats.stopPrank();
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
if (staker == delegationManager.delegationApprover(defaultOperator)) {
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too incorrectly?"
);
} else {
// verify that the salt is marked as used
assertTrue(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent not spent?"
);
}
}
/**
* @notice `staker` delegates to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA)
* via the `staker` calling `DelegationManager.delegateTo`
* The function should pass *only with a valid ECDSA signature from the `delegationApprover`, OR if called by the operator or their delegationApprover themselves
* Properly emits a `StakerDelegated` event
* Staker is correctly delegated after the call (i.e. correct storage update)
* Operator shares should increase by the amount of shares delegated
* Reverts if the staker is already delegated (to the operator or to anyone else)
* Reverts if the ‘operator’ is not actually registered as an operator
*/
function testFuzz_OperatorWhoRequiresECDSASignature_StrategyManagerShares(Randomness r) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
uint expiry = r.Uint256(block.timestamp, type(uint).max);
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
_registerOperatorWithDelegationApprover(defaultOperator);
// verify that the salt hasn't been used before
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
// calculate the delegationSigner's signature
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry =
_getApproverSignature(delegationSignerPrivateKey, staker, defaultOperator, salt, expiry);
// Set staker shares in StrategyManager
strategyManagerMock.addDeposit(staker, strategyMock, shares);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
_delegateTo_expectEmit_singleStrat(
DelegateToSingleStratEmitStruct({
staker: staker,
operator: defaultOperator,
strategy: strategyMock,
depositShares: shares,
depositScalingFactor: WAD
})
);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
cheats.stopPrank();
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, strategyMock.toArray());
_assertWithdrawableAndOperatorShares(
withdrawableShares[0], delegationManager.operatorShares(defaultOperator, strategyMock), "withdrawableShares not set correctly"
);
if (staker == delegationManager.delegationApprover(defaultOperator)) {
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too incorrectly?"
);
} else {
// verify that the salt is marked as used
assertTrue(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent not spent?"
);
}
}
/**
* @notice `staker` delegates to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA)
* via the `staker` calling `DelegationManager.delegateTo`
* The function should pass *only with a valid ECDSA signature from the `delegationApprover`, OR if called by the operator or their delegationApprover themselves
* Properly emits a `StakerDelegated` event
* Staker is correctly delegated after the call (i.e. correct storage update)
* Operator beaconShares should increase by the amount of shares delegated if beaconShares > 0
* Reverts if the staker is already delegated (to the operator or to anyone else)
* Reverts if the ‘operator’ is not actually registered as an operator
*/
function testFuzz_OperatorWhoRequiresECDSASignature_BeaconChainStrategyShares(Randomness r) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
uint expiry = r.Uint256(block.timestamp, type(uint).max);
int beaconShares = int(r.Uint256(1 gwei, MAX_ETH_SUPPLY));
_registerOperatorWithDelegationApprover(defaultOperator);
// verify that the salt hasn't been used before
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
// calculate the delegationSigner's signature
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry =
_getApproverSignature(delegationSignerPrivateKey, staker, defaultOperator, salt, expiry);
// Set staker shares in BeaconChainStrategy
eigenPodManagerMock.setPodOwnerShares(staker, beaconShares);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
_delegateTo_expectEmit_singleStrat(
DelegateToSingleStratEmitStruct({
staker: staker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
depositShares: uint(beaconShares),
depositScalingFactor: WAD
})
);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
cheats.stopPrank();
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: uint(beaconShares)
});
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, beaconChainETHStrategy.toArray());
_assertWithdrawableAndOperatorShares(
withdrawableShares[0],
delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy),
"withdrawableShares not set correctly"
);
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
if (staker == delegationManager.delegationApprover(defaultOperator)) {
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too incorrectly?"
);
} else {
// verify that the salt is marked as used
assertTrue(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent not spent?"
);
}
}
/**
* @notice `staker` delegates to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA)
* via the `staker` calling `DelegationManager.delegateTo`
* The function should pass *only with a valid ECDSA signature from the `delegationApprover`, OR if called by the operator or their delegationApprover themselves
* Properly emits a `StakerDelegated` event
* Staker is correctly delegated after the call (i.e. correct storage update)
* Operator beaconshares should increase by the amount of beaconShares delegated if beaconShares > 0
* Operator strategy manager shares should icnrease by amount of shares
* Reverts if the staker is already delegated (to the operator or to anyone else)
* Reverts if the ‘operator’ is not actually registered as an operator
*/
function testFuzz_OperatorWhoRequiresECDSASignature_BeaconChainAndStrategyManagerShares(Randomness r) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
uint expiry = r.Uint256(block.timestamp, type(uint).max);
int beaconShares = int(r.Uint256(1 gwei, MAX_ETH_SUPPLY));
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
// filter inputs, since this will fail when the staker is already registered as an operator
_registerOperatorWithDelegationApprover(defaultOperator);
// verify that the salt hasn't been used before
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
// calculate the delegationSigner's signature
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry =
_getApproverSignature(delegationSignerPrivateKey, staker, defaultOperator, salt, expiry);
// Set staker shares in BeaconChainStrategy and StrategyManager
uint[] memory depositScalingFactors = new uint[](2);
depositScalingFactors[0] = uint(WAD);
depositScalingFactors[1] = uint(WAD);
strategyManagerMock.addDeposit(staker, strategyMock, shares);
eigenPodManagerMock.setPodOwnerShares(staker, beaconShares);
(IStrategy[] memory strategiesToReturn, uint[] memory sharesToReturn) = delegationManager.getDepositedShares(staker);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
_delegateTo_expectEmit(
DelegateToEmitStruct({
staker: staker,
operator: defaultOperator,
strategies: strategiesToReturn,
depositShares: sharesToReturn,
depositScalingFactors: depositScalingFactors
})
);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
cheats.stopPrank();
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: uint(beaconShares)
});
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, strategiesToReturn);
_assertWithdrawableAndOperatorShares(
withdrawableShares[0],
delegationManager.operatorShares(defaultOperator, strategyMock),
"withdrawableShares for strategy not set correctly"
);
_assertWithdrawableAndOperatorShares(
withdrawableShares[1],
delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy),
"withdrawableShares for beacon strategy not set correctly"
);
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
if (staker == delegationManager.delegationApprover(defaultOperator)) {
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too incorrectly?"
);
} else {
// verify that the salt is marked as used
assertTrue(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent not spent?"
);
}
}
/**
* @notice delegateTo test with operator's delegationApprover address set to a contract address
* and check that reversion occurs when the signature is expired
*/
function testFuzz_Revert_WhenOperatorWhoRequiresEIP1271Signature_ExpiredDelegationApproverSignature(Randomness r) public rand(r) {
address staker = r.Address();
uint expiry = r.Uint256(0, block.timestamp - 1);
uint currTimestamp = r.Uint256(block.timestamp, type(uint).max);
// roll to a late timestamp
skip(currTimestamp);
_registerOperatorWithDelegationApprover(defaultOperator);
// create the signature struct
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry;
approverSignatureAndExpiry.expiry = expiry;
// try to delegate from the `staker` to the operator, and check reversion
cheats.startPrank(staker);
cheats.expectRevert(ISignatureUtilsMixinErrors.SignatureExpired.selector);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, emptySalt);
cheats.stopPrank();
}
/**
* @notice delegateTo test with operator's delegationApprover address set to a contract address
* and check that reversion occurs when the signature approverSalt is already used.
* Performed by delegating to operator, undelegating, and trying to reuse the same signature
*/
function testFuzz_Revert_WhenOperatorWhoRequiresEIP1271Signature_PreviouslyUsedSalt(Randomness r) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
uint expiry = r.Uint256(block.timestamp, type(uint).max);
// register *this contract* as an operator
// filter inputs, since this will fail when the staker is already registered as an operator
_registerOperatorWith1271DelegationApprover(defaultOperator);
// calculate the delegationSigner's signature
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry =
_getApproverSignature(delegationSignerPrivateKey, staker, defaultOperator, salt, expiry);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit StakerDelegated(staker, defaultOperator);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
delegationManager.undelegate(staker);
// Reusing same signature should revert with salt already being used
cheats.expectRevert(SaltSpent.selector);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
cheats.stopPrank();
}
/**
* @notice delegateTo test with operator's delegationApprover address set to a contract address that
* is non compliant with EIP1271
*/
function testFuzz_Revert_WhenOperatorWhoRequiresEIP1271Signature_NonCompliantWallet(Randomness r) public rand(r) {
address staker = r.Address();
uint expiry = r.Uint256(block.timestamp, type(uint).max);
// deploy a ERC1271MaliciousMock contract that will return an incorrect value when called
ERC1271MaliciousMock wallet = new ERC1271MaliciousMock();
_registerOperator(defaultOperator, address(wallet), emptyStringForMetadataURI);
// create the signature struct
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry;
approverSignatureAndExpiry.expiry = expiry;
// try to delegate from the `staker` to the operator, and check reversion
cheats.startPrank(staker);
// because the ERC1271MaliciousMock contract returns the wrong amount of data, we get a low-level "EvmError: Revert" message here rather than the error message bubbling up
cheats.expectRevert();
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, emptySalt);
cheats.stopPrank();
}
/**
* @notice delegateTo test with operator's delegationApprover address set to a contract address that
* returns a value other than the EIP1271 "magic bytes" and checking that reversion occurs appropriately
*/
function testFuzz_Revert_WhenOperatorWhoRequiresEIP1271Signature_IsValidSignatureFails(Randomness r) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
uint expiry = r.Uint256(block.timestamp, type(uint).max);
// deploy a ERC1271WalletMock contract that will return an incorrect value when called
// owner is the 0 address
ERC1271WalletMock wallet = new ERC1271WalletMock(address(1));
_registerOperator(defaultOperator, address(wallet), emptyStringForMetadataURI);
// calculate the delegationSigner's but this is not the correct signature from the wallet contract
// since the wallet owner is address(1)
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry =
_getApproverSignature(delegationSignerPrivateKey, staker, defaultOperator, salt, expiry);
// try to delegate from the `staker` to the operator, and check reversion
cheats.startPrank(staker);
// Signature should fail as the wallet will not return EIP1271_MAGICVALUE
cheats.expectRevert(ISignatureUtilsMixinErrors.InvalidSignature.selector);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, emptySalt);
cheats.stopPrank();
}
/**
* @notice `staker` delegates to an operator who requires signature verification through an EIP1271-compliant contract (i.e. the operator’s `delegationApprover` address is
* set to a nonzero and code-containing address) via the `staker` calling `DelegationManager.delegateTo`
* The function uses OZ's ERC1271WalletMock contract, and thus should pass *only when a valid ECDSA signature from the `owner` of the ERC1271WalletMock contract,
* OR if called by the operator or their delegationApprover themselves
* Properly emits a `StakerDelegated` event
* Staker is correctly delegated after the call (i.e. correct storage update)
* Reverts if the staker is already delegated (to the operator or to anyone else)
* Reverts if the ‘operator’ is not actually registered as an operator
*/
function testFuzz_OperatorWhoRequiresEIP1271Signature(Randomness r) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
uint expiry = r.Uint256(block.timestamp, type(uint).max);
_registerOperatorWith1271DelegationApprover(defaultOperator);
// verify that the salt hasn't been used before
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too early?"
);
// calculate the delegationSigner's signature
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry =
_getApproverSignature(delegationSignerPrivateKey, staker, defaultOperator, salt, expiry);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit StakerDelegated(staker, defaultOperator);
delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt);
cheats.stopPrank();
assertTrue(delegationManager.isDelegated(staker), "staker not delegated correctly");
assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address");
assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator");
// check that the nonce incremented appropriately
if (staker == defaultOperator || staker == delegationManager.delegationApprover(defaultOperator)) {
// verify that the salt is still marked as unused (since it wasn't checked or used)
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent too incorrectly?"
);
} else {
// verify that the salt is marked as used
assertTrue(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(defaultOperator), salt),
"salt somehow spent not spent?"
);
}
}
}
contract DelegationManagerUnitTests_increaseDelegatedShares is DelegationManagerUnitTests {
using ArrayLib for *;
using SlashingLib for *;
using Math for *;
/// @notice Verifies that `DelegationManager.increaseDelegatedShares` reverts if not called by the StrategyManager nor EigenPodManager
function testFuzz_Revert_increaseDelegatedShares_invalidCaller(Randomness r) public rand(r) {
address invalidCaller = r.Address();
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
cheats.expectRevert(IDelegationManagerErrors.OnlyStrategyManagerOrEigenPodManager.selector);
delegationManager.increaseDelegatedShares(invalidCaller, strategyMock, 0, shares);
}
/**
* @notice Verifies that `DelegationManager.increaseDelegatedShares` reverts when operator slashed 100% for a strategy
* and the staker has deposits in that strategy
*/
function testFuzz_Revert_increaseDelegatedShares_slashedOperator100Percent(Randomness r) public rand(r) {
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
address staker = r.Address();
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
// Set operator magnitude
_setOperatorMagnitude({operator: defaultOperator, strategy: strategyMock, magnitude: 0});
// delegate from the `staker` to the operator
_delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
uint _delegatedSharesBefore = delegationManager.operatorShares(delegationManager.delegatedTo(staker), strategyMock);
cheats.prank(address(strategyManagerMock));
cheats.expectRevert(FullySlashed.selector);
delegationManager.increaseDelegatedShares(staker, strategyMock, 0, shares);
uint delegatedSharesAfter = delegationManager.operatorShares(delegationManager.delegatedTo(staker), strategyMock);
assertEq(delegatedSharesAfter, _delegatedSharesBefore, "delegated shares incremented incorrectly");
assertEq(_delegatedSharesBefore, 0, "nonzero shares delegated to zero address!");
}
/**
* @notice Verifies that `DelegationManager.increaseDelegatedShares` reverts when operator slashed 100% for a strategy
* and the staker has deposits in that strategy. In this test case, the staker was initially deposited and delegated
* to the operator before the operator was slashed 100%.
* @dev Checks that withdrawable shares after 100% slashed is 0
* @dev Checks that as a staker, redepositing after 100% slashed reverts
*/
function testFuzz_Revert_increaseDelegatedShares_slashedOperator100PercentWithExistingStaker(Randomness r) public rand(r) {
address staker = r.Address();
uint64 initialMagnitude = r.Uint64(1, WAD);
uint existingShares = r.Uint256(1, MAX_STRATEGY_SHARES);
uint shares = r.Uint256(existingShares, MAX_STRATEGY_SHARES);
// 1. Register operator with initial operator magnitude and delegate staker to operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude({operator: defaultOperator, strategy: strategyMock, magnitude: initialMagnitude});
_delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
// 2. set staker initial shares and increase delegated shares
IStrategy[] memory strategiesDeposited = strategyMock.toArray();
uint[] memory sharesToReturn = existingShares.toArrayU256();
strategyManagerMock.setDeposits(staker, strategiesDeposited, sharesToReturn);
cheats.prank(address(strategyManagerMock));
delegationManager.increaseDelegatedShares(staker, strategyMock, 0, existingShares);
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: existingShares
});
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, strategiesDeposited);
_assertWithdrawableAndOperatorShares(
withdrawableShares[0], delegationManager.operatorShares(defaultOperator, strategyMock), "Shares not increased correctly"
);
// 3. Now set operator magnitude to 0 (100% slashed)
_setOperatorMagnitude({operator: defaultOperator, strategy: strategyMock, magnitude: 0});
// 4. Try to "redeposit" and expect a revert since strategy is 100% slashed
// staker's withdrawable shares should also be 0 now
cheats.prank(address(strategyManagerMock));
cheats.expectRevert(FullySlashed.selector);
delegationManager.increaseDelegatedShares(staker, strategyMock, existingShares, shares);
(withdrawableShares,) = delegationManager.getWithdrawableShares(staker, strategiesDeposited);
assertEq(withdrawableShares[0], 0, "All existing shares should be slashed");
}
/// @notice Verifies that there is no change in operatorShares if the staker is not delegated
function testFuzz_increaseDelegatedShares_noop(Randomness r) public rand(r) {
address staker = r.Address();
_registerOperatorWithBaseDetails(defaultOperator);
assertFalse(delegationManager.isDelegated(staker), "bad test setup");
cheats.prank(address(strategyManagerMock));
vm.recordLogs();
delegationManager.increaseDelegatedShares(staker, strategyMock, 0, 0);
assertEq(delegationManager.operatorShares(defaultOperator, strategyMock), 0, "shares should not have changed");
Vm.Log[] memory entries = vm.getRecordedLogs();
assertEq(0, entries.length, "should not have emitted any events");
}
/**
* @notice Verifies that `DelegationManager.increaseDelegatedShares` properly increases the delegated `shares` that the operator
* who the `staker` is delegated to has in the strategy
* Asserts:
* - depositScalingFactor, depositShares, withdrawableShares, operatorShares after deposit
* - correct operator shares after deposit
*
* @dev Checks that there is no change if the staker is not delegated
*/
function testFuzz_increaseDelegatedShares(Randomness r) public rand(r) {
address staker = r.Address();
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
bool delegateFromStakerToOperator = r.Boolean();
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
// delegate from the `staker` to the operator *if `delegateFromStakerToOperator` is 'true'*
if (delegateFromStakerToOperator) _delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
uint delegatedSharesBefore = delegationManager.operatorShares(delegationManager.delegatedTo(staker), strategyMock);
// deposit and increase delegated shares
strategyManagerMock.addDeposit(staker, strategyMock, shares);
if (delegationManager.isDelegated(staker)) {
_increaseDelegatedShares_expectEmit(
IncreaseDelegatedSharesEmitStruct({
staker: staker,
operator: delegationManager.delegatedTo(staker),
strategy: strategyMock,
sharesToIncrease: shares,
depositScalingFactor: WAD
})
);
}
cheats.prank(address(strategyManagerMock));
delegationManager.increaseDelegatedShares(staker, strategyMock, 0, shares);
_assertDeposit({
staker: staker,
operator: delegationManager.delegatedTo(staker),
strategy: strategyMock,
operatorSharesBefore: delegatedSharesBefore,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
// Assert correct end state values
uint delegatedSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, strategyMock.toArray());
if (delegationManager.isDelegated(staker)) {
_assertWithdrawableAndOperatorShares(withdrawableShares[0], delegatedSharesAfter, "Invalid withdrawable shares");
} else {
assertEq(delegatedSharesAfter, delegatedSharesBefore, "delegated shares incremented incorrectly");
assertEq(delegatedSharesBefore, 0, "nonzero shares delegated to zero address!");
}
}
function testFuzz_increaseDelegatedShares_beaconChainShares(Randomness r) public rand(r) {
address staker = r.Address();
uint shares = r.Uint256(1, MAX_ETH_SUPPLY);
uint64 beaconChainSlashingFactor = r.Uint64(1, WAD);
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
// delegate from the `staker` to the operator *if `delegateFromStakerToOperator` is 'true'*
_delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
uint delegatedSharesBefore = delegationManager.operatorShares(delegationManager.delegatedTo(staker), beaconChainETHStrategy);
// deposit and increase delegated shares
eigenPodManagerMock.setPodOwnerShares(staker, int(shares));
eigenPodManagerMock.setBeaconChainSlashingFactor(staker, beaconChainSlashingFactor);
_increaseDelegatedShares_expectEmit(
IncreaseDelegatedSharesEmitStruct({
staker: staker,
operator: delegationManager.delegatedTo(staker),
strategy: beaconChainETHStrategy,
sharesToIncrease: shares,
depositScalingFactor: uint(WAD).divWad(beaconChainSlashingFactor)
})
);
cheats.prank(address(strategyManagerMock));
delegationManager.increaseDelegatedShares(staker, beaconChainETHStrategy, 0, shares);
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: delegatedSharesBefore,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
// Assert correct end state values
uint delegatedSharesAfter = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, beaconChainETHStrategy.toArray());
_assertWithdrawableAndOperatorShares(withdrawableShares[0], delegatedSharesAfter, "Invalid withdrawable shares");
}
/**
* @notice Verifies that `DelegationManager.increaseDelegatedShares` properly increases the delegated `shares` that the operator
* who the `staker` is delegated to has in the strategy
* @dev Checks that there is no change if the staker is not delegated
*/
function testFuzz_increaseDelegatedShares_slashedOperator(Randomness r) public rand(r) {
address staker = r.Address();
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
uint64 magnitude = r.Uint64(1, WAD);
bool delegateFromStakerToOperator = r.Boolean();
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
// Set operator magnitude
_setOperatorMagnitude(defaultOperator, strategyMock, magnitude);
// delegate from the `staker` to the operator *if `delegateFromStakerToOperator` is 'true'*
if (delegateFromStakerToOperator) _delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
uint delegatedSharesBefore = delegationManager.operatorShares(delegationManager.delegatedTo(staker), strategyMock);
strategyManagerMock.addDeposit(staker, strategyMock, shares);
if (delegationManager.isDelegated(staker)) {
_increaseDelegatedShares_expectEmit(
IncreaseDelegatedSharesEmitStruct({
staker: staker,
operator: defaultOperator,
strategy: strategyMock,
sharesToIncrease: shares,
depositScalingFactor: uint(WAD).divWad(magnitude)
})
);
}
cheats.prank(address(strategyManagerMock));
delegationManager.increaseDelegatedShares(staker, strategyMock, 0, shares);
_assertDeposit({
staker: staker,
operator: delegationManager.delegatedTo(staker),
strategy: strategyMock,
operatorSharesBefore: delegatedSharesBefore,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
// Assert correct values
uint delegatedSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, strategyMock.toArray());
if (delegationManager.isDelegated(staker)) {
_assertWithdrawableAndOperatorShares(withdrawableShares[0], delegatedSharesAfter, "Invalid withdrawable shares");
} else {
assertEq(delegatedSharesAfter, delegatedSharesBefore, "delegated shares incremented incorrectly");
assertEq(delegatedSharesBefore, 0, "nonzero shares delegated to zero address!");
}
}
/**
* @notice Verifies that `DelegationManager.increaseDelegatedShares` properly increases the delegated `shares` for the
* `defaultOperator` who the staker is delegated to. Asserts for proper events emitted and correct withdrawable shares,
* despoitScalingFactor for the staker, and operator shares after deposit.
*/
function testFuzz_increaseDelegatedShares_slashedOperatorAndBeaconChainShares(Randomness r) public rand(r) {
address staker = r.Address();
uint shares = r.Uint256(1, MAX_ETH_SUPPLY);
uint64 maxMagnitude = r.Uint64(1, WAD);
uint64 beaconChainSlashingFactor = r.Uint64(1, WAD);
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
// Set operator magnitude
_setOperatorMagnitude(defaultOperator, beaconChainETHStrategy, maxMagnitude);
// delegate from the `staker` to the operator *if `delegateFromStakerToOperator` is 'true'*
_delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
uint delegatedSharesBefore = delegationManager.operatorShares(delegationManager.delegatedTo(staker), beaconChainETHStrategy);
// deposit and increase delegated shares
eigenPodManagerMock.setPodOwnerShares(staker, int(shares));
eigenPodManagerMock.setBeaconChainSlashingFactor(staker, beaconChainSlashingFactor);
_increaseDelegatedShares_expectEmit(
IncreaseDelegatedSharesEmitStruct({
staker: staker,
operator: delegationManager.delegatedTo(staker),
strategy: beaconChainETHStrategy,
sharesToIncrease: shares,
depositScalingFactor: uint(WAD).divWad(maxMagnitude.mulWad(beaconChainSlashingFactor))
})
);
cheats.prank(address(strategyManagerMock));
delegationManager.increaseDelegatedShares(staker, beaconChainETHStrategy, 0, shares);
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: delegatedSharesBefore,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
// Assert correct end state values
uint delegatedSharesAfter = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, beaconChainETHStrategy.toArray());
_assertWithdrawableAndOperatorShares(withdrawableShares[0], delegatedSharesAfter, "Invalid withdrawable shares");
}
/**
* @notice Verifies that `DelegationManager.increaseDelegatedShares` doesn't revert when operator slashed 100% for a strategy
* and the staker has deposits in a separate strategy
*/
function testFuzz_increaseDelegatedShares_slashedOperator100Percent(Randomness r) public rand(r) {
address staker = r.Address();
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
uint64 magnitude = r.Uint64(1, WAD);
IStrategy[] memory strategyArray = r.StrategyArray(1);
IStrategy strategy = strategyArray[0];
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
// Set operator magnitude for 100% slashed strategy
_setOperatorMagnitude({operator: defaultOperator, strategy: strategyMock, magnitude: 0});
// Set operator magnitude for non-100% slashed strategy
_setOperatorMagnitude({operator: defaultOperator, strategy: strategy, magnitude: magnitude});
// delegate from the `staker` to the operator
_delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
uint delegatedSharesBefore = delegationManager.operatorShares(delegationManager.delegatedTo(staker), strategy);
// deposit and increaseDelegatedShares
strategyManagerMock.addDeposit(staker, strategy, shares);
uint slashingFactor = _getSlashingFactor(staker, strategy, magnitude);
dsf.update(0, shares, slashingFactor);
_increaseDelegatedShares_expectEmit(
IncreaseDelegatedSharesEmitStruct({
staker: staker,
operator: defaultOperator,
strategy: strategy,
sharesToIncrease: shares,
depositScalingFactor: dsf.scalingFactor()
})
);
cheats.prank(address(strategyManagerMock));
delegationManager.increaseDelegatedShares(staker, strategy, 0, shares);
_assertDeposit({
staker: staker,
operator: defaultOperator,
strategy: strategy,
operatorSharesBefore: delegatedSharesBefore,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
// Assert correct end state values
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, strategyArray);
uint delegatedSharesAfter = delegationManager.operatorShares(delegationManager.delegatedTo(staker), strategy);
_assertWithdrawableAndOperatorShares(withdrawableShares[0], delegatedSharesAfter, "Invalid withdrawable shares");
}
/**
* @notice A unique test setup where impact of rounding can clearly be observed here.
* After making the initial deposit of 44182209037560531097078597505 shares, and the operator's magnitude is set to 999999999999990009,
* Each subsequent deposit amount of 1000 actually results in LESS withdrawable shares for the staker. There in an increasing drift
* between the operator's shares and the staker's withdrawable shares.
* The test below results in a drift difference of 4.418e13
*/
function test_increaseDelegatedShares_depositRepeatedly() public {
uint64 initialMagnitude = 999_999_999_999_990_009;
uint shares = 44_182_209_037_560_531_097_078_597_505;
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, initialMagnitude);
// Set the staker deposits in the strategies
IStrategy[] memory strategies = strategyMock.toArray();
strategyManagerMock.addDeposit(defaultStaker, strategyMock, shares);
// delegate from the `defaultStaker` to the operator
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
{
for (uint i = 0; i < 1000; ++i) {
cheats.prank(address(strategyManagerMock));
delegationManager.increaseDelegatedShares(defaultStaker, strategyMock, shares, 1000);
shares += 1000;
uint[] memory newDepositSharesArray = new uint[](1);
newDepositSharesArray[0] = shares;
strategyManagerMock.setDeposits(defaultStaker, strategies, newDepositSharesArray);
}
}
(uint[] memory withdrawableShares, uint[] memory depositShares) = delegationManager.getWithdrawableShares(defaultStaker, strategies);
assertEq(depositShares[0], shares, "staker deposit shares not reset correctly");
assertEq(
delegationManager.operatorShares(defaultOperator, strategyMock) - withdrawableShares[0],
44_182_209_037_566,
"drift should be 4.418e13 from previous tests"
);
}
}
contract DelegationManagerUnitTests_decreaseDelegatedShares is DelegationManagerUnitTests {
using ArrayLib for *;
using SlashingLib for *;
using Math for *;
function testFuzz_Revert_decreaseDelegatedShares_invalidCaller(Randomness r) public rand(r) {
address invalidCaller = r.Address();
address staker = r.Address();
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
uint64 beaconChainSlashingFactorDecrease = uint64(r.Uint256(0, WAD));
cheats.expectRevert(IDelegationManagerErrors.OnlyEigenPodManager.selector);
cheats.prank(invalidCaller);
delegationManager.decreaseDelegatedShares(staker, shares, beaconChainSlashingFactorDecrease);
}
/// @notice Verifies that there is no change in operatorShares if the staker is not delegated
function testFuzz_decreaseDelegatedShares_noop(Randomness r) public rand(r) {
address staker = r.Address();
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
uint64 beaconChainSlashingFactorDecrease = uint64(r.Uint256(0, WAD));
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
assertFalse(delegationManager.isDelegated(staker), "bad test setup");
cheats.prank(address(eigenPodManagerMock));
delegationManager.decreaseDelegatedShares(staker, shares, beaconChainSlashingFactorDecrease);
assertEq(delegationManager.operatorShares(defaultOperator, strategyMock), 0, "shares should not have changed");
}
/**
* @notice Verifies that `decreaseDelegatedShares` properly updates the staker's withdrawable shares
* and their delegated operator's shares are decreased by the correct amount.
* Ensures that after the decrease, the staker's withdrawableShares <= operatorShares,
* preventing any underflow for the operator's shares if they were all to be withdrawn.
*/
function testFuzz_decreaseDelegatedShares_nonSlashedOperator(Randomness r) public rand(r) {
int beaconShares = int(r.Uint256(1, MAX_ETH_SUPPLY));
uint sharesDecrease = r.Uint256(0, uint(beaconShares) - 1);
uint64 beaconChainSlashingFactor = r.Uint64(1, WAD);
// 1. Setup staker and delegate to operator
_registerOperatorWithBaseDetails(defaultOperator);
eigenPodManagerMock.setPodOwnerShares(defaultStaker, beaconShares);
eigenPodManagerMock.setBeaconChainSlashingFactor(defaultStaker, beaconChainSlashingFactor);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, beaconChainETHStrategy.toArray());
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDelegation({
staker: defaultStaker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: withdrawableShares[0],
depositSharesBefore: uint(beaconShares),
prevDsf: WAD
});
// 2. Perform beaconChain slash + decreaseDelegatedShares()
(uint64 prevBeaconSlashingFactor, uint64 newBeaconSlashingFactor) =
_setNewBeaconChainSlashingFactor(defaultStaker, beaconShares, sharesDecrease);
uint64 beaconChainSlashingFactorDecrease = prevBeaconSlashingFactor - newBeaconSlashingFactor;
assertEq(beaconChainSlashingFactor, prevBeaconSlashingFactor, "Bad test setup");
uint depositScalingFactor = uint(WAD);
// expected operatorShares decreased for event
uint operatorSharesToDecrease = _calcWithdrawableShares({
depositShares: uint(beaconShares),
depositScalingFactor: depositScalingFactor,
slashingFactor: beaconChainSlashingFactorDecrease
});
// expected events
_decreaseDelegatedShares_expectEmit(
DecreaseDelegatedSharesEmitStruct({staker: defaultStaker, operator: defaultOperator, sharesToDecrease: operatorSharesToDecrease})
);
cheats.prank(address(eigenPodManagerMock));
delegationManager.decreaseDelegatedShares(defaultStaker, uint(beaconShares), beaconChainSlashingFactorDecrease);
// 3. Assert correct values
uint expectedWithdrawableShares = _calcWithdrawableShares({
depositShares: uint(beaconShares),
depositScalingFactor: depositScalingFactor,
slashingFactor: newBeaconSlashingFactor
});
_assertSharesAfterBeaconSlash({
staker: defaultStaker,
withdrawableSharesBefore: withdrawableShares[0],
expectedWithdrawableShares: expectedWithdrawableShares,
prevBeaconSlashingFactor: prevBeaconSlashingFactor
});
// Assert correct end state values
(uint[] memory withdrawableSharesAfter,) = delegationManager.getWithdrawableShares(defaultStaker, beaconChainETHStrategy.toArray());
assertEq(
delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy) + operatorSharesToDecrease,
withdrawableShares[0],
"operator shares not decreased correctly"
);
_assertWithdrawableAndOperatorShares(
withdrawableSharesAfter[0],
delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy),
"Invalid withdrawable shares"
);
}
/**
* @notice Similar test to `testFuzz_decreaseDelegatedShares_nonSlashedOperator` but with
* a pre-slashed operator (maxMagnitude < WAD).
* Verifies that `decreaseDelegatedShares` properly updates the staker's withdrawable shares
* and their delegated operator's shares are decreased by the correct amount.
* Ensures that after the decrease, the staker's withdrawableShares <= operatorShares,
* preventing any underflow for the operator's shares if they were all to be withdrawn.
*/
function testFuzz_decreaseDelegatedShares_slashedOperator(Randomness r) public rand(r) {
int beaconShares = int(r.Uint256(1, MAX_ETH_SUPPLY));
uint sharesDecrease = r.Uint256(0, uint(beaconShares) - 1);
uint64 maxMagnitude = r.Uint64(1, WAD - 1);
uint64 beaconChainSlashingFactor = r.Uint64(1, WAD);
// 1. Setup staker and delegate to operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, beaconChainETHStrategy, maxMagnitude);
eigenPodManagerMock.setPodOwnerShares(defaultStaker, beaconShares);
eigenPodManagerMock.setBeaconChainSlashingFactor(defaultStaker, beaconChainSlashingFactor);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, beaconChainETHStrategy.toArray());
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDelegation({
staker: defaultStaker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: withdrawableShares[0],
depositSharesBefore: uint(beaconShares),
prevDsf: WAD
});
// 2. Perform beaconChain slash + decreaseDelegatedShares()
(uint64 prevBeaconSlashingFactor, uint64 newBeaconSlashingFactor) =
_setNewBeaconChainSlashingFactor(defaultStaker, beaconShares, sharesDecrease);
uint64 beaconChainSlashingFactorDecrease = prevBeaconSlashingFactor - newBeaconSlashingFactor;
assertEq(beaconChainSlashingFactor, prevBeaconSlashingFactor, "Bad test setup");
uint depositScalingFactor = uint(WAD).divWad(maxMagnitude);
// expected operatorShares decreased for event
uint operatorSharesToDecrease = _calcWithdrawableShares({
depositShares: uint(beaconShares),
depositScalingFactor: depositScalingFactor,
slashingFactor: maxMagnitude.mulWad(beaconChainSlashingFactorDecrease)
});
// expected events
_decreaseDelegatedShares_expectEmit(
DecreaseDelegatedSharesEmitStruct({staker: defaultStaker, operator: defaultOperator, sharesToDecrease: operatorSharesToDecrease})
);
cheats.prank(address(eigenPodManagerMock));
delegationManager.decreaseDelegatedShares(defaultStaker, uint(beaconShares), beaconChainSlashingFactorDecrease);
// 3. Assert correct values
uint expectedWithdrawableShares = _calcWithdrawableShares({
depositShares: uint(beaconShares),
depositScalingFactor: depositScalingFactor,
slashingFactor: maxMagnitude.mulWad(newBeaconSlashingFactor)
});
_assertSharesAfterBeaconSlash({
staker: defaultStaker,
withdrawableSharesBefore: withdrawableShares[0],
expectedWithdrawableShares: expectedWithdrawableShares,
prevBeaconSlashingFactor: prevBeaconSlashingFactor
});
// Assert correct end state values
(uint[] memory withdrawableSharesAfter,) = delegationManager.getWithdrawableShares(defaultStaker, beaconChainETHStrategy.toArray());
assertEq(
delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy) + operatorSharesToDecrease,
withdrawableShares[0],
"operator shares not decreased correctly"
);
_assertWithdrawableAndOperatorShares(
withdrawableSharesAfter[0],
delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy),
"Invalid withdrawable shares"
);
}
/**
* @notice Verifies that if a staker's beaconChainSlashingFactor is reduced to 0 if their entire balance
* is slashed. Their withdrawable shares should be 0 afterwards and decreasing operatorShares should
* not underflow and revert either.
*/
function testFuzz_decreaseDelegatedShares_entireBalance(Randomness r) public rand(r) {
int beaconShares = int(r.Uint256(1, MAX_ETH_SUPPLY));
uint64 maxMagnitude = r.Uint64(1, WAD);
uint64 beaconChainSlashingFactor = r.Uint64(1, WAD);
// 1. Setup staker and delegate to operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, beaconChainETHStrategy, maxMagnitude);
eigenPodManagerMock.setPodOwnerShares(defaultStaker, beaconShares);
eigenPodManagerMock.setBeaconChainSlashingFactor(defaultStaker, beaconChainSlashingFactor);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, beaconChainETHStrategy.toArray());
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDelegation({
staker: defaultStaker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: withdrawableShares[0],
depositSharesBefore: uint(beaconShares),
prevDsf: WAD
});
// 2. Perform beaconChain slash + decreaseDelegatedShares()
(uint64 prevBeaconSlashingFactor, uint64 newBeaconSlashingFactor) =
_setNewBeaconChainSlashingFactor(defaultStaker, beaconShares, uint(beaconShares));
assertEq(beaconChainSlashingFactor, prevBeaconSlashingFactor, "Bad test setup");
uint64 beaconChainSlashingFactorDecrease = prevBeaconSlashingFactor - newBeaconSlashingFactor;
uint depositScalingFactor = uint(WAD).divWad(maxMagnitude);
// expected operatorShares decreased for event
uint operatorSharesToDecrease = _calcWithdrawableShares({
depositShares: uint(beaconShares),
depositScalingFactor: depositScalingFactor,
slashingFactor: maxMagnitude.mulWad(beaconChainSlashingFactorDecrease)
});
// expected events
_decreaseDelegatedShares_expectEmit(
DecreaseDelegatedSharesEmitStruct({staker: defaultStaker, operator: defaultOperator, sharesToDecrease: operatorSharesToDecrease})
);
cheats.prank(address(eigenPodManagerMock));
delegationManager.decreaseDelegatedShares(defaultStaker, uint(beaconShares), prevBeaconSlashingFactor);
// 3. Assert correct values
uint expectedWithdrawableShares = _calcWithdrawableShares({
depositShares: uint(beaconShares),
depositScalingFactor: depositScalingFactor,
slashingFactor: maxMagnitude.mulWad(newBeaconSlashingFactor)
});
assertEq(expectedWithdrawableShares, 0, "All shares should be slashed");
assertEq(eigenPodManagerMock.beaconChainSlashingFactor(defaultStaker), 0, "beaconChainSlashingFactor should be 0");
_assertSharesAfterBeaconSlash({
staker: defaultStaker,
withdrawableSharesBefore: withdrawableShares[0],
expectedWithdrawableShares: expectedWithdrawableShares,
prevBeaconSlashingFactor: prevBeaconSlashingFactor
});
assertEq(
delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy) + operatorSharesToDecrease,
withdrawableShares[0],
"operator shares not decreased correctly"
);
}
}
contract DelegationManagerUnitTests_undelegate is DelegationManagerUnitTests {
using SlashingLib for uint;
using ArrayLib for *;
using Math for uint;
// @notice Verifies that undelegating is not possible when the "undelegation paused" switch is flipped
function testFuzz_Revert_undelegate_paused(Randomness r) public rand(r) {
address staker = r.Address();
address operator = r.Address();
_registerOperatorWithBaseDetails(operator);
_delegateToOperatorWhoAcceptsAllStakers(staker, operator);
// set the pausing flag
cheats.prank(pauser);
delegationManager.pause(2 ** PAUSED_ENTER_WITHDRAWAL_QUEUE);
cheats.prank(staker);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
delegationManager.undelegate(staker);
}
function testFuzz_Revert_undelegate_notDelegated(Randomness r) public rand(r) {
address undelegatedStaker = r.Address();
assertFalse(delegationManager.isDelegated(undelegatedStaker), "bad test setup");
cheats.prank(undelegatedStaker);
cheats.expectRevert(NotActivelyDelegated.selector);
delegationManager.undelegate(undelegatedStaker);
}
// @notice Verifies that an operator cannot undelegate from themself (this should always be forbidden)
function testFuzz_Revert_undelegate_stakerIsOperator(Randomness r) public rand(r) {
address operator = r.Address();
_registerOperatorWithBaseDetails(operator);
cheats.prank(operator);
cheats.expectRevert(OperatorsCannotUndelegate.selector);
delegationManager.undelegate(operator);
}
/**
* @notice verifies that `DelegationManager.undelegate` reverts if trying to undelegate an operator from themselves
*/
function testFuzz_Revert_undelegate_operatorCannotForceUndelegateThemself(Randomness r) public rand(r) {
address delegationApprover = r.Address();
bool callFromOperatorOrApprover = r.Boolean();
// register *this contract* as an operator with the default `delegationApprover`
_registerOperatorWithDelegationApprover(defaultOperator);
address caller;
if (callFromOperatorOrApprover) caller = delegationApprover;
else caller = defaultOperator;
// try to call the `undelegate` function and check for reversion
cheats.prank(caller);
cheats.expectRevert(OperatorsCannotUndelegate.selector);
delegationManager.undelegate(defaultOperator);
}
/**
* @notice Verifies that the `undelegate` function has proper access controls (can only be called by the operator who the `staker` has delegated
* to or the operator's `delegationApprover`), or the staker themselves
*/
function testFuzz_Revert_undelegate_invalidCaller(Randomness r) public rand(r) {
address invalidCaller = r.Address();
address staker = r.Address();
_registerOperatorWithDelegationApprover(defaultOperator);
_delegateToOperatorWhoRequiresSig(staker, defaultOperator);
cheats.prank(invalidCaller);
cheats.expectRevert(CallerCannotUndelegate.selector);
delegationManager.undelegate(staker);
}
/**
* Staker is undelegated from an operator, via a call to `undelegate`, properly originating from the staker's address.
* Reverts if the staker is themselves an operator (i.e. they are delegated to themselves)
* Does nothing if the staker is already undelegated
* Properly undelegates the staker, i.e. the staker becomes “delegated to” the zero address, and `isDelegated(staker)` returns ‘false’
* Emits a `StakerUndelegated` event
*/
function testFuzz_undelegate_noDelegateableShares(Randomness r) public rand(r) {
address staker = r.Address();
// register *this contract* as an operator and delegate from the `staker` to them
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit StakerUndelegated(staker, delegationManager.delegatedTo(staker));
cheats.prank(staker);
bytes32[] memory withdrawalRoots = delegationManager.undelegate(staker);
assertEq(withdrawalRoots.length, 0, "withdrawalRoot should be an empty array");
assertEq(delegationManager.delegatedTo(staker), address(0), "undelegated staker should be delegated to zero address");
assertFalse(delegationManager.isDelegated(staker), "staker not undelegated");
}
/**
* @notice Verifies that the `undelegate` function allows for a force undelegation
*/
function testFuzz_undelegate_forceUndelegation_noDelegateableShares(Randomness r) public rand(r) {
address staker = r.Address();
bytes32 salt = r.Bytes32();
bool callFromOperatorOrApprover = r.Boolean();
_registerOperatorWithDelegationApprover(defaultOperator);
_delegateToOperatorWhoRequiresSig(staker, defaultOperator, salt);
address caller;
if (callFromOperatorOrApprover) caller = defaultApprover;
else caller = defaultOperator;
Withdrawal memory withdrawal;
_undelegate_expectEmit_singleStrat(
UndelegateEmitStruct({
staker: staker,
operator: defaultOperator,
strategy: IStrategy(address(0)),
depositSharesQueued: 0,
operatorSharesDecreased: 0,
withdrawal: withdrawal,
withdrawalRoot: bytes32(0),
depositScalingFactor: WAD,
forceUndelegated: true
})
);
cheats.prank(caller);
bytes32[] memory withdrawalRoots = delegationManager.undelegate(staker);
assertEq(withdrawalRoots.length, 0, "withdrawalRoot should be an empty array");
assertEq(delegationManager.delegatedTo(staker), address(0), "undelegated staker should be delegated to zero address");
assertFalse(delegationManager.isDelegated(staker), "staker not undelegated");
}
/**
* @notice Verifies that the `undelegate` function properly queues a withdrawal for all shares of the staker
*/
function testFuzz_undelegate_nonSlashedOperator(Randomness r) public rand(r) {
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
IStrategy[] memory strategyArray = r.StrategyArray(1);
IStrategy strategy = strategyArray[0];
// Set the staker deposits in the strategies
strategyManagerMock.addDeposit(defaultStaker, strategy, shares);
// register *this contract* as an operator and delegate from the `staker` to them
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
// Format queued withdrawal
(, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategy, depositSharesToWithdraw: shares});
// Undelegate the staker
_undelegate_expectEmit_singleStrat(
UndelegateEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
depositSharesQueued: shares,
operatorSharesDecreased: shares,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot,
depositScalingFactor: WAD,
forceUndelegated: false
})
);
cheats.prank(defaultStaker);
delegationManager.undelegate(defaultStaker);
// Checks - delegation status
assertEq(delegationManager.delegatedTo(defaultStaker), address(0), "undelegated staker should be delegated to zero address");
assertFalse(delegationManager.isDelegated(defaultStaker), "staker not undelegated");
// Checks - operator & staker shares
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
operatorSharesBefore: shares,
depositSharesBefore: shares,
depositSharesWithdrawn: shares,
depositScalingFactor: uint(WAD),
slashingFactor: uint(WAD)
});
}
/**
* @notice Verifies that the `undelegate` function properly queues a withdrawal for all shares of the staker
* @notice The operator should have its shares slashed prior to the staker's deposit
*/
function testFuzz_undelegate_preSlashedOperator(Randomness r) public rand(r) {
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
uint64 operatorMagnitude = r.Uint64(1, WAD);
IStrategy strategy = IStrategy(r.Address());
// register *this contract* as an operator & set its slashed magnitude
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategy, operatorMagnitude);
// Set the staker deposits in the strategies
strategyManagerMock.addDeposit(defaultStaker, strategy, shares);
// delegate from the `staker` to them
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: uint(WAD).divWad(operatorMagnitude),
depositAmount: shares
});
// Format queued withdrawal
(, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategy, depositSharesToWithdraw: shares});
// Calculate operatorShares decreased, may be off of shares due to rounding
uint depositScalingFactor = delegationManager.depositScalingFactor(defaultStaker, strategy);
assertTrue(depositScalingFactor > WAD, "bad test setup");
uint operatorSharesDecreased = _calcWithdrawableShares(shares, depositScalingFactor, operatorMagnitude);
assertLe(operatorSharesDecreased, shares, "operatorSharesDecreased should be <= shares");
// Undelegate the staker
_undelegate_expectEmit_singleStrat(
UndelegateEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
depositSharesQueued: shares,
operatorSharesDecreased: operatorSharesDecreased,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot,
depositScalingFactor: WAD,
forceUndelegated: false
})
);
cheats.prank(defaultStaker);
delegationManager.undelegate(defaultStaker);
// Checks - delegation status
assertEq(delegationManager.delegatedTo(defaultStaker), address(0), "undelegated staker should be delegated to zero address");
assertFalse(delegationManager.isDelegated(defaultStaker), "staker not undelegated");
// Checks - operator & staker shares
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
operatorSharesBefore: shares,
depositSharesBefore: shares,
depositSharesWithdrawn: shares,
depositScalingFactor: uint(WAD).divWad(operatorMagnitude),
slashingFactor: uint(operatorMagnitude)
});
(uint[] memory stakerWithdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, strategy.toArray());
assertEq(stakerWithdrawableShares[0], 0, "staker withdrawable shares not calculated correctly");
}
/**
* @notice Verifies that the `undelegate` function properly queues a withdrawal for all shares of the staker
* @notice The operator should have its shares slashed prior to the staker's deposit
*/
function testFuzz_undelegate_slashedWhileStaked(Randomness r) public rand(r) {
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
uint64 prevMaxMagnitude = r.Uint64(2, WAD);
uint64 newMaxMagnitude = r.Uint64(1, prevMaxMagnitude - 1);
IStrategy strategy = IStrategy(r.Address());
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategy, prevMaxMagnitude);
// Set the staker deposits in the strategies
strategyManagerMock.addDeposit(defaultStaker, strategy, shares);
// delegate from the `defaultStaker` to the operator
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
assertEq(delegationManager.operatorShares(defaultOperator, strategy), shares, "operatorShares should increment correctly");
// Set operator magnitude
{
(uint[] memory withdrawableSharesBefore,) = delegationManager.getWithdrawableShares(defaultStaker, strategy.toArray());
uint delegatedSharesBefore = delegationManager.operatorShares(defaultOperator, strategy);
_setOperatorMagnitude(defaultOperator, strategy, newMaxMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategy, prevMaxMagnitude, newMaxMagnitude);
(, uint operatorSharesAfterSlash) = _assertOperatorSharesAfterSlash({
operator: defaultOperator,
strategy: strategy,
operatorSharesBefore: delegatedSharesBefore,
prevMaxMagnitude: prevMaxMagnitude,
newMaxMagnitude: newMaxMagnitude
});
uint expectedWithdrawable = _calcWithdrawableShares(
shares, uint(WAD).divWad(prevMaxMagnitude), _getSlashingFactor(defaultStaker, strategy, newMaxMagnitude)
);
_assertSharesAfterSlash({
staker: defaultStaker,
strategy: strategy,
withdrawableSharesBefore: withdrawableSharesBefore[0],
expectedWithdrawableShares: expectedWithdrawable,
prevMaxMagnitude: prevMaxMagnitude,
currMaxMagnitude: newMaxMagnitude
});
// Get withdrawable shares
(uint[] memory withdrawableSharesAfter, uint[] memory depositSharesAfter) =
delegationManager.getWithdrawableShares(defaultStaker, strategy.toArray());
_assertWithdrawableAndOperatorShares(withdrawableSharesAfter[0], operatorSharesAfterSlash, "Invalid withdrawable shares");
assertEq(depositSharesAfter[0], shares, "Invalid deposit shares");
assertEq(delegationManager.depositScalingFactor(defaultStaker, strategy), uint(WAD).divWad(prevMaxMagnitude), "bad test setup");
}
// Format queued withdrawal
(uint[] memory withdrawableShares, uint[] memory depositShares) =
delegationManager.getWithdrawableShares(defaultStaker, strategy.toArray());
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategy);
{
(, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategy, depositSharesToWithdraw: shares});
// Undelegate the staker
_undelegate_expectEmit_singleStrat(
UndelegateEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
depositSharesQueued: shares,
operatorSharesDecreased: withdrawableShares[0],
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot,
depositScalingFactor: WAD,
forceUndelegated: false
})
);
cheats.prank(defaultStaker);
delegationManager.undelegate(defaultStaker);
}
// Checks - delegation status
assertEq(delegationManager.delegatedTo(defaultStaker), address(0), "undelegated staker should be delegated to zero address");
assertFalse(delegationManager.isDelegated(defaultStaker), "staker not undelegated");
// Checks - operator & staker shares
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
operatorSharesBefore: operatorSharesBefore,
depositSharesBefore: shares,
depositSharesWithdrawn: shares,
depositScalingFactor: uint(WAD).divWad(prevMaxMagnitude),
slashingFactor: uint(newMaxMagnitude)
});
(withdrawableShares, depositShares) = delegationManager.getWithdrawableShares(defaultStaker, strategy.toArray());
assertEq(withdrawableShares[0], 0, "staker withdrawable shares not calculated correctly");
assertEq(depositShares[0], 0, "staker deposit shares not reset correctly");
}
/**
* @notice Verifies that the `undelegate` function properly undelegates a staker even though their shares
* were slashed entirely.
*/
function testFuzz_undelegate_slashedOperator100PercentWhileStaked(Randomness r) public rand(r) {
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
IStrategy[] memory strategyArray = r.StrategyArray(1);
IStrategy strategy = strategyArray[0];
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
// Set the staker deposits in the strategies
strategyManagerMock.addDeposit(defaultStaker, strategy, shares);
// delegate from the `defaultStaker` to the operator
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
// Set operator magnitude
uint64 operatorMagnitude = 0;
uint operatorSharesAfterSlash;
{
_setOperatorMagnitude(defaultOperator, strategy, operatorMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategy, WAD, 0);
operatorSharesAfterSlash = delegationManager.operatorShares(defaultOperator, strategy);
assertEq(operatorSharesAfterSlash, 0, "operator shares not fully slashed");
}
(, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategy, depositSharesToWithdraw: shares});
uint depositScalingFactor = delegationManager.depositScalingFactor(defaultStaker, strategy);
assertEq(depositScalingFactor, WAD, "bad test setup");
// Get withdrawable and deposit shares
{
(uint[] memory withdrawableSharesBefore, uint[] memory depositSharesBefore) =
delegationManager.getWithdrawableShares(defaultStaker, strategyArray);
assertEq(withdrawableSharesBefore[0], 0, "withdrawable shares should be 0 after being slashed fully");
assertEq(depositSharesBefore[0], shares, "deposit shares should be unchanged after being slashed fully");
}
// Undelegate the staker
_undelegate_expectEmit_singleStrat(
UndelegateEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
depositSharesQueued: shares,
operatorSharesDecreased: 0,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot,
depositScalingFactor: WAD,
forceUndelegated: false
})
);
cheats.prank(defaultStaker);
delegationManager.undelegate(defaultStaker);
// Checks - delegation status
assertEq(delegationManager.delegatedTo(defaultStaker), address(0), "undelegated staker should be delegated to zero address");
assertFalse(delegationManager.isDelegated(defaultStaker), "staker not undelegated");
// Checks - operator & staker shares
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
operatorSharesBefore: 0,
depositSharesBefore: shares,
depositSharesWithdrawn: shares,
depositScalingFactor: uint(WAD),
slashingFactor: 0
});
assertEq(delegationManager.operatorShares(defaultOperator, strategy), 0, "operator shares not decreased correctly");
(uint[] memory stakerWithdrawableShares, uint[] memory depositShares) =
delegationManager.getWithdrawableShares(defaultStaker, strategyArray);
assertEq(stakerWithdrawableShares[0], 0, "staker withdrawable shares not calculated correctly");
assertEq(depositShares[0], 0, "staker deposit shares not reset correctly");
}
function testFuzz_undelegate_slashedOperatorCloseTo100(Randomness r) public rand(r) {
address[] memory stakers = r.StakerArray(r.Uint32(1, 8));
uint64 prevMaxMagnitude = r.Uint64(2, WAD);
uint64 newMaxMagnitude = 1;
// 1. register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, prevMaxMagnitude);
// 2. Stakers deposits in the strategyMock
{
for (uint i = 0; i < stakers.length; ++i) {
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
strategyManagerMock.addDeposit(stakers[i], strategyMock, shares);
stakerDepositShares[stakers[i]] = shares;
}
}
// 3. Delegate from the `stakers` to the operator
{
uint totalWithdrawable = 0;
for (uint i = 0; i < stakers.length; ++i) {
{
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock);
_delegateToOperatorWhoAcceptsAllStakers(stakers[i], defaultOperator);
_assertDeposit({
staker: stakers[i],
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: operatorSharesBefore,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: stakerDepositShares[stakers[i]]
});
}
(uint[] memory withdrawableSharesBefore,) = delegationManager.getWithdrawableShares(stakers[i], strategyMock.toArray());
totalWithdrawable += withdrawableSharesBefore[0];
}
assertLe(
totalWithdrawable, delegationManager.operatorShares(defaultOperator, strategyMock), "should be <= op shares due to rounding"
);
}
// 4. Slash operator - Set operator magnitude and call burnOperatorShares
{
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock);
_setOperatorMagnitude(defaultOperator, strategyMock, newMaxMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategyMock, prevMaxMagnitude, newMaxMagnitude);
_assertOperatorSharesAfterSlash({
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: operatorSharesBefore,
prevMaxMagnitude: prevMaxMagnitude,
newMaxMagnitude: newMaxMagnitude
});
}
// 5. Undelegate the stakers with expected events
uint totalOperatorSharesDecreased = 0;
for (uint i = 0; i < stakers.length; ++i) {
(, Withdrawal memory withdrawal, bytes32 withdrawalRoot) = _setUpQueueWithdrawalsSingleStrat({
staker: stakers[i],
strategy: strategyMock,
depositSharesToWithdraw: stakerDepositShares[stakers[i]]
});
uint operatorSharesDecreased = _calcWithdrawableShares(
stakerDepositShares[stakers[i]], delegationManager.depositScalingFactor(stakers[i], strategyMock), newMaxMagnitude
);
_undelegate_expectEmit_singleStrat(
UndelegateEmitStruct({
staker: stakers[i],
operator: defaultOperator,
strategy: strategyMock,
depositSharesQueued: stakerDepositShares[stakers[i]],
operatorSharesDecreased: operatorSharesDecreased,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot,
depositScalingFactor: WAD,
forceUndelegated: false
})
);
cheats.prank(stakers[i]);
delegationManager.undelegate(stakers[i]);
totalOperatorSharesDecreased += operatorSharesDecreased;
}
// 6. Checks - delegation status and staker,operator shares
assertEq(delegationManager.delegatedTo(defaultStaker), address(0), "undelegated staker should be delegated to zero address");
assertFalse(delegationManager.isDelegated(defaultStaker), "staker not undelegated");
for (uint i = 0; i < stakers.length; ++i) {
(uint[] memory stakerWithdrawableShares, uint[] memory stakerDepositShares) =
delegationManager.getWithdrawableShares(stakers[i], strategyMock.toArray());
assertEq(stakerWithdrawableShares[0], 0, "staker withdrawable shares not calculated correctly");
assertEq(stakerDepositShares[0], 0, "staker deposit shares not reset correctly");
}
}
/**
* @notice Given an operator with slashed magnitude, delegate, undelegate, and then delegate back to the same operator with
* completing withdrawals as shares. This should result in the operatorShares after the second delegation being <= the shares from the first delegation.
*/
function testFuzz_undelegate_delegateAgainWithRounding(Randomness r) public rand(r) {
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
// set magnitude to 66% to ensure rounding when calculating `toShares`
uint64 operatorMagnitude = 333_333_333_333_333_333;
// register *this contract* as an operator & set its slashed magnitude
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, operatorMagnitude);
// Set the staker deposits in the strategies
strategyManagerMock.addDeposit(defaultStaker, strategyMock, shares);
// delegate from the `staker` to them
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: shares
});
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock);
// Format queued withdrawal
(, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: shares});
uint slashingFactor = _getSlashingFactor(defaultStaker, strategyMock, operatorMagnitude);
uint operatorSharesDecreased =
_calcWithdrawableShares(shares, delegationManager.depositScalingFactor(defaultStaker, strategyMock), slashingFactor);
// Undelegate the staker
cheats.prank(defaultStaker);
_undelegate_expectEmit_singleStrat(
UndelegateEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategyMock,
depositSharesQueued: shares,
operatorSharesDecreased: operatorSharesDecreased,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot,
depositScalingFactor: WAD,
forceUndelegated: false
})
);
delegationManager.undelegate(defaultStaker);
// Checks - delegation status
assertEq(delegationManager.delegatedTo(defaultStaker), address(0), "undelegated staker should be delegated to zero address");
assertFalse(delegationManager.isDelegated(defaultStaker), "staker not undelegated");
// Checks - operator & staker shares
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: operatorSharesBefore,
depositSharesBefore: shares,
depositSharesWithdrawn: shares,
depositScalingFactor: uint(WAD).divWad(operatorMagnitude),
slashingFactor: operatorMagnitude
});
(uint[] memory stakerWithdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, strategyMock.toArray());
assertEq(stakerWithdrawableShares[0], 0, "staker withdrawable shares not calculated correctly");
// // Re-delegate the staker to the operator again. The shares should have increased but may be less than from before due to rounding
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// complete withdrawal as shares, should add back delegated shares to operator due to delegating again
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = IERC20(strategyMock.underlyingToken());
cheats.roll(withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks() + 1);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, false);
uint operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock);
assertLe(
operatorSharesAfter, operatorSharesBefore, "operator shares should be less than or equal to before due to potential rounding"
);
}
}
contract DelegationManagerUnitTests_redelegate is DelegationManagerUnitTests {
using ArrayLib for *;
// @notice Verifies that redelegating is not possible when the "delegation paused" switch is flipped
function testFuzz_Revert_redelegate_delegatePaused(Randomness r) public {
address staker = r.Address();
address newOperator = r.Address();
// register *this contract* as an operator and delegate from the `staker` to them
_registerOperatorWithBaseDetails(defaultOperator);
_registerOperatorWithBaseDetails(newOperator);
_delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
// set the pausing flag
cheats.prank(pauser);
delegationManager.pause(2 ** PAUSED_NEW_DELEGATION);
cheats.prank(staker);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
delegationManager.redelegate(newOperator, emptyApproverSignatureAndExpiry, emptySalt);
}
// @notice Verifies that redelegating is not possible when the "undelegation paused" switch is flipped
function testFuzz_Revert_redelegate_undelegatePaused(Randomness r) public {
address staker = r.Address();
address newOperator = r.Address();
// register *this contract* as an operator and delegate from the `staker` to them
_registerOperatorWithBaseDetails(defaultOperator);
_registerOperatorWithBaseDetails(newOperator);
_delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
// set the pausing flag
cheats.prank(pauser);
delegationManager.pause(2 ** PAUSED_ENTER_WITHDRAWAL_QUEUE);
cheats.prank(staker);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
delegationManager.redelegate(newOperator, emptyApproverSignatureAndExpiry, emptySalt);
}
function testFuzz_Revert_redelegate_notDelegated(Randomness r) public {
address undelegatedStaker = r.Address();
assertFalse(delegationManager.isDelegated(undelegatedStaker), "bad test setup");
_registerOperatorWithBaseDetails(defaultOperator);
cheats.prank(undelegatedStaker);
cheats.expectRevert(NotActivelyDelegated.selector);
delegationManager.redelegate(defaultOperator, emptyApproverSignatureAndExpiry, emptySalt);
}
// @notice Verifies that an operator cannot undelegate from themself (this should always be forbidden)
function testFuzz_Revert_redelegate_stakerIsOperator(Randomness r) public {
address operator = r.Address();
_registerOperatorWithBaseDetails(operator);
_registerOperatorWithBaseDetails(defaultOperator);
cheats.prank(operator);
cheats.expectRevert(OperatorsCannotUndelegate.selector);
delegationManager.redelegate(defaultOperator, emptyApproverSignatureAndExpiry, emptySalt);
}
/// @notice Verifies that `staker` cannot redelegate to an unregistered `operator`
function testFuzz_Revert_redelegateToUnregisteredOperator(Randomness r) public {
address staker = r.Address();
address operator = r.Address();
assertFalse(delegationManager.isOperator(operator), "incorrect test input?");
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
// try to delegate and check that the call reverts
cheats.prank(staker);
cheats.expectRevert(OperatorNotRegistered.selector);
delegationManager.redelegate(operator, emptyApproverSignatureAndExpiry, emptySalt);
}
function testFuzz_Revert_redelegate_ExpiredSignature(Randomness r) public {
// roll to a very late timestamp
skip(type(uint).max / 2);
address staker = r.Address();
address newOperator = r.Address();
uint expiry = r.Uint256(0, block.timestamp - 1);
bytes32 salt = r.Bytes32();
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
_registerOperatorWithDelegationApprover(newOperator);
// calculate the delegationSigner's signature
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry =
_getApproverSignature(delegationSignerPrivateKey, staker, newOperator, salt, expiry);
// delegate from the `staker` to the operator
cheats.startPrank(staker);
cheats.expectRevert(ISignatureUtilsMixinErrors.SignatureExpired.selector);
delegationManager.redelegate(newOperator, approverSignatureAndExpiry, salt);
cheats.stopPrank();
}
function testFuzz_Revert_redelegate_SpentSalt(Randomness r) public {
address staker = r.Address();
address newOperator = r.Address();
uint expiry = r.Uint256(block.timestamp, block.timestamp + 100);
bytes32 salt = r.Bytes32();
_registerOperatorWithBaseDetails(defaultOperator);
_registerOperatorWithDelegationApprover(newOperator);
// verify that the salt hasn't been used before
assertFalse(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(newOperator), salt),
"salt somehow spent too early?"
);
// calculate the delegationSigner's signature
ISignatureUtilsMixinTypes.SignatureWithExpiry memory approverSignatureAndExpiry =
_getApproverSignature(delegationSignerPrivateKey, staker, newOperator, salt, expiry);
// Spend salt by delegating normally first
cheats.startPrank(staker);
delegationManager.delegateTo(newOperator, approverSignatureAndExpiry, salt);
assertTrue(
delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(newOperator), salt),
"salt somehow spent not spent?"
);
// redelegate to a different operator
delegationManager.redelegate(defaultOperator, emptyApproverSignatureAndExpiry, emptySalt);
// Now try to redelegate to the original operator using the invalid signature
cheats.expectRevert(SaltSpent.selector);
delegationManager.redelegate(newOperator, approverSignatureAndExpiry, salt);
cheats.stopPrank();
}
/**
* @notice Verifies that the `redelegate` function properly queues a withdrawal for all shares of the staker
* ... and delegates to a new operator
*/
function testFuzz_redelegate_noSlashing(Randomness r) public {
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
IStrategy[] memory strategyArray = r.StrategyArray(1);
IStrategy strategy = strategyArray[0];
// Set the staker deposits in the strategies
strategyManagerMock.addDeposit(defaultStaker, strategy, shares);
// register *this contract* as an operator and delegate from the `staker` to them
address newOperator = r.Address();
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_registerOperatorWithBaseDetails(newOperator);
// Format queued withdrawal
(, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategy, depositSharesToWithdraw: shares});
// Redelegate the staker
_undelegate_expectEmit_singleStrat(
UndelegateEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
depositSharesQueued: shares,
operatorSharesDecreased: shares,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot,
depositScalingFactor: WAD,
forceUndelegated: false
})
);
_delegateTo_expectEmit_singleStrat(
DelegateToSingleStratEmitStruct({
staker: defaultStaker,
operator: newOperator,
strategy: strategyMock,
depositShares: 0,
depositScalingFactor: WAD
})
);
cheats.prank(defaultStaker);
delegationManager.redelegate(newOperator, emptyApproverSignatureAndExpiry, emptySalt);
// Checks - delegation status
assertEq(delegationManager.delegatedTo(defaultStaker), newOperator, "undelegated staker should be delegated to new operator");
assertTrue(delegationManager.isDelegated(defaultStaker), "staker should still be delegated");
// Checks - operator & staker shares
assertEq(delegationManager.operatorShares(defaultOperator, strategyMock), 0, "operator shares not decreased correctly");
assertEq(delegationManager.operatorShares(newOperator, strategyMock), 0, "operator shares should not have been added");
(uint[] memory stakerWithdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, strategyArray);
assertEq(stakerWithdrawableShares[0], 0, "staker withdrawable shares not calculated correctly");
}
/**
* @notice This function tests to ensure that a delegator can re-delegate to an operator after undelegating.
* Asserts the shares after re-delegating are the same as originally. No slashing is done in this test.
*/
function testFuzz_undelegate_redelegateWithSharesBack(Randomness r) public rand(r) {
address staker = r.Address();
address operator = r.Address();
uint strategyShares = r.Uint256(1, MAX_STRATEGY_SHARES);
int beaconShares = int(r.Uint256(1 gwei, MAX_ETH_SUPPLY));
bool completeAsShares = r.Boolean();
// 1. Set staker shares
strategyManagerMock.addDeposit(staker, strategyMock, strategyShares);
eigenPodManagerMock.setPodOwnerShares(staker, beaconShares);
(IStrategy[] memory strategiesToReturn,) = delegationManager.getDepositedShares(staker);
// 2. register operator and delegate staker to operator
_registerOperatorWithBaseDetails(operator);
_delegateToOperatorWhoAcceptsAllStakers(staker, operator);
_assertDeposit({
staker: staker,
operator: operator,
strategy: strategyMock,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: uint(WAD),
depositAmount: strategyShares
});
_assertDeposit({
staker: staker,
operator: operator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: uint(WAD),
depositAmount: uint(beaconShares)
});
// 3. Setup queued withdrawals from `undelegate`
// queued withdrawals done for single strat as this is how undelegate queue withdraws
(, Withdrawal memory strategyWithdrawal,) =
_setUpQueueWithdrawalsSingleStrat({staker: staker, strategy: strategyMock, depositSharesToWithdraw: strategyShares});
(, Withdrawal memory beaconWithdrawal,) = _setUpQueueWithdrawalsSingleStrat({
staker: staker,
strategy: IStrategy(address(beaconChainETHStrategy)),
depositSharesToWithdraw: uint(beaconShares)
});
beaconWithdrawal.nonce = 1; // Ensure nonce is greater for second withdrawal
cheats.prank(staker);
delegationManager.undelegate(staker);
// 4. Delegate to operator again with shares added back
{
cheats.roll(block.number + delegationManager.minWithdrawalDelayBlocks() + 1);
IERC20[] memory strategyTokens = new IERC20[](1);
strategyTokens[0] = IERC20(strategyMock.underlyingToken());
IERC20[] memory beaconTokens = new IERC20[](1);
beaconTokens[0] = IERC20(address(beaconChainETHStrategy));
if (completeAsShares) {
// delegate first and complete withdrawal
_delegateToOperatorWhoAcceptsAllStakers(staker, operator);
cheats.startPrank(staker);
delegationManager.completeQueuedWithdrawal(strategyWithdrawal, strategyTokens, false);
delegationManager.completeQueuedWithdrawal(beaconWithdrawal, beaconTokens, false);
cheats.stopPrank();
} else {
// complete withdrawal first and then delegate
cheats.startPrank(staker);
delegationManager.completeQueuedWithdrawal(strategyWithdrawal, strategyTokens, false);
delegationManager.completeQueuedWithdrawal(beaconWithdrawal, beaconTokens, false);
cheats.stopPrank();
_delegateToOperatorWhoAcceptsAllStakers(staker, operator);
}
}
// 5. assert correct shares and delegation state
assertTrue(delegationManager.isDelegated(staker), "staker should be delegated");
assertEq(delegationManager.delegatedTo(staker), operator, "staker should be delegated to operator");
(uint[] memory stakerShares,) = delegationManager.getWithdrawableShares(staker, strategiesToReturn);
assertEq(
delegationManager.operatorShares(operator, strategyMock), stakerShares[0], "operator shares should be equal to strategyShares"
);
assertEq(uint(eigenPodManagerMock.podOwnerDepositShares(staker)), stakerShares[1], "beacon shares should be equal to beaconShares");
}
}
contract DelegationManagerUnitTests_queueWithdrawals is DelegationManagerUnitTests {
using SlashingLib for *;
using ArrayLib for *;
function test_Revert_WhenEnterQueueWithdrawalsPaused() public {
cheats.prank(pauser);
delegationManager.pause(2 ** PAUSED_ENTER_WITHDRAWAL_QUEUE);
(QueuedWithdrawalParams[] memory queuedWithdrawalParams,,) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: 100});
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
}
function test_Revert_WhenQueueWithdrawalParamsLengthMismatch() public {
IStrategy[] memory strategyArray = strategyMock.toArray();
uint[] memory shareAmounts = new uint[](2);
shareAmounts[0] = 100;
shareAmounts[1] = 100;
QueuedWithdrawalParams[] memory queuedWithdrawalParams = new QueuedWithdrawalParams[](1);
queuedWithdrawalParams[0] =
QueuedWithdrawalParams({strategies: strategyArray, depositShares: shareAmounts, __deprecated_withdrawer: address(0)});
cheats.expectRevert(InputArrayLengthMismatch.selector);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
}
function testFuzz_IgnoresWithdrawerField(address withdrawer) public {
_depositIntoStrategies(defaultStaker, strategyMock.toArray(), uint(100).toArrayU256());
(QueuedWithdrawalParams[] memory queuedWithdrawalParams,,) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: 100});
// set the ignored field to a different address. the dm should ignore this.
queuedWithdrawalParams[0].__deprecated_withdrawer = withdrawer;
cheats.prank(defaultStaker);
bytes32 withdrawalRoot = delegationManager.queueWithdrawals(queuedWithdrawalParams)[0];
(Withdrawal memory withdrawal,) = delegationManager.getQueuedWithdrawal(withdrawalRoot);
assertEq(withdrawal.staker, defaultStaker, "staker should be msg.sender");
assertEq(withdrawal.withdrawer, defaultStaker, "withdrawer should be msg.sender");
}
function test_Revert_WhenEmptyStrategiesArray() public {
IStrategy[] memory strategyArray = new IStrategy[](0);
uint[] memory shareAmounts = new uint[](0);
QueuedWithdrawalParams[] memory queuedWithdrawalParams = new QueuedWithdrawalParams[](1);
queuedWithdrawalParams[0] =
QueuedWithdrawalParams({strategies: strategyArray, depositShares: shareAmounts, __deprecated_withdrawer: address(0)});
cheats.expectRevert(InputArrayLengthZero.selector);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
}
/**
* @notice Verifies that `DelegationManager.queueWithdrawals` properly queues a withdrawal for the `withdrawer`
* from the `strategy` for the `sharesAmount`.
* - Asserts that staker is delegated to the operator
* - Asserts that shares for delegatedTo operator are decreased by `sharesAmount`
* - Asserts that staker cumulativeWithdrawalsQueued nonce is incremented
* - Checks that event was emitted with correct withdrawalRoot and withdrawal
*/
function testFuzz_queueWithdrawal_SingleStrat_nonSlashedOperator(Randomness r) public rand(r) {
uint depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES);
uint withdrawalAmount = r.Uint256(1, depositAmount);
bool depositBeaconChainShares = r.Boolean();
// sharesAmounts is single element so returns single strategy
IStrategy[] memory strategies =
_deployAndDepositIntoStrategies(defaultStaker, depositAmount.toArrayU256(), depositBeaconChainShares);
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategies[0],
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: uint(WAD),
depositAmount: depositAmount
});
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategies[0], depositSharesToWithdraw: withdrawalAmount});
assertEq(delegationManager.delegatedTo(defaultStaker), defaultOperator, "staker should be delegated to operator");
uint nonceBefore = delegationManager.cumulativeWithdrawalsQueued(defaultStaker);
uint delegatedSharesBefore = delegationManager.operatorShares(defaultOperator, strategies[0]);
// queueWithdrawals
_queueWithdrawals_expectEmit(
QueueWithdrawalsEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
queuedWithdrawalParams: queuedWithdrawalParams,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot
})
);
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategies[0],
operatorSharesBefore: delegatedSharesBefore,
depositSharesBefore: depositAmount,
depositSharesWithdrawn: withdrawalAmount,
depositScalingFactor: uint(WAD),
slashingFactor: uint(WAD)
});
_assertQueuedWithdrawalExists(defaultStaker, withdrawal);
uint nonceAfter = delegationManager.cumulativeWithdrawalsQueued(defaultStaker);
assertEq(nonceBefore + 1, nonceAfter, "staker nonce should have incremented");
}
/**
* @notice Verifies that `DelegationManager.queueWithdrawals` properly queues a withdrawal for the `withdrawer`
* from the `strategy` for the `sharesAmount`. Operator is slashed prior to the staker's deposit
* - Asserts that staker is delegated to the operator
* - Asserts that shares for delegatedTo operator are decreased by `sharesAmount`
* - Asserts that staker cumulativeWithdrawalsQueued nonce is incremented
* - Checks that event was emitted with correct withdrawalRoot and withdrawal
*/
function testFuzz_queueWithdrawal_SingleStrat_preSlashedOperator(Randomness r) public rand(r) {
uint depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES);
uint withdrawalAmount = r.Uint256(1, depositAmount);
uint64 maxMagnitude = r.Uint64(1, WAD);
// Slash the operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, maxMagnitude);
// Deposit for staker & delegate
strategyManagerMock.addDeposit(defaultStaker, strategyMock, depositAmount);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: depositAmount
});
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: withdrawalAmount});
assertEq(delegationManager.delegatedTo(defaultStaker), defaultOperator, "staker should be delegated to operator");
uint nonceBefore = delegationManager.cumulativeWithdrawalsQueued(defaultStaker);
uint delegatedSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock);
// queueWithdrawals
_queueWithdrawals_expectEmit(
QueueWithdrawalsEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
queuedWithdrawalParams: queuedWithdrawalParams,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot
})
);
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: delegatedSharesBefore,
depositSharesBefore: depositAmount,
depositSharesWithdrawn: withdrawalAmount,
depositScalingFactor: delegationManager.depositScalingFactor(defaultStaker, strategyMock),
slashingFactor: uint(maxMagnitude)
});
_assertQueuedWithdrawalExists(defaultStaker, withdrawal);
uint nonceAfter = delegationManager.cumulativeWithdrawalsQueued(defaultStaker);
assertEq(nonceBefore + 1, nonceAfter, "staker nonce should have incremented");
}
/**
* @notice Verifies that `DelegationManager.queueWithdrawals` properly queues a withdrawal for the `withdrawer`
* from the `strategy` for the `sharesAmount`. Operator is slashed while the staker is deposited
* - Asserts that staker is delegated to the operator
* - Asserts that shares for delegatedTo operator are decreased by `sharesAmount`
* - Asserts that staker cumulativeWithdrawalsQueued nonce is incremented
* - Checks that event was emitted with correct withdrawalRoot and withdrawal
*/
function testFuzz_queueWithdrawal_SingleStrat_slashedWhileStaked(Randomness r) public rand(r) {
uint depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES);
uint withdrawalAmount = r.Uint256(1, depositAmount);
uint64 prevMaxMagnitude = r.Uint64(2, WAD);
uint64 newMaxMagnitude = r.Uint64(1, prevMaxMagnitude - 1);
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, prevMaxMagnitude);
// Deposit for staker & delegate
strategyManagerMock.addDeposit(defaultStaker, strategyMock, depositAmount);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: depositAmount
});
// Slash the operator
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock);
_setOperatorMagnitude(defaultOperator, strategyMock, newMaxMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategyMock, prevMaxMagnitude, newMaxMagnitude);
// Assertions on amount burned
(uint operatorSharesSlashed,) = _assertOperatorSharesAfterSlash({
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: operatorSharesBefore,
prevMaxMagnitude: prevMaxMagnitude,
newMaxMagnitude: newMaxMagnitude
});
uint nonceBefore = delegationManager.cumulativeWithdrawalsQueued(defaultStaker);
{
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: withdrawalAmount});
// queueWithdrawals
_queueWithdrawals_expectEmit(
QueueWithdrawalsEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
queuedWithdrawalParams: queuedWithdrawalParams,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot
})
);
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
_assertQueuedWithdrawalExists(defaultStaker, withdrawal);
}
uint slashingFactor = _getSlashingFactor(defaultStaker, strategyMock, newMaxMagnitude);
assertEq(nonceBefore + 1, delegationManager.cumulativeWithdrawalsQueued(defaultStaker), "staker nonce should have incremented");
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: depositAmount - operatorSharesSlashed,
depositSharesBefore: depositAmount,
depositSharesWithdrawn: withdrawalAmount,
depositScalingFactor: uint(WAD).divWad(prevMaxMagnitude),
slashingFactor: slashingFactor
});
}
/**
* @notice Verifies that `DelegationManager.queueWithdrawals` queues an empty withdrawal for the `withdrawer`
* from the `strategy` for the `sharesAmount` since the Operator is slashed 100% while the staker is deposited
* - Asserts that queuing a withdrawal results in an empty withdrawal when the operator is slashed 100%
* - Asserts that staker withdrawableShares after is 0
* - Checks that event was emitted with correct withdrawalRoot and withdrawal
*/
function testFuzz_queueWithdrawal_SingleStrat_slashed100PercentWhileStaked(Randomness r) public rand(r) {
uint depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES);
// Register operator, deposit for staker & delegate
_registerOperatorWithBaseDetails(defaultOperator);
strategyManagerMock.addDeposit(defaultStaker, strategyMock, depositAmount);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: depositAmount
});
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({
staker: defaultStaker,
strategy: strategyMock,
depositSharesToWithdraw: 0 // expected 0 since slashed 100%
});
// Slash the operator
uint64 operatorMagnitude = 0;
_setOperatorMagnitude(defaultOperator, strategyMock, operatorMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategyMock, WAD, 0);
_assertOperatorSharesAfterSlash({
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: depositAmount,
prevMaxMagnitude: WAD,
newMaxMagnitude: operatorMagnitude
});
assertEq(delegationManager.delegatedTo(defaultStaker), defaultOperator, "staker should be delegated to operator");
// queueWithdrawals should result in an empty withdrawal
_queueWithdrawals_expectEmit(
QueueWithdrawalsEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
queuedWithdrawalParams: queuedWithdrawalParams,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot
})
);
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, strategyMock.toArray());
assertEq(withdrawableShares[0], 0, "withdrawable shares should be 0 after being slashed fully");
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategyMock,
operatorSharesBefore: 0,
depositSharesBefore: depositAmount,
depositSharesWithdrawn: 0,
depositScalingFactor: uint(WAD),
slashingFactor: 0
});
_assertQueuedWithdrawalExists(defaultStaker, withdrawal);
}
/**
* @notice Verifies that `DelegationManager.queueWithdrawals` properly queues a withdrawal for the `withdrawer`
* with multiple strategies and sharesAmounts. Operator has default WAD maxMagnitude for all strategies.
* Depending on number of strategies randomized, deposits sharesAmounts into each strategy for the staker and delegates to operator.
* For each strategy,
* - Asserts that staker is delegated to the operator
* - Asserts that the staker withdrawal is queued both with the root and actual Withdrawal struct in storage
* - Asserts that the operator shares decrease by the expected withdrawn shares
* - Checks that event was emitted with correct withdrawalRoot and withdrawal
*/
function testFuzz_queueWithdrawal_MultipleStrats_nonSlashedOperator(Randomness r) public rand(r) {
uint32 numStrategies = r.Uint32(1, 32);
bool depositBeaconChainShares = r.Boolean();
(uint[] memory depositAmounts, uint[] memory withdrawalAmounts,,) =
_fuzzDepositWithdrawalAmounts({r: r, numStrategies: numStrategies});
IStrategy[] memory strategies = _deployAndDepositIntoStrategies(defaultStaker, depositAmounts, depositBeaconChainShares);
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
for (uint i = 0; i < strategies.length; ++i) {
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategies[i],
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: 0,
depositAmount: depositAmounts[i]
});
}
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawals({staker: defaultStaker, strategies: strategies, depositWithdrawalAmounts: withdrawalAmounts});
// Before queueWithdrawal state values
uint nonceBefore = delegationManager.cumulativeWithdrawalsQueued(defaultStaker);
assertEq(delegationManager.delegatedTo(defaultStaker), defaultOperator, "staker should be delegated to operator");
uint[] memory delegatedSharesBefore = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
delegatedSharesBefore[i] = delegationManager.operatorShares(defaultOperator, strategies[i]);
}
// queueWithdrawals
_queueWithdrawals_expectEmit(
QueueWithdrawalsEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
queuedWithdrawalParams: queuedWithdrawalParams,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot
})
);
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
// Post queueWithdrawal state values
for (uint i = 0; i < strategies.length; i++) {
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategies[i],
operatorSharesBefore: delegatedSharesBefore[i],
depositSharesBefore: depositAmounts[i],
depositSharesWithdrawn: withdrawalAmounts[i],
depositScalingFactor: uint(WAD),
slashingFactor: uint(WAD)
});
}
assertEq(delegationManager.delegatedTo(defaultStaker), defaultOperator, "staker should be delegated to operator");
uint nonceAfter = delegationManager.cumulativeWithdrawalsQueued(defaultStaker);
assertEq(nonceBefore + 1, nonceAfter, "staker nonce should have incremented");
_assertQueuedWithdrawalExists(defaultStaker, withdrawal);
}
/**
* @notice Verifies that `DelegationManager.queueWithdrawals` properly queues a withdrawal for the `withdrawer`
* with multiple strategies and sharesAmounts. Operator has random maxMagnitudes for each strategy.
* Depending on number of strategies randomized, deposits sharesAmounts into each strategy for the staker and delegates to operator.
* For each strategy,
* - Asserts that staker is delegated to the operator
* - Asserts that shares for delegatedTo operator are decreased by `depositAmount`
* - Asserts that staker cumulativeWithdrawalsQueued nonce is incremented
* - Checks that event was emitted with correct withdrawalRoot and withdrawal
*/
function testFuzz_queueWithdrawal_MultipleStrats_preSlashedOperator(Randomness r) public rand(r) {
// 1. Setup
// - fuzz numbers of strategies, deposit and withdraw amounts, and prev/new magnitudes for each strategy respectively
// - deposit into strategies, delegate to operator
bool depositBeaconChainShares = r.Boolean();
IStrategy[] memory strategies = r.StrategyArray(r.Uint32(1, 32));
if (depositBeaconChainShares) strategies[strategies.length - 1] = beaconChainETHStrategy;
(uint[] memory depositAmounts, uint[] memory withdrawalAmounts, uint64[] memory prevMaxMagnitudes,) =
_fuzzDepositWithdrawalAmounts({r: r, numStrategies: uint32(strategies.length)});
_registerOperatorWithBaseDetails(defaultOperator);
allocationManagerMock.setMaxMagnitudes(defaultOperator, strategies, prevMaxMagnitudes);
_depositIntoStrategies(defaultStaker, strategies, depositAmounts);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Check deposit state for all strategies after delegating
for (uint i = 0; i < strategies.length; ++i) {
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategies[i],
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: depositAmounts[i]
});
}
uint nonceBefore = delegationManager.cumulativeWithdrawalsQueued(defaultStaker);
// 2. Setup and call queued withdrawals
{
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawals({staker: defaultStaker, strategies: strategies, depositWithdrawalAmounts: withdrawalAmounts});
// expected events emitted
_queueWithdrawals_expectEmit(
QueueWithdrawalsEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
queuedWithdrawalParams: queuedWithdrawalParams,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot
})
);
// 3. call `DelegationManager.queueWithdrawals`
_queueWithdrawals(defaultStaker, queuedWithdrawalParams, withdrawal);
}
// 4. Post queueWithdrawal state values
// Post queueWithdrawal state values
for (uint i = 0; i < strategies.length; i++) {
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategies[i],
operatorSharesBefore: depositAmounts[i],
depositSharesBefore: depositAmounts[i],
depositSharesWithdrawn: withdrawalAmounts[i],
depositScalingFactor: uint(WAD).divWad(prevMaxMagnitudes[i]),
slashingFactor: prevMaxMagnitudes[i]
});
}
assertEq(delegationManager.delegatedTo(defaultStaker), defaultOperator, "staker should be delegated to operator");
assertEq(nonceBefore + 1, delegationManager.cumulativeWithdrawalsQueued(defaultStaker), "staker nonce should have incremented");
_assertQueuedWithdrawalExists(defaultStaker);
}
/**
* @notice Verifies that `DelegationManager.queueWithdrawals` properly queues a withdrawal for the `withdrawer`
* with multiple strategies and sharesAmounts. Operator has random maxMagnitudes for each strategy.
* Depending on number of strategies randomized, deposits sharesAmounts into each strategy for the staker and delegates to operator.
* After depositing, the operator gets slashed for each of the strategies and has new maxMagnitudes set.
* For each strategy,
* - Asserts that staker is delegated to the operator
* - Asserts that shares for delegatedTo operator are decreased by `depositAmount`
* - Asserts that staker cumulativeWithdrawalsQueued nonce is incremented
* - Checks that event was emitted with correct withdrawalRoot and withdrawal
*/
function testFuzz_queueWithdrawal_MultipleStrats_slashedWhileStaked(Randomness r) public rand(r) {
// 1. Setup
// - fuzz numbers of strategies, deposit and withdraw amounts, and prev/new magnitudes for each strategy respectively
// - deposit into strategies, delegate to operator
IStrategy[] memory strategies = r.StrategyArray(r.Uint32(1, 32));
bool depositBeaconChainShares = r.Boolean();
if (depositBeaconChainShares) strategies[strategies.length - 1] = beaconChainETHStrategy;
(uint[] memory depositAmounts, uint[] memory withdrawalAmounts, uint64[] memory prevMaxMagnitudes, uint64[] memory newMaxMagnitudes)
= _fuzzDepositWithdrawalAmounts({r: r, numStrategies: uint32(strategies.length)});
_registerOperatorWithBaseDetails(defaultOperator);
allocationManagerMock.setMaxMagnitudes(defaultOperator, strategies, prevMaxMagnitudes);
_depositIntoStrategies(defaultStaker, strategies, depositAmounts);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Check deposit state for all strategies after delegating
for (uint i = 0; i < strategies.length; ++i) {
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategies[i],
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: depositAmounts[i]
});
}
// 2. Slash operator while staker is delegated and staked
allocationManagerMock.setMaxMagnitudes(defaultOperator, strategies, newMaxMagnitudes);
cheats.startPrank(address(allocationManagerMock));
uint nonceBefore = delegationManager.cumulativeWithdrawalsQueued(defaultStaker);
uint[] memory slashedOperatorShares = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategies[i]);
delegationManager.slashOperatorShares(defaultOperator, strategies[i], prevMaxMagnitudes[i], newMaxMagnitudes[i]);
// Assert correct amount of shares slashed from operator
(slashedOperatorShares[i],) = _assertOperatorSharesAfterSlash({
operator: defaultOperator,
strategy: strategies[i],
operatorSharesBefore: operatorSharesBefore,
prevMaxMagnitude: prevMaxMagnitudes[i],
newMaxMagnitude: newMaxMagnitudes[i]
});
}
cheats.stopPrank();
// 3. Setup and call queued withdrawals
{
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawals({staker: defaultStaker, strategies: strategies, depositWithdrawalAmounts: withdrawalAmounts});
// expected events emitted
_queueWithdrawals_expectEmit(
QueueWithdrawalsEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
queuedWithdrawalParams: queuedWithdrawalParams,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot
})
);
// 4. call `DelegationManager.queueWithdrawals`
_queueWithdrawals(defaultStaker, queuedWithdrawalParams, withdrawal);
_assertQueuedWithdrawalExists(defaultStaker, withdrawal);
}
// 5. Post queueWithdrawal state values
for (uint i = 0; i < strategies.length; i++) {
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategies[i],
operatorSharesBefore: depositAmounts[i] - slashedOperatorShares[i],
depositSharesBefore: depositAmounts[i],
depositSharesWithdrawn: withdrawalAmounts[i],
depositScalingFactor: uint(WAD).divWad(prevMaxMagnitudes[i]),
slashingFactor: newMaxMagnitudes[i]
});
}
assertEq(delegationManager.delegatedTo(defaultStaker), defaultOperator, "staker should be delegated to operator");
assertEq(nonceBefore + 1, delegationManager.cumulativeWithdrawalsQueued(defaultStaker), "staker nonce should have incremented");
}
/**
* @notice Same test as `testFuzz_queueWithdrawal_MultipleStrats_slashedWhileStaked` but with one strategy having 0 newMaxMagnitude
* - Asserts that the strategy with 0 newMaxMagnitude has 0 delegated shares before and after withdrawal
* - Asserts that the staker withdrawn shares for the strategy with 0 newMaxMagnitude is 0
*/
function testFuzz_queueWithdrawal_MultipleStrats__slashed100PercentWhileStaked(Randomness r) public rand(r) {
// 1. Setup
// - fuzz numbers of strategies, deposit and withdraw amounts, and prev/new magnitudes for each strategy respectively
// - deposit into strategies, delegate to operator
uint32 numStrats = r.Uint32(1, 32);
IStrategy[] memory strategies = r.StrategyArray(numStrats);
bool depositBeaconChainShares = r.Boolean();
if (depositBeaconChainShares) strategies[numStrats - 1] = beaconChainETHStrategy;
(uint[] memory depositAmounts, uint[] memory withdrawalAmounts, uint64[] memory prevMaxMagnitudes, uint64[] memory newMaxMagnitudes)
= _fuzzDepositWithdrawalAmounts({r: r, numStrategies: numStrats});
// randomly choose strategy to have 0 newMaxMagnitude
uint zeroMagnitudeIndex = r.Uint256(0, numStrats - 1);
newMaxMagnitudes[zeroMagnitudeIndex] = 0;
_registerOperatorWithBaseDetails(defaultOperator);
allocationManagerMock.setMaxMagnitudes(defaultOperator, strategies, prevMaxMagnitudes);
_depositIntoStrategies(defaultStaker, strategies, depositAmounts);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Check deposit state for all strategies after delegating
for (uint i = 0; i < strategies.length; ++i) {
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategies[i],
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: depositAmounts[i]
});
}
// 2. Slash operator while staker is delegated and staked
uint nonceBefore = delegationManager.cumulativeWithdrawalsQueued(defaultStaker);
uint[] memory slashedOperatorShares = new uint[](strategies.length);
allocationManagerMock.setMaxMagnitudes(defaultOperator, strategies, newMaxMagnitudes);
cheats.startPrank(address(allocationManagerMock));
for (uint i = 0; i < strategies.length; i++) {
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategies[i]);
delegationManager.slashOperatorShares(defaultOperator, strategies[i], prevMaxMagnitudes[i], newMaxMagnitudes[i]);
// Assertions on amount burned
(slashedOperatorShares[i],) = _assertOperatorSharesAfterSlash({
operator: defaultOperator,
strategy: strategies[i],
operatorSharesBefore: operatorSharesBefore,
prevMaxMagnitude: prevMaxMagnitudes[i],
newMaxMagnitude: newMaxMagnitudes[i]
});
// additional assertion checks for strategy that was slashed 100%
if (zeroMagnitudeIndex == i) {
assertEq(slashedOperatorShares[i], operatorSharesBefore, "expected slashed operator shares to be full amount");
assertEq(delegationManager.operatorShares(defaultOperator, strategies[i]), 0, "expected operator shares to be 0");
}
}
cheats.stopPrank();
// 3. Setup and call queued withdrawals
{
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawals({staker: defaultStaker, strategies: strategies, depositWithdrawalAmounts: withdrawalAmounts});
// expected events emitted
_queueWithdrawals_expectEmit(
QueueWithdrawalsEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
queuedWithdrawalParams: queuedWithdrawalParams,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot
})
);
// 4. call `DelegationManager.queueWithdrawals`
_queueWithdrawals(defaultStaker, queuedWithdrawalParams, withdrawal);
_assertQueuedWithdrawalExists(defaultStaker, withdrawal);
}
// 5. Post queueWithdrawal state values
for (uint i = 0; i < strategies.length; i++) {
if (zeroMagnitudeIndex == i) assertEq(newMaxMagnitudes[i], 0, "expected new max magnitude to be 0");
_assertWithdrawal({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategies[i],
operatorSharesBefore: depositAmounts[i] - slashedOperatorShares[i],
depositSharesBefore: depositAmounts[i],
depositSharesWithdrawn: withdrawalAmounts[i],
depositScalingFactor: uint(WAD).divWad(prevMaxMagnitudes[i]),
slashingFactor: zeroMagnitudeIndex == i ? 0 : newMaxMagnitudes[i]
});
}
assertEq(delegationManager.delegatedTo(defaultStaker), defaultOperator, "staker should be delegated to operator");
assertEq(nonceBefore + 1, delegationManager.cumulativeWithdrawalsQueued(defaultStaker), "staker nonce should have incremented");
}
}
contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManagerUnitTests {
using ArrayLib for *;
using SlashingLib for *;
using Math for uint;
function test_Revert_WhenExitWithdrawalQueuePaused() public {
cheats.prank(pauser);
delegationManager.pause(2 ** PAUSED_EXIT_WITHDRAWAL_QUEUE);
_registerOperatorWithBaseDetails(defaultOperator);
(
Withdrawal memory withdrawal,
IERC20[] memory tokens,
/* bytes32 withdrawalRoot */
) = _setUpCompleteQueuedWithdrawalSingleStrat({
staker: defaultStaker,
depositAmount: 100,
withdrawalAmount: 100,
isBeaconChainStrategy: false
});
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// single withdrawal interface
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, false);
IERC20[][] memory tokensArray = new IERC20[][](1);
tokensArray[0] = tokens;
bool[] memory receiveAsTokens = new bool[](1);
receiveAsTokens[0] = false;
Withdrawal[] memory withdrawals = new Withdrawal[](1);
withdrawals[0] = withdrawal;
// multiple Withdrawal interface
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
delegationManager.completeQueuedWithdrawals(withdrawals, tokensArray, receiveAsTokens);
}
function test_Revert_WhenInputArrayLengthMismatch() public {
_registerOperatorWithBaseDetails(defaultOperator);
(
Withdrawal memory withdrawal,
IERC20[] memory tokens,
/* bytes32 withdrawalRoot */
) = _setUpCompleteQueuedWithdrawalSingleStrat({
staker: defaultStaker,
depositAmount: 100,
withdrawalAmount: 100,
isBeaconChainStrategy: false
});
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Roll to completion block
cheats.roll(withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks() + 1);
// resize tokens array
IERC20[] memory newTokens = new IERC20[](0);
cheats.prank(defaultStaker);
cheats.expectRevert(InputArrayLengthMismatch.selector);
delegationManager.completeQueuedWithdrawal(withdrawal, newTokens, false);
// check that the withdrawal completes otherwise
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, true);
}
function test_Revert_WhenWithdrawerNotCaller(Randomness r) public rand(r) {
address invalidCaller = r.Address();
_registerOperatorWithBaseDetails(defaultOperator);
(Withdrawal memory withdrawal, IERC20[] memory tokens,) = _setUpCompleteQueuedWithdrawalSingleStrat({
staker: defaultStaker,
depositAmount: 100,
withdrawalAmount: 100,
isBeaconChainStrategy: false
});
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
cheats.expectRevert(WithdrawerNotCaller.selector);
cheats.prank(invalidCaller);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, false);
}
function test_Revert_WhenInvalidWithdrawalRoot() public {
_registerOperatorWithBaseDetails(defaultOperator);
(Withdrawal memory withdrawal, IERC20[] memory tokens, bytes32 withdrawalRoot) = _setUpCompleteQueuedWithdrawalSingleStrat({
staker: defaultStaker,
depositAmount: 100,
withdrawalAmount: 100,
isBeaconChainStrategy: false
});
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
assertTrue(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRoot should be pending");
cheats.roll(withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks() + 1);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, true);
assertFalse(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRoot should be completed and marked false now");
cheats.expectRevert(WithdrawalNotQueued.selector);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, false);
}
/**
* @notice should revert if MIN_WITHDRAWAL_DELAY_BLOCKS has not passed, and if
* delegationManager.getCompletableTimestamp returns a value greater than MIN_WITHDRAWAL_DELAY_BLOCKS
* then it should revert if the validBlockNumber has not passed either.
*/
function test_Revert_WhenWithdrawalDelayNotPassed(Randomness r) public rand(r) {
uint32 numStrategies = r.Uint32(1, 32);
bool receiveAsTokens = r.Boolean();
(uint[] memory depositAmounts, uint[] memory withdrawalAmounts,,) = _fuzzDepositWithdrawalAmounts(r, numStrategies);
_registerOperatorWithBaseDetails(defaultOperator);
(
Withdrawal memory withdrawal,
IERC20[] memory tokens,
/* bytes32 withdrawalRoot */
) = _setUpCompleteQueuedWithdrawal({
staker: defaultStaker,
depositAmounts: depositAmounts,
withdrawalAmounts: withdrawalAmounts,
depositBeaconChainShares: false
});
// prank as withdrawer address
cheats.roll(withdrawal.startBlock + MIN_WITHDRAWAL_DELAY_BLOCKS);
cheats.expectRevert(WithdrawalDelayNotElapsed.selector);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens);
}
/// @notice Verifies that when we complete a withdrawal as shares after a full slash, we clear the withdrawal
function test_clearWithdrawal_fullySlashed() public {
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, WAD);
// Set the staker deposits in the strategies
uint depositAmount = 100e18;
strategyManagerMock.addDeposit(defaultStaker, strategyMock, depositAmount);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Queue withdrawal
uint withdrawalAmount = depositAmount;
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal,) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: withdrawalAmount});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
// Warp to just before the MIN_WITHDRAWAL_DELAY_BLOCKS
cheats.roll(withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks());
// Slash all of operator's shares
_setOperatorMagnitude(defaultOperator, strategyMock, 0);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategyMock, WAD, 0);
// Complete withdrawal as shares and check that withdrawal was cleared
cheats.roll(block.number + 1);
IERC20[] memory tokens = strategyMock.underlyingToken().toArray();
bytes32 withdrawalRoot = delegationManager.calculateWithdrawalRoot(withdrawal);
assertTrue(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawal should be pending before completion");
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, false);
assertFalse(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawal should be cleared after completion");
// Assert that no shares were added back
assertEq(delegationManager.operatorShares(defaultOperator, strategyMock), 0, "operator shares should remain 0");
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, strategyMock.toArray());
assertEq(withdrawableShares[0], 0, "withdrawable shares should be 0");
}
/**
* Test completing multiple queued withdrawals for a single strategy by passing in the withdrawals
*/
function test_completeQueuedWithdrawals_MultipleWithdrawals(Randomness r) public rand(r) {
address staker = r.Address();
uint depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES);
uint numWithdrawals = r.Uint256(2, 20);
bool receiveAsTokens = r.Boolean();
(Withdrawal[] memory withdrawals, IERC20[][] memory tokens, bytes32[] memory withdrawalRoots) =
_setUpCompleteQueuedWithdrawalsSingleStrat({staker: staker, depositAmount: depositAmount, numWithdrawals: numWithdrawals});
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, withdrawals[0].strategies[0]);
for (uint i = 0; i < withdrawalRoots.length; i++) {
assertTrue(delegationManager.pendingWithdrawals(withdrawalRoots[i]), "withdrawalRoots should be pending");
}
bool[] memory receiveAsTokensArray = receiveAsTokens.toArray(numWithdrawals);
// completeQueuedWithdrawal
cheats.roll(withdrawals[0].startBlock + delegationManager.minWithdrawalDelayBlocks() + 1);
_completeQueuedWithdrawals_expectEmit(
CompleteQueuedWithdrawalsEmitStruct({withdrawals: withdrawals, tokens: tokens, receiveAsTokens: receiveAsTokensArray})
);
cheats.prank(staker);
delegationManager.completeQueuedWithdrawals(withdrawals, tokens, receiveAsTokensArray);
// assertion checks
(uint[] memory withdrawableShares, uint[] memory depositShares) =
delegationManager.getWithdrawableShares(staker, withdrawals[0].strategies);
uint operatorSharesAfter = delegationManager.operatorShares(defaultOperator, withdrawals[0].strategies[0]);
if (receiveAsTokens) {
assertEq(withdrawableShares[0], 0, "withdrawable shares should be 0 from withdrawing all");
assertEq(depositShares[0], 0, "deposit shares should be 0 from withdrawing all");
assertEq(operatorSharesAfter, operatorSharesBefore, "operator shares should be unchanged");
} else {
assertEq(withdrawableShares[0], depositAmount * numWithdrawals, "withdrawable shares should be added back as shares");
assertEq(depositShares[0], depositAmount * numWithdrawals, "deposit shares should be added back as shares");
assertEq(operatorSharesAfter, operatorSharesBefore + depositAmount * numWithdrawals, "operator shares should be unchanged");
}
_assertWithdrawalRootsComplete(staker, withdrawals);
assertEq(
delegationManager.depositScalingFactor(staker, withdrawals[0].strategies[0]), uint(WAD), "deposit scaling factor should be WAD"
);
}
/**
* @notice Verifies that `DelegationManager.completeQueuedWithdrawal` properly completes a queued withdrawal for the `withdrawer`
* for a single strategy.
* - Asserts that the withdrawalRoot is True before `completeQueuedWithdrawal` and False after
* - Asserts that event `WithdrawalCompleted` is emitted with withdrawalRoot
* if receiveAsTokens is true
* - Asserts operatorShares is unchanged after `completeQueuedWithdrawal`
* - Asserts that the staker's withdrawable shares, deposit shares, and depositScalingFactors are unchanged
* if receiveAsTokens is false
* - Asserts operatorShares is increased correctly after `completeQueuedWithdrawal`
* - Asserts that the staker's withdrawable shares, deposit shares, and depositScalingFactors are updated correctly
*/
function test_completeQueuedWithdrawal_SingleStrat(Randomness r) public rand(r) {
uint depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES);
uint withdrawalAmount = r.Uint256(1, depositAmount);
bool receiveAsTokens = r.Boolean();
_registerOperatorWithBaseDetails(defaultOperator);
(Withdrawal memory withdrawal, IERC20[] memory tokens, bytes32 withdrawalRoot) = _setUpCompleteQueuedWithdrawalSingleStrat({
staker: defaultStaker,
depositAmount: depositAmount,
withdrawalAmount: withdrawalAmount,
isBeaconChainStrategy: false
});
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, withdrawal.strategies[0]);
assertTrue(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRoot should be pending");
// completeQueuedWithdrawal
cheats.roll(withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks() + 1);
_completeQueuedWithdrawal_expectEmit(
CompleteQueuedWithdrawalEmitStruct({withdrawal: withdrawal, tokens: tokens, receiveAsTokens: receiveAsTokens})
);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens);
_assertCompletedWithdrawal(
AssertCompletedWithdrawalStruct({
staker: defaultStaker,
currentOperator: defaultOperator,
withdrawal: withdrawal,
receiveAsTokens: receiveAsTokens,
operatorSharesBefore: operatorSharesBefore.toArrayU256(),
withdrawableSharesBefore: (depositAmount - withdrawalAmount).toArrayU256(),
depositSharesBefore: (depositAmount - withdrawalAmount).toArrayU256(),
prevDepositScalingFactors: uint(WAD).toArrayU256(),
slashingFactors: uint(WAD).toArrayU256(),
beaconChainSlashingFactor: WAD
})
);
}
/**
* @notice Verifies that `DelegationManager.completeQueuedWithdrawal` properly completes a queued withdrawal for the `withdrawer`
* for a single strategy. Withdraws as tokens so there are no operator shares increase.
* - Asserts that the withdrawalRoot is True before `completeQueuedWithdrawal` and False after
* - Asserts operatorShares is decreased after the operator is slashed
* - Checks that event `WithdrawalCompleted` is emitted with withdrawalRoot
* - Asserts that the shares the staker completed withdrawal for are less than what is expected since its operator is slashed
*/
function test_completeQueuedWithdrawal_SingleStrat_slashOperatorDuringQueue(Randomness r) public rand(r) {
uint depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES);
uint withdrawalAmount = r.Uint256(1, depositAmount);
bool receiveAsTokens = r.Boolean();
uint64 prevMaxMagnitude = r.Uint64(2, WAD);
uint64 newMaxMagnitude = r.Uint64(1, prevMaxMagnitude - 1);
// Deposit Staker
strategyManagerMock.addDeposit(defaultStaker, strategyMock, depositAmount);
// Register operator and delegate to it
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, prevMaxMagnitude);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Queue withdrawal
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: withdrawalAmount});
{
uint operatorSharesBeforeQueue = delegationManager.operatorShares(defaultOperator, strategyMock);
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
assertTrue(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRoot should be pending");
uint operatorSharesAfterQueue = delegationManager.operatorShares(defaultOperator, strategyMock);
uint sharesWithdrawn = _calcWithdrawableShares({
depositShares: withdrawalAmount,
depositScalingFactor: uint(WAD).divWad(prevMaxMagnitude),
slashingFactor: uint(prevMaxMagnitude)
});
assertEq(
operatorSharesAfterQueue, operatorSharesBeforeQueue - sharesWithdrawn, "operator shares should be decreased after queue"
);
}
// Slash operator while staker has queued withdrawal
{
uint operatorSharesAfterQueue = delegationManager.operatorShares(defaultOperator, strategyMock);
(uint sharesToDecrement,) = _calcSlashedAmount({
operatorShares: operatorSharesAfterQueue,
prevMaxMagnitude: prevMaxMagnitude,
newMaxMagnitude: newMaxMagnitude
});
_setOperatorMagnitude(defaultOperator, strategyMock, newMaxMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, withdrawal.strategies[0], prevMaxMagnitude, newMaxMagnitude);
uint operatorSharesAfterSlash = delegationManager.operatorShares(defaultOperator, strategyMock);
assertEq(
operatorSharesAfterSlash, operatorSharesAfterQueue - sharesToDecrement, "operator shares should be decreased after slash"
);
}
// Complete queue withdrawal
(uint[] memory withdrawableShares, uint[] memory depositShares) =
delegationManager.getWithdrawableShares(defaultStaker, withdrawal.strategies);
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock);
{
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = IERC20(strategyMock.underlyingToken());
cheats.roll(withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks() + 1);
_completeQueuedWithdrawal_expectEmit(
CompleteQueuedWithdrawalEmitStruct({withdrawal: withdrawal, tokens: tokens, receiveAsTokens: receiveAsTokens})
);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens);
}
_assertCompletedWithdrawal(
AssertCompletedWithdrawalStruct({
staker: defaultStaker,
currentOperator: defaultOperator,
withdrawal: withdrawal,
receiveAsTokens: receiveAsTokens,
operatorSharesBefore: operatorSharesBefore.toArrayU256(),
withdrawableSharesBefore: withdrawableShares,
depositSharesBefore: depositShares,
prevDepositScalingFactors: uint(WAD).divWad(prevMaxMagnitude).toArrayU256(),
slashingFactors: uint(newMaxMagnitude).toArrayU256(),
beaconChainSlashingFactor: WAD
})
);
}
/**
* @notice Verifies that `DelegationManager.completeQueuedWithdrawal` properly completes a queued withdrawal for the `withdrawer`
* for the BeaconChainStrategy. Withdraws as tokens so there are no operator shares increase.
* - Asserts that the withdrawalRoot is True before `completeQueuedWithdrawal` and False after
* - Asserts operatorShares is decreased after staker is slashed
* - Checks that event `WithdrawalCompleted` is emitted with withdrawalRoot
* - Asserts that the shares the staker completed withdrawal for are less than what is expected since the staker is slashed during queue
*/
function test_completeQueuedWithdrawal_BeaconStrat_slashStakerDuringQueue(Randomness r) public rand(r) {
int depositAmount = int(r.Uint256(1, MAX_ETH_SUPPLY));
uint withdrawalAmount = r.Uint256(1, uint(depositAmount));
uint64 initialBCSF = r.Uint64(2, WAD);
uint sharesDecrease = r.Uint256(1, uint(depositAmount) - withdrawalAmount);
bool receiveAsTokens = r.Boolean();
// Deposit Staker
eigenPodManagerMock.setBeaconChainSlashingFactor(defaultStaker, initialBCSF);
eigenPodManagerMock.setPodOwnerShares(defaultStaker, depositAmount);
// Register operator and delegate to it
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Queue withdrawal
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({
staker: defaultStaker,
strategy: beaconChainETHStrategy,
depositSharesToWithdraw: withdrawalAmount
});
uint64 prevBeaconSlashingFactor;
uint64 newBeaconSlashingFactor;
{
uint sharesToDecrement = _calcWithdrawableShares({
depositShares: withdrawalAmount,
depositScalingFactor: uint(WAD),
slashingFactor: uint(initialBCSF)
});
uint operatorSharesBeforeQueue = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy);
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
assertTrue(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRoot should be pending");
uint operatorSharesAfterQueue = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy);
assertEq(
operatorSharesAfterQueue, operatorSharesBeforeQueue - sharesToDecrement, "operator shares should be decreased after queue"
);
// Slash the staker for beacon chain shares while it has queued a withdrawal
// simulate the operations done in EigenPodManager._reduceSlashingFactor
(uint[] memory withdrawableSharesBefore,) =
delegationManager.getWithdrawableShares(defaultStaker, beaconChainETHStrategy.toArray());
uint currentPodShares = uint(depositAmount) - withdrawalAmount;
(prevBeaconSlashingFactor, newBeaconSlashingFactor) =
_decreaseBeaconChainShares({staker: defaultStaker, beaconShares: int(currentPodShares), sharesDecrease: sharesDecrease});
uint expectedWithdrawableShares = _calcWithdrawableShares({
depositShares: uint(currentPodShares),
depositScalingFactor: uint(WAD),
slashingFactor: uint(newBeaconSlashingFactor)
});
_assertSharesAfterBeaconSlash(defaultStaker, withdrawableSharesBefore[0], expectedWithdrawableShares, prevBeaconSlashingFactor);
}
// Complete queue withdrawal
(uint[] memory withdrawableShares, uint[] memory depositShares) =
delegationManager.getWithdrawableShares(defaultStaker, beaconChainETHStrategy.toArray());
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy);
{
IERC20[] memory tokens = new IERC20[](1);
cheats.roll(withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks() + 1);
_completeQueuedWithdrawal_expectEmit(
CompleteQueuedWithdrawalEmitStruct({withdrawal: withdrawal, tokens: tokens, receiveAsTokens: receiveAsTokens})
);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens);
}
_assertCompletedWithdrawal(
AssertCompletedWithdrawalStruct({
staker: defaultStaker,
currentOperator: defaultOperator,
withdrawal: withdrawal,
receiveAsTokens: receiveAsTokens,
operatorSharesBefore: operatorSharesBefore.toArrayU256(),
withdrawableSharesBefore: withdrawableShares,
depositSharesBefore: depositShares,
prevDepositScalingFactors: uint(WAD).toArrayU256(),
slashingFactors: uint(WAD).toArrayU256(), // beaconChainSlashingFactor is separate from slashingFactors input
beaconChainSlashingFactor: newBeaconSlashingFactor
})
);
}
/**
* @notice Verifies that `DelegationManager.completeQueuedWithdrawal` properly completes a queued withdrawal for the `withdrawer`
* for the BeaconChainStrategy. Withdraws as tokens so there are no operator shares increase.
* - Asserts that the withdrawalRoot is True before `completeQueuedWithdrawal` and False after
* - Asserts operatorShares is decreased after staker is slashed and after the operator is slashed
* - Checks that event `WithdrawalCompleted` is emitted with withdrawalRoot
* - Asserts that the shares the staker completed withdrawal for are less than what is expected since both the staker and its operator are slashed during queue
*/
function test_completeQueuedWithdrawal_BeaconStratWithdrawAsTokens_slashStakerAndOperator(Randomness r) public rand(r) {
int depositAmount = int(r.Uint256(1, MAX_ETH_SUPPLY));
uint withdrawalAmount = r.Uint256(1, uint(depositAmount));
bool receiveAsTokens = r.Boolean();
// Deposit Staker
eigenPodManagerMock.setPodOwnerShares(defaultStaker, int(depositAmount));
// Register operator and delegate to it
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
uint operatorSharesBeforeQueue = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy);
// Queue withdrawal
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({
staker: defaultStaker,
strategy: beaconChainETHStrategy,
depositSharesToWithdraw: withdrawalAmount
});
uint64 newBeaconSlashingFactor;
{
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
assertTrue(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRoot should be pending");
uint operatorSharesAfterQueue = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy);
assertEq(
operatorSharesAfterQueue, operatorSharesBeforeQueue - withdrawalAmount, "operator shares should be decreased after queue"
);
// Slash the staker for beacon chain shares while it has queued a withdrawal
// simulate the operations done in EigenPodManager._reduceSlashingFactor
(, newBeaconSlashingFactor) = _decreaseBeaconChainShares({
staker: defaultStaker,
beaconShares: depositAmount - int(withdrawalAmount),
sharesDecrease: (uint(depositAmount) - withdrawalAmount) / 2
});
uint operatorSharesAfterBeaconSlash = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy);
assertEq(
operatorSharesAfterBeaconSlash,
operatorSharesAfterQueue.ceilDiv(2),
"operator shares should be decreased after beaconChain slash"
);
// Slash the operator for beacon chain shares
uint64 operatorMagnitude = 5e17;
_setOperatorMagnitude(defaultOperator, withdrawal.strategies[0], operatorMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, withdrawal.strategies[0], WAD, operatorMagnitude);
uint operatorSharesAfterAVSSlash = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy);
assertApproxEqAbs(
operatorSharesAfterAVSSlash, operatorSharesAfterBeaconSlash / 2, 1, "operator shares should be decreased after AVS slash"
);
}
// Complete queue withdrawal
(uint[] memory withdrawableShares, uint[] memory depositShares) =
delegationManager.getWithdrawableShares(defaultStaker, beaconChainETHStrategy.toArray());
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy);
IERC20[] memory tokens = new IERC20[](1);
cheats.roll(withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks() + 1);
_completeQueuedWithdrawal_expectEmit(
CompleteQueuedWithdrawalEmitStruct({withdrawal: withdrawal, tokens: tokens, receiveAsTokens: receiveAsTokens})
);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens);
_assertCompletedWithdrawal(
AssertCompletedWithdrawalStruct({
staker: defaultStaker,
currentOperator: defaultOperator,
withdrawal: withdrawal,
receiveAsTokens: receiveAsTokens,
operatorSharesBefore: operatorSharesBefore.toArrayU256(),
withdrawableSharesBefore: withdrawableShares,
depositSharesBefore: depositShares,
prevDepositScalingFactors: uint(WAD).toArrayU256(),
slashingFactors: 5e17.toArrayU256(),
beaconChainSlashingFactor: newBeaconSlashingFactor
})
);
}
/**
* @notice Verifies that `DelegationManager.completeQueuedWithdrawal` properly completes a queued withdrawal for the `withdrawer`
* for a single strategy. Withdraws as shares so if the withdrawer is delegated, operator shares increase. In the test case, this only
* happens if staker and withdrawer are fuzzed the same address (i.e. staker == withdrawer)
* - Asserts that the withdrawalRoot is True before `completeQueuedWithdrawal` and False after
* - Asserts if staker == withdrawer, operatorShares increase, otherwise operatorShares are unchanged
* - Checks that event `WithdrawalCompleted` is emitted with withdrawalRoot
*/
function testFuzz_completeQueuedWithdrawal_SingleStratWithdrawAsShares_nonSlashedOperator(Randomness r) public rand(r) {
address staker = r.Address();
uint128 depositAmount = r.Uint128();
uint128 withdrawalAmount = r.Uint128(1, depositAmount);
_registerOperatorWithBaseDetails(defaultOperator);
(Withdrawal memory withdrawal, IERC20[] memory tokens, bytes32 withdrawalRoot) = _setUpCompleteQueuedWithdrawalSingleStrat({
staker: staker,
depositAmount: depositAmount,
withdrawalAmount: withdrawalAmount,
isBeaconChainStrategy: false
});
_delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator);
uint operatorSharesBefore = delegationManager.operatorShares(defaultOperator, withdrawal.strategies[0]);
assertTrue(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRoot should be pending");
// Set delegationManager on strategyManagerMock so it can call back into delegationManager
strategyManagerMock.setDelegationManager(delegationManager);
// completeQueuedWithdrawal
cheats.roll(withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks() + 1);
_completeQueuedWithdrawal_expectEmit(
CompleteQueuedWithdrawalEmitStruct({withdrawal: withdrawal, tokens: tokens, receiveAsTokens: false})
);
cheats.prank(staker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, false);
uint operatorSharesAfter = delegationManager.operatorShares(defaultOperator, withdrawal.strategies[0]);
// Since staker is delegated, operatorShares get incremented
assertEq(operatorSharesAfter, operatorSharesBefore + withdrawalAmount, "operator shares not increased correctly");
assertFalse(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRoot should be completed and marked false now");
}
function testFuzz_completeQueuedWithdrawals_OutOfOrderBlocking(Randomness r) public {
uint totalDepositShares = r.Uint256(4, 100 ether);
uint depositSharesPerWithdrawal = totalDepositShares / 4;
_registerOperatorWithBaseDetails(defaultOperator);
strategyManagerMock.addDeposit(defaultStaker, strategyMock, totalDepositShares);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
QueuedWithdrawalParams[] memory queuedParams = new QueuedWithdrawalParams[](4);
Withdrawal[] memory withdrawals = new Withdrawal[](4);
uint startBlock = block.number;
uint nonce = delegationManager.cumulativeWithdrawalsQueued(defaultStaker);
for (uint i; i < 4; ++i) {
cheats.roll(startBlock + i);
(QueuedWithdrawalParams[] memory params, Withdrawal memory withdrawal,) =
_setUpQueueWithdrawalsSingleStrat(defaultStaker, strategyMock, depositSharesPerWithdrawal);
withdrawal.nonce = nonce;
nonce += 1;
(queuedParams[i], withdrawals[i]) = (params[0], withdrawal);
}
uint delay = delegationManager.minWithdrawalDelayBlocks();
cheats.startPrank(defaultStaker);
cheats.roll(startBlock);
delegationManager.queueWithdrawals(queuedParams[0].toArray());
cheats.roll(startBlock + 1);
delegationManager.queueWithdrawals(queuedParams[1].toArray());
(Withdrawal[] memory firstWithdrawals,) = delegationManager.getQueuedWithdrawals(defaultStaker);
cheats.roll(startBlock + 2);
delegationManager.queueWithdrawals(queuedParams[2].toArray());
cheats.roll(startBlock + 3);
delegationManager.queueWithdrawals(queuedParams[3].toArray());
IERC20[][] memory tokens = new IERC20[][](2);
for (uint i; i < 2; ++i) {
tokens[i] = strategyMock.underlyingToken().toArray();
}
bytes32 root1 = delegationManager.calculateWithdrawalRoot(withdrawals[0]);
bytes32 root2 = delegationManager.calculateWithdrawalRoot(withdrawals[1]);
bytes32 root1_view = delegationManager.calculateWithdrawalRoot(firstWithdrawals[0]);
bytes32 root2_view = delegationManager.calculateWithdrawalRoot(firstWithdrawals[1]);
assertEq(root1, root1_view, "withdrawal root should be the same");
assertEq(root2, root2_view, "withdrawal root should be the same");
cheats.roll(startBlock + delay + 2);
delegationManager.completeQueuedWithdrawals(firstWithdrawals, tokens, true.toArray(2));
// Throws `WithdrawalNotQueued`.
cheats.roll(startBlock + delay + 3);
delegationManager.completeQueuedWithdrawals(withdrawals[2].toArray(), tokens, true.toArray());
cheats.stopPrank();
}
}
contract DelegationManagerUnitTests_slashingShares is DelegationManagerUnitTests {
using ArrayLib for *;
using SlashingLib for *;
using Math for *;
/// @notice Verifies that `DelegationManager.slashOperatorShares` reverts if not called by the AllocationManager
function testFuzz_Revert_slashOperatorShares_invalidCaller(Randomness r) public rand(r) {
address invalidCaller = r.Address();
cheats.startPrank(invalidCaller);
cheats.expectRevert(IDelegationManagerErrors.OnlyAllocationManager.selector);
delegationManager.slashOperatorShares(defaultOperator, strategyMock, 0, 0);
}
/// @notice Verifies that there is no change in shares if the staker is not delegatedd
function testFuzz_Revert_slashOperatorShares_noop() public {
_registerOperatorWithBaseDetails(defaultOperator);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategyMock, WAD, WAD / 2);
assertEq(delegationManager.operatorShares(defaultOperator, strategyMock), 0, "shares should not have changed");
}
/// @notice Verifies that shares are burnable for a withdrawal slashed just before the MIN_WITHDRAWAL_DELAY_BLOCKS is hit
function test_sharesBurnableAtMinDelayBlocks() public {
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, WAD);
// Set the staker deposits in the strategies
uint depositAmount = 100e18;
strategyManagerMock.addDeposit(defaultStaker, strategyMock, depositAmount);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Queue withdrawal
uint withdrawalAmount = depositAmount;
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal,) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: withdrawalAmount});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
// Warp to just before the MIN_WITHDRAWAL_DELAY_BLOCKS
cheats.roll(withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks());
uint slashableSharesInQueue = delegationManager.getSlashableSharesInQueue(defaultOperator, strategyMock);
// Slash all of operator's shares
_setOperatorMagnitude(defaultOperator, strategyMock, 0);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategyMock, WAD, 0);
uint slashableSharesInQueueAfter = delegationManager.getSlashableSharesInQueue(defaultOperator, strategyMock);
// Complete withdrawal as tokens and assert that nothing is returned and withdrawal is cleared
cheats.roll(block.number + 1);
IERC20[] memory tokens = strategyMock.underlyingToken().toArray();
bytes32 withdrawalRoot = delegationManager.calculateWithdrawalRoot(withdrawal);
assertTrue(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawal should be pending before completion");
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, true);
assertFalse(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawal should be cleared after completion");
assertEq(
slashableSharesInQueue,
depositAmount,
"the withdrawal in queue from block.number - minWithdrawalDelayBlocks should still be included"
);
assertEq(slashableSharesInQueueAfter, 0, "slashable shares in queue should be 0 after burning");
}
/// @notice Verifies that shares are NOT burnable for a withdrawal queued just before the MIN_WITHDRAWAL_DELAY_BLOCKS
function test_sharesNotBurnableWhenWithdrawalCompletable() public {
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, WAD);
// Set the staker deposits in the strategies
uint depositAmount = 100e18;
strategyManagerMock.addDeposit(defaultStaker, strategyMock, depositAmount);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Queue withdrawal
uint withdrawalAmount = depositAmount;
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal,) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: withdrawalAmount});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
// Warp to completion time
cheats.roll(withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks() + 1);
uint slashableShares = delegationManager.getSlashableSharesInQueue(defaultOperator, strategyMock);
assertEq(slashableShares, 0, "shares should not be slashable");
// Slash all of operator's shares
_setOperatorMagnitude(defaultOperator, strategyMock, 0);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategyMock, WAD, 0);
// Complete withdrawal as tokens and assert that we call back into the SM with 100 tokens
IERC20[] memory tokens = strategyMock.underlyingToken().toArray();
cheats.expectCall(
address(strategyManagerMock),
abi.encodeWithSelector(
IShareManager.withdrawSharesAsTokens.selector, defaultStaker, strategyMock, strategyMock.underlyingToken(), 100e18
)
);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, true);
}
/**
* @notice Queues 5 withdrawals at different blocks. Then, warps such that the first 2 are completable. Validates the slashable shares
*/
function test_slashableSharesInQueue() public {
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, WAD);
// Set the staker deposits in the strategies
uint depositAmount = 120e18;
strategyManagerMock.addDeposit(defaultStaker, strategyMock, depositAmount);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Queue 5 withdrawals
uint startBlock = block.number;
uint withdrawalAmount = depositAmount / 6;
for (uint i = 0; i < 5; i++) {
(QueuedWithdrawalParams[] memory queuedWithdrawalParams,,) = _setUpQueueWithdrawalsSingleStrat({
staker: defaultStaker,
strategy: strategyMock,
depositSharesToWithdraw: withdrawalAmount
});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
cheats.roll(startBlock + i + 1);
}
// Warp to completion time for the first 2 withdrawals
// First withdrawal queued at startBlock. Second queued at startBlock + 1
cheats.roll(startBlock + 1 + delegationManager.minWithdrawalDelayBlocks() + 1);
// Get slashable shares
uint slashableSharesInQueue = delegationManager.getSlashableSharesInQueue(defaultOperator, strategyMock);
assertEq(slashableSharesInQueue, depositAmount / 6 * 3, "slashable shares in queue should be 3/6 of the deposit amount");
// Slash all of operator's shares
_setOperatorMagnitude(defaultOperator, strategyMock, 0);
cheats.prank(address(allocationManagerMock));
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesDecreased(
defaultOperator,
address(0),
strategyMock,
depositAmount / 6 // 1 withdrawal not queued so decreased
);
delegationManager.slashOperatorShares(defaultOperator, strategyMock, WAD, 0);
// Assert slashable shares
slashableSharesInQueue = delegationManager.getSlashableSharesInQueue(defaultOperator, strategyMock);
assertEq(slashableSharesInQueue, 0);
}
/**
* @notice Verifies that `DelegationManager.slashOperatorShares` properly decreases the delegated `shares` that the operator
* who the `defaultStaker` is delegated to has in the strategies
*/
function testFuzz_slashOperatorShares_slashedOperator(Randomness r) public rand(r) {
// sanity-filtering on fuzzed input length & staker
IStrategy[] memory strategies = r.StrategyArray(16);
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
uint64 prevMaxMagnitude = r.Uint64(2, WAD);
uint64 newMaxMagnitude = r.Uint64(1, prevMaxMagnitude);
bool hasBeaconChainStrategy = r.Boolean();
if (hasBeaconChainStrategy) {
// Set last strategy in array as beacon chain strategy
strategies[strategies.length - 1] = beaconChainETHStrategy;
}
// Register operator
_registerOperatorWithBaseDetails(defaultOperator);
// Set the staker deposits in the strategies
uint[] memory sharesToSet = new uint[](strategies.length);
uint[] memory depositScalingFactors = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
strategies[i] = IStrategy(random().Address());
sharesToSet[i] = shares;
depositScalingFactors[i] = uint(WAD).divWad(uint(prevMaxMagnitude));
_setOperatorMagnitude(defaultOperator, strategies[i], prevMaxMagnitude);
}
// Okay to set beacon chain shares in SM mock, wont' be called by DM
strategyManagerMock.setDeposits(defaultStaker, strategies, sharesToSet);
if (hasBeaconChainStrategy) eigenPodManagerMock.setPodOwnerShares(defaultStaker, int(uint(shares)));
// events expected emitted for each strategy
_delegateTo_expectEmit(
DelegateToEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
strategies: strategies,
depositShares: sharesToSet,
depositScalingFactors: depositScalingFactors
})
);
// delegate from the `staker` to the operator
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
address delegatedTo = delegationManager.delegatedTo(defaultStaker);
// check shares before call to `slashOperatorShares`
for (uint i = 0; i < strategies.length; ++i) {
// store delegated shares in a mapping
delegatedSharesBefore[strategies[i]] = delegationManager.operatorShares(delegatedTo, strategies[i]);
// also construct an array which we'll use in another loop
totalSharesForStrategyInArray[address(strategies[i])] += shares;
}
// for each strategy in `strategies`, decrease delegated shares by `shares`
{
cheats.startPrank(address(allocationManagerMock));
for (uint i = 0; i < strategies.length; ++i) {
uint currentShares = delegationManager.operatorShares(defaultOperator, strategies[i]);
uint sharesToDecrease = SlashingLib.calcSlashedAmount({
operatorShares: currentShares,
prevMaxMagnitude: prevMaxMagnitude,
newMaxMagnitude: newMaxMagnitude
});
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesDecreased(defaultOperator, address(0), strategies[i], sharesToDecrease);
delegationManager.slashOperatorShares(defaultOperator, strategies[i], prevMaxMagnitude, newMaxMagnitude);
// Also update maxMagnitude in ALM mock
_setOperatorMagnitude(defaultOperator, strategies[i], newMaxMagnitude);
totalSharesDecreasedForStrategy[strategies[i]] += sharesToDecrease;
}
cheats.stopPrank();
}
// check shares after call to `slashOperatorShares`
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, strategies);
for (uint i = 0; i < strategies.length; ++i) {
uint delegatedSharesAfter = delegationManager.operatorShares(delegatedTo, strategies[i]);
assertEq(
delegatedSharesAfter,
delegatedSharesBefore[strategies[i]] - totalSharesDecreasedForStrategy[strategies[i]],
"delegated shares did not decrement correctly"
);
_assertWithdrawableAndOperatorShares(
withdrawableShares[i], delegatedSharesAfter, "withdrawable and operator shares not decremented correctly"
);
}
}
/**
* @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_slashOperatorShares_NoQueuedWithdrawals(Randomness r) public rand(r) {
address operator = r.Address();
address staker = r.Address();
uint64 initMagnitude = WAD;
uint64 newMagnitude = 5e17;
uint 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();
uint[] memory sharesArray = shares.toArrayU256();
strategyManagerMock.setDeposits(staker, strategyArray, sharesArray);
// delegate from the `staker` to the operator
_delegateToOperatorWhoAcceptsAllStakers(staker, operator);
uint operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock);
uint queuedSlashableSharesBefore = delegationManager.getSlashableSharesInQueue(operator, strategyMock);
// calculate burned shares, should be halved
uint sharesToBurn = shares / 2;
// Burn shares
_slashOperatorShares_expectEmit(
SlashOperatorSharesEmitStruct({
operator: operator,
strategy: strategyMock,
sharesToDecrease: sharesToBurn,
sharesToBurn: sharesToBurn
})
);
// Assert OperatorSharesSlashed event was emitted with correct params
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesSlashed(operator, strategyMock, sharesToBurn);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: operator,
strategy: strategyMock,
prevMaxMagnitude: initMagnitude,
newMaxMagnitude: newMagnitude
});
uint queuedSlashableSharesAfter = delegationManager.getSlashableSharesInQueue(operator, strategyMock);
uint 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_slashOperatorShares_NoQueuedWithdrawalsInWindow(Randomness r) public rand(r) {
// 1. Randomize operator and staker info
// Operator info
address operator = r.Address();
uint64 newMagnitude = 5e17;
// First staker
address staker1 = r.Address();
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
// Second Staker, will queue withdraw shares
address staker2 = r.Address();
uint depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES);
uint withdrawAmount = r.Uint256(1, depositAmount);
// 2. Register the operator, set the staker deposits, and delegate the 2 stakers to them
_registerOperatorWithBaseDetails(operator);
strategyManagerMock.addDeposit(staker1, strategyMock, shares);
strategyManagerMock.addDeposit(staker2, strategyMock, depositAmount);
_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, 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.minWithdrawalDelayBlocks() + 1);
}
uint operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock);
uint 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%
uint sharesToBurn = (shares + depositAmount - withdrawAmount) / 2;
// 4. Burn shares
_setOperatorMagnitude(operator, strategyMock, newMagnitude);
_slashOperatorShares_expectEmit(
SlashOperatorSharesEmitStruct({
operator: operator,
strategy: strategyMock,
sharesToDecrease: sharesToBurn,
sharesToBurn: sharesToBurn
})
);
// Assert OperatorSharesSlashed event was emitted with correct params
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesSlashed(operator, strategyMock, sharesToBurn);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: operator,
strategy: strategyMock,
prevMaxMagnitude: WAD,
newMaxMagnitude: newMagnitude
});
// 5. Assert expected values
uint queuedSlashableSharesAfter = delegationManager.getSlashableSharesInQueue(operator, strategyMock);
uint 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_slashOperatorShares_SingleSlashableWithdrawal(Randomness r) public rand(r) {
// 1. Randomize operator and staker info
// Operator info
address operator = r.Address();
uint64 newMagnitude = 25e16;
// First staker
address staker1 = r.Address();
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
// Second Staker, will queue withdraw shares
address staker2 = r.Address();
uint depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES);
uint withdrawAmount = r.Uint256(1, depositAmount);
// 2. Register the operator, set the staker deposits, and delegate the 2 stakers to them
_registerOperatorWithBaseDetails(operator);
strategyManagerMock.addDeposit(staker1, strategyMock, shares);
strategyManagerMock.addDeposit(staker2, strategyMock, depositAmount);
_delegateToOperatorWhoAcceptsAllStakers(staker1, operator);
_delegateToOperatorWhoAcceptsAllStakers(staker2, operator);
// 3. Queue withdrawal for staker2 so that the withdrawal is slashable
{
(QueuedWithdrawalParams[] memory queuedWithdrawalParams,,) =
_setUpQueueWithdrawalsSingleStrat({staker: 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"
);
}
uint operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock);
uint 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%
uint sharesToDecrease = (shares + depositAmount - withdrawAmount) * 3 / 4;
uint sharesToBurn = sharesToDecrease + withdrawAmount * 3 / 4;
// 4. Burn shares
_setOperatorMagnitude(operator, strategyMock, newMagnitude);
_slashOperatorShares_expectEmit(
SlashOperatorSharesEmitStruct({
operator: operator,
strategy: strategyMock,
sharesToDecrease: sharesToDecrease,
sharesToBurn: sharesToBurn
})
);
// Assert OperatorSharesSlashed event was emitted with correct params
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesSlashed(operator, strategyMock, sharesToBurn);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: operator,
strategy: strategyMock,
prevMaxMagnitude: WAD,
newMaxMagnitude: newMagnitude
});
// 5. Assert expected values
uint queuedSlashableSharesAfter = delegationManager.getSlashableSharesInQueue(operator, strategyMock);
uint 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_slashOperatorShares_MultipleSlashableWithdrawals(Randomness r) public rand(r) {
// 1. Randomize operator and staker info
// Operator info
address operator = r.Address();
uint64 newMagnitude = 25e16;
// Staker and withdrawing amounts
address staker = r.Address();
uint depositAmount = r.Uint256(3, MAX_STRATEGY_SHARES);
uint withdrawAmount1 = r.Uint256(2, depositAmount);
uint withdrawAmount2 = r.Uint256(1, depositAmount - withdrawAmount1);
// 2. Register the operator, set the staker deposits, and delegate the 2 stakers to them
_registerOperatorWithBaseDetails(operator);
strategyManagerMock.addDeposit(staker, strategyMock, depositAmount);
_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, 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, 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"
);
}
uint operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock);
uint 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%.
uint sharesToDecrease = (depositAmount - withdrawAmount1 - withdrawAmount2) * 3 / 4;
uint sharesToBurn = sharesToDecrease + (delegationManager.getSlashableSharesInQueue(operator, strategyMock) * 3 / 4);
// 4. Burn shares
_setOperatorMagnitude(operator, strategyMock, newMagnitude);
_slashOperatorShares_expectEmit(
SlashOperatorSharesEmitStruct({
operator: operator,
strategy: strategyMock,
sharesToDecrease: sharesToDecrease,
sharesToBurn: sharesToBurn
})
);
// Assert OperatorSharesSlashed event was emitted with correct params
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesSlashed(operator, strategyMock, sharesToBurn);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: operator,
strategy: strategyMock,
prevMaxMagnitude: WAD,
newMaxMagnitude: newMagnitude
});
// 5. Assert expected values
uint queuedSlashableSharesAfter = delegationManager.getSlashableSharesInQueue(operator, strategyMock);
uint 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 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_slashOperatorShares_MultipleWithdrawalsMultipleSlashings(Randomness r) public rand(r) {
address operator = r.Address();
address staker = r.Address();
uint depositAmount = r.Uint256(3, MAX_STRATEGY_SHARES);
uint depositSharesToWithdraw1 = r.Uint256(1, depositAmount);
uint depositSharesToWithdraw2 = r.Uint256(1, depositAmount - depositSharesToWithdraw1);
uint64 newMagnitude = 50e16;
// 2. Register the operator, set the staker deposits, and delegate the 2 stakers to them
_registerOperatorWithBaseDetails(operator);
strategyManagerMock.addDeposit(staker, strategyMock, depositAmount);
_delegateToOperatorWhoAcceptsAllStakers(staker, operator);
// 3. Queue withdrawal for staker and slash operator for 50%
{
(QueuedWithdrawalParams[] memory queuedWithdrawalParams,,) = _setUpQueueWithdrawalsSingleStrat({
staker: staker,
strategy: strategyMock,
depositSharesToWithdraw: depositSharesToWithdraw1
});
// 3.1 queue a withdrawal for the staker
cheats.prank(staker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
uint operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock);
uint queuedSlashableSharesBefore = delegationManager.getSlashableSharesInQueue(operator, strategyMock);
uint sharesToDecrease = (depositAmount - depositSharesToWithdraw1) / 2;
uint sharesToBurn = sharesToDecrease + depositSharesToWithdraw1 / 2;
// 3.2 Burn shares
_setOperatorMagnitude(operator, strategyMock, newMagnitude);
_slashOperatorShares_expectEmit(
SlashOperatorSharesEmitStruct({
operator: operator,
strategy: strategyMock,
sharesToDecrease: sharesToDecrease,
sharesToBurn: sharesToBurn
})
);
// Assert OperatorSharesSlashed event was emitted with correct params
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesSlashed(operator, strategyMock, sharesToBurn);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
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 60% again
newMagnitude = 25e16;
{
(QueuedWithdrawalParams[] memory queuedWithdrawalParams,,) = _setUpQueueWithdrawalsSingleStrat({
staker: staker,
strategy: strategyMock,
depositSharesToWithdraw: depositSharesToWithdraw2
});
// 4.1 queue a withdrawal for the staker
cheats.prank(staker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
uint operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock);
uint queuedSlashableSharesBefore = delegationManager.getSlashableSharesInQueue(operator, strategyMock);
uint sharesToDecrease = operatorSharesBefore / 2;
uint sharesToBurn = sharesToDecrease + (depositSharesToWithdraw1 + depositSharesToWithdraw2) / 4;
// 4.2 Burn shares
_setOperatorMagnitude(operator, strategyMock, newMagnitude);
_slashOperatorShares_expectEmit(
SlashOperatorSharesEmitStruct({
operator: operator,
strategy: strategyMock,
sharesToDecrease: sharesToDecrease,
sharesToBurn: sharesToBurn
})
);
// Assert OperatorSharesSlashed event was emitted with correct params
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesSlashed(operator, strategyMock, sharesToBurn);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: operator,
strategy: strategyMock,
prevMaxMagnitude: newMagnitude * 2,
newMaxMagnitude: newMagnitude
});
// 4.3 Assert slashable shares and operator shares
assertEq(
queuedSlashableSharesBefore,
(depositSharesToWithdraw1 + depositSharesToWithdraw2) / 2,
"Slashable shares in queue before should be both queued withdrawal amounts halved"
);
assertEq(
delegationManager.getSlashableSharesInQueue(operator, strategyMock),
queuedSlashableSharesBefore / 2,
"Slashable shares in queue should be halved again 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_slashOperatorShares_Timings(Randomness r) public rand(r) {
// 1. Randomize operator and staker info
// Operator info
address operator = r.Address();
uint64 newMagnitude = 25e16;
// staker
address staker = r.Address();
uint depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES);
// 2. Register the operator, set the staker deposits, and delegate the staker to them
_registerOperatorWithBaseDetails(operator);
strategyManagerMock.addDeposit(staker, strategyMock, depositAmount);
_delegateToOperatorWhoAcceptsAllStakers(staker, operator);
// 3. Queue withdrawal for staker and roll blocks forward so that the withdrawal is completable
uint completableBlock;
{
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal,) =
_setUpQueueWithdrawalsSingleStrat({staker: 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.minWithdrawalDelayBlocks() + 1;
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();
}
uint operatorSharesBefore = delegationManager.operatorShares(operator, strategyMock);
// 4. Burn 0 shares when new magnitude is set
_setOperatorMagnitude(operator, strategyMock, newMagnitude);
_slashOperatorShares_expectEmit(
SlashOperatorSharesEmitStruct({operator: operator, strategy: strategyMock, sharesToDecrease: 0, sharesToBurn: 0})
);
// Assert OperatorSharesSlashed event was emitted with correct params
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesSlashed(operator, strategyMock, 0);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: operator,
strategy: strategyMock,
prevMaxMagnitude: WAD,
newMaxMagnitude: newMagnitude
});
// 5. Assert expected values
uint 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 Ensure that no burning takes place for the beaconChainETHStrategy when the operator is slashed
* and there are no slashable shares in the queue. Note: this will be implemented in a future release with
* consideration of the Pectra upgrade.
*/
function testFuzz_slashOperatorShares_BeaconChainStrategy(Randomness r) public rand(r) {
// 1. Randomize operator and staker info
// Operator info
address operator = r.Address();
uint64 newMagnitude = 25e16;
// First staker
address staker1 = r.Address();
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
// Second Staker, will queue withdraw shares
address staker2 = r.Address();
uint depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES);
uint withdrawAmount = r.Uint256(1, depositAmount);
// 2. Register the operator, set the staker deposits, and delegate the 2 stakers to them
_registerOperatorWithBaseDetails(operator);
eigenPodManagerMock.setPodOwnerShares(staker1, int(shares));
eigenPodManagerMock.setPodOwnerShares(staker2, int(depositAmount));
_delegateToOperatorWhoAcceptsAllStakers(staker1, operator);
_delegateToOperatorWhoAcceptsAllStakers(staker2, operator);
// 3. Queue withdrawal for staker2 so that the withdrawal is slashable
{
(QueuedWithdrawalParams[] memory queuedWithdrawalParams,,) = _setUpQueueWithdrawalsSingleStrat({
staker: staker2,
strategy: beaconChainETHStrategy,
depositSharesToWithdraw: withdrawAmount
});
cheats.prank(staker2);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
assertEq(
delegationManager.getSlashableSharesInQueue(operator, beaconChainETHStrategy),
withdrawAmount,
"there should be withdrawAmount slashable shares in queue"
);
}
uint operatorSharesBefore = delegationManager.operatorShares(operator, beaconChainETHStrategy);
uint queuedSlashableSharesBefore = delegationManager.getSlashableSharesInQueue(operator, beaconChainETHStrategy);
// 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%
uint sharesToDecrease = (shares + depositAmount - withdrawAmount) * 3 / 4;
uint sharesToBurn = sharesToDecrease + (delegationManager.getSlashableSharesInQueue(operator, beaconChainETHStrategy) * 3 / 4);
// 4. Burn shares
_setOperatorMagnitude(operator, beaconChainETHStrategy, newMagnitude);
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesDecreased(operator, address(0), beaconChainETHStrategy, sharesToDecrease);
// Assert OperatorSharesSlashed event was emitted with correct params
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesSlashed(operator, beaconChainETHStrategy, sharesToBurn);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: operator,
strategy: beaconChainETHStrategy,
prevMaxMagnitude: WAD,
newMaxMagnitude: newMagnitude
});
// 5. Assert expected values
uint queuedSlashableSharesAfter = delegationManager.getSlashableSharesInQueue(operator, beaconChainETHStrategy);
uint operatorSharesAfter = delegationManager.operatorShares(operator, beaconChainETHStrategy);
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 sharesToDecrease");
}
/**
* @notice This test demonstrates that the rate that withdrawable shares decrease from slashing is at LEAST
* greater than or equal to the rate that the operator shares decrease from slashing.
* We want this property otherwise undelegating/queue withdrawing all shares as a staker could lead to a underflow revert.
* Note: If the SlashingLib.calcSlashedAmount function were to round down (overslash) then this test would fail.
*/
function test_slashOperatorShares_slashedRepeatedly() public {
uint64 initialMagnitude = 90_009;
uint shares = 40_000_000_004_182_209_037_560_531_097_078_597_505;
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, initialMagnitude);
// Set the staker deposits in the strategies
strategyManagerMock.addDeposit(defaultStaker, strategyMock, shares);
// delegate from the `defaultStaker` to the operator
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Set operator magnitude
uint64 newOperatorMagnitude = initialMagnitude;
for (uint i = 0; i < 100; ++i) {
uint64 slashMagnitude = 100;
newOperatorMagnitude -= slashMagnitude;
_setOperatorMagnitude(defaultOperator, strategyMock, newOperatorMagnitude);
// Assert OperatorSharesSlashed event was emitted with correct params
cheats.expectEmit(true, true, true, true, address(delegationManager));
emit OperatorSharesSlashed(defaultOperator, strategyMock, 44_440_000_449_046_438_731_194_137_360_795_695);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(
defaultOperator, strategyMock, newOperatorMagnitude + slashMagnitude, newOperatorMagnitude
);
uint operatorSharesAfterSlash = delegationManager.operatorShares(defaultOperator, strategyMock);
(uint[] memory withdrawableShares, uint[] memory depositShares) =
delegationManager.getWithdrawableShares(defaultStaker, strategyMock.toArray());
assertEq(depositShares[0], shares, "staker deposit shares not reset correctly");
assertLe(withdrawableShares[0], operatorSharesAfterSlash, "withdrawable should always be <= operatorShares even after rounding");
}
}
/**
* @notice This unit test will slash a staker's beaconChainETH strategy shares from both on EigenLayer
* and also on the beaconChain. This test ensures that the order of slashing does not matter and nets
* the same withdrawableShares for the staker whether slashing occurred on the beaconChain, or on EigenLayer first.
*/
function testFuzz_beaconSlashAndAVSSlash(Randomness r) public rand(r) {
uint64 initialMagnitude = r.Uint64(2, WAD);
uint64 newMaxMagnitude = r.Uint64(1, initialMagnitude);
// note: beaconShares only goes negative when performing withdrawal -- and this will change post-migration
// so it's ok to make this assumption of positive shares
int beaconShares = int(r.Uint256(2, MAX_ETH_SUPPLY));
uint sharesDecrease = r.Uint256(1, uint(beaconShares) - 1);
////////////////////////////
// 0. setup operator and staker with Beacon Chain stake
////////////////////////////
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, beaconChainETHStrategy, initialMagnitude);
eigenPodManagerMock.setPodOwnerShares(defaultStaker, beaconShares);
// delegate staker to operator with expected events emitted
_delegateTo_expectEmit(
DelegateToEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
strategies: beaconChainETHStrategy.toArray(),
depositShares: uint(beaconShares).toArrayU256(),
depositScalingFactors: uint(WAD).divWad(initialMagnitude).toArrayU256()
})
);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_assertDeposit({
staker: defaultStaker,
operator: defaultOperator,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: uint(beaconShares)
});
uint[] memory withdrawableShares;
uint64 newBeaconSlashingFactor;
// withdrawable shares after both slashing, this will be checked with the other scenario when
// slashing in reverse order
uint sharesAfterAllSlashing;
////////////////////////////
// 1. do beacon chain slash then AVS slash
////////////////////////////
{
// Slash beaconChain first
{
(withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, beaconChainETHStrategy.toArray());
uint beaconSharesBeforeSlash = withdrawableShares[0];
uint64 prevBeaconChainSlashingFactor;
(prevBeaconChainSlashingFactor, newBeaconSlashingFactor) =
_decreaseBeaconChainShares(defaultStaker, beaconShares, sharesDecrease);
uint expectedWithdrawableShares = _calcWithdrawableShares({
depositShares: uint(beaconShares),
depositScalingFactor: uint(WAD).divWad(initialMagnitude),
slashingFactor: initialMagnitude.mulWad(newBeaconSlashingFactor)
});
_assertSharesAfterBeaconSlash({
staker: defaultStaker,
withdrawableSharesBefore: beaconSharesBeforeSlash,
expectedWithdrawableShares: expectedWithdrawableShares,
prevBeaconSlashingFactor: prevBeaconChainSlashingFactor
});
}
// Slash on EigenLayer second
{
(withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, beaconChainETHStrategy.toArray());
uint beaconSharesBeforeSlash = withdrawableShares[0];
// do a slash via an AVS
_setOperatorMagnitude(defaultOperator, beaconChainETHStrategy, newMaxMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, beaconChainETHStrategy, initialMagnitude, newMaxMagnitude);
// save the outcome
(withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, beaconChainETHStrategy.toArray());
uint beaconSharesAfterSecondSlash = withdrawableShares[0];
uint expectedWithdrawable = _calcWithdrawableShares(
uint(beaconShares),
uint(WAD).divWad(initialMagnitude),
_getSlashingFactor(defaultStaker, beaconChainETHStrategy, newMaxMagnitude)
);
_assertSharesAfterSlash({
staker: defaultStaker,
strategy: beaconChainETHStrategy,
withdrawableSharesBefore: beaconSharesBeforeSlash,
expectedWithdrawableShares: expectedWithdrawable,
prevMaxMagnitude: initialMagnitude,
currMaxMagnitude: newMaxMagnitude
});
sharesAfterAllSlashing = beaconSharesAfterSecondSlash;
}
}
////////////////////////////
// 2. do AVS slash then beacon chain slash
////////////////////////////
// initialize new staker and operator with same initial conditions
delegationManager.undelegate(defaultStaker);
_registerOperatorWithBaseDetails(defaultOperator2);
_setOperatorMagnitude(defaultOperator2, beaconChainETHStrategy, initialMagnitude);
eigenPodManagerMock.setPodOwnerShares(defaultStaker2, beaconShares);
eigenPodManagerMock.setBeaconChainSlashingFactor(defaultStaker2, WAD);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker2, defaultOperator2);
_assertDeposit({
staker: defaultStaker2,
operator: defaultOperator2,
strategy: beaconChainETHStrategy,
operatorSharesBefore: 0,
withdrawableSharesBefore: 0,
depositSharesBefore: 0,
prevDsf: WAD,
depositAmount: uint(beaconShares)
});
{
// Slash on EigenLayer first
{
(withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker2, beaconChainETHStrategy.toArray());
uint beaconSharesBeforeSlash = withdrawableShares[0];
_setOperatorMagnitude(defaultOperator2, beaconChainETHStrategy, newMaxMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator2, beaconChainETHStrategy, initialMagnitude, newMaxMagnitude);
uint expectedWithdrawable = _calcWithdrawableShares(
uint(beaconShares),
uint(WAD).divWad(initialMagnitude),
_getSlashingFactor(defaultStaker2, beaconChainETHStrategy, newMaxMagnitude)
);
_assertSharesAfterSlash({
staker: defaultStaker2,
strategy: beaconChainETHStrategy,
withdrawableSharesBefore: beaconSharesBeforeSlash,
expectedWithdrawableShares: expectedWithdrawable,
prevMaxMagnitude: initialMagnitude,
currMaxMagnitude: newMaxMagnitude
});
}
// Slash beaconChain second
{
(withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker2, beaconChainETHStrategy.toArray());
uint beaconSharesBeforeSlash = withdrawableShares[0];
uint64 prevBeaconChainSlashingFactor;
(prevBeaconChainSlashingFactor, newBeaconSlashingFactor) =
_decreaseBeaconChainShares(defaultStaker2, beaconShares, sharesDecrease);
uint expectedWithdrawableShares = _calcWithdrawableShares({
depositShares: uint(beaconShares),
depositScalingFactor: uint(WAD).divWad(initialMagnitude),
slashingFactor: newMaxMagnitude.mulWad(newBeaconSlashingFactor)
});
_assertSharesAfterBeaconSlash({
staker: defaultStaker2,
withdrawableSharesBefore: beaconSharesBeforeSlash,
expectedWithdrawableShares: expectedWithdrawableShares,
prevBeaconSlashingFactor: prevBeaconChainSlashingFactor
});
}
}
////////////////////////////
// 3. Confirm withdrawable shares are the same regardless of order of operations in Test 1 or Test 2
////////////////////////////
(withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker2, beaconChainETHStrategy.toArray());
assertEq(withdrawableShares[0], sharesAfterAllSlashing, "shares after all slashing should be the same");
}
}
/// @notice Fuzzed Unit tests to compare totalWitdrawable shares for an operator vs their actual operatorShares.
/// Requires the WRITE_CSV_TESTS env variable to be set to true to output to a test file
contract DelegationManagerUnitTests_SharesUnderflowChecks is DelegationManagerUnitTests {
using ArrayLib for *;
using SlashingLib for *;
/**
* @notice Fuzzed tests
* Single staker with fuzzed starting shares and magnitude.
* Slash 100 magnitude and deposit 100 shares for 100 iterations.
*/
/// forge-config: default.fuzz.runs = 50
function testFuzz_slashDepositRepeatedly(Randomness r) public rand(r) {
uint64 initMagnitude = r.Uint64(10_000, WAD);
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
cheats.assume(initMagnitude % 2 != 0);
cheats.assume(shares % 2 != 0);
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, initMagnitude);
// Set the staker deposits in the strategies
IStrategy[] memory strategies = strategyMock.toArray();
{
uint[] memory sharesToSet = new uint[](1);
sharesToSet[0] = shares;
strategyManagerMock.setDeposits(defaultStaker, strategies, sharesToSet);
}
// delegate from the `defaultStaker` to the operator
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Slash and deposit more for each iteration
uint64 currMagnitude = initMagnitude;
{
uint newDepositShares = shares;
for (uint i = 0; i < 100; ++i) {
// 1. slash operator for 100 magnitude
uint64 slashMagnitude = 100;
currMagnitude -= slashMagnitude;
_setOperatorMagnitude(defaultOperator, strategyMock, currMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: defaultOperator,
strategy: strategyMock,
prevMaxMagnitude: currMagnitude + slashMagnitude,
newMaxMagnitude: currMagnitude
});
// 2. deposit again
uint sharesIncrease = 1000;
cheats.prank(address(strategyManagerMock));
delegationManager.increaseDelegatedShares(defaultStaker, strategyMock, newDepositShares, sharesIncrease);
newDepositShares += sharesIncrease;
uint[] memory newDepositSharesArray = new uint[](1);
newDepositSharesArray[0] = newDepositShares;
strategyManagerMock.setDeposits(defaultStaker, strategies, newDepositSharesArray);
}
}
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, strategies);
assertLe(
withdrawableShares[0],
delegationManager.operatorShares(defaultOperator, strategyMock),
"withdrawableShares should be less than or equal to operatorShares"
);
if (cheats.envOr("WRITE_CSV_TESTS", false)) {
cheats.writeLine(
"./test.csv",
string(
abi.encodePacked(
cheats.toString(initMagnitude),
", ",
cheats.toString(shares),
", ",
cheats.toString(delegationManager.operatorShares(defaultOperator, strategyMock)),
", ",
cheats.toString(withdrawableShares[0]),
", ",
cheats.toString(
stdMath.delta(delegationManager.operatorShares(defaultOperator, strategyMock), withdrawableShares[0])
)
)
)
);
}
}
/**
* @notice Fuzzed tests
* Single staker with fuzzed starting shares and magnitude.
* Slash 100 magnitude and fuzz deposit amount for 100 iterations.
*/
/// forge-config: default.fuzz.runs = 50
function testFuzz_slashDepositRepeatedly_randDeposits(Randomness r) public rand(r) {
uint64 initMagnitude = r.Uint64(10_000, WAD);
uint depositAmount = r.Uint256(1, 1e34);
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES / 1e4);
cheats.assume(initMagnitude % 2 != 0);
cheats.assume(shares % 2 != 0);
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, initMagnitude);
// Set the staker deposits in the strategies
IStrategy[] memory strategies = strategyMock.toArray();
{
uint[] memory sharesToSet = new uint[](1);
sharesToSet[0] = shares;
strategyManagerMock.setDeposits(defaultStaker, strategies, sharesToSet);
}
// delegate from the `defaultStaker` to the operator
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Slash and deposit more for each iteration
uint64 currMagnitude = initMagnitude;
{
uint newDepositShares = shares;
for (uint i = 0; i < 100; ++i) {
// 1. slash operator for 100 magnitude
uint64 slashMagnitude = 100;
currMagnitude -= slashMagnitude;
_setOperatorMagnitude(defaultOperator, strategyMock, currMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: defaultOperator,
strategy: strategyMock,
prevMaxMagnitude: currMagnitude + slashMagnitude,
newMaxMagnitude: currMagnitude
});
// 2. deposit again
cheats.prank(address(strategyManagerMock));
delegationManager.increaseDelegatedShares(defaultStaker, strategyMock, newDepositShares, depositAmount);
newDepositShares += depositAmount;
uint[] memory newDepositSharesArray = new uint[](1);
newDepositSharesArray[0] = newDepositShares;
strategyManagerMock.setDeposits(defaultStaker, strategies, newDepositSharesArray);
}
}
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, strategies);
assertLe(
withdrawableShares[0],
delegationManager.operatorShares(defaultOperator, strategyMock),
"withdrawableShares should be less than or equal to operatorShares"
);
if (cheats.envOr("WRITE_CSV_TESTS", false)) {
cheats.writeLine(
"./test2.csv",
string(
abi.encodePacked(
cheats.toString(initMagnitude),
", ",
cheats.toString(shares),
", ",
cheats.toString(depositAmount),
", ",
cheats.toString(delegationManager.operatorShares(defaultOperator, strategyMock)),
", ",
cheats.toString(withdrawableShares[0]),
", ",
cheats.toString(
stdMath.delta(delegationManager.operatorShares(defaultOperator, strategyMock), withdrawableShares[0])
)
)
)
);
}
}
/**
* @notice Fuzzed tests
* For 500 stakers, deposit `shares` amount and delegate to the operator. After each staker delegates,
* slash 100 magnitude.
*/
/// forge-config: default.fuzz.runs = 50
function testFuzz_depositMultipleStakers_slash_repeatedly(Randomness r) public rand(r) {
uint64 initMagnitude = r.Uint64(50_000, WAD);
uint shares = r.Uint256(MAX_STRATEGY_SHARES / 1e7, MAX_STRATEGY_SHARES / 1e4);
cheats.assume(initMagnitude % 2 != 0);
cheats.assume(shares % 2 != 0);
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, initMagnitude);
// Set the staker deposits in the strategies
IStrategy[] memory strategies = strategyMock.toArray();
uint[] memory sharesToSet = new uint[](1);
sharesToSet[0] = shares;
uint numStakers = 500;
address[] memory stakers = new address[](numStakers);
// Slash and deposit more for each iteration
uint64 currMagnitude = initMagnitude;
{
for (uint i = 0; i < numStakers; ++i) {
// 1. deposit and delegate new staker
stakers[i] = random().Address();
strategyManagerMock.setDeposits(stakers[i], strategies, sharesToSet);
_delegateToOperatorWhoAcceptsAllStakers(stakers[i], defaultOperator);
// 2. slash operator for 100 magnitude
uint64 slashMagnitude = 100;
currMagnitude -= slashMagnitude;
_setOperatorMagnitude(defaultOperator, strategyMock, currMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: defaultOperator,
strategy: strategyMock,
prevMaxMagnitude: currMagnitude + slashMagnitude,
newMaxMagnitude: currMagnitude
});
}
}
uint operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock);
uint totalWithdrawableShares = 0;
for (uint i = 0; i < numStakers; ++i) {
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(stakers[i], strategies);
totalWithdrawableShares += withdrawableShares[0];
}
assertLe(totalWithdrawableShares, operatorSharesAfter, "withdrawableShares should be less than or equal to operatorShares");
if (cheats.envOr("WRITE_CSV_TESTS", false)) {
cheats.writeLine(
"./test3.csv",
string(
abi.encodePacked(
cheats.toString(initMagnitude),
", ", // initial magnitude
cheats.toString(shares),
", ", // amount each staker deposits
cheats.toString(operatorSharesAfter),
", ", // operator shares after all slashing and deposits
cheats.toString(totalWithdrawableShares),
", ", // total withdrawable shares from all stakers
cheats.toString(stdMath.delta(operatorSharesAfter, totalWithdrawableShares)) // delta difference between opShares and total withdrawable
)
)
);
}
}
/**
* @notice Fuzzed tests
* For 500 stakers, deposit `shares` amount and delegate to the operator. After each staker delegates,
* slash 1000 magnitude. Initial magnitude is very small so this will slash larger proportions.
*/
/// forge-config: default.fuzz.runs = 50
function testFuzz_depositMultipleStakers_slashLargeMagnitudes(Randomness r) public rand(r) {
uint64 initMagnitude = r.Uint64(50_000, WAD);
uint shares = r.Uint256(MAX_STRATEGY_SHARES / 1e7, MAX_STRATEGY_SHARES / 1e4);
cheats.assume(initMagnitude % 2 != 0);
cheats.assume(shares % 2 != 0);
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, initMagnitude);
// Set the staker deposits in the strategies
IStrategy[] memory strategies = strategyMock.toArray();
uint[] memory sharesToSet = new uint[](1);
sharesToSet[0] = shares;
uint numStakers = 500;
address[] memory stakers = new address[](numStakers);
// Slash and deposit more for each iteration
uint64 currMagnitude = initMagnitude;
{
for (uint i = 0; i < numStakers; ++i) {
// 1. deposit and delegate new staker
stakers[i] = random().Address();
strategyManagerMock.setDeposits(stakers[i], strategies, sharesToSet);
_delegateToOperatorWhoAcceptsAllStakers(stakers[i], defaultOperator);
// 2. slash operator for 100 magnitude
uint64 slashMagnitude = 100;
currMagnitude -= slashMagnitude;
_setOperatorMagnitude(defaultOperator, strategyMock, currMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: defaultOperator,
strategy: strategyMock,
prevMaxMagnitude: currMagnitude + slashMagnitude,
newMaxMagnitude: currMagnitude
});
}
}
uint operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock);
uint totalWithdrawableShares = 0;
for (uint i = 0; i < numStakers; ++i) {
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(stakers[i], strategies);
totalWithdrawableShares += withdrawableShares[0];
}
assertLe(totalWithdrawableShares, operatorSharesAfter, "withdrawableShares should be less than or equal to operatorShares");
if (cheats.envOr("WRITE_CSV_TESTS", false)) {
cheats.writeLine(
"./test4.csv",
string(
abi.encodePacked(
cheats.toString(initMagnitude),
", ", // initial magnitude
cheats.toString(shares),
", ", // amount each staker deposits
cheats.toString(operatorSharesAfter),
", ", // operator shares after all slashing and deposits
cheats.toString(totalWithdrawableShares),
", ", // total withdrawable shares from all stakers
cheats.toString(stdMath.delta(operatorSharesAfter, totalWithdrawableShares)) // delta difference between opShares and total withdrawable
)
)
);
}
}
/**
* @notice Same as above `testFuzz_depositMultipleStakers_slashLargeMagnitudes` test but with slashing
* 1 magnitude instead of 100.
*/
/// forge-config: default.fuzz.runs = 50
function testFuzz_depositMultipleStakers_slashSmallMagnitudes(Randomness r) public rand(r) {
uint64 initMagnitude = r.Uint64(1000, WAD);
uint shares = r.Uint256(MAX_STRATEGY_SHARES / 1e7, MAX_STRATEGY_SHARES / 1e4);
cheats.assume(initMagnitude % 2 != 0);
cheats.assume(shares % 2 != 0);
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, initMagnitude);
// Set the staker deposits in the strategies
IStrategy[] memory strategies = strategyMock.toArray();
uint[] memory sharesToSet = new uint[](1);
sharesToSet[0] = shares;
uint numStakers = 500;
address[] memory stakers = new address[](numStakers);
// Slash and deposit more for each iteration
uint64 currMagnitude = initMagnitude;
{
for (uint i = 0; i < numStakers; ++i) {
// 1. deposit and delegate new staker
stakers[i] = random().Address();
strategyManagerMock.setDeposits(stakers[i], strategies, sharesToSet);
_delegateToOperatorWhoAcceptsAllStakers(stakers[i], defaultOperator);
// 2. slash operator for 100 magnitude
uint64 slashMagnitude = 1;
currMagnitude -= slashMagnitude;
_setOperatorMagnitude(defaultOperator, strategyMock, currMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: defaultOperator,
strategy: strategyMock,
prevMaxMagnitude: currMagnitude + slashMagnitude,
newMaxMagnitude: currMagnitude
});
}
}
uint operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock);
uint totalWithdrawableShares = 0;
for (uint i = 0; i < numStakers; ++i) {
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(stakers[i], strategies);
totalWithdrawableShares += withdrawableShares[0];
}
assertLe(totalWithdrawableShares, operatorSharesAfter, "withdrawableShares should be less than or equal to operatorShares");
if (cheats.envOr("WRITE_CSV_TESTS", false)) {
cheats.writeLine(
"./test5.csv",
string(
abi.encodePacked(
cheats.toString(initMagnitude),
", ", // initial magnitude
cheats.toString(shares),
", ", // amount each staker deposits
cheats.toString(operatorSharesAfter),
", ", // operator shares after all slashing and deposits
cheats.toString(totalWithdrawableShares),
", ", // total withdrawable shares from all stakers
cheats.toString(stdMath.delta(operatorSharesAfter, totalWithdrawableShares)) // delta difference between opShares and total withdrawable
)
)
);
}
}
/**
* @notice Setup 500 delegated stakers who each deposit `shares` amount.
* Then slash 1 magnitude 500 times and then compare amount of shares that can be withdrawn vs operatorShares
*/
/// forge-config: default.fuzz.runs = 50
function testFuzz_depositMultipleStakersOnce_slashSmallMagnitudes(Randomness r) public rand(r) {
uint64 initMagnitude = r.Uint64(1000, WAD);
uint shares = r.Uint256(MAX_STRATEGY_SHARES / 1e7, MAX_STRATEGY_SHARES / 1e4);
cheats.assume(initMagnitude % 2 != 0);
cheats.assume(shares % 2 != 0);
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, initMagnitude);
// Set the staker deposits in the strategies
IStrategy[] memory strategies = strategyMock.toArray();
uint[] memory sharesToSet = new uint[](1);
sharesToSet[0] = shares;
uint numStakers = 500;
address[] memory stakers = new address[](numStakers);
// deposit all stakers one time
for (uint i = 0; i < numStakers; ++i) {
// 1. deposit and delegate new staker
stakers[i] = random().Address();
strategyManagerMock.setDeposits(stakers[i], strategies, sharesToSet);
_delegateToOperatorWhoAcceptsAllStakers(stakers[i], defaultOperator);
}
// Slash and deposit more for each iteration
uint64 currMagnitude = initMagnitude;
{
for (uint i = 0; i < numStakers; ++i) {
// 2. slash operator for 100 magnitude
uint64 slashMagnitude = 1;
currMagnitude -= slashMagnitude;
_setOperatorMagnitude(defaultOperator, strategyMock, currMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares({
operator: defaultOperator,
strategy: strategyMock,
prevMaxMagnitude: currMagnitude + slashMagnitude,
newMaxMagnitude: currMagnitude
});
}
}
uint operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock);
uint totalWithdrawableShares = 0;
for (uint i = 0; i < numStakers; ++i) {
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(stakers[i], strategies);
totalWithdrawableShares += withdrawableShares[0];
}
assertLe(totalWithdrawableShares, operatorSharesAfter, "withdrawableShares should be less than or equal to operatorShares");
if (cheats.envOr("WRITE_CSV_TESTS", false)) {
cheats.writeLine(
"./test6.csv",
string(
abi.encodePacked(
cheats.toString(initMagnitude),
", ", // initial magnitude
cheats.toString(shares),
", ", // amount each staker deposits
cheats.toString(operatorSharesAfter),
", ", // operator shares after all slashing and deposits
cheats.toString(totalWithdrawableShares),
", ", // total withdrawable shares from all stakers
cheats.toString(stdMath.delta(operatorSharesAfter, totalWithdrawableShares)) // delta difference between opShares and total withdrawable
)
)
);
}
}
}
contract DelegationManagerUnitTests_Rounding is DelegationManagerUnitTests {}
/**
* @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),
* checks that staker is still delegated and operator still registered, staker undelegates, checks staker not delegated and operator
* is still registered
* 2. RegisterOperator, Deposit, Delegate, Queue, Complete
* 3. RegisterOperator, Mock Slash(set maxMagnitudes), Deposit/Delegate, Queue, Complete
* 4. RegisterOperator, Deposit/Delegate, Mock Slash(set maxMagnitudes), Queue, Complete
* 5. RegisterOperator, Mock Slash(set maxMagnitudes), Deposit/Delegate, Queue, Mock Slash(set maxMagnitudes), Complete
* 7. RegisterOperator, Deposit/Delegate, Mock Slash 100% (set maxMagnitudes), Undelegate, Complete non 100% slashed strategies
* 8. RegisterOperator, Deposit/Delegate, Undelegate, Re delegate to another operator, Mock Slash 100% (set maxMagnitudes), Complete as shares
* (withdrawals should have been slashed even though delegated to a new operator)
* 9. Invariant check getWithdrawableShares = sum(deposits), Multiple deposits with operator who has never been slashed
* 10. Invariant check getWithdrawableShares = sum(deposits), Multiple deposits with operator who HAS been been slashed
*/
contract DelegationManagerUnitTests_Lifecycle is DelegationManagerUnitTests {
using ArrayLib for *;
// 2. RegisterOperator, Deposit, Delegate, Queue, Complete
function test_register_operator_deposit_delegate_queue_complete(Randomness r) public rand(r) {
address operator = r.Address();
address staker = r.Address();
IStrategy[] memory strategies = strategyMock.toArray();
uint[] memory depositShares = uint(100 ether).toArrayU256();
// 1) Register operator.
_registerOperatorWithBaseDetails(operator);
// 2) Mock deposit into SM.
strategyManagerMock.setDeposits(staker, strategies, depositShares);
// 3) Staker delegates to operator.
_delegateToOperatorWhoAcceptsAllStakers(staker, operator);
// 3) Staker queues withdrawals.
QueuedWithdrawalParams[] memory queuedWithdrawalParams = new QueuedWithdrawalParams[](1);
queuedWithdrawalParams[0] =
QueuedWithdrawalParams({strategies: strategies, depositShares: depositShares, __deprecated_withdrawer: address(0)});
cheats.prank(staker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
// 4) Complete queued withdrawals.
Withdrawal memory withdrawal = Withdrawal({
staker: staker,
delegatedTo: operator,
withdrawer: staker,
nonce: 0,
startBlock: uint32(block.number),
strategies: strategies,
scaledShares: depositShares
});
bytes32 withdrawalRoot = keccak256(abi.encode(withdrawal));
assertTrue(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRoot should be pending");
cheats.roll(block.number + delegationManager.minWithdrawalDelayBlocks() + 1);
cheats.prank(staker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokenMock.toArray(), false);
assertFalse(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRoot should not be pending");
// Checks
assertEq(delegationManager.cumulativeWithdrawalsQueued(staker), 1, "staker nonce should have incremented");
assertEq(delegationManager.operatorShares(operator, strategies[0]), 100 ether, "operator shares should be 0 after withdrawal");
}
/**
* @notice While delegated to an operator who becomes 100% slashed. When the staker undelegates and queues a withdrawal
* for all their shares which are now 0, the withdrawal should be completed with 0 shares even if they delegate to a new operator
* who has not been slashed.
* Note: This specifically tests that the completeQueuedWithdrawal is looking up the correct maxMagnitude for the operator
*/
function testFuzz_undelegate_slashOperator100Percent_delegate_complete(Randomness r) public rand(r) {
uint shares = r.Uint256(1, MAX_STRATEGY_SHARES);
address newOperator = r.Address();
IStrategy[] memory strategyArray = r.StrategyArray(1);
IStrategy strategy = strategyArray[0];
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
// Set the staker deposits in the strategies
strategyManagerMock.addDeposit(defaultStaker, strategy, shares);
// delegate from the `defaultStaker` to the operator
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Set operator magnitude
uint64 operatorMagnitude = 0;
uint operatorSharesAfterSlash;
{
_setOperatorMagnitude(defaultOperator, strategy, operatorMagnitude);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategy, WAD, 0);
operatorSharesAfterSlash = delegationManager.operatorShares(defaultOperator, strategy);
assertEq(operatorSharesAfterSlash, 0, "operator shares not fully slashed");
}
(, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategy, depositSharesToWithdraw: shares});
uint depositScalingFactor = delegationManager.depositScalingFactor(defaultStaker, strategy);
assertEq(depositScalingFactor, WAD, "bad test setup");
// Get withdrawable and deposit shares
{
(uint[] memory withdrawableSharesBefore, uint[] memory depositSharesBefore) =
delegationManager.getWithdrawableShares(defaultStaker, strategyArray);
assertEq(withdrawableSharesBefore[0], 0, "withdrawable shares should be 0 after being slashed fully");
assertEq(depositSharesBefore[0], shares, "deposit shares should be unchanged after being slashed fully");
}
// Undelegate the staker
_undelegate_expectEmit_singleStrat(
UndelegateEmitStruct({
staker: defaultStaker,
operator: defaultOperator,
strategy: strategy,
depositSharesQueued: shares,
operatorSharesDecreased: 0,
withdrawal: withdrawal,
withdrawalRoot: withdrawalRoot,
depositScalingFactor: WAD,
forceUndelegated: false
})
);
cheats.prank(defaultStaker);
delegationManager.undelegate(defaultStaker);
// Checks - delegation status
assertEq(delegationManager.delegatedTo(defaultStaker), address(0), "undelegated staker should be delegated to zero address");
assertFalse(delegationManager.isDelegated(defaultStaker), "staker not undelegated");
// Checks - operator & staker shares
assertEq(delegationManager.operatorShares(defaultOperator, strategy), 0, "operator shares not decreased correctly");
(uint[] memory stakerWithdrawableShares, uint[] memory depositShares) =
delegationManager.getWithdrawableShares(defaultStaker, strategyArray);
assertEq(stakerWithdrawableShares[0], 0, "staker withdrawable shares not calculated correctly");
assertEq(depositShares[0], 0, "staker deposit shares not reset correctly");
// delegate to the `newOperator` who has never been slashed
// Ensure that completing withdrawal now still results in 0 shares
_registerOperatorWithBaseDetails(newOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, newOperator);
(stakerWithdrawableShares, depositShares) = delegationManager.getWithdrawableShares(defaultStaker, strategyArray);
assertEq(stakerWithdrawableShares[0], 0, "staker withdrawable shares not calculated correctly");
assertEq(depositShares[0], 0, "staker deposit shares not reset correctly");
cheats.roll(withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks() + 1);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokenMock.toArray(), false);
(stakerWithdrawableShares, depositShares) = delegationManager.getWithdrawableShares(defaultStaker, strategyArray);
assertEq(stakerWithdrawableShares[0], 0, "staker withdrawable shares not calculated correctly");
assertEq(depositShares[0], 0, "staker deposit shares not reset correctly");
assertEq(delegationManager.operatorShares(newOperator, strategy), 0, "new operator shares should be unchanged");
}
}
contract DelegationManagerUnitTests_ConvertToDepositShares is DelegationManagerUnitTests {
using ArrayLib for *;
function test_convertToDepositShares_noSlashing() public {
uint shares = 100 ether;
// Set the staker deposits in the strategies
strategyManagerMock.addDeposit(defaultStaker, strategyMock, shares);
_checkDepositSharesConvertCorrectly(strategyMock.toArray(), shares.toArrayU256());
}
function test_convertToDepositShares_withSlashing() public {
IStrategy[] memory strategies = strategyMock.toArray();
uint[] memory shares = uint(100 ether).toArrayU256();
// Set the staker deposits in the strategies
strategyManagerMock.addDeposit(defaultStaker, strategies[0], shares[0]);
// register *this contract* as an operator
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, WAD / 3);
_checkDepositSharesConvertCorrectly(strategies, shares);
// queue and complete a withdrawal for half the deposit shares
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, strategies);
_queueAndCompleteWithdrawalForSingleStrategy(strategies[0], shares[0] / 2);
// queued a withdrawal for half the deposit shares, and added back as withdrawable shares
shares[0] = shares[0] / 2 + withdrawableShares[0] / 2;
_checkDepositSharesConvertCorrectly(strategies, shares);
}
function test_convertToDepositShares_beaconChainETH() public {
IStrategy[] memory strategies = beaconChainETHStrategy.toArray();
uint[] memory shares = uint(100 ether).toArrayU256();
// Set the staker deposits in the strategies
eigenPodManagerMock.setPodOwnerShares(defaultStaker, int(shares[0]));
uint[] memory depositShares = delegationManager.convertToDepositShares(defaultStaker, strategies, shares);
assertEq(depositShares[0], shares[0], "deposit shares not converted correctly");
// delegate to an operator and slash
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_setOperatorMagnitude(defaultOperator, beaconChainETHStrategy, WAD / 3);
_checkDepositSharesConvertCorrectly(strategies, shares);
// slash on beacon chain by 1/3
_decreaseBeaconChainShares(defaultStaker, int(shares[0]), shares[0] / 3);
_checkDepositSharesConvertCorrectly(strategies, shares);
// queue and complete a withdrawal for half the deposit shares
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, strategies);
_queueAndCompleteWithdrawalForSingleStrategy(strategies[0], shares[0] / 2);
// queued a withdrawal for half the deposit shares, and added back as withdrawable shares
shares[0] = shares[0] / 2 + withdrawableShares[0] / 2;
_checkDepositSharesConvertCorrectly(strategies, shares);
}
function _checkDepositSharesConvertCorrectly(IStrategy[] memory strategies, uint[] memory expectedDepositShares) public view {
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(defaultStaker, strategies);
// get the deposit shares
uint[] memory depositShares = delegationManager.convertToDepositShares(defaultStaker, strategies, withdrawableShares);
for (uint i = 0; i < strategies.length; i++) {
assertApproxEqRel(expectedDepositShares[i], depositShares[i], APPROX_REL_DIFF, "deposit shares not converted correctly");
// make sure that the deposit shares are less than or equal to the shares,
// so this value is sane to input into `completeQueuedWithdrawals`
assertLe(depositShares[i], expectedDepositShares[i], "deposit shares should be less than or equal to expected deposit shares");
}
// get the deposit shares
uint[] memory oneThirdWithdrawableShares = new uint[](strategies.length);
for (uint i = 0; i < strategies.length; i++) {
oneThirdWithdrawableShares[i] = withdrawableShares[i] / 3;
}
uint[] memory oneThirdDepositShares =
delegationManager.convertToDepositShares(defaultStaker, strategies, oneThirdWithdrawableShares);
for (uint i = 0; i < strategies.length; i++) {
assertApproxEqRel(
expectedDepositShares[i] / 3, oneThirdDepositShares[i], APPROX_REL_DIFF, "deposit shares not converted correctly"
);
}
}
function _queueAndCompleteWithdrawalForSingleStrategy(IStrategy strategy, uint shares) public {
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal,) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategy, depositSharesToWithdraw: shares});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
cheats.roll(block.number + delegationManager.minWithdrawalDelayBlocks() + 1);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokenMock.toArray(), false);
}
}
contract DelegationManagerUnitTests_getQueuedWithdrawals is DelegationManagerUnitTests {
using ArrayLib for *;
using SlashingLib for *;
function _withdrawalRoot(Withdrawal memory withdrawal) internal pure returns (bytes32) {
return keccak256(abi.encode(withdrawal));
}
function test_getQueuedWithdrawals_Correctness(Randomness r) public rand(r) {
uint numStrategies = r.Uint256(2, 8);
uint[] memory depositShares = r.Uint256Array({len: numStrategies, min: 2, max: 100 ether});
IStrategy[] memory strategies = _deployAndDepositIntoStrategies(defaultStaker, depositShares, false);
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
for (uint i; i < numStrategies; ++i) {
uint newStakerShares = depositShares[i] / 2;
_setOperatorMagnitude(defaultOperator, strategies[i], 0.5 ether);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategies[i], WAD, 0.5 ether);
uint afterSlash = delegationManager.operatorShares(defaultOperator, strategies[i]);
assertApproxEqAbs(afterSlash, newStakerShares, 1, "bad operator shares after slash");
}
// Queue withdrawals.
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawals({staker: defaultStaker, strategies: strategies, depositWithdrawalAmounts: depositShares});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
// Get queued withdrawals.
(Withdrawal[] memory withdrawals, uint[][] memory shares) = delegationManager.getQueuedWithdrawals(defaultStaker);
// Checks
for (uint i; i < strategies.length; ++i) {
uint newStakerShares = depositShares[i] / 2;
assertApproxEqAbs(shares[0][i], newStakerShares, 1, "staker shares should be decreased by half +- 1");
}
assertEq(
_withdrawalRoot(withdrawal), _withdrawalRoot(withdrawals[0]), "_withdrawalRoot(withdrawal) != _withdrawalRoot(withdrawals[0])"
);
assertEq(_withdrawalRoot(withdrawal), withdrawalRoot, "_withdrawalRoot(withdrawal) != withdrawalRoot");
}
function test_getQueuedWithdrawals_TotalQueuedGreaterThanTotalStrategies(Randomness r) public rand(r) {
uint totalDepositShares = r.Uint256(2, 100 ether);
_registerOperatorWithBaseDetails(defaultOperator);
strategyManagerMock.addDeposit(defaultStaker, strategyMock, totalDepositShares);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
uint newStakerShares = totalDepositShares / 2;
_setOperatorMagnitude(defaultOperator, strategyMock, 0.5 ether);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategyMock, WAD, 0.5 ether);
uint afterSlash = delegationManager.operatorShares(defaultOperator, strategyMock);
assertApproxEqAbs(afterSlash, newStakerShares, 1, "bad operator shares after slash");
// Queue withdrawals.
(QueuedWithdrawalParams[] memory queuedWithdrawalParams0, Withdrawal memory withdrawal0, bytes32 withdrawalRoot0) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: totalDepositShares / 2});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams0);
(QueuedWithdrawalParams[] memory queuedWithdrawalParams1, Withdrawal memory withdrawal1, bytes32 withdrawalRoot1) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: totalDepositShares / 2});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams1);
// Get queued withdrawals.
(Withdrawal[] memory withdrawals, uint[][] memory shares) = delegationManager.getQueuedWithdrawals(defaultStaker);
// Sanity
assertEq(withdrawals.length, 2, "withdrawal.length != 2");
assertEq(withdrawals[0].strategies.length, 1, "withdrawals[0].strategies.length != 1");
assertEq(withdrawals[1].strategies.length, 1, "withdrawals[1].strategies.length != 1");
// Checks
assertApproxEqAbs(shares[0][0], newStakerShares / 2, 1, "shares[0][0] != newStakerShares");
assertApproxEqAbs(shares[1][0], newStakerShares / 2, 1, "shares[1][0] != newStakerShares");
assertEq(_withdrawalRoot(withdrawal0), _withdrawalRoot(withdrawals[0]), "withdrawal0 != withdrawals[0]");
assertEq(_withdrawalRoot(withdrawal1), _withdrawalRoot(withdrawals[1]), "withdrawal1 != withdrawals[1]");
assertEq(_withdrawalRoot(withdrawal0), withdrawalRoot0, "_withdrawalRoot(withdrawal0) != withdrawalRoot0");
assertEq(_withdrawalRoot(withdrawal1), withdrawalRoot1, "_withdrawalRoot(withdrawal1) != withdrawalRoot1");
}
/**
* @notice Assert that the shares returned in the view function `getQueuedWithdrawals` are unaffected from a
* slash that occurs after the withdrawal is completed. Also assert that completing the withdrawal matches the
* expected withdrawn shares from the view function.
* Slashing on the completableBlock of the withdrawal should have no affect on the withdrawn shares.
*/
function test_getQueuedWithdrawals_SlashAfterWithdrawalCompletion(Randomness r) public rand(r) {
uint depositAmount = r.Uint256(1, MAX_STRATEGY_SHARES);
// Deposit Staker
strategyManagerMock.addDeposit(defaultStaker, strategyMock, depositAmount);
// Register operator and delegate to it
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Queue withdrawal
(QueuedWithdrawalParams[] memory queuedWithdrawalParams, Withdrawal memory withdrawal, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: depositAmount});
{
uint operatorSharesBeforeQueue = delegationManager.operatorShares(defaultOperator, strategyMock);
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
assertTrue(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRoot should be pending");
uint operatorSharesAfterQueue = delegationManager.operatorShares(defaultOperator, strategyMock);
uint sharesWithdrawn =
_calcWithdrawableShares({depositShares: depositAmount, depositScalingFactor: uint(WAD), slashingFactor: uint(WAD)});
assertEq(
operatorSharesAfterQueue, operatorSharesBeforeQueue - sharesWithdrawn, "operator shares should be decreased after queue"
);
}
// Slash operator 50% while staker has queued withdrawal
{
uint operatorSharesAfterQueue = delegationManager.operatorShares(defaultOperator, strategyMock);
(uint sharesToDecrement,) =
_calcSlashedAmount({operatorShares: operatorSharesAfterQueue, prevMaxMagnitude: uint64(WAD), newMaxMagnitude: 50e16});
_setOperatorMagnitude(defaultOperator, strategyMock, 50e16);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, withdrawal.strategies[0], uint64(WAD), 50e16);
uint operatorSharesAfterSlash = delegationManager.operatorShares(defaultOperator, strategyMock);
assertEq(
operatorSharesAfterSlash, operatorSharesAfterQueue - sharesToDecrement, "operator shares should be decreased after slash"
);
}
// Assert that the getQueuedWithdrawals returns shares that are halved as a result of being slashed 50%
{
(Withdrawal[] memory withdrawals, uint[][] memory shares) = delegationManager.getQueuedWithdrawals(defaultStaker);
assertEq(withdrawals.length, 1, "withdrawals.length != 1");
assertEq(withdrawals[0].strategies.length, 1, "withdrawals[0].strategies.length != 1");
assertEq(shares[0][0], depositAmount / 2, "shares[0][0] != depositAmount / 2");
}
// Roll blocks to after withdrawal completion
uint32 completableBlock = withdrawal.startBlock + delegationManager.minWithdrawalDelayBlocks() + 1;
cheats.roll(completableBlock);
// slash operator 50% again
{
uint operatorShares = delegationManager.operatorShares(defaultOperator, strategyMock);
(uint sharesToDecrement,) =
_calcSlashedAmount({operatorShares: operatorShares, prevMaxMagnitude: 50e16, newMaxMagnitude: 25e16});
_setOperatorMagnitude(defaultOperator, strategyMock, 25e16);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, withdrawal.strategies[0], 50e16, 25e16);
uint operatorSharesAfterSecondSlash = delegationManager.operatorShares(defaultOperator, strategyMock);
assertEq(operatorSharesAfterSecondSlash, operatorShares - sharesToDecrement, "operator shares should be decreased after slash");
}
// Assert that the getQueuedWithdrawals returns shares that are halved as a result of being slashed 50% and hasn't been
// affected by the second slash
uint expectedSharesIncrease = depositAmount / 2;
uint queuedWithdrawableShares;
{
(Withdrawal[] memory withdrawals, uint[][] memory shares) = delegationManager.getQueuedWithdrawals(defaultStaker);
queuedWithdrawableShares = shares[0][0];
assertEq(withdrawals.length, 1, "withdrawals.length != 1");
assertEq(withdrawals[0].strategies.length, 1, "withdrawals[0].strategies.length != 1");
assertEq(queuedWithdrawableShares, depositAmount / 2, "queuedWithdrawableShares != withdrawalAmount / 2");
}
// Complete queued Withdrawal with shares added back. Since total deposit slashed by 50% and not 75%
(uint[] memory withdrawableSharesBefore,) = delegationManager.getWithdrawableShares(defaultStaker, withdrawal.strategies);
cheats.prank(defaultStaker);
delegationManager.completeQueuedWithdrawal(withdrawal, tokenMock.toArray(), false);
(uint[] memory withdrawableSharesAfter,) = delegationManager.getWithdrawableShares(defaultStaker, withdrawal.strategies);
// Added shares
assertEq(
withdrawableSharesAfter[0],
withdrawableSharesBefore[0] + expectedSharesIncrease,
"withdrawableShares should be increased by expectedSharesIncrease"
);
assertEq(expectedSharesIncrease, queuedWithdrawableShares, "expectedSharesIncrease should be equal to queuedWithdrawableShares");
assertEq(block.number, completableBlock, "block.number should be the completableBlock");
}
function test_getQueuedWithdrawals_UsesCorrectOperatorMagnitude() public {
// Alice deposits 100 shares into strategy
uint depositAmount = 100e18;
_depositIntoStrategies(defaultStaker, strategyMock.toArray(), depositAmount.toArrayU256());
// Register operator with magnitude of 0.5 and delegate Alice to them
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
_setOperatorMagnitude(defaultOperator, strategyMock, 0.5 ether);
// Alice queues withdrawal of all 100 shares while operator magnitude is 0.5
// This means she should get back 50 shares (100 * 0.5)
(QueuedWithdrawalParams[] memory queuedWithdrawalParams,, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: depositAmount});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
// Alice undelegates, which would normally update operator's magnitude to 1.0
// This tests that the withdrawal still uses the original 0.5 magnitude from when it was queued
cheats.prank(defaultStaker);
delegationManager.undelegate(defaultStaker);
// Get shares from withdrawal - should return 50 shares (100 * 0.5) using original magnitude
// rather than incorrectly returning 100 shares (100 * 1.0) using new magnitude
(, uint[] memory shares) = delegationManager.getQueuedWithdrawal(withdrawalRoot);
assertEq(shares[0], 50e18, "shares should be 50e18 (100e18 * 0.5) using original magnitude");
}
}
contract DelegationManagerUnitTests_getQueuedWithdrawal is DelegationManagerUnitTests {
using ArrayLib for *;
using SlashingLib for *;
function test_getQueuedWithdrawal_Correctness(Randomness r) public rand(r) {
// Set up initial deposit
uint depositAmount = r.Uint256(1 ether, 100 ether);
_depositIntoStrategies(defaultStaker, strategyMock.toArray(), depositAmount.toArrayU256());
// Register operator and delegate
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Queue withdrawal
(QueuedWithdrawalParams[] memory queuedWithdrawalParams,, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: depositAmount});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
// Get shares from queued withdrawal
(, uint[] memory shares) = delegationManager.getQueuedWithdrawal(withdrawalRoot);
// Verify withdrawal details match
assertEq(shares.length, 1, "incorrect shares array length");
assertEq(shares[0], depositAmount, "incorrect shares amount");
}
function test_getQueuedWithdrawal_AfterSlashing(Randomness r) public rand(r) {
// Set up initial deposit
uint depositAmount = r.Uint256(1 ether, 100 ether);
_depositIntoStrategies(defaultStaker, strategyMock.toArray(), depositAmount.toArrayU256());
// Register operator and delegate
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Queue withdrawal
(QueuedWithdrawalParams[] memory queuedWithdrawalParams,, bytes32 withdrawalRoot) =
_setUpQueueWithdrawalsSingleStrat({staker: defaultStaker, strategy: strategyMock, depositSharesToWithdraw: depositAmount});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
// Slash operator by 50%
_setOperatorMagnitude(defaultOperator, strategyMock, 0.5 ether);
cheats.prank(address(allocationManagerMock));
delegationManager.slashOperatorShares(defaultOperator, strategyMock, WAD, 0.5 ether);
// Get shares from queued withdrawal
(, uint[] memory shares) = delegationManager.getQueuedWithdrawal(withdrawalRoot);
// Verify withdrawal details match and shares are slashed
assertEq(shares.length, 1, "incorrect shares array length");
assertEq(shares[0], depositAmount / 2, "shares not properly slashed");
}
function test_getQueuedWithdrawal_NonexistentWithdrawal() public view {
bytes32 nonexistentRoot = bytes32(uint(1));
(, uint[] memory shares) = delegationManager.getQueuedWithdrawal(nonexistentRoot);
assertEq(shares.length, 0, "shares array should be empty");
}
function test_getQueuedWithdrawal_MultipleStrategies(Randomness r) public rand(r) {
// Set up multiple strategies with deposits
uint numStrategies = r.Uint256(2, 5);
uint[] memory depositShares = r.Uint256Array({len: numStrategies, min: 1 ether, max: 100 ether});
IStrategy[] memory strategies = _deployAndDepositIntoStrategies(defaultStaker, depositShares, false);
// Register operator and delegate
_registerOperatorWithBaseDetails(defaultOperator);
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
// Queue withdrawals for multiple strategies
(QueuedWithdrawalParams[] memory queuedWithdrawalParams,, bytes32 withdrawalRoot) =
_setUpQueueWithdrawals({staker: defaultStaker, strategies: strategies, depositWithdrawalAmounts: depositShares});
cheats.prank(defaultStaker);
delegationManager.queueWithdrawals(queuedWithdrawalParams);
// Get shares from queued withdrawal
(, uint[] memory shares) = delegationManager.getQueuedWithdrawal(withdrawalRoot);
// Verify withdrawal details and shares for each strategy
assertEq(shares.length, numStrategies, "incorrect shares array length");
for (uint i = 0; i < numStrategies; i++) {
assertEq(shares[i], depositShares[i], "incorrect shares amount for strategy");
}
}
function testFuzz_getQueuedWithdrawal_EmptyWithdrawal(bytes32 withdrawalRoot) public view {
(, uint[] memory shares) = delegationManager.getQueuedWithdrawal(withdrawalRoot);
assertEq(shares.length, 0, "sanity check");
}
}
````
## File: src/test/unit/DeployFromScratch.t.sol
````
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console2} from "forge-std/Test.sol";
import {DeployFromScratch} from "script/deploy/local/deploy_from_scratch.slashing.s.sol";
// NOTE: Run the following command to deploy from scratch in an anvil instance:
// RUST_LOG=forge,foundry=trace forge script script/deploy/local/Deploy_From_Scratch.s.sol --slow \
// --rpc-url http://127.0.0.1:8545 \
// --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
// --broadcast \
// --sig "run(string memory configFile)" \
// -- local/deploy_from_scratch.slashing.anvil.config.json
contract DeployTest is Test {
DeployFromScratch public deployer;
function setUp() public {
deployer = new DeployFromScratch();
}
function test_DeployFromScratch() public {
// Deploy, expecting no revert.
deployer.run("local/deploy_from_scratch.slashing.anvil.config.json");
}
}
````
## File: src/test/unit/EigenPodManagerUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "src/contracts/pods/EigenPodManager.sol";
import "src/contracts/pods/EigenPodPausingConstants.sol";
import "src/test/utils/EigenLayerUnitTestSetup.sol";
import "src/test/harnesses/EigenPodManagerWrapper.sol";
import "src/test/mocks/EigenPodMock.sol";
import "src/test/mocks/ETHDepositMock.sol";
contract EigenPodManagerUnitTests is EigenLayerUnitTestSetup, IEigenPodManagerEvents {
// Contracts Under Test: EigenPodManager
EigenPodManager public eigenPodManagerImplementation;
EigenPodManager public eigenPodManager;
using stdStorage for StdStorage;
// Mocks
IETHPOSDeposit public ethPOSMock;
IEigenPod public eigenPodMockImplementation;
IBeacon public eigenPodBeacon; // Proxy for eigenPodMockImplementation
// Constants
uint public constant GWEI_TO_WEI = 1e9;
address public defaultStaker = address(this);
IEigenPod public defaultPod;
address public initialOwner = address(this);
IStrategy public constant beaconChainETHStrategy = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);
function setUp() public virtual override {
EigenLayerUnitTestSetup.setUp();
// Deploy Mocks
ethPOSMock = new ETHPOSDepositMock();
eigenPodMockImplementation = new EigenPodMock();
eigenPodBeacon = new UpgradeableBeacon(address(eigenPodMockImplementation));
// Deploy EPM Implementation & Proxy
eigenPodManagerImplementation =
new EigenPodManager(ethPOSMock, eigenPodBeacon, IDelegationManager(address(delegationManagerMock)), pauserRegistry, "v9.9.9");
eigenPodManager = EigenPodManager(
address(
new TransparentUpgradeableProxy(
address(eigenPodManagerImplementation),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(EigenPodManager.initialize.selector, initialOwner, 0 /*initialPausedStatus*/ )
)
)
);
// Set defaultPod
defaultPod = eigenPodManager.getPod(defaultStaker);
// Exclude the zero address, and the eigenPodManager itself from fuzzed inputs
isExcludedFuzzAddress[address(0)] = true;
isExcludedFuzzAddress[address(eigenPodManager)] = true;
}
/**
*
* Helper Functions/Modifiers
*
*/
function _initializePodWithShares(address podOwner, int shares) internal {
// Deploy pod
_deployAndReturnEigenPodForStaker(podOwner);
if (shares >= 0) {
cheats.prank(address(delegationManagerMock));
eigenPodManager.addShares(podOwner, beaconChainETHStrategy, uint(shares));
} else {
EigenPodManagerWrapper(address(eigenPodManager)).setPodOwnerShares(podOwner, shares);
}
}
modifier deployPodForStaker(address staker) {
_deployAndReturnEigenPodForStaker(staker);
_;
}
function _deployAndReturnEigenPodForStaker(address staker) internal returns (IEigenPod deployedPod) {
deployedPod = eigenPodManager.getPod(staker);
cheats.prank(staker);
eigenPodManager.createPod();
return deployedPod;
}
function _checkPodDeployed(address staker, address expectedPod, uint numPodsBefore) internal view {
assertEq(address(eigenPodManager.ownerToPod(staker)), expectedPod, "Expected pod not deployed");
assertEq(eigenPodManager.numPods(), numPodsBefore + 1, "Num pods not incremented");
}
}
contract EigenPodManagerUnitTests_Initialization_Setters is EigenPodManagerUnitTests {
/**
*
* Initialization Tests
*
*/
function test_initialization() public view {
// Check max pods, beacon chain, owner, and pauser
assertEq(eigenPodManager.owner(), initialOwner, "Initialization: owner incorrect");
assertEq(address(eigenPodManager.pauserRegistry()), address(pauserRegistry), "Initialization: pauser registry incorrect");
assertEq(eigenPodManager.paused(), 0, "Initialization: paused value not 0");
// Check storage variables
assertEq(address(eigenPodManager.ethPOS()), address(ethPOSMock), "Initialization: ethPOS incorrect");
assertEq(address(eigenPodManager.eigenPodBeacon()), address(eigenPodBeacon), "Initialization: eigenPodBeacon incorrect");
assertEq(
address(eigenPodManager.delegationManager()), address(delegationManagerMock), "Initialization: delegationManager incorrect"
);
}
function test_initialize_revert_alreadyInitialized() public {
cheats.expectRevert("Initializable: contract is already initialized");
eigenPodManager.initialize(initialOwner, 0 /*initialPausedStatus*/ );
}
}
contract EigenPodManagerUnitTests_CreationTests is EigenPodManagerUnitTests {
function test_createPod() public {
// Get expected pod address and pods before
IEigenPod expectedPod = eigenPodManager.getPod(defaultStaker);
uint numPodsBefore = eigenPodManager.numPods();
// Create pod
cheats.expectEmit(true, true, true, true);
emit PodDeployed(address(expectedPod), defaultStaker);
eigenPodManager.createPod();
// Check pod deployed
_checkPodDeployed(defaultStaker, address(defaultPod), numPodsBefore);
}
function test_createPod_revert_alreadyCreated() public deployPodForStaker(defaultStaker) {
cheats.expectRevert(IEigenPodManagerErrors.EigenPodAlreadyExists.selector);
eigenPodManager.createPod();
}
}
contract EigenPodManagerUnitTests_ProofTimestampSetterTests is EigenPodManagerUnitTests {
function testFuzz_setProofTimestampSetter_revert_notOwner(address notOwner) public filterFuzzedAddressInputs(notOwner) {
cheats.assume(notOwner != initialOwner);
cheats.prank(notOwner);
cheats.expectRevert("Ownable: caller is not the owner");
eigenPodManager.setProofTimestampSetter(address(1));
}
function test_setProofTimestampSetter() public {
address newSetter = address(1);
cheats.expectEmit(true, true, true, true);
emit ProofTimestampSetterSet(newSetter);
cheats.prank(initialOwner);
eigenPodManager.setProofTimestampSetter(newSetter);
assertEq(eigenPodManager.proofTimestampSetter(), newSetter, "Proof timestamp setter not set correctly");
}
function test_setPectraForkTimestamp_revert_notSetter(address notSetter) public filterFuzzedAddressInputs(notSetter) {
// First set a proof timestamp setter
address setter = address(1);
cheats.prank(initialOwner);
eigenPodManager.setProofTimestampSetter(setter);
// Try to set timestamp from non-setter address
cheats.assume(notSetter != setter);
cheats.prank(notSetter);
cheats.expectRevert(IEigenPodManagerErrors.OnlyProofTimestampSetter.selector);
eigenPodManager.setPectraForkTimestamp(1);
}
function test_setPectraForkTimestamp() public {
// First set a proof timestamp setter
address setter = address(1);
cheats.prank(initialOwner);
eigenPodManager.setProofTimestampSetter(setter);
// Set new timestamp
uint64 newTimestamp = 1;
cheats.expectEmit(true, true, true, true);
emit PectraForkTimestampSet(newTimestamp);
cheats.prank(setter);
eigenPodManager.setPectraForkTimestamp(newTimestamp);
assertEq(eigenPodManager.pectraForkTimestamp(), newTimestamp, "Pectra fork timestamp not set correctly");
}
}
contract EigenPodManagerUnitTests_StakeTests is EigenPodManagerUnitTests {
function test_stake_podAlreadyDeployed() public deployPodForStaker(defaultStaker) {
// Declare dummy variables
bytes memory pubkey = bytes("pubkey");
bytes memory sig = bytes("sig");
bytes32 depositDataRoot = bytes32("depositDataRoot");
// Stake
eigenPodManager.stake{value: 32 ether}(pubkey, sig, depositDataRoot);
// Expect pod has 32 ether
assertEq(address(defaultPod).balance, 32 ether, "ETH not staked in EigenPod");
}
function test_stake_newPodDeployed() public {
// Declare dummy variables
bytes memory pubkey = bytes("pubkey");
bytes memory sig = bytes("sig");
bytes32 depositDataRoot = bytes32("depositDataRoot");
// Stake
eigenPodManager.stake{value: 32 ether}(pubkey, sig, depositDataRoot);
// Check pod deployed
_checkPodDeployed(defaultStaker, address(defaultPod), 0); // staker, defaultPod, numPodsBefore
// Expect pod has 32 ether
assertEq(address(defaultPod).balance, 32 ether, "ETH not staked in EigenPod");
}
}
contract EigenPodManagerUnitTests_ShareUpdateTests is EigenPodManagerUnitTests {
// Wrapper contract that exposes the internal `_calculateChangeInDelegatableShares` function
EigenPodManagerWrapper public eigenPodManagerWrapper;
function setUp() public virtual override {
super.setUp();
// Upgrade eigenPodManager to wrapper
eigenPodManagerWrapper = new EigenPodManagerWrapper(
ethPOSMock, eigenPodBeacon, IDelegationManager(address(delegationManagerMock)), pauserRegistry, "v9.9.9"
);
eigenLayerProxyAdmin.upgrade(ITransparentUpgradeableProxy(payable(address(eigenPodManager))), address(eigenPodManagerWrapper));
}
/**
*
* Add Shares Tests
*
*/
function testFuzz_addShares_revert_notDelegationManager(address notDelegationManager)
public
filterFuzzedAddressInputs(notDelegationManager)
{
cheats.assume(notDelegationManager != address(delegationManagerMock));
cheats.prank(notDelegationManager);
cheats.expectRevert(IEigenPodManagerErrors.OnlyDelegationManager.selector);
eigenPodManager.addShares(defaultStaker, IStrategy(address(0)), 0);
}
function test_addShares_revert_podOwnerZeroAddress() public {
cheats.prank(address(delegationManagerMock));
cheats.expectRevert(IEigenPodErrors.InputAddressZero.selector);
eigenPodManager.addShares(address(0), beaconChainETHStrategy, 0);
}
function testFuzz_addShares_revert_sharesNegative(int shares) public {
cheats.assume(shares < 0);
cheats.prank(address(delegationManagerMock));
cheats.expectRevert(IEigenPodManagerErrors.SharesNegative.selector);
eigenPodManager.addShares(defaultStaker, beaconChainETHStrategy, uint(shares));
}
function testFuzz_addShares(uint shares) public {
// Fuzz inputs
cheats.assume(defaultStaker != address(0));
shares = shares - (shares % GWEI_TO_WEI); // Round down to nearest Gwei
cheats.assume(int(shares) >= 0);
// Add shares
cheats.prank(address(delegationManagerMock));
eigenPodManager.addShares(defaultStaker, beaconChainETHStrategy, shares);
// Check storage update
assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), int(shares), "Incorrect number of shares added");
}
function test_addShares_negativeInitial() public {
_initializePodWithShares(defaultStaker, -1);
cheats.prank(address(delegationManagerMock));
(uint prevDepositShares, uint addedShares) = eigenPodManager.addShares(defaultStaker, beaconChainETHStrategy, 5);
assertEq(prevDepositShares, 0);
assertEq(addedShares, 4);
}
function testFuzz_addShares_negativeSharesInitial(int sharesToStart, int sharesToAdd) public {
cheats.assume(sharesToStart < 0);
cheats.assume(sharesToAdd >= 0);
_initializePodWithShares(defaultStaker, sharesToStart);
int expectedDepositShares = sharesToStart + sharesToAdd;
cheats.prank(address(delegationManagerMock));
cheats.expectEmit(true, true, true, true, address(eigenPodManager));
emit PodSharesUpdated(defaultStaker, sharesToAdd);
cheats.expectEmit(true, true, true, true, address(eigenPodManager));
emit NewTotalShares(defaultStaker, expectedDepositShares);
(uint prevDepositShares, uint addedShares) = eigenPodManager.addShares(defaultStaker, beaconChainETHStrategy, uint(sharesToAdd));
// validate that prev shares return 0 since we started from a negative balance
assertEq(prevDepositShares, 0);
// If we now have positive shares, expect return
if (expectedDepositShares > 0) assertEq(addedShares, uint(expectedDepositShares));
// We still have negative shares, return 0
else assertEq(addedShares, 0);
}
/**
*
* Remove Shares Tests
*
*/
function testFuzz_removeDepositShares_revert_notDelegationManager(address notDelegationManager)
public
filterFuzzedAddressInputs(notDelegationManager)
{
cheats.assume(notDelegationManager != address(delegationManagerMock));
cheats.prank(notDelegationManager);
cheats.expectRevert(IEigenPodManagerErrors.OnlyDelegationManager.selector);
eigenPodManager.removeDepositShares(defaultStaker, beaconChainETHStrategy, 0);
}
function testFuzz_removeDepositShares_revert_sharesNegative(uint224 sharesToRemove) public {
cheats.assume(sharesToRemove > 0);
cheats.prank(address(delegationManagerMock));
cheats.expectRevert(IEigenPodManagerErrors.SharesNegative.selector);
eigenPodManager.removeDepositShares(defaultStaker, beaconChainETHStrategy, sharesToRemove);
}
function testFuzz_removeDepositShares_revert_tooManySharesRemoved(uint224 sharesToAdd, uint224 sharesToRemove) public {
// Constrain inputs
cheats.assume(sharesToRemove > sharesToAdd);
uint sharesAdded = sharesToAdd * GWEI_TO_WEI;
uint sharesRemoved = sharesToRemove * GWEI_TO_WEI;
// Initialize pod with shares
_initializePodWithShares(defaultStaker, int(sharesAdded));
// Remove shares
cheats.prank(address(delegationManagerMock));
cheats.expectRevert(IEigenPodManagerErrors.SharesNegative.selector);
eigenPodManager.removeDepositShares(defaultStaker, beaconChainETHStrategy, sharesRemoved);
}
function testFuzz_removeShares(uint224 sharesToAdd, uint224 sharesToRemove) public {
// Constrain inputs
cheats.assume(sharesToRemove <= sharesToAdd);
uint sharesAdded = sharesToAdd * GWEI_TO_WEI;
uint sharesRemoved = sharesToRemove * GWEI_TO_WEI;
// Initialize pod with shares
_initializePodWithShares(defaultStaker, int(sharesAdded));
// Remove shares
cheats.prank(address(delegationManagerMock));
eigenPodManager.removeDepositShares(defaultStaker, beaconChainETHStrategy, sharesRemoved);
// Check storage
assertEq(
eigenPodManager.podOwnerDepositShares(defaultStaker), int(sharesAdded - sharesRemoved), "Incorrect number of shares removed"
);
}
function testFuzz_removeDepositShares_zeroShares(address podOwner, uint shares) public filterFuzzedAddressInputs(podOwner) {
// Constrain inputs
cheats.assume(podOwner != address(0));
cheats.assume(shares < type(uint).max / 2);
shares = shares - (shares % GWEI_TO_WEI); // Round down to nearest Gwei
assertTrue(int(shares) % int(GWEI_TO_WEI) == 0, "Shares must be a whole Gwei amount");
// Initialize pod with shares
_initializePodWithShares(podOwner, int(shares));
// Remove shares
cheats.prank(address(delegationManagerMock));
eigenPodManager.removeDepositShares(podOwner, beaconChainETHStrategy, shares);
// Check storage update
assertEq(eigenPodManager.podOwnerDepositShares(podOwner), 0, "Shares not reset to zero");
}
}
contract EigenPodManagerUnitTests_WithdrawSharesAsTokensTests is EigenPodManagerUnitTests {
// Wrapper contract that exposes the internal `_calculateChangeInDelegatableShares` function
EigenPodManagerWrapper public eigenPodManagerWrapper;
function setUp() public virtual override {
super.setUp();
// Upgrade eigenPodManager to wrapper
eigenPodManagerWrapper = new EigenPodManagerWrapper(
ethPOSMock, eigenPodBeacon, IDelegationManager(address(delegationManagerMock)), pauserRegistry, "v9.9.9"
);
eigenLayerProxyAdmin.upgrade(ITransparentUpgradeableProxy(payable(address(eigenPodManager))), address(eigenPodManagerWrapper));
}
/**
*
* WithdrawSharesAsTokens Tests
*
*/
function test_withdrawSharesAsTokens_revert_invalidStrategy() public {
cheats.prank(address(delegationManagerMock));
cheats.expectRevert(IEigenPodManagerErrors.InvalidStrategy.selector);
eigenPodManager.withdrawSharesAsTokens(defaultStaker, IStrategy(address(0)), IERC20(address(0)), 0);
}
function test_withdrawSharesAsTokens_revert_podOwnerZeroAddress() public {
cheats.prank(address(delegationManagerMock));
cheats.expectRevert(IEigenPodErrors.InputAddressZero.selector);
eigenPodManager.withdrawSharesAsTokens(address(0), beaconChainETHStrategy, IERC20(address(0)), 0);
}
function testFuzz_withdrawSharesAsTokens_revert_sharesNegative(int shares) public {
cheats.assume(shares < 0);
cheats.prank(address(delegationManagerMock));
cheats.expectRevert(IEigenPodManagerErrors.SharesNegative.selector);
eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(0)), uint(shares));
}
/**
* @notice The `withdrawSharesAsTokens` is called in the `completeQueuedWithdrawal` function from the
* delegationManager. When a withdrawal is queued in the delegationManager, `removeDepositShares is called`
*/
function test_withdrawSharesAsTokens_m2NegativeShares_reduceEntireDeficit() public {
// Shares to initialize & withdraw
int sharesBeginning = -100e18;
uint sharesToWithdraw = 101e18;
// Deploy Pod And initialize with negative shares
_initializePodWithShares(defaultStaker, sharesBeginning);
assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), sharesBeginning, "Shares not initialized correctly");
// Withdraw shares
cheats.prank(address(delegationManagerMock));
cheats.expectEmit(true, true, true, true);
emit PodSharesUpdated(defaultStaker, 100e18);
cheats.expectEmit(true, true, true, true);
emit NewTotalShares(defaultStaker, 0);
// Expect call to EigenPod for the withdrawal
cheats.expectCall(
address(defaultPod), abi.encodeWithSelector(IEigenPod.withdrawRestakedBeaconChainETH.selector, defaultStaker, 1e18), 1
);
eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(0)), sharesToWithdraw);
// Check storage update
assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), int(0), "Shares not reduced to 0");
}
function test_withdrawSharesAsTokens_m2NegativeShares_partialDeficitReduction() public {
// Shares to initialize & withdraw
int sharesBeginning = -100e18;
uint sharesToWithdraw = 50e18;
// Deploy Pod And initialize with negative shares
_initializePodWithShares(defaultStaker, sharesBeginning);
assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), sharesBeginning, "Shares not initialized correctly");
// Withdraw shares
cheats.prank(address(delegationManagerMock));
cheats.expectEmit(true, true, true, true);
emit PodSharesUpdated(defaultStaker, 50e18);
cheats.expectEmit(true, true, true, true);
emit NewTotalShares(defaultStaker, -50e18);
// Assert that no call is made by passing in zero for the count
bytes memory emptyBytes;
cheats.expectCall(
address(defaultPod),
emptyBytes, // Cheatcode checks a partial match starting at the first byte of the calldata
0
);
eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(0)), sharesToWithdraw);
// Check storage update
int expectedShares = sharesBeginning + int(sharesToWithdraw);
assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), expectedShares, "Shares not reduced to expected amount");
}
function test_withdrawSharesAsTokens_withdrawPositive() public {
// Shares to initialize & withdraw
int sharesBeginning = 100e18;
uint sharesToWithdraw = 50e18;
// Deploy Pod And initialize with negative shares
_initializePodWithShares(defaultStaker, sharesBeginning);
// Withdraw shares
cheats.prank(address(delegationManagerMock));
// Expect call to EigenPod for the withdrawal
cheats.expectCall(
address(defaultPod),
abi.encodeWithSelector(IEigenPod.withdrawRestakedBeaconChainETH.selector, defaultStaker, sharesToWithdraw),
1
);
eigenPodManager.withdrawSharesAsTokens(defaultStaker, beaconChainETHStrategy, IERC20(address(0)), sharesToWithdraw);
// Check storage remains the same
assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), sharesBeginning, "Shares should not be adjusted");
}
}
contract EigenPodManagerUnitTests_BeaconChainETHBalanceUpdateTests is EigenPodManagerUnitTests {
// Wrapper contract that exposes the internal `_calculateChangeInDelegatableShares` function
EigenPodManagerWrapper public eigenPodManagerWrapper;
function setUp() public virtual override {
super.setUp();
// Upgrade eigenPodManager to wrapper
eigenPodManagerWrapper = new EigenPodManagerWrapper(
ethPOSMock, eigenPodBeacon, IDelegationManager(address(delegationManagerMock)), pauserRegistry, "v9.9.9"
);
eigenLayerProxyAdmin.upgrade(ITransparentUpgradeableProxy(payable(address(eigenPodManager))), address(eigenPodManagerWrapper));
}
function testFuzz_revert_notPod(address invalidCaller)
public
filterFuzzedAddressInputs(invalidCaller)
deployPodForStaker(defaultStaker)
{
cheats.assume(invalidCaller != address(defaultPod));
cheats.prank(invalidCaller);
cheats.expectRevert(IEigenPodManagerErrors.OnlyEigenPod.selector);
eigenPodManager.recordBeaconChainETHBalanceUpdate(defaultStaker, 0, 0);
}
function test_revert_zeroAddress() public {
IEigenPod zeroAddressPod = _deployAndReturnEigenPodForStaker(address(0));
cheats.prank(address(zeroAddressPod));
cheats.expectRevert(IEigenPodErrors.InputAddressZero.selector);
eigenPodManager.recordBeaconChainETHBalanceUpdate(address(0), 0, 0);
}
function testFuzz_revert_nonWholeGweiAmount(int sharesDelta) public deployPodForStaker(defaultStaker) {
cheats.assume(sharesDelta % int(GWEI_TO_WEI) != 0);
cheats.prank(address(defaultPod));
cheats.expectRevert(IEigenPodManagerErrors.SharesNotMultipleOfGwei.selector);
eigenPodManager.recordBeaconChainETHBalanceUpdate(defaultStaker, 0, sharesDelta);
}
function testFuzz_revert_negativeDepositShares(int224 sharesBefore) public {
cheats.assume(sharesBefore < 0);
// Initialize shares
_initializePodWithShares(defaultStaker, sharesBefore);
// Record balance update
cheats.prank(address(defaultPod));
cheats.expectRevert(IEigenPodManagerErrors.LegacyWithdrawalsNotCompleted.selector);
eigenPodManager.recordBeaconChainETHBalanceUpdate(defaultStaker, 0, 0);
}
function testFuzz_noCall_zeroBalanceUpdate(uint sharesBefore, uint prevRestakedBalanceWei) public {
// Constrain Inputs
sharesBefore = bound(sharesBefore, 0, type(uint224).max) * uint(GWEI_TO_WEI);
prevRestakedBalanceWei = bound(prevRestakedBalanceWei, 0, type(uint).max);
// Initialize shares
_initializePodWithShares(defaultStaker, int(sharesBefore));
// Add 0 shares, expect no call to DM
bytes memory emptyBytes;
cheats.prank(address(defaultPod));
cheats.expectCall(
address(delegationManagerMock),
emptyBytes, // Cheatcode checks a partial match starting at the first byte of the calldata
0 // No call is made
);
eigenPodManager.recordBeaconChainETHBalanceUpdate(defaultStaker, prevRestakedBalanceWei, 0);
}
function testFuzz_recordPositiveBalanceUpdate(uint sharesBefore, uint sharesDelta, uint prevRestakedBalanceWei) public {
// Constrain inputs
sharesBefore = bound(sharesBefore, 0, type(uint224).max) * uint(GWEI_TO_WEI);
sharesDelta = bound(sharesDelta, 1, type(uint224).max) * uint(GWEI_TO_WEI);
prevRestakedBalanceWei = bound(prevRestakedBalanceWei, 0, type(uint).max);
// Initialize shares
_initializePodWithShares(defaultStaker, int(sharesBefore));
uint64 prevSlashingFactor = eigenPodManager.beaconChainSlashingFactor(defaultStaker);
// Add shares
cheats.expectEmit(true, true, true, true);
emit PodSharesUpdated(defaultStaker, int(sharesDelta));
cheats.expectEmit(true, true, true, true);
emit NewTotalShares(defaultStaker, int(sharesBefore + sharesDelta));
cheats.prank(address(defaultPod));
eigenPodManager.recordBeaconChainETHBalanceUpdate(defaultStaker, prevRestakedBalanceWei, int(sharesDelta));
// Check storage
// Note that this is a unit test, we don't validate that the withdrawable shares are updated correctly
// See the integration tests for checking scaling factors and withdrawable shares
assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), int(sharesBefore + sharesDelta), "Shares not updated correctly");
assertEq(eigenPodManager.beaconChainSlashingFactor(defaultStaker), prevSlashingFactor, "bcsf should not change");
}
function testFuzz_recordNegativeBalanceUpdate(uint sharesBefore, uint sharesDelta, uint prevRestakedBalanceWei) public {
// Constrain inputs
sharesBefore = bound(sharesBefore, 0, type(uint224).max) * uint(GWEI_TO_WEI);
prevRestakedBalanceWei = bound(prevRestakedBalanceWei, 1, type(uint224).max);
sharesDelta = bound(sharesDelta, 1, prevRestakedBalanceWei) * uint(GWEI_TO_WEI);
prevRestakedBalanceWei *= GWEI_TO_WEI;
// Initialize shares
_initializePodWithShares(defaultStaker, int(sharesBefore));
uint64 prevSlashingFactor = eigenPodManager.beaconChainSlashingFactor(defaultStaker);
// Not checking the new slashing factor - just checking the invariant that new <= prev
cheats.expectEmit(true, true, true, false);
emit BeaconChainSlashingFactorDecreased(defaultStaker, 0, 0);
cheats.prank(address(defaultPod));
eigenPodManager.recordBeaconChainETHBalanceUpdate(defaultStaker, prevRestakedBalanceWei, -int(sharesDelta));
assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), int(sharesBefore), "Shares should not be adjusted");
assertTrue(eigenPodManager.beaconChainSlashingFactor(defaultStaker) <= prevSlashingFactor, "bcsf should always decrease");
}
}
contract EigenPodManagerUnitTests_increaseBurnableShares is EigenPodManagerUnitTests {
function testFuzz_onlyDelegationManager(address invalidCaller) public filterFuzzedAddressInputs(invalidCaller) {
cheats.assume(invalidCaller != address(delegationManagerMock));
cheats.prank(invalidCaller);
cheats.expectRevert(IEigenPodManagerErrors.OnlyDelegationManager.selector);
eigenPodManager.increaseBurnableShares(beaconChainETHStrategy, 1 ether);
}
function testFuzz_singleDeposit(uint increasedBurnableShares) public {
cheats.expectEmit(true, true, true, true, address(eigenPodManager));
emit BurnableETHSharesIncreased(increasedBurnableShares);
cheats.prank(address(delegationManagerMock));
eigenPodManager.increaseBurnableShares(beaconChainETHStrategy, increasedBurnableShares);
assertEq(eigenPodManager.burnableETHShares(), increasedBurnableShares, "Burnable shares not updated correctly");
}
function testFuzz_existingDeposit(uint existingBurnableShares, uint increasedBurnableShares) public {
// prevent overflow
cheats.assume(existingBurnableShares < type(uint).max - increasedBurnableShares);
cheats.expectEmit(true, true, true, true, address(eigenPodManager));
emit BurnableETHSharesIncreased(existingBurnableShares);
cheats.prank(address(delegationManagerMock));
eigenPodManager.increaseBurnableShares(beaconChainETHStrategy, existingBurnableShares);
assertEq(eigenPodManager.burnableETHShares(), existingBurnableShares, "Burnable shares not setup correctly");
cheats.expectEmit(true, true, true, true, address(eigenPodManager));
emit BurnableETHSharesIncreased(increasedBurnableShares);
cheats.prank(address(delegationManagerMock));
eigenPodManager.increaseBurnableShares(beaconChainETHStrategy, increasedBurnableShares);
assertEq(
eigenPodManager.burnableETHShares(), existingBurnableShares + increasedBurnableShares, "Burnable shares not updated correctly"
);
}
}
````
## File: src/test/unit/EigenPodUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "@openzeppelin/contracts/utils/Create2.sol";
import "src/contracts/pods/EigenPod.sol";
import "src/contracts/pods/EigenPodPausingConstants.sol";
import "src/test/mocks/ETHDepositMock.sol";
import "src/test/mocks/ERC20Mock.sol";
import "src/test/harnesses/EigenPodHarness.sol";
import "src/test/utils/ProofParsing.sol";
import "src/test/utils/EigenLayerUnitTestSetup.sol";
import "src/test/integration/mocks/BeaconChainMock.t.sol";
import "src/test/integration/mocks/BeaconChainMock_Deneb.t.sol";
import "src/test/integration/mocks/EIP_4788_Oracle_Mock.t.sol";
import "src/test/utils/EigenPodUser.t.sol";
contract EigenPodUnitTests is EigenLayerUnitTestSetup, EigenPodPausingConstants, IEigenPodEvents {
using BytesLib for bytes;
using BeaconChainProofs for *;
// Contract Under Test: EigenPod
EigenPod public eigenPod;
EigenPod public podImplementation;
IBeacon public eigenPodBeacon;
// BeaconChain Mock Setup
TimeMachine public timeMachine;
ETHPOSDepositMock ethPOSDepositMock;
BeaconChainMock public beaconChain;
EIP_4788_Oracle_Mock constant EIP_4788_ORACLE = EIP_4788_Oracle_Mock(0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02);
uint public numStakers;
address defaultProofSubmitter = cheats.addr(uint(0xDEADBEEF));
// Beacon chain genesis time when running locally
// Multiple of 12 for sanity's sake
uint64 constant GENESIS_TIME_LOCAL = 1 hours * 12;
uint constant TIME_TILL_STALE_BALANCE = 2 weeks;
bytes internal constant beaconProxyBytecode =
hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564";
function setUp() public virtual override {
// Setup
EigenLayerUnitTestSetup.setUp();
// Create time machine and beacon chain. Set block time to beacon chain genesis time
// beaconChainMock will also etch 4788 precompile
ethPOSDepositMock = new ETHPOSDepositMock();
cheats.warp(GENESIS_TIME_LOCAL);
timeMachine = new TimeMachine();
beaconChain = new BeaconChainMock(EigenPodManager(address(eigenPodManagerMock)), GENESIS_TIME_LOCAL);
// Deploy EigenPod
podImplementation = new EigenPod(ethPOSDepositMock, IEigenPodManager(address(eigenPodManagerMock)), GENESIS_TIME_LOCAL, "v9.9.9");
// Deploy Beacon
eigenPodBeacon = new UpgradeableBeacon(address(podImplementation));
// Deploy Proxy same way as EigenPodManager does
eigenPod = EigenPod(
payable(
Create2.deploy(
0,
bytes32(uint(uint160(address(this)))),
// set the beacon address to the eigenPodBeacon
abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))
)
)
);
// Etch 4788 precompile
cheats.etch(address(EIP_4788_ORACLE), type(EIP_4788_Oracle_Mock).runtimeCode);
// Store the eigenPodBeacon address in the eigenPod beacon proxy
bytes32 beaconSlot = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;
cheats.store(address(eigenPod), beaconSlot, bytes32(uint(uint160(address(eigenPodBeacon)))));
// Initialize pod
eigenPod.initialize(address(this));
// Set a proof submitter
eigenPod.setProofSubmitter(defaultProofSubmitter);
}
/**
*
* EIGENPOD Helpers
*
*/
modifier timewarp() {
uint curState = timeMachine.travelToLast();
_;
timeMachine.travel(curState);
}
function _seedPodWithETH(uint ethAmount) internal {
cheats.deal(address(this), ethAmount);
bool result;
bytes memory data;
(result, data) = address(eigenPod).call{value: ethAmount}("");
}
function _newEigenPodStaker(uint rand) internal returns (EigenPodUser, uint) {
string memory stakerName;
EigenPodUser staker;
stakerName = string.concat("Staker", cheats.toString(numStakers));
staker = new EigenPodUser(stakerName);
uint amount = bound(rand, 1 ether, 4096 ether);
cheats.deal(address(staker), amount);
numStakers++;
return (staker, amount);
}
/// @dev Opposite of Endian.fromLittleEndianUint64
function _toLittleEndianUint64(uint64 num) internal pure returns (bytes32) {
uint lenum;
// Rearrange the bytes from big-endian to little-endian format
lenum |= uint((num & 0xFF) << 56);
lenum |= uint((num & 0xFF00) << 40);
lenum |= uint((num & 0xFF0000) << 24);
lenum |= uint((num & 0xFF000000) << 8);
lenum |= uint((num & 0xFF00000000) >> 8);
lenum |= uint((num & 0xFF0000000000) >> 24);
lenum |= uint((num & 0xFF000000000000) >> 40);
lenum |= uint((num & 0xFF00000000000000) >> 56);
// Shift the little-endian bytes to the end of the bytes32 value
return bytes32(lenum << 192);
}
/**
*
* verifyWithdrawalCredentials Assertions
*
*/
function assert_Snap_Added_ActiveValidatorCount(EigenPodUser staker, uint addedValidators, string memory err) internal {
uint curActiveValidatorCount = _getActiveValidatorCount(staker);
uint prevActiveValidatorCount = _getPrevActiveValidatorCount(staker);
assertEq(prevActiveValidatorCount + addedValidators, curActiveValidatorCount, err);
}
function assert_Snap_Removed_ActiveValidatorCount(EigenPodUser staker, uint removedValidators, string memory err) internal {
uint curActiveValidatorCount = _getActiveValidatorCount(staker);
uint prevActiveValidatorCount = _getPrevActiveValidatorCount(staker);
assertEq(curActiveValidatorCount + removedValidators, prevActiveValidatorCount, err);
}
function assert_Snap_Unchanged_ActiveValidatorCount(EigenPodUser staker, string memory err) internal {
uint curActiveValidatorCount = _getActiveValidatorCount(staker);
uint prevActiveValidatorCount = _getPrevActiveValidatorCount(staker);
assertEq(curActiveValidatorCount, prevActiveValidatorCount, err);
}
function _getActiveValidatorCount(EigenPodUser staker) internal view returns (uint) {
EigenPod pod = staker.pod();
return pod.activeValidatorCount();
}
function _getPrevActiveValidatorCount(EigenPodUser staker) internal timewarp returns (uint) {
return _getActiveValidatorCount(staker);
}
function assert_Snap_Added_ActiveValidators(EigenPodUser staker, uint40[] memory addedValidators, string memory err) internal {
bytes32[] memory pubkeyHashes = beaconChain.getPubkeyHashes(addedValidators);
IEigenPodTypes.VALIDATOR_STATUS[] memory curStatuses = _getValidatorStatuses(staker, pubkeyHashes);
IEigenPodTypes.VALIDATOR_STATUS[] memory prevStatuses = _getPrevValidatorStatuses(staker, pubkeyHashes);
for (uint i = 0; i < curStatuses.length; i++) {
assertTrue(prevStatuses[i] == IEigenPodTypes.VALIDATOR_STATUS.INACTIVE, err);
assertTrue(curStatuses[i] == IEigenPodTypes.VALIDATOR_STATUS.ACTIVE, err);
}
}
function assert_Snap_Removed_ActiveValidators(EigenPodUser staker, uint40[] memory removedValidators, string memory err) internal {
bytes32[] memory pubkeyHashes = beaconChain.getPubkeyHashes(removedValidators);
IEigenPodTypes.VALIDATOR_STATUS[] memory curStatuses = _getValidatorStatuses(staker, pubkeyHashes);
IEigenPodTypes.VALIDATOR_STATUS[] memory prevStatuses = _getPrevValidatorStatuses(staker, pubkeyHashes);
for (uint i = 0; i < curStatuses.length; i++) {
assertTrue(prevStatuses[i] == IEigenPodTypes.VALIDATOR_STATUS.ACTIVE, err);
assertTrue(curStatuses[i] == IEigenPodTypes.VALIDATOR_STATUS.WITHDRAWN, err);
}
}
function _getValidatorStatuses(EigenPodUser staker, bytes32[] memory pubkeyHashes)
internal
view
returns (IEigenPodTypes.VALIDATOR_STATUS[] memory)
{
EigenPod pod = staker.pod();
IEigenPodTypes.VALIDATOR_STATUS[] memory statuses = new IEigenPodTypes.VALIDATOR_STATUS[](pubkeyHashes.length);
for (uint i = 0; i < statuses.length; i++) {
statuses[i] = pod.validatorStatus(pubkeyHashes[i]);
}
return statuses;
}
function _getPrevValidatorStatuses(EigenPodUser staker, bytes32[] memory pubkeyHashes)
internal
timewarp
returns (IEigenPodTypes.VALIDATOR_STATUS[] memory)
{
return _getValidatorStatuses(staker, pubkeyHashes);
}
/**
*
* startCheckpoint Assertions
*
*/
function check_StartCheckpoint_State(EigenPodUser staker) internal {
assert_ProofsRemainingEqualsActive(staker, "checkpoint proofs remaining should equal active validator count");
assert_Snap_Created_Checkpoint(staker, "staker should have created a new checkpoint");
}
function assert_ProofsRemainingEqualsActive(EigenPodUser staker, string memory err) internal view {
EigenPod pod = staker.pod();
assertEq(pod.currentCheckpoint().proofsRemaining, pod.activeValidatorCount(), err);
}
function assert_Snap_Created_Checkpoint(EigenPodUser staker, string memory err) internal {
uint64 curCheckpointTimestamp = _getCheckpointTimestamp(staker);
uint64 prevCheckpointTimestamp = _getPrevCheckpointTimestamp(staker);
assertEq(prevCheckpointTimestamp, 0, err);
assertTrue(curCheckpointTimestamp != 0, err);
}
function _getCheckpointTimestamp(EigenPodUser staker) internal view returns (uint64) {
EigenPod pod = staker.pod();
return pod.currentCheckpointTimestamp();
}
function _getPrevCheckpointTimestamp(EigenPodUser staker) internal timewarp returns (uint64) {
return _getCheckpointTimestamp(staker);
}
/**
*
* verifyCheckpointProofs
*
*/
/// @notice assumes positive rewards and that the checkpoint will be finalized
function _expectEventsVerifyCheckpointProofs(
EigenPodUser staker,
uint40[] memory validators,
BeaconChainProofs.BalanceProof[] memory proofs
) internal {
EigenPod pod = staker.pod();
int totalBalanceDeltaGWei = 0;
uint64 checkpointTimestamp = pod.currentCheckpointTimestamp();
for (uint i = 0; i < validators.length; i++) {
IEigenPodTypes.ValidatorInfo memory info = pod.validatorPubkeyHashToInfo(proofs[i].pubkeyHash);
uint64 prevBalanceGwei = info.restakedBalanceGwei;
uint64 newBalanceGwei = BeaconChainProofs.getBalanceAtIndex(proofs[i].balanceRoot, validators[i]);
int128 balanceDeltaGwei = _calcBalanceDelta({newAmountGwei: newBalanceGwei, previousAmountGwei: prevBalanceGwei});
if (newBalanceGwei != prevBalanceGwei) {
cheats.expectEmit(true, true, true, true, address(pod));
emit ValidatorBalanceUpdated(validators[i], checkpointTimestamp, newBalanceGwei);
}
if (newBalanceGwei == 0) {
cheats.expectEmit(true, true, true, true, address(pod));
emit ValidatorWithdrawn(checkpointTimestamp, validators[i]);
}
cheats.expectEmit(true, true, true, true, address(pod));
emit ValidatorCheckpointed(checkpointTimestamp, validators[i]);
totalBalanceDeltaGWei += balanceDeltaGwei;
}
int totalShareDeltaWei = (int128(uint128(pod.currentCheckpoint().podBalanceGwei)) + totalBalanceDeltaGWei) * int(1 gwei);
cheats.expectEmit(true, true, true, true, address(pod));
emit CheckpointFinalized(checkpointTimestamp, totalShareDeltaWei);
}
/// @dev Calculates the delta between two Gwei amounts and returns as an int256
function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) {
return int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei));
}
}
contract EigenPodUnitTests_Initialization is EigenPodUnitTests {
function test_constructor() public {
EigenPod pod = new EigenPod(ethPOSDepositMock, IEigenPodManager(address(eigenPodManagerMock)), GENESIS_TIME_LOCAL, "v9.9.9");
assertTrue(pod.ethPOS() == ethPOSDepositMock, "should have set ethPOS correctly");
assertTrue(address(pod.eigenPodManager()) == address(eigenPodManagerMock), "should have set eigenpodmanager correctly");
assertTrue(pod.GENESIS_TIME() == GENESIS_TIME_LOCAL, "should have set genesis time correctly");
}
function test_initialization() public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
EigenPod pod = staker.pod();
// Check podOwner and restaked
assertEq(pod.podOwner(), address(staker), "Pod owner incorrectly set");
// Check immutable storage
assertEq(address(pod.ethPOS()), address(ethPOSDepositMock), "EthPOS incorrectly set");
assertEq(address(pod.eigenPodManager()), address(eigenPodManagerMock), "EigenPodManager incorrectly set");
assertEq(pod.GENESIS_TIME(), GENESIS_TIME_LOCAL, "LOCAL genesis time incorrectly set");
}
function test_initialize_revert_alreadyInitialized() public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
EigenPod pod = staker.pod();
cheats.expectRevert("Initializable: contract is already initialized");
pod.initialize(address(this));
}
function test_initialize_revert_emptyPodOwner() public {
EigenPod pod = new EigenPod(ethPOSDepositMock, IEigenPodManager(address(eigenPodManagerMock)), GENESIS_TIME_LOCAL, "v9.9.9");
// un-initialize pod
cheats.store(address(pod), 0, 0);
cheats.expectRevert(IEigenPodErrors.InputAddressZero.selector);
pod.initialize(address(0));
}
function test_setProofSubmitter_revert_notPodOwner(address invalidCaller) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
EigenPod pod = staker.pod();
cheats.assume(invalidCaller != address(staker));
cheats.prank(invalidCaller);
cheats.expectRevert(IEigenPodErrors.OnlyEigenPodOwner.selector);
pod.setProofSubmitter(invalidCaller);
}
function test_setProofSubmitter(address newProofSubmitter) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
EigenPod pod = staker.pod();
address prevProofSubmitter = pod.proofSubmitter();
cheats.prank(address(staker));
cheats.expectEmit(true, true, true, true, address(pod));
emit ProofSubmitterUpdated(prevProofSubmitter, newProofSubmitter);
pod.setProofSubmitter(newProofSubmitter);
assertEq(pod.proofSubmitter(), newProofSubmitter, "did not update proof submitter");
}
}
contract EigenPodUnitTests_EPMFunctions is EigenPodUnitTests {
/**
*
* stake() tests
*
*/
// Beacon chain staking constnats
bytes public constant pubkey = hex"88347ed1c492eedc97fc8c506a35d44d81f27a0c7a1c661b35913cfd15256c0cccbd34a83341f505c7de2983292f2cab";
bytes public signature;
bytes32 public depositDataRoot;
function testFuzz_stake_revert_notEigenPodManager(address invalidCaller) public {
cheats.assume(invalidCaller != address(eigenPodManagerMock));
cheats.deal(invalidCaller, 32 ether);
cheats.prank(invalidCaller);
cheats.expectRevert(IEigenPodErrors.OnlyEigenPodManager.selector);
eigenPod.stake{value: 32 ether}(pubkey, signature, depositDataRoot);
}
function testFuzz_stake_revert_invalidValue(uint value) public {
cheats.assume(value != 32 ether);
cheats.deal(address(eigenPodManagerMock), value);
cheats.prank(address(eigenPodManagerMock));
cheats.expectRevert(IEigenPodErrors.MsgValueNot32ETH.selector);
eigenPod.stake{value: value}(pubkey, signature, depositDataRoot);
}
function test_stake() public {
cheats.deal(address(eigenPodManagerMock), 32 ether);
// Expect emit
vm.expectEmit(true, true, true, true);
emit EigenPodStaked(pubkey);
// Stake
cheats.prank(address(eigenPodManagerMock));
eigenPod.stake{value: 32 ether}(pubkey, signature, depositDataRoot);
// Check eth transferred
assertEq(address(ethPOSDepositMock).balance, 32 ether, "Incorrect amount transferred");
}
/**
*
* withdrawRestakedBeaconChainETH() tests
*
*/
function testFuzz_withdrawRestakedBeaconChainETH_revert_notEigenPodManager(address invalidCaller, address recipient, uint randAmount)
public
filterFuzzedAddressInputs(invalidCaller)
{
// Setup EigenPod Staker
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
EigenPod pod = staker.pod();
// ensure invalid caller causing revert
cheats.assume(invalidCaller != address(eigenPodManagerMock));
cheats.prank(invalidCaller);
cheats.expectRevert(IEigenPodErrors.OnlyEigenPodManager.selector);
pod.withdrawRestakedBeaconChainETH(recipient, randAmount);
}
function testFuzz_withdrawRestakedBeaconChainETH_revert_withdrawAmountTooLarge(uint rand, address recipient, uint randAmountWei)
public
{
// Setup EigenPod Staker
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
beaconChain.advanceEpoch();
staker.startCheckpoint();
staker.completeCheckpoint();
// ensure amount is too large
uint64 withdrawableRestakedExecutionLayerGwei = pod.withdrawableRestakedExecutionLayerGwei();
randAmountWei = randAmountWei - (randAmountWei % 1 gwei);
cheats.assume((randAmountWei / 1 gwei) > withdrawableRestakedExecutionLayerGwei);
cheats.expectRevert(IEigenPodErrors.InsufficientWithdrawableBalance.selector);
cheats.prank(address(eigenPodManagerMock));
pod.withdrawRestakedBeaconChainETH(recipient, randAmountWei);
}
function testFuzz_withdrawRestakedBeaconChainETH(uint rand, uint randAmountWei) public {
// Setup EigenPod Staker
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
beaconChain.advanceEpoch();
staker.startCheckpoint();
staker.completeCheckpoint();
// ensure valid fuzzed wei amounts
uint64 withdrawableRestakedExecutionLayerGwei = pod.withdrawableRestakedExecutionLayerGwei();
randAmountWei = randAmountWei - (randAmountWei % 1 gwei);
cheats.assume((randAmountWei / 1 gwei) <= withdrawableRestakedExecutionLayerGwei);
address recipient = cheats.addr(uint(123_456_789));
cheats.prank(address(eigenPodManagerMock));
cheats.expectEmit(true, true, true, true, address(pod));
emit RestakedBeaconChainETHWithdrawn(recipient, randAmountWei);
pod.withdrawRestakedBeaconChainETH(recipient, randAmountWei);
assertEq(address(recipient).balance, randAmountWei, "recipient should have received withdrawn balance");
assertEq(
address(pod).balance,
uint(withdrawableRestakedExecutionLayerGwei * 1 gwei) - randAmountWei,
"pod balance should have decreased by withdrawn eth"
);
assertEq(
pod.withdrawableRestakedExecutionLayerGwei(),
withdrawableRestakedExecutionLayerGwei - uint64(randAmountWei / 1 gwei),
"withdrawableRestakedExecutionLayerGwei should have decreased by amount withdrawn"
);
}
function testFuzz_withdrawRestakedBeaconChainETH_AmountGweiNotDivisibleByGwei(uint rand, uint randAmountWei) public {
// Setup EigenPod Staker
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
beaconChain.advanceEpoch();
staker.startCheckpoint();
staker.completeCheckpoint();
// ensure valid fuzzed wei amounts
uint64 withdrawableRestakedExecutionLayerGwei = pod.withdrawableRestakedExecutionLayerGwei();
uint randAmountWeiAdjusted = randAmountWei - (randAmountWei % 1 gwei);
cheats.assume((randAmountWei / 1 gwei) <= withdrawableRestakedExecutionLayerGwei);
address recipient = cheats.addr(uint(123_456_789));
cheats.prank(address(eigenPodManagerMock));
cheats.expectEmit(true, true, true, true, address(pod));
emit RestakedBeaconChainETHWithdrawn(recipient, randAmountWeiAdjusted);
pod.withdrawRestakedBeaconChainETH(recipient, randAmountWei);
assertEq(address(recipient).balance, randAmountWeiAdjusted, "recipient should have received withdrawn balance");
assertEq(
address(pod).balance,
uint(withdrawableRestakedExecutionLayerGwei * 1 gwei) - randAmountWeiAdjusted,
"pod balance should have decreased by withdrawn eth"
);
assertEq(
pod.withdrawableRestakedExecutionLayerGwei(),
withdrawableRestakedExecutionLayerGwei - uint64(randAmountWeiAdjusted / 1 gwei),
"withdrawableRestakedExecutionLayerGwei should have decreased by amount withdrawn"
);
}
}
contract EigenPodUnitTests_recoverTokens is EigenPodUnitTests {
/**
*
* recoverTokens() tests
*
*/
function testFuzz_recoverTokens_revert_notPodOwner(address invalidCaller) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
EigenPod pod = staker.pod();
address podOwner = pod.podOwner();
cheats.assume(invalidCaller != podOwner);
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = IERC20(address(0x123));
uint[] memory amounts = new uint[](1);
amounts[0] = 1;
cheats.prank(invalidCaller);
cheats.expectRevert(IEigenPodErrors.OnlyEigenPodOwner.selector);
pod.recoverTokens(tokens, amounts, podOwner);
}
function test_recoverTokens_revert_whenPaused() public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
EigenPod pod = staker.pod();
address podOwner = pod.podOwner();
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = IERC20(address(0x123));
uint[] memory amounts = new uint[](1);
amounts[0] = 1;
// pause recoverTokens
cheats.prank(pauser);
eigenPodManagerMock.pause(1 << PAUSED_NON_PROOF_WITHDRAWALS);
cheats.prank(podOwner);
cheats.expectRevert(IEigenPodErrors.CurrentlyPaused.selector);
pod.recoverTokens(tokens, amounts, podOwner);
}
function test_recoverTokens_revert_invalidLengths() public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
EigenPod pod = staker.pod();
address podOwner = pod.podOwner();
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = IERC20(address(0x123));
uint[] memory amounts = new uint[](2);
amounts[0] = 1;
amounts[1] = 1;
cheats.startPrank(podOwner);
cheats.expectRevert(IEigenPodErrors.InputArrayLengthMismatch.selector);
pod.recoverTokens(tokens, amounts, podOwner);
cheats.stopPrank();
}
function test_recoverTokens() public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
EigenPod pod = staker.pod();
address podOwner = pod.podOwner();
// Deploy dummy token
IERC20 dummyToken = new ERC20Mock();
dummyToken.transfer(address(pod), 1e18);
// Recover tokens
address recipient = address(0x123);
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = dummyToken;
uint[] memory amounts = new uint[](1);
amounts[0] = 1e18;
cheats.prank(podOwner);
pod.recoverTokens(tokens, amounts, recipient);
// Checks
assertEq(dummyToken.balanceOf(recipient), 1e18, "Incorrect amount recovered");
}
}
contract EigenPodUnitTests_verifyWithdrawalCredentials is EigenPodUnitTests, ProofParsing {
/**
*
* verifyWithdrawalCredentials() tests
*
*/
/// @notice revert when verify wc is not called by pod owner
function testFuzz_revert_callerIsNotPodOwnerOrProofSubmitter(address invalidCaller) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
(uint40[] memory validators,,) = staker.startValidators();
EigenPod pod = staker.pod();
address podOwner = pod.podOwner();
address proofSubmitter = pod.proofSubmitter();
CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators);
cheats.assume(invalidCaller != podOwner && invalidCaller != proofSubmitter);
cheats.prank(invalidCaller);
cheats.expectRevert(IEigenPodErrors.OnlyEigenPodOwnerOrProofSubmitter.selector);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
}
/// @notice test verify wc reverts when paused
function test_revert_verifyWithdrawalCredentialsPaused() public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
(uint40[] memory validators,,) = staker.startValidators();
cheats.prank(pauser);
eigenPodManagerMock.pause(2 ** PAUSED_EIGENPODS_VERIFY_CREDENTIALS);
cheats.expectRevert(IEigenPodErrors.CurrentlyPaused.selector);
staker.verifyWithdrawalCredentials(validators);
}
/// @notice beaconTimestamp must be after the current checkpoint
function testFuzz_revert_beaconTimestampInvalid() public {
cheats.warp(10 days);
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
// Ensure we have more than one validator (_newEigenPodStaker allocates a nonzero amt of eth)
cheats.deal(address(staker), address(staker).balance + 32 ether);
(uint40[] memory validators,,) = staker.startValidators();
uint40[] memory firstValidator = new uint40[](1);
firstValidator[0] = validators[0];
staker.verifyWithdrawalCredentials(firstValidator);
// Start a checkpoint so `currentCheckpointTimestamp` is nonzero
staker.startCheckpoint();
// Try to verify withdrawal credentials at the current block
cheats.expectRevert(IEigenPodErrors.BeaconTimestampTooFarInPast.selector);
staker.verifyWithdrawalCredentials(validators);
}
/// @notice Check for revert on input array mismatch lengths
function testFuzz_revert_inputArrayLengthsMismatch(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
EigenPod pod = staker.pod();
CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators);
uint40[] memory invalidValidatorIndices = new uint40[](validators.length + 1);
bytes[] memory invalidValidatorFieldsProofs = new bytes[](proofs.validatorFieldsProofs.length + 1);
bytes32[][] memory invalidValidatorFields = new bytes32[][](proofs.validatorFields.length + 1);
cheats.startPrank(address(staker));
cheats.expectRevert(IEigenPodErrors.InputArrayLengthMismatch.selector);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: invalidValidatorIndices,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
cheats.expectRevert(IEigenPodErrors.InputArrayLengthMismatch.selector);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: validators,
validatorFieldsProofs: invalidValidatorFieldsProofs,
validatorFields: proofs.validatorFields
});
cheats.expectRevert(IEigenPodErrors.InputArrayLengthMismatch.selector);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: invalidValidatorFields
});
cheats.stopPrank();
}
/// @notice Check beaconStateRootProof reverts on invalid length or invalid proof
function testFuzz_revert_beaconStateRootProofInvalid(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
EigenPod pod = staker.pod();
CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators);
bytes memory proofWithInvalidLength = new bytes(proofs.stateRootProof.proof.length + 1);
BeaconChainProofs.StateRootProof memory invalidStateRootProof =
BeaconChainProofs.StateRootProof({beaconStateRoot: proofs.stateRootProof.beaconStateRoot, proof: proofWithInvalidLength});
cheats.startPrank(address(staker));
cheats.expectRevert(BeaconChainProofs.InvalidProofLength.selector);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: invalidStateRootProof,
validatorIndices: validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
// Change the proof to have an invalid value
bytes1 randValue = bytes1(keccak256(abi.encodePacked(proofs.stateRootProof.proof[0])));
uint proofLength = proofs.stateRootProof.proof.length;
uint randIndex = bound(rand, 0, proofLength - 1);
proofs.stateRootProof.proof[randIndex] = randValue;
cheats.expectRevert(BeaconChainProofs.InvalidProof.selector);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
cheats.stopPrank();
}
/// @notice attempt to verify validator credentials in both ACTIVE and WITHDRAWN states
/// check reverts
function testFuzz_revert_validatorsWithdrawn(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
// now that validators are ACTIVE, ensure we can't verify them again
cheats.expectRevert(IEigenPodErrors.CredentialsAlreadyVerified.selector);
staker.verifyWithdrawalCredentials(validators);
staker.exitValidators(validators);
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
staker.completeCheckpoint();
beaconChain.advanceEpoch_NoRewards();
// now that validators are WITHDRAWN, ensure we can't verify them again
cheats.expectRevert(IEigenPodErrors.CredentialsAlreadyVerified.selector);
staker.verifyWithdrawalCredentials(validators);
}
/// @notice attempt to verify validator credentials after they have exited
/// check reverts
function testFuzz_revert_validatorsExited(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
// Exit validators from beacon chain and withdraw to pod
staker.exitValidators(validators);
beaconChain.advanceEpoch();
// now that validators are exited, ensure we can't verify them
cheats.expectRevert(IEigenPodErrors.ValidatorIsExitingBeaconChain.selector);
staker.verifyWithdrawalCredentials(validators);
}
/// @notice modify withdrawal credentials to cause a revert
function testFuzz_revert_invalidWithdrawalAddress(uint rand, bytes32 invalidWithdrawalCredentials) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
EigenPod pod = staker.pod();
CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators);
// Set invalid withdrawal credentials in validatorFields
uint VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1;
proofs.validatorFields[0][VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX] = invalidWithdrawalCredentials;
cheats.startPrank(address(staker));
cheats.expectRevert(IEigenPodErrors.WithdrawalCredentialsNotForEigenPod.selector);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
cheats.stopPrank();
}
/// @notice test verify wc reverts when fork timestamp is zero
function test_revert_forkTimestampZero() public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
(uint40[] memory validators,,) = staker.startValidators();
// Set zero fork timestamp to zero
eigenPodManagerMock.setPectraForkTimestamp(0);
cheats.expectRevert(IEigenPodErrors.ForkTimestampZero.selector);
staker.verifyWithdrawalCredentials(validators);
}
/// @notice modify validator field length to cause a revert
function testFuzz_revert_invalidValidatorFields(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
EigenPod pod = staker.pod();
CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators);
// change validator field length to invalid value
bytes32[] memory invalidValidatorFields = new bytes32[](BeaconChainProofs.VALIDATOR_FIELDS_LENGTH + 1);
for (uint i = 0; i < BeaconChainProofs.VALIDATOR_FIELDS_LENGTH; i++) {
invalidValidatorFields[i] = proofs.validatorFields[0][i];
}
proofs.validatorFields[0] = invalidValidatorFields;
cheats.startPrank(address(staker));
cheats.expectRevert(BeaconChainProofs.InvalidValidatorFieldsLength.selector);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
cheats.stopPrank();
}
/// @notice modify validator activation epoch to cause a revert
function testFuzz_revert_activationEpochNotSet(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
EigenPod pod = staker.pod();
CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators);
proofs.validatorFields[0][BeaconChainProofs.VALIDATOR_ACTIVATION_EPOCH_INDEX] =
_toLittleEndianUint64(BeaconChainProofs.FAR_FUTURE_EPOCH);
cheats.startPrank(address(staker));
cheats.expectRevert(IEigenPodErrors.ValidatorInactiveOnBeaconChain.selector);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
cheats.stopPrank();
}
/// @notice modify validator proof length to cause a revert
function testFuzz_revert_invalidValidatorProofLength(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
EigenPod pod = staker.pod();
CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators);
// add an element to the proof
proofs.validatorFieldsProofs[0] = new bytes(proofs.validatorFieldsProofs[0].length + 32);
cheats.startPrank(address(staker));
cheats.expectRevert(BeaconChainProofs.InvalidProofLength.selector);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
cheats.stopPrank();
}
/// @notice modify validator pubkey to cause a revert
function testFuzz_revert_invalidValidatorProof(uint rand, bytes32 randPubkey) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
EigenPod pod = staker.pod();
CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators);
// change validator pubkey to an invalid value causing a revert
uint VALIDATOR_PUBKEY_INDEX = 0;
proofs.validatorFields[0][VALIDATOR_PUBKEY_INDEX] = randPubkey;
cheats.startPrank(address(staker));
cheats.expectRevert(BeaconChainProofs.InvalidProof.selector);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
cheats.stopPrank();
}
/// @notice fuzz test a eigenPod with multiple validators. Using timemachine to assert values over time
function testFuzz_verifyWithdrawalCredentials(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
// Complete a quick empty checkpoint so we have a nonzero value for `lastCheckpointedAt`
staker.startCheckpoint();
beaconChain.advanceEpoch_NoRewards();
CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators);
for (uint i; i < validators.length; i++) {
cheats.expectEmit(true, true, true, true, address(pod));
emit ValidatorRestaked(validators[i]);
cheats.expectEmit(true, true, true, true, address(pod));
emit ValidatorBalanceUpdated(validators[i], pod.lastCheckpointTimestamp(), beaconChain.effectiveBalance(validators[i]));
}
// staker.verifyWithdrawalCredentials(validators);
// Prank either the staker or proof submitter
cheats.prank(rand % 2 == 1 ? address(staker) : pod.proofSubmitter());
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
assert_Snap_Added_ActiveValidatorCount(staker, validators.length, "staker should have increased active validator count");
assert_Snap_Added_ActiveValidators(staker, validators, "validators should each be active");
// Check ValidatorInfo values for each validator
for (uint i = 0; i < validators.length; i++) {
bytes32 pubkeyHash = beaconChain.pubkeyHash(validators[i]);
bytes memory pubkey = beaconChain.pubkey(validators[i]);
IEigenPodTypes.ValidatorInfo memory info = pod.validatorPubkeyHashToInfo(pubkeyHash);
IEigenPodTypes.ValidatorInfo memory pkInfo = pod.validatorPubkeyToInfo(pubkey);
assertTrue(pod.validatorStatus(pubkey) == IEigenPodTypes.VALIDATOR_STATUS.ACTIVE, "validator status should be active");
assertEq(keccak256(abi.encode(info)), keccak256(abi.encode(pkInfo)), "validator info should be identical");
assertEq(info.validatorIndex, validators[i], "should have assigned correct validator index");
assertEq(info.restakedBalanceGwei, beaconChain.effectiveBalance(validators[i]), "should have restaked full effective balance");
assertEq(info.lastCheckpointedAt, pod.lastCheckpointTimestamp(), "should have recorded correct update time");
}
}
}
contract EigenPodUnitTests_startCheckpoint is EigenPodUnitTests {
/**
*
* startCheckpoint() tests
*
*/
/// @notice revert when startCheckpoint is not called by pod owner
function testFuzz_revert_callerIsNotPodOwnerOrProofSubmitter(uint rand, address invalidCaller) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
EigenPod pod = staker.pod();
address podOwner = pod.podOwner();
address proofSubmitter = pod.proofSubmitter();
cheats.assume(invalidCaller != podOwner && invalidCaller != proofSubmitter);
cheats.prank(invalidCaller);
cheats.expectRevert(IEigenPodErrors.OnlyEigenPodOwnerOrProofSubmitter.selector);
pod.startCheckpoint({revertIfNoBalance: false});
}
/// @notice test startCheckpoint reverts when paused
function testFuzz_revert_startCheckpointPaused(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
staker.startValidators();
cheats.prank(pauser);
eigenPodManagerMock.pause(2 ** PAUSED_START_CHECKPOINT);
cheats.expectRevert(IEigenPodErrors.CurrentlyPaused.selector);
staker.startCheckpoint();
}
/// @notice startCheckpoint should revert if another checkpoint already in progress
function testFuzz_revert_checkpointAlreadyStarted(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
staker.startCheckpoint();
cheats.expectRevert(IEigenPodErrors.CheckpointAlreadyActive.selector);
staker.startCheckpoint();
}
/// @notice startCheckpoint should revert if a checkpoint has already been completed this block
function testFuzz_revert_checkpointTwicePerBlock(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
staker.startCheckpoint();
staker.completeCheckpoint();
cheats.expectRevert(IEigenPodErrors.CannotCheckpointTwiceInSingleBlock.selector);
staker.startCheckpoint();
}
/// @notice if no rewards and revertIfNoBalance is set, startCheckpoint should revert
function testFuzz_revert_revertIfNoBalanceIsSet(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
EigenPod pod = staker.pod();
beaconChain.advanceEpoch_NoRewards();
cheats.prank(pod.podOwner());
cheats.expectRevert(IEigenPodErrors.NoBalanceToCheckpoint.selector);
pod.startCheckpoint({revertIfNoBalance: true});
}
/// @notice fuzz test an eigenpod with multiple validators and starting a checkpoint
function testFuzz_startCheckpoint(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
cheats.expectEmit(true, true, true, true, address(staker.pod()));
emit CheckpointCreated(uint64(block.timestamp), EIP_4788_ORACLE.timestampToBlockRoot(block.timestamp), validators.length);
staker.startCheckpoint();
check_StartCheckpoint_State(staker);
assertEq(
pod.currentCheckpoint().proofsRemaining, uint24(validators.length), "should have one proof remaining pre verified validator"
);
}
/// @notice fuzz test an eigenpod with multiple validators and starting a checkpoint
function testFuzz_startCheckpoint_AsProofSubmitter(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
cheats.expectEmit(true, true, true, true, address(staker.pod()));
emit CheckpointCreated(uint64(block.timestamp), EIP_4788_ORACLE.timestampToBlockRoot(block.timestamp), validators.length);
cheats.prank(pod.proofSubmitter());
pod.startCheckpoint(false);
check_StartCheckpoint_State(staker);
assertEq(
pod.currentCheckpoint().proofsRemaining, uint24(validators.length), "should have one proof remaining pre verified validator"
);
}
}
contract EigenPodUnitTests_verifyCheckpointProofs is EigenPodUnitTests {
/**
*
* verifyCheckpointProofs() tests
*
*/
/// @notice test verifyCheckpointProofs reverts when paused
function testFuzz_revert_verifyCheckpointProofsPaused(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp());
cheats.prank(pauser);
eigenPodManagerMock.pause(2 ** PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS);
cheats.expectRevert(IEigenPodErrors.CurrentlyPaused.selector);
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
}
/// @notice verifyCheckpointProofs should revert if checkpoint not in progress
function testFuzz_revert_checkpointNotStarted(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp());
cheats.expectRevert(IEigenPodErrors.NoActiveCheckpoint.selector);
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
}
/// @notice should revert if fork timestamp zero
function test_revert_forkTimestampZero() public {
// Setup verifyCheckpointProofs
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
beaconChain.advanceEpoch();
staker.startCheckpoint();
// Set forkTimestamp to zero
eigenPodManagerMock.setPectraForkTimestamp(0);
cheats.expectRevert(IEigenPodErrors.ForkTimestampZero.selector);
staker.completeCheckpoint();
}
/// @notice invalid proof length should revert
function testFuzz_revert_verifyBalanceContainerInvalidLengths(uint rand) public {
// Setup verifyCheckpointProofs
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
beaconChain.advanceEpoch();
staker.startCheckpoint();
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp());
// change the length of balanceContainerProof to cause a revert
proofs.balanceContainerProof.proof = new bytes(proofs.balanceContainerProof.proof.length + 1);
cheats.expectRevert(BeaconChainProofs.InvalidProofLength.selector);
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
}
/// @notice change one of the bytes in the balanceContainer proof to cause a revert
function testFuzz_revert_verifyBalanceContainerInvalidProof(uint rand) public {
// Setup verifyCheckpointProofs
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
beaconChain.advanceEpoch();
staker.startCheckpoint();
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp());
// randomly change one of the bytes in the proof to make the proof invalid
bytes1 randValue = bytes1(keccak256(abi.encodePacked(proofs.balanceContainerProof.proof[0])));
proofs.balanceContainerProof.proof[0] = randValue;
cheats.expectRevert(BeaconChainProofs.InvalidProof.selector);
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
}
/// @notice invalid balance proof length should revert
function testFuzz_revert_verifyValidatorBalanceInvalidLength(uint rand) public {
// Setup verifyCheckpointProofs
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
beaconChain.advanceEpoch();
staker.startCheckpoint();
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp());
// change the length of balance proof to cause a revert
proofs.balanceProofs[0].proof = new bytes(proofs.balanceProofs[0].proof.length + 1);
cheats.expectRevert(BeaconChainProofs.InvalidProofLength.selector);
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
}
/// @notice change one of the bytes in one of the balance proofs to cause a revert
function testFuzz_revert_verifyValidatorBalanceInvalidProof(uint rand) public {
// Setup verifyCheckpointProofs
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
beaconChain.advanceEpoch();
staker.startCheckpoint();
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp());
// randomly change one of the bytes in the first proof to make the proof invalid
bytes1 randValue = bytes1(keccak256(abi.encodePacked(proofs.balanceProofs[0].proof[0])));
proofs.balanceProofs[0].proof[0] = randValue;
cheats.expectRevert(BeaconChainProofs.InvalidProof.selector);
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
}
/// @notice test that verifyCheckpointProofs skips proofs submitted for non-ACTIVE validators
function testFuzz_verifyCheckpointProofs_skipIfNotActive(uint rand) public {
// Setup verifyCheckpointProofs
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators1,,) = staker.startValidators();
// Ensure we have more than one validator (_newEigenPodStaker allocates a nonzero amt of eth)
cheats.deal(address(staker), address(staker).balance + 32 ether);
(uint40[] memory validators2,,) = staker.startValidators();
// Create a joint set of validators & verify withdrawal credentials
uint40[] memory validators = new uint40[](validators1.length + validators2.length);
for (uint i = 0; i < validators1.length; i++) {
validators[i] = validators1[i];
}
for (uint i = 0; i < validators2.length; i++) {
validators[validators1.length + i] = validators2[i];
}
staker.verifyWithdrawalCredentials(validators);
// Exit a validator and advance epoch so the exit is picked up next checkpoint
uint40[] memory exitedValidator = new uint40[](1);
exitedValidator[0] = validators[0];
uint64 exitedBalanceGwei = staker.exitValidators(exitedValidator);
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(exitedValidator, pod.currentCheckpointTimestamp());
// verify checkpoint proof for one exited validator
// manually create a snapshot here for Snap checks
timeMachine.createSnapshot();
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
assertEq(0, pod.withdrawableRestakedExecutionLayerGwei(), "should not have updated withdrawable balance");
assertEq(pod.currentCheckpoint().proofsRemaining, validators.length - 1, "should have decreased proofs remaining by 1");
assert_Snap_Removed_ActiveValidatorCount(staker, 1, "should have removed one validator from active set");
assert_Snap_Removed_ActiveValidators(staker, exitedValidator, "should have set validator status to WITHDRAWN");
// attempt to submit the same proof and ensure that checkpoint did not progress
// the call should succeed, but nothing should happen
// manually create a snapshot here for Snap checks
timeMachine.createSnapshot();
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
assertEq(0, pod.withdrawableRestakedExecutionLayerGwei(), "should not have updated withdrawable balance");
assertEq(pod.currentCheckpoint().proofsRemaining, validators.length - 1, "should not have decreased proofs remaining");
assert_Snap_Unchanged_ActiveValidatorCount(staker, "should have the same active validator count");
// finally, finish the checkpoint by submitting all proofs
proofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp());
// manually create a snapshot here for Snap checks
timeMachine.createSnapshot();
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
assert_Snap_Unchanged_ActiveValidatorCount(staker, "should have the same active validator count after completing checkpoint");
assertEq(exitedBalanceGwei, pod.withdrawableRestakedExecutionLayerGwei(), "exited balance should now be withdrawable");
assertEq(pod.currentCheckpointTimestamp(), 0, "checkpoint should be complete");
}
/// @notice test that verifyCheckpointProofs skips duplicate checkpoint proofs
function testFuzz_verifyCheckpointProofs_skipIfAlreadyProven(uint rand) public {
// Setup verifyCheckpointProofs
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators1,,) = staker.startValidators();
// Ensure we have more than one validator (_newEigenPodStaker allocates a nonzero amt of eth)
cheats.deal(address(staker), address(staker).balance + 32 ether);
(uint40[] memory validators2,,) = staker.startValidators();
// Create a joint set of validators & verify withdrawal credentials
uint40[] memory validators = new uint40[](validators1.length + validators2.length);
for (uint i = 0; i < validators1.length; i++) {
validators[i] = validators1[i];
}
for (uint i = 0; i < validators2.length; i++) {
validators[validators1.length + i] = validators2[i];
}
staker.verifyWithdrawalCredentials(validators);
// Advance epoch and generate rewards on the beacon chain
beaconChain.advanceEpoch_NoWithdraw();
// select a single validator to submit multiple times
uint40[] memory singleValidator = new uint40[](1);
singleValidator[0] = validators[0];
staker.startCheckpoint();
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(singleValidator, pod.currentCheckpointTimestamp());
// verify checkpoint proof for one validator
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
assertEq(pod.currentCheckpoint().proofsRemaining, validators.length - 1, "should have decreased proofs remaining by 1");
// attempt to submit the same proof and ensure that checkpoint did not progress
// the call should succeed, but nothing should happen
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
assertEq(pod.currentCheckpoint().proofsRemaining, validators.length - 1, "should not have decreased proofs remaining");
// finally, finish the checkpoint by submitting all proofs
proofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp());
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
assertEq(pod.currentCheckpointTimestamp(), 0, "checkpoint should be complete");
// Check ValidatorInfo values for each validator
for (uint i = 0; i < validators.length; i++) {
bytes32 pubkeyHash = beaconChain.pubkeyHash(validators[i]);
IEigenPodTypes.ValidatorInfo memory info = pod.validatorPubkeyHashToInfo(pubkeyHash);
assertEq(info.restakedBalanceGwei, beaconChain.currentBalance(validators[i]), "should have restaked full current balance");
assertEq(info.lastCheckpointedAt, pod.lastCheckpointTimestamp(), "should have recorded correct update time");
}
}
/// @notice test that verifyCheckpointProofs sets validators to WITHDRAWN if they are exited
function testFuzz_verifyCheckpointProofs_validatorExits(uint rand) public {
// Setup verifyCheckpointProofs
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
// Exit validators and advance epoch so exits are picked up in next checkpoint
uint64 exitedBalanceGwei = staker.exitValidators(validators);
beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp());
// Verify checkpoint proofs emit the expected values
_expectEventsVerifyCheckpointProofs(staker, validators, proofs.balanceProofs);
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
assertEq(pod.currentCheckpointTimestamp(), 0, "checkpoint should be complete");
assertEq(pod.withdrawableRestakedExecutionLayerGwei(), exitedBalanceGwei, "exited balance should be withdrawable");
// Check ValidatorInfo values for each validator
for (uint i = 0; i < validators.length; i++) {
bytes32 pubkeyHash = beaconChain.pubkeyHash(validators[i]);
IEigenPodTypes.ValidatorInfo memory info = pod.validatorPubkeyHashToInfo(pubkeyHash);
assertEq(info.restakedBalanceGwei, 0, "should have 0 restaked balance");
assertEq(info.lastCheckpointedAt, pod.lastCheckpointTimestamp(), "should have recorded correct update time");
assertTrue(info.status == IEigenPodTypes.VALIDATOR_STATUS.WITHDRAWN, "should have recorded correct update time");
}
}
/// @notice fuzz test an eigenPod with multiple validators and verifyCheckpointProofs
function testFuzz_verifyCheckpointProofs(uint rand, bool epochRewards) public {
// Setup verifyCheckpointProofs
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
if (epochRewards) beaconChain.advanceEpoch();
else beaconChain.advanceEpoch_NoRewards();
staker.startCheckpoint();
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp());
// Verify checkpoint proofs emit the expected values
_expectEventsVerifyCheckpointProofs(staker, validators, proofs.balanceProofs);
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
assertEq(pod.currentCheckpointTimestamp(), 0, "checkpoint should be complete");
}
}
contract EigenPodUnitTests_verifyStaleBalance is EigenPodUnitTests {
/// @notice test verifyStaleBalance reverts when paused
function testFuzz_revert_verifyStaleBalancePaused(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validators[0]);
cheats.prank(pauser);
eigenPodManagerMock.pause(2 ** PAUSED_VERIFY_STALE_BALANCE);
cheats.expectRevert(IEigenPodErrors.CurrentlyPaused.selector);
pod.verifyStaleBalance({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
proof: proofs.validatorProof
});
}
/// @notice test verifyStaleBalance reverts when paused via the PAUSED_START_CHECKPOINT flag
function testFuzz_revert_verifyStaleBalancePausedViaStartCheckpoint(uint rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validators[0]);
cheats.prank(pauser);
eigenPodManagerMock.pause(2 ** PAUSED_START_CHECKPOINT);
cheats.expectRevert(IEigenPodErrors.CurrentlyPaused.selector);
pod.verifyStaleBalance({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
proof: proofs.validatorProof
});
}
/// @notice verifyStaleBalance should revert if validator balance too stale
function testFuzz_revert_validatorBalanceNotStale(uint rand) public {
// setup eigenpod staker and validators
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
uint40 validator = validators[0];
beaconChain.advanceEpoch();
staker.verifyWithdrawalCredentials(validators);
staker.startCheckpoint();
staker.completeCheckpoint();
uint64 lastCheckpointTimestamp = pod.lastCheckpointTimestamp();
// proof for given beaconTimestamp is not yet stale, this should revert
StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator);
cheats.expectRevert(IEigenPodErrors.BeaconTimestampTooFarInPast.selector);
pod.verifyStaleBalance({
beaconTimestamp: lastCheckpointTimestamp,
stateRootProof: proofs.stateRootProof,
proof: proofs.validatorProof
});
}
/// @notice checks staleness condition when a pod has never completed a checkpoint before
/// The only value that will result in a revert here is `beaconTimestamp == 0`
function testFuzz_revert_validatorBalanceNotStale_NeverCheckpointed(uint rand) public {
// setup eigenpod staker and validators
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
uint40 validator = validators[0];
beaconChain.advanceEpoch();
staker.verifyWithdrawalCredentials(validators);
// proof for given beaconTimestamp is not yet stale, this should revert
StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator);
cheats.expectRevert(IEigenPodErrors.BeaconTimestampTooFarInPast.selector);
pod.verifyStaleBalance({beaconTimestamp: 0, stateRootProof: proofs.stateRootProof, proof: proofs.validatorProof});
}
/// @notice verifyStaleBalance should revert if validator status is not ACTIVE
function testFuzz_revert_validatorStatusNotActive(uint rand) public {
// setup eigenpod staker and validators
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
uint40 validator = validators[0];
// Advance epoch and use stale balance proof without verifyingWithdrawalCredentials
// validator should be INACTIVE and cause a revert
beaconChain.advanceEpoch();
StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator);
cheats.expectRevert(IEigenPodErrors.ValidatorNotActiveInPod.selector);
pod.verifyStaleBalance({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
proof: proofs.validatorProof
});
}
/// @notice verifyStaleBalance should revert if validator is not slashed
function testFuzz_revert_validatorNotSlashed(uint rand) public {
// setup eigenpod staker and validators
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
uint40 validator = validators[0];
staker.verifyWithdrawalCredentials(validators);
// Advance epoch and use stale balance proof where the validator has not been slashed
// this should cause a revert
beaconChain.advanceEpoch();
StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator);
cheats.expectRevert(IEigenPodErrors.ValidatorNotSlashedOnBeaconChain.selector);
pod.verifyStaleBalance({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
proof: proofs.validatorProof
});
}
/// @notice verifyStaleBalance should revert with forkTimestamp of 0
function test_revert_forkTimestampZero() public {
// setup eigenpod staker and validators
(EigenPodUser staker,) = _newEigenPodStaker({rand: 0});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
uint40 validator = validators[0];
staker.verifyWithdrawalCredentials(validators);
// Slash validators and advance epoch
beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch();
StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator);
// Set forkTimestamp to zero
eigenPodManagerMock.setPectraForkTimestamp(0);
cheats.expectRevert(IEigenPodErrors.ForkTimestampZero.selector);
pod.verifyStaleBalance({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
proof: proofs.validatorProof
});
}
/// @notice verifyStaleBalance should revert with invalid beaconStateRoot proof length
function testFuzz_revert_beaconStateRootProofInvalidLength(uint rand) public {
// setup eigenpod staker and validators
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
uint40 validator = validators[0];
staker.verifyWithdrawalCredentials(validators);
// Slash validators and advance epoch
beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch();
StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator);
// change the proof to have an invalid length
bytes memory proofWithInvalidLength = new bytes(proofs.stateRootProof.proof.length + 1);
BeaconChainProofs.StateRootProof memory invalidStateRootProof =
BeaconChainProofs.StateRootProof({beaconStateRoot: proofs.stateRootProof.beaconStateRoot, proof: proofWithInvalidLength});
cheats.expectRevert(BeaconChainProofs.InvalidProofLength.selector);
pod.verifyStaleBalance({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: invalidStateRootProof,
proof: proofs.validatorProof
});
}
/// @notice verifyStaleBalance should revert with invalid beaconStateRoot proof
function testFuzz_revert_beaconStateRootProofInvalid(uint rand) public {
// setup eigenpod staker and validators
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
uint40 validator = validators[0];
staker.verifyWithdrawalCredentials(validators);
// Slash validators and advance epoch
beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch();
StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator);
// change the proof to have an invalid value
bytes1 randValue = bytes1(keccak256(abi.encodePacked(proofs.stateRootProof.proof[0])));
uint proofLength = proofs.stateRootProof.proof.length;
uint randIndex = bound(rand, 0, proofLength - 1);
proofs.stateRootProof.proof[randIndex] = randValue;
cheats.expectRevert(BeaconChainProofs.InvalidProof.selector);
pod.verifyStaleBalance({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
proof: proofs.validatorProof
});
}
/// @notice verifyStaleBalance should revert with invalid validatorFields and validator proof length
function testFuzz_revert_validatorContainerProofInvalidLength(uint rand) public {
// setup eigenpod staker and validators
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
uint40 validator = validators[0];
staker.verifyWithdrawalCredentials(validators);
// Slash validators and advance epoch
beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch();
StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator);
// change the proof to have an invalid length
uint proofLength = proofs.validatorProof.proof.length;
bytes memory invalidProof = new bytes(proofLength + 1);
BeaconChainProofs.ValidatorProof memory invalidValidatorProof =
BeaconChainProofs.ValidatorProof({validatorFields: proofs.validatorProof.validatorFields, proof: invalidProof});
cheats.expectRevert(BeaconChainProofs.InvalidProofLength.selector);
pod.verifyStaleBalance({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
proof: invalidValidatorProof
});
// Change the validator fields to have an invalid length
bytes32[] memory validatorFieldsInvalidLength = new bytes32[](proofs.validatorProof.validatorFields.length + 1);
for (uint i = 0; i < proofs.validatorProof.validatorFields.length; i++) {
validatorFieldsInvalidLength[i] = proofs.validatorProof.validatorFields[i];
}
proofs.validatorProof.validatorFields = validatorFieldsInvalidLength;
cheats.expectRevert(BeaconChainProofs.InvalidValidatorFieldsLength.selector);
pod.verifyStaleBalance({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
proof: proofs.validatorProof
});
}
/// @notice verifyStaleBalance should revert with invalid validatorContainer proof
function testFuzz_revert_validatorContainerProofInvalid(uint rand, bytes32 randWithdrawalCredentials) public {
// setup eigenpod staker and validators
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
uint40 validator = validators[0];
staker.verifyWithdrawalCredentials(validators);
// Slash validators and advance epoch
beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch();
StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator);
// change validator withdrawal creds to an invalid value causing a revert
uint VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1;
proofs.validatorProof.validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX] = randWithdrawalCredentials;
cheats.expectRevert(BeaconChainProofs.InvalidProof.selector);
pod.verifyStaleBalance({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
proof: proofs.validatorProof
});
}
function testFuzz_verifyStaleBalance(uint rand) public {
// setup eigenpod staker and validators
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
uint40 validator = validators[0];
staker.verifyWithdrawalCredentials(validators);
// Slash validators and advance epoch
beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Minor);
beaconChain.advanceEpoch();
StaleBalanceProofs memory proofs = beaconChain.getStaleBalanceProofs(validator);
cheats.expectEmit(true, true, true, true, address(staker.pod()));
emit CheckpointCreated(uint64(block.timestamp), EIP_4788_ORACLE.timestampToBlockRoot(block.timestamp), validators.length);
pod.verifyStaleBalance({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
proof: proofs.validatorProof
});
check_StartCheckpoint_State(staker);
}
}
/// @notice Tests that deneb proofs are not successful against pectra state
contract EigenPodUnitTests_DenebProofsAgainstPectra is EigenPodUnitTests {
/// @notice Set the beacon chain mock to Deneb Forkable
/// @notice The EigenPods by default use Pectra state given the early `proofTimestamp`
function setUp() public override {
EigenPodUnitTests.setUp();
// Set beaconChainMock to Deneb Forkable
beaconChain = BeaconChainMock(new BeaconChainMock_DenebForkable(EigenPodManager(address(eigenPodManagerMock)), GENESIS_TIME_LOCAL));
}
function testFuzz_revert_verifyWC_DenebAgainstPectra(uint24 rand) public {
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
(uint40[] memory validators,,) = staker.startValidators();
EigenPod pod = staker.pod();
// Get credentials proofs on Deneb state
CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators);
cheats.prank(address(staker));
cheats.expectRevert(BeaconChainProofs.InvalidProofLength.selector);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
}
function testFuzz_revert_completeCheckpoint_DenebAgainstPectra(uint24 rand) public {
// Forward proof timestamp so that EigenPods prove everything against Deneb state
eigenPodManagerMock.setPectraForkTimestamp(type(uint64).max);
// Setup verifyCheckpointProofs
(EigenPodUser staker,) = _newEigenPodStaker({rand: rand});
EigenPod pod = staker.pod();
(uint40[] memory validators,,) = staker.startValidators();
staker.verifyWithdrawalCredentials(validators);
beaconChain.advanceEpoch();
// Get checkpoint proofs on Deneb state
staker.startCheckpoint();
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp());
// Rewind proof timestamp such that EigenPods prove everything against Pectra state
eigenPodManagerMock.setPectraForkTimestamp(1 hours * 12);
cheats.expectRevert(BeaconChainProofs.InvalidProofLength.selector);
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
}
}
contract EigenPodHarnessSetup is EigenPodUnitTests {
// Harness that exposes internal functions for test
EigenPodHarness public eigenPodHarnessImplementation;
EigenPodHarness public eigenPodHarness;
function setUp() public virtual override {
EigenPodUnitTests.setUp();
// Deploy EP Harness
eigenPodHarnessImplementation =
new EigenPodHarness(ethPOSDepositMock, IEigenPodManager(address(eigenPodManagerMock)), GENESIS_TIME_LOCAL, "v9.9.9");
// Upgrade eigenPod to harness
UpgradeableBeacon(address(eigenPodBeacon)).upgradeTo(address(eigenPodHarnessImplementation));
eigenPodHarness = EigenPodHarness(payable(eigenPod));
}
}
/// @notice No unit tests as of now but would be good to add specific unit tests using proofs from our proofGen library
/// for a EigenPod on Holesky
contract EigenPodUnitTests_proofParsingTests is EigenPodHarnessSetup, ProofParsing {
using BytesLib for bytes;
using BeaconChainProofs for *;
// Params to _verifyWithdrawalCredentials, can be set in test or helper function
uint64 oracleTimestamp;
bytes32 beaconStateRoot;
uint40 validatorIndex;
bytes validatorFieldsProof;
bytes32[] validatorFields;
function _assertWithdrawalCredentialsSet(uint restakedBalanceGwei) internal view {
IEigenPodTypes.ValidatorInfo memory validatorInfo = eigenPodHarness.validatorPubkeyHashToInfo(validatorFields[0]);
assertEq(uint8(validatorInfo.status), uint8(IEigenPodTypes.VALIDATOR_STATUS.ACTIVE), "Validator status should be active");
assertEq(validatorInfo.validatorIndex, validatorIndex, "Validator index incorrectly set");
assertEq(validatorInfo.lastCheckpointedAt, oracleTimestamp, "Last checkpointed at timestamp incorrectly set");
assertEq(validatorInfo.restakedBalanceGwei, restakedBalanceGwei, "Restaked balance gwei not set correctly");
}
function _setWithdrawalCredentialParams() public {
// Set beacon state root, validatorIndex
beaconStateRoot = getBeaconStateRoot();
validatorIndex = uint40(getValidatorIndex());
validatorFieldsProof = getWithdrawalCredentialProof(); // Validator fields are proven here
validatorFields = getValidatorFields();
// Get an oracle timestamp
cheats.warp(GENESIS_TIME_LOCAL + 1 days);
oracleTimestamp = uint64(block.timestamp);
}
///@notice Effective balance is > 32 ETH
modifier setWithdrawalCredentialsExcess() {
// Set JSON and params
setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json");
// Set beacon state root, validatorIndex
beaconStateRoot = getBeaconStateRoot();
validatorIndex = uint40(getValidatorIndex());
validatorFieldsProof = getWithdrawalCredentialProof(); // Validator fields are proven here
validatorFields = getValidatorFields();
// Get an oracle timestamp
cheats.warp(GENESIS_TIME_LOCAL + 1 days);
oracleTimestamp = uint64(block.timestamp);
eigenPodHarness.verifyWithdrawalCredentials(oracleTimestamp, beaconStateRoot, validatorIndex, validatorFieldsProof, validatorFields);
_;
}
}
````
## File: src/test/unit/PausableUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "../../contracts/permissions/PauserRegistry.sol";
import "../harnesses/PausableHarness.sol";
contract PausableUnitTests is Test {
Vm cheats = Vm(VM_ADDRESS);
PauserRegistry public pauserRegistry;
PausableHarness public pausable;
address public pauser = address(555);
address public unpauser = address(999);
uint public initPausedStatus = 0;
mapping(address => bool) public isExcludedFuzzAddress;
/// @notice Emitted when the pause is triggered by `account`, and changed to `newPausedStatus`.
event Paused(address indexed account, uint newPausedStatus);
/// @notice Emitted when the pause is lifted by `account`, and changed to `newPausedStatus`.
event Unpaused(address indexed account, uint newPausedStatus);
function setUp() public virtual {
address[] memory pausers = new address[](1);
pausers[0] = pauser;
pauserRegistry = new PauserRegistry(pausers, unpauser);
pausable = new PausableHarness(pauserRegistry);
pausable.initializePauser(initPausedStatus);
}
function testPause(uint previousPausedStatus, uint newPausedStatus) public {
newPausedStatus = previousPausedStatus | newPausedStatus;
cheats.startPrank(pauser);
cheats.expectEmit(true, true, true, true, address(pausable));
emit Paused(pauser, previousPausedStatus);
pausable.pause(previousPausedStatus);
cheats.stopPrank();
require(pausable.paused() == previousPausedStatus, "previousPausedStatus not set correctly");
cheats.startPrank(pauser);
cheats.expectEmit(true, true, true, true, address(pausable));
emit Paused(pauser, newPausedStatus);
pausable.pause(newPausedStatus);
cheats.stopPrank();
require(pausable.paused() == newPausedStatus, "newPausedStatus not set correctly");
}
function testPause_RevertsWhenCalledByNotPauser(address notPauser, uint newPausedStatus) public {
cheats.assume(notPauser != pauser);
cheats.startPrank(notPauser);
cheats.expectRevert(IPausable.OnlyPauser.selector);
pausable.pause(newPausedStatus);
cheats.stopPrank();
}
function testPauseAll(uint previousPausedStatus) public {
cheats.startPrank(pauser);
cheats.expectEmit(true, true, true, true, address(pausable));
emit Paused(pauser, previousPausedStatus);
pausable.pause(previousPausedStatus);
cheats.stopPrank();
require(pausable.paused() == previousPausedStatus, "previousPausedStatus not set correctly");
cheats.startPrank(pauser);
cheats.expectEmit(true, true, true, true, address(pausable));
emit Paused(pauser, type(uint).max);
pausable.pauseAll();
cheats.stopPrank();
require(pausable.paused() == type(uint).max, "newPausedStatus not set correctly");
}
function testPauseAll_RevertsWhenCalledByNotPauser(address notPauser) public {
cheats.assume(notPauser != pauser);
cheats.startPrank(notPauser);
cheats.expectRevert(IPausable.OnlyPauser.selector);
pausable.pauseAll();
cheats.stopPrank();
}
function testPause_RevertsWhenTryingToUnpause(uint previousPausedStatus, uint newPausedStatus) public {
// filter to only fuzzed inputs which would (improperly) flip any bits to '0'.
cheats.assume(previousPausedStatus & newPausedStatus != previousPausedStatus);
cheats.startPrank(pauser);
cheats.expectEmit(true, true, true, true, address(pausable));
emit Paused(pauser, previousPausedStatus);
pausable.pause(previousPausedStatus);
cheats.stopPrank();
require(pausable.paused() == previousPausedStatus, "previousPausedStatus not set correctly");
cheats.startPrank(pauser);
cheats.expectRevert(IPausable.InvalidNewPausedStatus.selector);
pausable.pause(newPausedStatus);
cheats.stopPrank();
}
function testPauseSingleIndex(uint previousPausedStatus, uint8 indexToPause) public {
uint newPausedStatus = (2 ** indexToPause);
testPause(previousPausedStatus, newPausedStatus);
require(pausable.paused(indexToPause), "index is not paused");
}
function testUnpause(uint previousPausedStatus, uint newPausedStatus) public {
// ensure newPausedStatus is a subset of previousPausedStatus (i.e. only unpause)
previousPausedStatus = type(uint).max; // all bits set to 1
newPausedStatus = previousPausedStatus & (type(uint).max - 1); // clear lowest bit
cheats.startPrank(pauser);
cheats.expectEmit(true, true, true, true, address(pausable));
emit Paused(pauser, previousPausedStatus);
pausable.pause(previousPausedStatus);
cheats.stopPrank();
require(pausable.paused() == previousPausedStatus, "previousPausedStatus not set correctly");
cheats.startPrank(pausable.pauserRegistry().unpauser());
cheats.expectEmit(true, true, true, true, address(pausable));
emit Unpaused(pausable.pauserRegistry().unpauser(), newPausedStatus);
pausable.unpause(newPausedStatus);
cheats.stopPrank();
require(pausable.paused() == newPausedStatus, "newPausedStatus not set correctly");
}
function testUnpause_RevertsWhenCalledByNotUnpauser(address notUnpauser, uint newPausedStatus) public {
cheats.assume(notUnpauser != pausable.pauserRegistry().unpauser());
cheats.startPrank(notUnpauser);
cheats.expectRevert(IPausable.OnlyUnpauser.selector);
pausable.unpause(newPausedStatus);
cheats.stopPrank();
}
function testUnpause_RevertsWhenTryingToPause(uint previousPausedStatus, uint newPausedStatus) public {
// filter to only fuzzed inputs which would (improperly) flip any bits to '1'.
cheats.assume(~previousPausedStatus & ~newPausedStatus != ~previousPausedStatus);
cheats.startPrank(pauser);
pausable.pause(previousPausedStatus);
cheats.stopPrank();
require(pausable.paused() == previousPausedStatus, "previousPausedStatus not set correctly");
cheats.startPrank(pausable.pauserRegistry().unpauser());
cheats.expectRevert(IPausable.InvalidNewPausedStatus.selector);
pausable.unpause(newPausedStatus);
cheats.stopPrank();
}
}
````
## File: src/test/unit/PauserRegistryUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "../../contracts/interfaces/IPausable.sol";
import "../../contracts/permissions/PauserRegistry.sol";
contract PauserRegistryUnitTests is Test {
Vm cheats = Vm(VM_ADDRESS);
PauserRegistry public pauserRegistry;
address public pauser = address(555);
address public unpauser = address(999);
mapping(address => bool) public isExcludedFuzzAddress;
event PauserStatusChanged(address pauser, bool canPause);
event UnpauserChanged(address previousUnpauser, address newUnpauser);
function setUp() public virtual {
address[] memory pausers = new address[](1);
pausers[0] = pauser;
pauserRegistry = new PauserRegistry(pausers, unpauser);
}
function testSetIsPauserTrue(address newPauser) public {
cheats.assume(newPauser != address(0));
cheats.startPrank(pauserRegistry.unpauser());
cheats.expectEmit(true, true, true, true, address(pauserRegistry));
emit PauserStatusChanged(newPauser, true);
pauserRegistry.setIsPauser(newPauser, true);
cheats.stopPrank();
require(pauserRegistry.isPauser(newPauser), "newPauser not set correctly");
}
function testSetIsPauserFalse() public {
cheats.startPrank(pauserRegistry.unpauser());
cheats.expectEmit(true, true, true, true, address(pauserRegistry));
emit PauserStatusChanged(pauser, false);
pauserRegistry.setIsPauser(pauser, false);
cheats.stopPrank();
require(!pauserRegistry.isPauser(pauser), "pauser not set correctly");
}
function testSetUnpauser(address newUnpauser) public {
cheats.assume(newUnpauser != address(0));
cheats.startPrank(pauserRegistry.unpauser());
address oldAddress = pauserRegistry.unpauser();
cheats.expectEmit(true, true, true, true, address(pauserRegistry));
emit UnpauserChanged(oldAddress, newUnpauser);
pauserRegistry.setUnpauser(newUnpauser);
cheats.stopPrank();
require(pauserRegistry.unpauser() == newUnpauser, "pauser not set correctly");
}
function testSetPauser_RevertsWhenCallingFromNotUnpauser(address notUnpauser, address newPauser) public {
cheats.assume(notUnpauser != pauserRegistry.unpauser());
cheats.assume(newPauser != address(0));
cheats.startPrank(notUnpauser);
cheats.expectRevert(IPausable.OnlyUnpauser.selector);
pauserRegistry.setIsPauser(newPauser, true);
cheats.stopPrank();
}
function testSetUnpauser_RevertsWhenCallingFromNotUnpauser(address notUnpauser, address newUnpauser) public {
cheats.assume(notUnpauser != pauserRegistry.unpauser());
cheats.assume(newUnpauser != address(0));
cheats.startPrank(notUnpauser);
cheats.expectRevert(IPausable.OnlyUnpauser.selector);
pauserRegistry.setUnpauser(newUnpauser);
cheats.stopPrank();
}
function testSetPauser_RevertsWhenSettingToZeroAddress() public {
address newPauser = address(0);
cheats.startPrank(pauserRegistry.unpauser());
cheats.expectRevert(IPauserRegistry.InputAddressZero.selector);
pauserRegistry.setIsPauser(newPauser, true);
cheats.stopPrank();
}
function testSetUnpauser_RevertsWhenSettingToZeroAddress() public {
address newUnpauser = address(0);
cheats.startPrank(pauserRegistry.unpauser());
cheats.expectRevert(IPauserRegistry.InputAddressZero.selector);
pauserRegistry.setUnpauser(newUnpauser);
cheats.stopPrank();
}
}
````
## File: src/test/unit/PermissionControllerUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "src/contracts/permissions/PermissionController.sol";
import "src/contracts/interfaces/IPermissionController.sol";
import "src/test/utils/EigenLayerUnitTestSetup.sol";
contract PermissionControllerUnitTests is EigenLayerUnitTestSetup, IPermissionControllerEvents, IPermissionControllerErrors {
address account = address(0x1);
address admin = address(0x2);
address admin2 = address(0x3);
address appointee1 = address(0x4);
address appointee2 = address(0x5);
address target1;
address target2;
bytes4 selector1 = IDelegationManager.updateOperatorMetadataURI.selector;
bytes4 selector2 = IAllocationManager.modifyAllocations.selector;
function setUp() public virtual override {
// Setup - already deploys permissionController
EigenLayerUnitTestSetup.setUp();
// Set targets
target1 = address(delegationManagerMock);
target2 = address(allocationManagerMock);
}
}
contract PermissionControllerUnitTests_AddPendingAdmin is PermissionControllerUnitTests {
function testFuzz_getAdmin_notSet(address account) public view filterFuzzedAddressInputs(account) {
address[] memory admins = permissionController.getAdmins(account);
assertEq(admins[0], account, "Account is not admin");
assertTrue(permissionController.isAdmin(account, account), "Account is not admin");
}
/// @notice Tests the setAdmin function when it has not been initialized
function test_revert_caller_not_account() public {
cheats.expectRevert(IPermissionControllerErrors.NotAdmin.selector);
permissionController.addPendingAdmin(account, admin);
}
function test_revert_adminPending() public {
// Set admin to be pending
cheats.startPrank(account);
permissionController.addPendingAdmin(account, admin);
// Expect revert
cheats.expectRevert(IPermissionControllerErrors.AdminAlreadyPending.selector);
permissionController.addPendingAdmin(account, admin);
}
function test_addPendingAdmin_initialSet() public {
// Expect emit
cheats.expectEmit(true, true, true, true);
emit PendingAdminAdded(account, admin);
cheats.prank(account);
permissionController.addPendingAdmin(account, admin);
// Check storage
address[] memory pendingAdmins = permissionController.getPendingAdmins(account);
assertEq(pendingAdmins[0], admin, "Admin not set to pending");
assertTrue(permissionController.isPendingAdmin(account, admin), "Pending admin not set correctly");
assertFalse(permissionController.isAdmin(account, admin), "Pending admin not set correctly");
}
}
contract PermissionControllerUnitTests_RemovePendingAdmin is PermissionControllerUnitTests {
function setUp() public virtual override {
// Setup
PermissionControllerUnitTests.setUp();
// Set admin to be pending
cheats.prank(account);
permissionController.addPendingAdmin(account, admin);
}
function test_revert_notAdmin() public {
cheats.expectRevert(IPermissionControllerErrors.NotAdmin.selector);
permissionController.removePendingAdmin(account, admin);
}
function test_revert_notPending() public {
// Expect revert
cheats.prank(account);
cheats.expectRevert(IPermissionControllerErrors.AdminNotPending.selector);
permissionController.removePendingAdmin(account, admin2);
}
function test_removePendingAdmin() public {
// Expect emit
cheats.expectEmit(true, true, true, true);
emit PendingAdminRemoved(account, admin);
cheats.prank(account);
permissionController.removePendingAdmin(account, admin);
// Check storage
address[] memory pendingAdmins = permissionController.getPendingAdmins(account);
assertEq(pendingAdmins.length, 0, "Pending admin not removed");
}
}
contract PermissionControllerUnitTests_AcceptAdmin is PermissionControllerUnitTests {
function setUp() public virtual override {
// Setup
PermissionControllerUnitTests.setUp();
// Set admin
cheats.prank(account);
permissionController.addPendingAdmin(account, admin);
}
function test_revert_adminNotPending() public {
cheats.prank(admin2);
cheats.expectRevert(IPermissionControllerErrors.AdminNotPending.selector);
permissionController.acceptAdmin(account);
}
function test_acceptAdmin() public {
// Expect emit
cheats.expectEmit(true, true, true, true);
emit AdminSet(account, admin);
cheats.prank(admin);
permissionController.acceptAdmin(account);
// Check storage
address[] memory admins = permissionController.getAdmins(account);
address[] memory pendingAdmins = permissionController.getPendingAdmins(account);
assertEq(pendingAdmins.length, 0, "Pending admin not removed");
assertEq(admins.length, 1, "Additional admin not added");
assertFalse(permissionController.isAdmin(account, account), "Account should no longer be admin");
assertTrue(permissionController.isAdmin(account, admin), "Admin not set");
assertTrue(permissionController.canCall(account, admin, address(0), bytes4(0)), "Admin cannot call");
}
function test_addMultipleAdmin_fromAccount() public {
// Add another pending admin
cheats.prank(account);
permissionController.addPendingAdmin(account, admin2);
// Assert pending admins length
address[] memory pendingAdmins = permissionController.getPendingAdmins(account);
assertEq(pendingAdmins.length, 2, "Additional pending admin not added");
// Accept admins
cheats.prank(admin);
permissionController.acceptAdmin(account);
cheats.prank(admin2);
permissionController.acceptAdmin(account);
// Check storage
address[] memory admins = permissionController.getAdmins(account);
assertEq(admins.length, 2, "Additional admin not added");
assertTrue(permissionController.isAdmin(account, admin), "Old admin should still be admin");
assertTrue(permissionController.isAdmin(account, admin2), "New admin not set correctly");
assertTrue(permissionController.canCall(account, admin, address(0), bytes4(0)), "Admin cannot call");
assertTrue(permissionController.canCall(account, admin2, address(0), bytes4(0)), "Admin cannot call");
}
function test_addMultipleAdmin_newAdminAdds() public {
// Accept admin
cheats.startPrank(admin);
permissionController.acceptAdmin(account);
// Add another pending admin
permissionController.addPendingAdmin(account, admin2);
cheats.stopPrank();
// Accept admins
cheats.prank(admin2);
permissionController.acceptAdmin(account);
// Check storage
address[] memory admins = permissionController.getAdmins(account);
assertEq(admins.length, 2, "Additional admin not added");
assertTrue(permissionController.isAdmin(account, admin), "Old admin should still be admin");
assertTrue(permissionController.isAdmin(account, admin2), "New admin not set correctly");
assertTrue(permissionController.canCall(account, admin, address(0), bytes4(0)), "Admin cannot call");
assertTrue(permissionController.canCall(account, admin2, address(0), bytes4(0)), "Admin cannot call");
}
}
contract PermissionControllerUnitTests_RemoveAdmin is PermissionControllerUnitTests {
function setUp() public virtual override {
// Setup
PermissionControllerUnitTests.setUp();
// Set admins
cheats.startPrank(account);
permissionController.addPendingAdmin(account, admin);
permissionController.addPendingAdmin(account, admin2);
cheats.stopPrank();
// Accept admins
cheats.prank(admin);
permissionController.acceptAdmin(account);
cheats.prank(admin2);
permissionController.acceptAdmin(account);
}
function test_revert_notAdmin() public {
cheats.prank(appointee1);
cheats.expectRevert(IPermissionControllerErrors.NotAdmin.selector);
permissionController.removeAdmin(account, admin);
}
function test_revert_adminNotSet() public {
cheats.prank(admin);
cheats.expectRevert(IPermissionControllerErrors.AdminNotSet.selector);
permissionController.removeAdmin(account, appointee1);
}
function test_revert_cannotHaveZeroAdmins() public {
// Remove admin2
cheats.startPrank(admin);
permissionController.removeAdmin(account, admin2);
// Remove admin
cheats.expectRevert(IPermissionControllerErrors.CannotHaveZeroAdmins.selector);
permissionController.removeAdmin(account, admin);
}
function test_removeAdmin() public {
// Expect emit
cheats.expectEmit(true, true, true, true);
emit AdminRemoved(account, admin);
cheats.prank(admin);
permissionController.removeAdmin(account, admin);
// Check storage
assertFalse(permissionController.isAdmin(account, admin), "Admin not removed");
assertFalse(permissionController.canCall(account, admin, address(0), bytes4(0)), "Admin can still call");
// Assert that admin2 is now the only admin
address[] memory admins = permissionController.getAdmins(account);
assertEq(admins[0], admin2, "Account is not admin");
assertTrue(permissionController.isAdmin(account, admin2), "Admin2 is not admin");
assertFalse(permissionController.isAdmin(account, admin), "Account should not be admin");
}
}
contract PermissionControllerUnitTests_SetAppointee is PermissionControllerUnitTests {
function setUp() public virtual override {
// Setup
PermissionControllerUnitTests.setUp();
// Set & accept admin
cheats.prank(account);
permissionController.addPendingAdmin(account, admin);
cheats.prank(admin);
permissionController.acceptAdmin(account);
}
function test_revert_notAdmin() public {
cheats.expectRevert(IPermissionControllerErrors.NotAdmin.selector);
permissionController.setAppointee(account, appointee1, address(0), bytes4(0));
}
function test_setAppointee() public {
// Expect emit
cheats.expectEmit(true, true, true, true);
emit AppointeeSet(account, appointee1, target1, selector1);
cheats.prank(admin);
permissionController.setAppointee(account, appointee1, target1, selector1);
// Validate Permissions
_validateSetAppointee(account, appointee1, target1, selector1);
}
function test_revert_appointeeAlreadySet() public {
// Set appointee
cheats.startPrank(admin);
permissionController.setAppointee(account, appointee1, target1, selector1);
cheats.expectRevert(IPermissionControllerErrors.AppointeeAlreadySet.selector);
permissionController.setAppointee(account, appointee1, target1, selector1);
cheats.stopPrank();
}
function test_setMultipleAppointees() public {
// Set appointees
cheats.startPrank(admin);
permissionController.setAppointee(account, appointee1, target1, selector1);
permissionController.setAppointee(account, appointee1, target2, selector2);
permissionController.setAppointee(account, appointee2, target1, selector1);
cheats.stopPrank();
// Validate Permissions
_validateSetAppointee(account, appointee1, target1, selector1);
_validateSetAppointee(account, appointee1, target2, selector2);
_validateSetAppointee(account, appointee2, target1, selector1);
}
function _validateSetAppointee(address accountToCheck, address appointee, address target, bytes4 selector) internal view {
assertTrue(permissionController.canCall(accountToCheck, appointee, target, selector));
_validateAppointeePermissions(accountToCheck, appointee, target, selector);
_validateGetAppointees(accountToCheck, appointee, target, selector);
}
function _validateAppointeePermissions(address accountToCheck, address appointee, address target, bytes4 selector) internal view {
(address[] memory targets, bytes4[] memory selectors) = permissionController.getAppointeePermissions(accountToCheck, appointee);
bool foundTargetSelector = false;
for (uint i = 0; i < targets.length; ++i) {
if (targets[i] == target) {
assertEq(selectors[i], selector, "Selector does not match target");
foundTargetSelector = true;
break;
}
}
assertTrue(foundTargetSelector, "Appointee does not have permission for given target and selector");
}
function _validateGetAppointees(address accountToCheck, address appointee, address target, bytes4 selector) internal view {
(address[] memory appointees) = permissionController.getAppointees(accountToCheck, target, selector);
bool foundAppointee = false;
for (uint i = 0; i < appointees.length; ++i) {
if (appointees[i] == appointee) {
foundAppointee = true;
break;
}
}
assertTrue(foundAppointee, "Appointee not in list of appointees for given target and selector");
}
}
contract PermissionControllerUnitTests_RemoveAppointee is PermissionControllerUnitTests {
function setUp() public virtual override {
// Setup
PermissionControllerUnitTests.setUp();
// Set & accept admin
cheats.prank(account);
permissionController.addPendingAdmin(account, admin);
cheats.prank(admin);
permissionController.acceptAdmin(account);
// Set appointees
cheats.startPrank(admin);
permissionController.setAppointee(account, appointee1, target1, selector1);
permissionController.setAppointee(account, appointee1, target2, selector2);
permissionController.setAppointee(account, appointee2, target1, selector1);
cheats.stopPrank();
}
function test_revert_notAdmin() public {
cheats.expectRevert(IPermissionControllerErrors.NotAdmin.selector);
permissionController.removeAppointee(account, appointee1, target1, selector1);
}
function test_removeAppointee() public {
// Expect emit
cheats.expectEmit(true, true, true, true);
emit AppointeeRemoved(account, appointee1, target1, selector1);
cheats.prank(admin);
permissionController.removeAppointee(account, appointee1, target1, selector1);
// Validate Permissions
_validateRemoveAppointee(account, appointee1, target1, selector1);
}
function test_revert_appointeeNotSet() public {
cheats.expectRevert(IPermissionControllerErrors.AppointeeNotSet.selector);
cheats.prank(admin);
permissionController.removeAppointee(account, appointee2, target2, selector2);
}
function test_removeMultipleAppointees() public {
// Remove appointees
cheats.startPrank(admin);
permissionController.removeAppointee(account, appointee1, target1, selector1);
permissionController.removeAppointee(account, appointee1, target2, selector2);
permissionController.removeAppointee(account, appointee2, target1, selector1);
cheats.stopPrank();
// Validate Permissions
_validateRemoveAppointee(account, appointee1, target1, selector1);
_validateRemoveAppointee(account, appointee1, target2, selector2);
_validateRemoveAppointee(account, appointee2, target1, selector1);
}
// Tests that the encoding from adding an appointee is properly decoded
function test_symmetricalTargetSelector() public view {
// Test Decoding
(address[] memory targets, bytes4[] memory selectors) = permissionController.getAppointeePermissions(account, appointee2);
assertEq(targets.length, 1, "Incorrect number of targets");
assertEq(selectors.length, 1, "Incorrect number of selectors");
assertEq(targets[0], target1, "Incorrect target");
assertEq(selectors[0], selector1, "Incorrect selector");
}
function _validateRemoveAppointee(address accountToCheck, address appointee, address target, bytes4 selector) internal view {
assertFalse(permissionController.canCall(accountToCheck, appointee, target, selector));
_validateAppointeePermissionsRemoved(accountToCheck, appointee, target, selector);
_validateGetAppointeesRemoved(accountToCheck, appointee, target, selector);
}
function _validateAppointeePermissionsRemoved(address accountToCheck, address appointee, address target, bytes4 selector)
internal
view
{
(address[] memory targets, bytes4[] memory selectors) = permissionController.getAppointeePermissions(accountToCheck, appointee);
bool foundTargetSelector = false;
for (uint i = 0; i < targets.length; ++i) {
if (targets[i] == target && selectors[i] == selector) {
foundTargetSelector = true;
break;
}
}
assertFalse(foundTargetSelector, "Appointee still has permission for given target and selector");
}
function _validateGetAppointeesRemoved(address accountToCheck, address appointee, address target, bytes4 selector) internal view {
(address[] memory appointees) = permissionController.getAppointees(accountToCheck, target, selector);
bool foundAppointee = false;
for (uint i = 0; i < appointees.length; ++i) {
if (appointees[i] == appointee) {
foundAppointee = true;
break;
}
}
assertFalse(foundAppointee, "Appointee still in list of appointees for given target and selector");
}
}
````
## File: src/test/unit/RewardsCoordinatorUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/mocks/ERC1271WalletMock.sol";
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol";
import "src/contracts/core/RewardsCoordinator.sol";
import "src/contracts/strategies/StrategyBase.sol";
import "src/test/utils/EigenLayerUnitTestSetup.sol";
import "src/test/mocks/Reenterer.sol";
import "src/test/mocks/ERC20Mock.sol";
/**
* @notice Unit testing of the RewardsCoordinator contract
* Contracts tested: RewardsCoordinator
* Contracts not mocked: StrategyBase, PauserRegistry
*/
contract RewardsCoordinatorUnitTests is EigenLayerUnitTestSetup, IRewardsCoordinatorEvents, IRewardsCoordinatorErrors {
// used for stack too deep
struct FuzzAVSRewardsSubmission {
address avs;
uint startTimestamp;
uint duration;
uint amount;
}
// Contract under test
RewardsCoordinator public rewardsCoordinator;
RewardsCoordinator public rewardsCoordinatorImplementation;
// Mocks
IERC20 token1;
IERC20 token2;
IERC20 token3;
IStrategy strategyMock1;
IStrategy strategyMock2;
IStrategy strategyMock3;
StrategyBase strategyImplementation;
uint mockTokenInitialSupply = 1e38 - 1;
StrategyAndMultiplier[] defaultStrategyAndMultipliers;
// Config Variables
/// @notice intervals(epochs) are 1 day: https://github.com/eigenfoundation/ELIPs/blob/main/ELIPs/ELIP-001.md#updated-calculation-interval-seconds
uint32 CALCULATION_INTERVAL_SECONDS = 1 days;
/// @notice Max duration is 5 epochs (2 weeks * 5 = 10 weeks in seconds)
uint32 MAX_REWARDS_DURATION = 70 days;
/// @notice Lower bound start range is ~3 months into the past, multiple of CALCULATION_INTERVAL_SECONDS
uint32 MAX_RETROACTIVE_LENGTH = 84 days;
/// @notice Upper bound start range is ~1 month into the future, multiple of CALCULATION_INTERVAL_SECONDS
uint32 MAX_FUTURE_LENGTH = 28 days;
/// @notice absolute min timestamp that a rewards can start at
uint32 GENESIS_REWARDS_TIMESTAMP = 1_712_188_800;
/// @notice Equivalent to 100%, but in basis points.
uint16 internal constant ONE_HUNDRED_IN_BIPS = 10_000;
/// @notice Delay in timestamp before a posted root can be claimed against
uint32 activationDelay = 7 days;
/// @notice the split for all operators across all avss
uint16 defaultSplitBips = 1000;
IERC20[] rewardTokens;
// RewardsCoordinator Constants
/// @dev Index for flag that pauses calling createAVSRewardsSubmission
uint8 internal constant PAUSED_AVS_REWARDS_SUBMISSION = 0;
/// @dev Index for flag that pauses calling createRewardsForAllSubmission
uint8 internal constant PAUSED_REWARDS_FOR_ALL_SUBMISSION = 1;
/// @dev Index for flag that pauses claiming
uint8 internal constant PAUSED_PROCESS_CLAIM = 2;
/// @dev Index for flag that pauses submitRoots
uint8 internal constant PAUSED_SUBMIT_ROOTS = 3;
/// @dev Index for flag that pauses rewardAllStakersAndOperators
uint8 internal constant PAUSED_REWARD_ALL_STAKERS_AND_OPERATORS = 4;
/// @dev Index for flag that pauses calling createOperatorDirectedAVSRewardsSubmission
uint8 internal constant PAUSED_OPERATOR_DIRECTED_AVS_REWARDS_SUBMISSION = 5;
/// @dev Index for flag that pauses calling setOperatorAVSSplit
uint8 internal constant PAUSED_OPERATOR_AVS_SPLIT = 6;
/// @dev Index for flag that pauses calling setOperatorPISplit
uint8 internal constant PAUSED_OPERATOR_PI_SPLIT = 7;
/// @dev Index for flag that pauses calling setOperatorSetSplit
uint8 internal constant PAUSED_OPERATOR_SET_SPLIT = 8;
/// @dev Index for flag that pauses calling setOperatorSetPerformanceRewardsSubmission
uint8 internal constant PAUSED_OPERATOR_DIRECTED_OPERATOR_SET_REWARDS_SUBMISSION = 9;
// RewardsCoordinator entities
address rewardsUpdater = address(1000);
address defaultAVS = address(1001);
address defaultClaimer = address(1002);
address rewardsForAllSubmitter = address(1003);
address defaultAppointee = address(1004);
function setUp() public virtual override {
// Setup
EigenLayerUnitTestSetup.setUp();
// Deploy RewardsCoordinator proxy and implementation
rewardsCoordinatorImplementation = new RewardsCoordinator(
IRewardsCoordinatorTypes.RewardsCoordinatorConstructorParams({
delegationManager: IDelegationManager(address(delegationManagerMock)),
strategyManager: IStrategyManager(address(strategyManagerMock)),
allocationManager: IAllocationManager(address(allocationManagerMock)),
pauserRegistry: pauserRegistry,
permissionController: IPermissionController(address(permissionController)),
CALCULATION_INTERVAL_SECONDS: CALCULATION_INTERVAL_SECONDS,
MAX_REWARDS_DURATION: MAX_REWARDS_DURATION,
MAX_RETROACTIVE_LENGTH: MAX_RETROACTIVE_LENGTH,
MAX_FUTURE_LENGTH: MAX_FUTURE_LENGTH,
GENESIS_REWARDS_TIMESTAMP: GENESIS_REWARDS_TIMESTAMP,
version: "v9.9.9"
})
);
rewardsCoordinator = RewardsCoordinator(
address(
new TransparentUpgradeableProxy(
address(rewardsCoordinatorImplementation),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(
RewardsCoordinator.initialize.selector,
address(this), // initOwner
0, // 0 is initialPausedStatus
rewardsUpdater,
activationDelay,
defaultSplitBips
)
)
)
);
// Deploy mock token and strategy
token1 = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, address(this));
token2 = new ERC20PresetFixedSupply("jeo boden", "MOCK2", mockTokenInitialSupply, address(this));
token3 = new ERC20PresetFixedSupply("pepe wif avs", "MOCK3", mockTokenInitialSupply, address(this));
strategyImplementation = new StrategyBase(IStrategyManager(address(strategyManagerMock)), pauserRegistry, "v9.9.9");
strategyMock1 = StrategyBase(
address(
new TransparentUpgradeableProxy(
address(strategyImplementation),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(StrategyBase.initialize.selector, token1)
)
)
);
strategyMock2 = StrategyBase(
address(
new TransparentUpgradeableProxy(
address(strategyImplementation),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(StrategyBase.initialize.selector, token2)
)
)
);
strategyMock3 = StrategyBase(
address(
new TransparentUpgradeableProxy(
address(strategyImplementation),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(StrategyBase.initialize.selector, token3)
)
)
);
IStrategy[] memory strategies = new IStrategy[](3);
strategies[0] = strategyMock1;
strategies[1] = strategyMock2;
strategies[2] = strategyMock3;
strategies = _sortArrayAsc(strategies);
strategyManagerMock.setStrategyWhitelist(strategies[0], true);
strategyManagerMock.setStrategyWhitelist(strategies[1], true);
strategyManagerMock.setStrategyWhitelist(strategies[2], true);
defaultStrategyAndMultipliers.push(StrategyAndMultiplier(IStrategy(address(strategies[0])), 1e18));
defaultStrategyAndMultipliers.push(StrategyAndMultiplier(IStrategy(address(strategies[1])), 2e18));
defaultStrategyAndMultipliers.push(StrategyAndMultiplier(IStrategy(address(strategies[2])), 3e18));
rewardsCoordinator.setRewardsForAllSubmitter(rewardsForAllSubmitter, true);
rewardsCoordinator.setRewardsUpdater(rewardsUpdater);
// Exclude from fuzzed tests
isExcludedFuzzAddress[address(rewardsCoordinator)] = true;
isExcludedFuzzAddress[address(rewardsUpdater)] = true;
// Set the timestamp to some time after the genesis rewards timestamp
cheats.warp(GENESIS_REWARDS_TIMESTAMP + 5 days);
}
/// @notice deploy token to owner and approve rewardsCoordinator. Used for deploying reward tokens
function _deployMockRewardTokens(address owner, uint numTokens) internal virtual {
cheats.startPrank(owner);
for (uint i = 0; i < numTokens; ++i) {
IERC20 token = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, owner);
rewardTokens.push(token);
token.approve(address(rewardsCoordinator), mockTokenInitialSupply);
}
cheats.stopPrank();
}
function _getBalanceForTokens(IERC20[] memory tokens, address holder) internal view returns (uint[] memory) {
uint[] memory balances = new uint[](tokens.length);
for (uint i = 0; i < tokens.length; ++i) {
balances[i] = tokens[i].balanceOf(holder);
}
return balances;
}
function _maxTimestamp(uint32 timestamp1, uint32 timestamp2) internal pure returns (uint32) {
return timestamp1 > timestamp2 ? timestamp1 : timestamp2;
}
function _assertRewardsClaimedEvents(bytes32 root, RewardsMerkleClaim memory claim, address recipient) internal {
address earner = claim.earnerLeaf.earner;
address claimer = rewardsCoordinator.claimerFor(earner);
if (claimer == address(0)) claimer = earner;
IERC20 token;
uint claimedAmount;
for (uint i = 0; i < claim.tokenLeaves.length; ++i) {
token = claim.tokenLeaves[i].token;
claimedAmount = rewardsCoordinator.cumulativeClaimed(earner, token);
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit RewardsClaimed(root, earner, claimer, recipient, token, claim.tokenLeaves[i].cumulativeEarnings - claimedAmount);
}
}
/// @notice given address and array of reward tokens, return array of cumulativeClaimed amonts
function _getCumulativeClaimed(address earner, RewardsMerkleClaim memory claim) internal view returns (uint[] memory) {
uint[] memory totalClaimed = new uint[](claim.tokenLeaves.length);
for (uint i = 0; i < claim.tokenLeaves.length; ++i) {
totalClaimed[i] = rewardsCoordinator.cumulativeClaimed(earner, claim.tokenLeaves[i].token);
}
return totalClaimed;
}
/// @notice given a claim, return the new cumulativeEarnings for each token
function _getCumulativeEarnings(RewardsMerkleClaim memory claim) internal pure returns (uint[] memory) {
uint[] memory earnings = new uint[](claim.tokenLeaves.length);
for (uint i = 0; i < claim.tokenLeaves.length; ++i) {
earnings[i] = claim.tokenLeaves[i].cumulativeEarnings;
}
return earnings;
}
function _getClaimTokenBalances(address earner, RewardsMerkleClaim memory claim) internal view returns (uint[] memory) {
uint[] memory balances = new uint[](claim.tokenLeaves.length);
for (uint i = 0; i < claim.tokenLeaves.length; ++i) {
balances[i] = claim.tokenLeaves[i].token.balanceOf(earner);
}
return balances;
}
/// @dev Sort to ensure that the array is in ascending order for strategies
function _sortArrayAsc(IStrategy[] memory arr) internal pure returns (IStrategy[] memory) {
uint l = arr.length;
for (uint i = 0; i < l; i++) {
for (uint j = i + 1; j < l; j++) {
if (address(arr[i]) > address(arr[j])) {
IStrategy temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
}
contract RewardsCoordinatorUnitTests_initializeAndSetters is RewardsCoordinatorUnitTests {
function testFuzz_setClaimerFor(address earner, address claimer) public filterFuzzedAddressInputs(earner) {
cheats.startPrank(earner);
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit ClaimerForSet(earner, rewardsCoordinator.claimerFor(earner), claimer);
rewardsCoordinator.setClaimerFor(claimer);
assertEq(claimer, rewardsCoordinator.claimerFor(earner), "claimerFor not set");
cheats.stopPrank();
}
function test_setClaimerFor_UAM_revert_staker(address earner, address claimer) public filterFuzzedAddressInputs(earner) {
cheats.prank(earner);
cheats.expectRevert(InvalidEarner.selector);
rewardsCoordinator.setClaimerFor(earner, claimer);
}
function test_setClaimerFor_UAM_AVS() public {
address avs = address(1000);
address claimer = address(1001);
// Set AVS
allocationManagerMock.setAVSSetCount(avs, 1);
// Initialize UAM
cheats.prank(avs);
permissionController.setAppointee(
avs, defaultAppointee, address(rewardsCoordinator), bytes4(keccak256("setClaimerFor(address,address)"))
);
// Set claimer for AVS
cheats.startPrank(defaultAppointee);
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit ClaimerForSet(avs, rewardsCoordinator.claimerFor(avs), claimer);
rewardsCoordinator.setClaimerFor(avs, claimer);
cheats.stopPrank();
assertEq(claimer, rewardsCoordinator.claimerFor(avs), "claimerFor not set");
}
function test_setClaimerFor_UAM_Operator() public {
address operator = address(1000);
address claimer = address(1001);
// Set operator
delegationManagerMock.setIsOperator(operator, true);
// Initialize UAM
cheats.prank(operator);
permissionController.setAppointee(
operator, defaultAppointee, address(rewardsCoordinator), bytes4(keccak256("setClaimerFor(address,address)"))
);
// Set claimer for operator
cheats.startPrank(defaultAppointee);
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit ClaimerForSet(operator, rewardsCoordinator.claimerFor(operator), claimer);
rewardsCoordinator.setClaimerFor(operator, claimer);
cheats.stopPrank();
assertEq(claimer, rewardsCoordinator.claimerFor(operator), "claimerFor not set");
}
function testFuzz_setActivationDelay(uint32 activationDelay) public {
cheats.startPrank(rewardsCoordinator.owner());
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit ActivationDelaySet(rewardsCoordinator.activationDelay(), activationDelay);
rewardsCoordinator.setActivationDelay(activationDelay);
assertEq(activationDelay, rewardsCoordinator.activationDelay(), "activationDelay not set");
cheats.stopPrank();
}
function testFuzz_setActivationDelay_Revert_WhenNotOwner(address caller, uint32 activationDelay)
public
filterFuzzedAddressInputs(caller)
{
cheats.assume(caller != rewardsCoordinator.owner());
cheats.prank(caller);
cheats.expectRevert("Ownable: caller is not the owner");
rewardsCoordinator.setActivationDelay(activationDelay);
}
function testFuzz_setDefaultOperatorSplit(uint16 defaultSplitBips) public {
cheats.startPrank(rewardsCoordinator.owner());
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit DefaultOperatorSplitBipsSet(rewardsCoordinator.defaultOperatorSplitBips(), defaultSplitBips);
rewardsCoordinator.setDefaultOperatorSplit(defaultSplitBips);
assertEq(defaultSplitBips, rewardsCoordinator.defaultOperatorSplitBips(), "defaultOperatorSplitBips not set");
cheats.stopPrank();
}
function testFuzz_setDefaultOperatorSplit_Revert_WhenNotOwner(address caller, uint16 defaultSplitBips)
public
filterFuzzedAddressInputs(caller)
{
cheats.assume(caller != rewardsCoordinator.owner());
cheats.prank(caller);
cheats.expectRevert("Ownable: caller is not the owner");
rewardsCoordinator.setDefaultOperatorSplit(defaultSplitBips);
}
function testFuzz_setRewardsUpdater(address newRewardsUpdater) public {
cheats.startPrank(rewardsCoordinator.owner());
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit RewardsUpdaterSet(rewardsCoordinator.rewardsUpdater(), newRewardsUpdater);
rewardsCoordinator.setRewardsUpdater(newRewardsUpdater);
assertEq(newRewardsUpdater, rewardsCoordinator.rewardsUpdater(), "rewardsUpdater not set");
cheats.stopPrank();
}
function testFuzz_setRewardsUpdater_Revert_WhenNotOwner(address caller, address newRewardsUpdater)
public
filterFuzzedAddressInputs(caller)
{
cheats.assume(caller != rewardsCoordinator.owner());
cheats.prank(caller);
cheats.expectRevert("Ownable: caller is not the owner");
rewardsCoordinator.setRewardsUpdater(newRewardsUpdater);
}
function testFuzz_setRewardsForAllSubmitter(address submitter, bool newValue) public {
cheats.startPrank(rewardsCoordinator.owner());
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit RewardsForAllSubmitterSet(submitter, rewardsCoordinator.isRewardsForAllSubmitter(submitter), newValue);
rewardsCoordinator.setRewardsForAllSubmitter(submitter, newValue);
assertEq(newValue, rewardsCoordinator.isRewardsForAllSubmitter(submitter), "isRewardsForAllSubmitter not set");
cheats.stopPrank();
}
function testFuzz_setRewardsForAllSubmitter_Revert_WhenNotOwner(address caller, address submitter, bool newValue)
public
filterFuzzedAddressInputs(caller)
{
cheats.assume(caller != rewardsCoordinator.owner());
cheats.prank(caller);
cheats.expectRevert("Ownable: caller is not the owner");
rewardsCoordinator.setRewardsForAllSubmitter(submitter, newValue);
}
}
contract RewardsCoordinatorUnitTests_setOperatorAVSSplit is RewardsCoordinatorUnitTests {
// Revert when paused
function testFuzz_Revert_WhenPaused(address operator, address avs, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS));
cheats.prank(pauser);
rewardsCoordinator.pause(2 ** PAUSED_OPERATOR_AVS_SPLIT);
cheats.prank(operator);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
rewardsCoordinator.setOperatorAVSSplit(operator, avs, split);
}
// Revert when split is greater than 100%
function testFuzz_Revert_WhenSplitGreaterThan100(address operator, address avs, uint16 split)
public
filterFuzzedAddressInputs(operator)
{
cheats.assume(operator != address(0));
split = uint16(bound(split, ONE_HUNDRED_IN_BIPS + 1, type(uint16).max));
cheats.prank(operator);
cheats.expectRevert(SplitExceedsMax.selector);
rewardsCoordinator.setOperatorAVSSplit(operator, avs, split);
}
function testFuzz_setOperatorAVSSplit(address operator, address avs, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS));
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
uint16 oldSplit = rewardsCoordinator.getOperatorAVSSplit(operator, avs);
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorAVSSplitBipsSet(operator, operator, avs, activatedAt, oldSplit, split);
cheats.prank(operator);
rewardsCoordinator.setOperatorAVSSplit(operator, avs, split);
assertEq(oldSplit, rewardsCoordinator.getOperatorAVSSplit(operator, avs), "Incorrect Operator split");
cheats.warp(activatedAt);
assertEq(split, rewardsCoordinator.getOperatorAVSSplit(operator, avs), "Incorrect Operator split");
}
function testFuzz_setOperatorAVSSplit_UAM(address operator, address avs, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
// Set UAM
cheats.prank(operator);
permissionController.setAppointee(
operator, defaultAppointee, address(rewardsCoordinator), IRewardsCoordinator.setOperatorAVSSplit.selector
);
split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS));
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
uint16 oldSplit = rewardsCoordinator.getOperatorAVSSplit(operator, avs);
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorAVSSplitBipsSet(defaultAppointee, operator, avs, activatedAt, oldSplit, split);
cheats.prank(defaultAppointee);
rewardsCoordinator.setOperatorAVSSplit(operator, avs, split);
assertEq(oldSplit, rewardsCoordinator.getOperatorAVSSplit(operator, avs), "Incorrect Operator split");
cheats.warp(activatedAt);
assertEq(split, rewardsCoordinator.getOperatorAVSSplit(operator, avs), "Incorrect Operator split");
}
// Testing that the split has been initialized for the first time.
function testFuzz_setOperatorAVSSplitFirstTime(address operator, address avs, uint16 split)
public
filterFuzzedAddressInputs(operator)
{
cheats.assume(operator != address(0));
split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS));
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
uint16 oldSplit = rewardsCoordinator.getOperatorAVSSplit(operator, avs);
// Check that the split returns the default split before initialization for the first time.
assertEq(oldSplit, rewardsCoordinator.defaultOperatorSplitBips(), "Operator split is not Default split before Initialization");
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorAVSSplitBipsSet(operator, operator, avs, activatedAt, oldSplit, split);
cheats.prank(operator);
rewardsCoordinator.setOperatorAVSSplit(operator, avs, split);
cheats.prank(address(this)); // Owner of RewardsCoordinator
// Change default split to check if it is returned before activation
rewardsCoordinator.setDefaultOperatorSplit(5000);
// Check that the split returns the default split before activation for the first time.
assertEq(
rewardsCoordinator.defaultOperatorSplitBips(),
rewardsCoordinator.getOperatorAVSSplit(operator, avs),
"Operator split is not Default split before Activation for first time"
);
cheats.warp(activatedAt);
assertEq(split, rewardsCoordinator.getOperatorAVSSplit(operator, avs), "Incorrect Operator split");
}
// Testing the split setting for a second time prior to the earlier activation.
function testFuzz_Revert_setOperatorAVSSplitSecondTimeBeforePriorActivation(
address operator,
address avs,
uint16 firstSplit,
uint16 secondSplit,
uint32 warpTime
) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
firstSplit = uint16(bound(firstSplit, 0, ONE_HUNDRED_IN_BIPS));
secondSplit = uint16(bound(secondSplit, 0, ONE_HUNDRED_IN_BIPS));
warpTime = uint32(bound(warpTime, uint32(block.timestamp), uint32(block.timestamp) + activationDelay));
// Setting First Split
cheats.prank(operator);
rewardsCoordinator.setOperatorAVSSplit(operator, avs, firstSplit);
// Warping to time before activation of First split
cheats.warp(warpTime);
// Trying to set Second Split
cheats.prank(operator);
cheats.expectRevert(PreviousSplitPending.selector);
rewardsCoordinator.setOperatorAVSSplit(operator, avs, secondSplit);
}
// Testing the split setting for a second time after earlier activation.
function testFuzz_setOperatorAVSSplitSecondTimeAfterPriorActivation(
address operator,
address avs,
uint16 firstSplit,
uint16 secondSplit,
uint32 warpTime
) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
firstSplit = uint16(bound(firstSplit, 0, ONE_HUNDRED_IN_BIPS));
secondSplit = uint16(bound(secondSplit, 0, ONE_HUNDRED_IN_BIPS));
warpTime = uint32(bound(warpTime, uint32(block.timestamp) + activationDelay + 1, type(uint32).max - activationDelay));
// Setting First Split
cheats.prank(operator);
rewardsCoordinator.setOperatorAVSSplit(operator, avs, firstSplit);
// Warping to time after activation of First split
cheats.warp(warpTime);
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
// Setting Second Split
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorAVSSplitBipsSet(operator, operator, avs, activatedAt, firstSplit, secondSplit);
cheats.prank(operator);
rewardsCoordinator.setOperatorAVSSplit(operator, avs, secondSplit);
assertEq(firstSplit, rewardsCoordinator.getOperatorAVSSplit(operator, avs), "Incorrect Operator split");
cheats.warp(activatedAt);
assertEq(secondSplit, rewardsCoordinator.getOperatorAVSSplit(operator, avs), "Incorrect Operator split");
}
}
contract RewardsCoordinatorUnitTests_setOperatorPISplit is RewardsCoordinatorUnitTests {
// Revert when paused
function testFuzz_Revert_WhenPaused(address operator, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS));
cheats.prank(pauser);
rewardsCoordinator.pause(2 ** PAUSED_OPERATOR_PI_SPLIT);
cheats.prank(operator);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
rewardsCoordinator.setOperatorPISplit(operator, split);
}
// Revert when split is greater than 100%
function testFuzz_Revert_WhenSplitGreaterThan100(address operator, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
split = uint16(bound(split, ONE_HUNDRED_IN_BIPS + 1, type(uint16).max));
cheats.prank(operator);
cheats.expectRevert(SplitExceedsMax.selector);
rewardsCoordinator.setOperatorPISplit(operator, split);
}
function testFuzz_setOperatorPISplit(address operator, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS));
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
uint16 oldSplit = rewardsCoordinator.getOperatorPISplit(operator);
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorPISplitBipsSet(operator, operator, activatedAt, oldSplit, split);
cheats.prank(operator);
rewardsCoordinator.setOperatorPISplit(operator, split);
assertEq(oldSplit, rewardsCoordinator.getOperatorPISplit(operator), "Incorrect Operator split");
cheats.warp(activatedAt);
assertEq(split, rewardsCoordinator.getOperatorPISplit(operator), "Incorrect Operator split");
}
function testFuzz_setOperatorPISplit_UAM(address operator, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
// Set UAM
cheats.prank(operator);
permissionController.setAppointee(
operator, defaultAppointee, address(rewardsCoordinator), IRewardsCoordinator.setOperatorPISplit.selector
);
split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS));
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
uint16 oldSplit = rewardsCoordinator.getOperatorPISplit(operator);
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorPISplitBipsSet(defaultAppointee, operator, activatedAt, oldSplit, split);
cheats.prank(defaultAppointee);
rewardsCoordinator.setOperatorPISplit(operator, split);
assertEq(oldSplit, rewardsCoordinator.getOperatorPISplit(operator), "Incorrect Operator split");
cheats.warp(activatedAt);
assertEq(split, rewardsCoordinator.getOperatorPISplit(operator), "Incorrect Operator split");
}
// Testing that the split has been initialized for the first time.
function testFuzz_setOperatorPISplitFirstTime(address operator, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS));
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
uint16 oldSplit = rewardsCoordinator.getOperatorPISplit(operator);
// Check that the split returns the default split before initialization for the first time.
assertEq(oldSplit, rewardsCoordinator.defaultOperatorSplitBips(), "Operator split is not Default split before Initialization");
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorPISplitBipsSet(operator, operator, activatedAt, oldSplit, split);
cheats.prank(operator);
rewardsCoordinator.setOperatorPISplit(operator, split);
cheats.prank(address(this)); // Owner of RewardsCoordinator
// Change default split to check if it is returned before activation
rewardsCoordinator.setDefaultOperatorSplit(5000);
// Check that the split returns the default split before activation for the first time.
assertEq(
rewardsCoordinator.defaultOperatorSplitBips(),
rewardsCoordinator.getOperatorPISplit(operator),
"Operator split is not Default split before Activation for first time"
);
cheats.warp(activatedAt);
assertEq(split, rewardsCoordinator.getOperatorPISplit(operator), "Incorrect Operator split");
}
// Testing the split setting for a second time prior to the earlier activation.
function testFuzz_Revert_setOperatorPISplitSecondTimeBeforePriorActivation(
address operator,
uint16 firstSplit,
uint16 secondSplit,
uint32 warpTime
) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
firstSplit = uint16(bound(firstSplit, 0, ONE_HUNDRED_IN_BIPS));
secondSplit = uint16(bound(secondSplit, 0, ONE_HUNDRED_IN_BIPS));
warpTime = uint32(bound(warpTime, uint32(block.timestamp), uint32(block.timestamp) + activationDelay));
// Setting First Split
cheats.prank(operator);
rewardsCoordinator.setOperatorPISplit(operator, firstSplit);
// Warping to time before activation of First split
cheats.warp(warpTime);
// Trying to set Second Split
cheats.prank(operator);
cheats.expectRevert(PreviousSplitPending.selector);
rewardsCoordinator.setOperatorPISplit(operator, secondSplit);
}
// Testing the split setting for a second time after earlier activation.
function testFuzz_setOperatorPISplitSecondTimeAfterPriorActivation(
address operator,
uint16 firstSplit,
uint16 secondSplit,
uint32 warpTime
) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
firstSplit = uint16(bound(firstSplit, 0, ONE_HUNDRED_IN_BIPS));
secondSplit = uint16(bound(secondSplit, 0, ONE_HUNDRED_IN_BIPS));
warpTime = uint32(bound(warpTime, uint32(block.timestamp) + activationDelay + 1, type(uint32).max - activationDelay));
// Setting First Split
cheats.prank(operator);
rewardsCoordinator.setOperatorPISplit(operator, firstSplit);
// Warping to time after activation of First split
cheats.warp(warpTime);
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
// Setting Second Split
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorPISplitBipsSet(operator, operator, activatedAt, firstSplit, secondSplit);
cheats.prank(operator);
rewardsCoordinator.setOperatorPISplit(operator, secondSplit);
assertEq(firstSplit, rewardsCoordinator.getOperatorPISplit(operator), "Incorrect Operator split");
cheats.warp(activatedAt);
assertEq(secondSplit, rewardsCoordinator.getOperatorPISplit(operator), "Incorrect Operator split");
}
}
contract RewardsCoordinatorUnitsTests_setOperatorSetSplit is RewardsCoordinatorUnitTests {
OperatorSet operatorSet;
function setUp() public virtual override {
RewardsCoordinatorUnitTests.setUp();
operatorSet = OperatorSet(address(this), 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
}
// Revert when paused
function testFuzz_Revert_WhenPaused(address operator, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS));
cheats.prank(pauser);
rewardsCoordinator.pause(2 ** PAUSED_OPERATOR_SET_SPLIT);
cheats.prank(operator);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, split);
}
// Revert when split is greater than 100%
function testFuzz_Revert_WhenSplitGreaterThan100(address operator, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
split = uint16(bound(split, ONE_HUNDRED_IN_BIPS + 1, type(uint16).max));
cheats.prank(operator);
cheats.expectRevert(SplitExceedsMax.selector);
rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, split);
}
function testFuzz_Revert_InvalidOperatorSet(address operator, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
operatorSet.id = 2; // Invalid operator set id
cheats.prank(operator);
cheats.expectRevert(InvalidOperatorSet.selector);
rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, split);
}
function testFuzz_setOperatorSetSplit(address operator, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS));
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
uint16 oldSplit = rewardsCoordinator.getOperatorSetSplit(operator, operatorSet);
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorSetSplitBipsSet(operator, operator, operatorSet, activatedAt, oldSplit, split);
cheats.prank(operator);
rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, split);
assertEq(oldSplit, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split");
cheats.warp(activatedAt);
assertEq(split, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split");
}
function testFuzz_setOperatorSetSplit_UAM(address operator, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
// Set UAM
cheats.prank(operator);
permissionController.setAppointee(
operator, defaultAppointee, address(rewardsCoordinator), IRewardsCoordinator.setOperatorSetSplit.selector
);
split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS));
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
uint16 oldSplit = rewardsCoordinator.getOperatorSetSplit(operator, operatorSet);
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorSetSplitBipsSet(defaultAppointee, operator, operatorSet, activatedAt, oldSplit, split);
cheats.prank(defaultAppointee);
rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, split);
assertEq(oldSplit, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split");
cheats.warp(activatedAt);
assertEq(split, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split");
}
// Testing that the split has been initialized for the first time.
function testFuzz_setOperatorSetSplitFirstTime(address operator, uint16 split) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS));
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
uint16 oldSplit = rewardsCoordinator.getOperatorSetSplit(operator, operatorSet);
assertEq(oldSplit, defaultSplitBips, "Operator split is not Default split before Initialization");
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorSetSplitBipsSet(operator, operator, operatorSet, activatedAt, oldSplit, split);
cheats.prank(operator);
rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, split);
assertEq(oldSplit, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split");
cheats.warp(activatedAt);
assertEq(split, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split");
}
// Testing the split setting for a second time prior to the earlier activation.
function testFuzz_Revert_setOperatorSetSplitSecondTimeBeforePriorActivation(
address operator,
uint16 firstSplit,
uint16 secondSplit,
uint32 warpTime
) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
firstSplit = uint16(bound(firstSplit, 0, ONE_HUNDRED_IN_BIPS));
secondSplit = uint16(bound(secondSplit, 0, ONE_HUNDRED_IN_BIPS));
warpTime = uint32(bound(warpTime, uint32(block.timestamp), uint32(block.timestamp) + activationDelay));
// Setting First Split
cheats.prank(operator);
rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, firstSplit);
// Warping to time before activation of First split
cheats.warp(warpTime);
// Trying to set Second Split
cheats.prank(operator);
cheats.expectRevert(PreviousSplitPending.selector);
rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, secondSplit);
}
// Testing the split setting for a second time after earlier activation.
function testFuzz_setOperatorSetSplitSecondTimeAfterPriorActivation(
address operator,
uint16 firstSplit,
uint16 secondSplit,
uint32 warpTime
) public filterFuzzedAddressInputs(operator) {
cheats.assume(operator != address(0));
firstSplit = uint16(bound(firstSplit, 0, ONE_HUNDRED_IN_BIPS));
secondSplit = uint16(bound(secondSplit, 0, ONE_HUNDRED_IN_BIPS));
warpTime = uint32(bound(warpTime, uint32(block.timestamp) + activationDelay + 1, type(uint32).max - activationDelay));
// Setting First Split
cheats.prank(operator);
rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, firstSplit);
// Warping to time after activation of First split
cheats.warp(warpTime);
uint32 activatedAt = uint32(block.timestamp) + activationDelay;
// Setting Second Split
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorSetSplitBipsSet(operator, operator, operatorSet, activatedAt, firstSplit, secondSplit);
cheats.prank(operator);
rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, secondSplit);
assertEq(firstSplit, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split");
cheats.warp(activatedAt);
assertEq(secondSplit, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split");
}
}
contract RewardsCoordinatorUnitTests_createAVSRewardsSubmission is RewardsCoordinatorUnitTests {
// Revert when paused
function test_Revert_WhenPaused() public {
cheats.prank(pauser);
rewardsCoordinator.pause(2 ** PAUSED_AVS_REWARDS_SUBMISSION);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
RewardsSubmission[] memory rewardsSubmissions;
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
}
// Revert from reentrancy
function test_Revert_WhenReentrancy(uint amount) public {
amount = bound(amount, 1, 1e38 - 1);
Reenterer reenterer = new Reenterer();
reenterer.prepareReturnData(abi.encode(amount));
address targetToUse = address(rewardsCoordinator);
uint msgValueToUse = 0;
_deployMockRewardTokens(address(this), 1);
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: IERC20(address(reenterer)),
amount: amount,
startTimestamp: uint32(block.timestamp),
duration: CALCULATION_INTERVAL_SECONDS
});
bytes memory calldataToUse = abi.encodeWithSelector(RewardsCoordinator.createAVSRewardsSubmission.selector, rewardsSubmissions);
reenterer.prepare(targetToUse, msgValueToUse, calldataToUse, bytes("ReentrancyGuard: reentrant call"));
cheats.expectRevert();
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
}
// Revert with 0 length strats and multipliers
function testFuzz_Revert_WhenEmptyStratsAndMultipliers(address avs, uint startTimestamp, uint duration, uint amount)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
amount = bound(amount, 1, mockTokenInitialSupply);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
StrategyAndMultiplier[] memory emptyStratsAndMultipliers;
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: emptyStratsAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration)
});
// 3. call createAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(InputArrayLengthZero.selector);
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
}
// Revert when amount > 1e38-1
function testFuzz_Revert_AmountTooLarge(address avs, uint startTimestamp, uint duration, uint amount)
public
filterFuzzedAddressInputs(avs)
{
// 1. Bound fuzz inputs
amount = bound(amount, 1e38, type(uint).max);
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", amount, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration)
});
// 3. Call createAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(AmountExceedsMax.selector);
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
}
function testFuzz_Revert_WhenDuplicateStrategies(address avs, uint startTimestamp, uint duration, uint amount)
public
filterFuzzedAddressInputs(avs)
{
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
amount = bound(amount, 1, mockTokenInitialSupply);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
StrategyAndMultiplier[] memory dupStratsAndMultipliers = new StrategyAndMultiplier[](2);
dupStratsAndMultipliers[0] = defaultStrategyAndMultipliers[0];
dupStratsAndMultipliers[1] = defaultStrategyAndMultipliers[0];
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: dupStratsAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration)
});
// 3. call createAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(StrategiesNotInAscendingOrder.selector);
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
}
// Revert with exceeding max duration
function testFuzz_Revert_WhenExceedingMaxDuration(address avs, uint startTimestamp, uint duration, uint amount)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
amount = bound(amount, 1, mockTokenInitialSupply);
duration = bound(duration, MAX_REWARDS_DURATION + 1, type(uint32).max);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration)
});
// 3. call createAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(DurationExceedsMax.selector);
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
}
// Revert with invalid interval seconds
function testFuzz_Revert_WhenInvalidIntervalSeconds(address avs, uint startTimestamp, uint duration, uint amount)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
amount = bound(amount, 1, mockTokenInitialSupply);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
cheats.assume(duration % CALCULATION_INTERVAL_SECONDS != 0);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration)
});
// 3. call createAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(InvalidDurationRemainder.selector);
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
}
// Revert when duration is 0
function testFuzz_Revert_WhenDurationIsZero(address avs, uint startTimestamp, uint amount) public filterFuzzedAddressInputs(avs) {
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
amount = bound(amount, 1, mockTokenInitialSupply);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: 0
});
// 3. call createAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(DurationIsZero.selector);
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
}
// Revert with retroactive rewards enabled and set too far in past
// - either before genesis rewards timestamp
// - before max retroactive length
function testFuzz_Revert_WhenRewardsSubmissionTooStale(
uint fuzzBlockTimestamp,
address avs,
uint startTimestamp,
uint duration,
uint amount
) public filterFuzzedAddressInputs(avs) {
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
fuzzBlockTimestamp = bound(fuzzBlockTimestamp, uint(MAX_RETROACTIVE_LENGTH), block.timestamp);
cheats.warp(fuzzBlockTimestamp);
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
amount = bound(amount, 1, mockTokenInitialSupply);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp =
bound(startTimestamp, 0, uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) - 1);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration)
});
// 3. call createAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(StartTimestampTooFarInPast.selector);
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
}
// Revert with start timestamp past max future length
function testFuzz_Revert_WhenRewardsSubmissionTooFarInFuture(address avs, uint startTimestamp, uint duration, uint amount)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
amount = bound(amount, 1, mockTokenInitialSupply);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp =
bound(startTimestamp, block.timestamp + uint(MAX_FUTURE_LENGTH) + 1 + CALCULATION_INTERVAL_SECONDS, type(uint32).max);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration)
});
// 3. call createAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(StartTimestampTooFarInFuture.selector);
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
}
// Revert with non whitelisted strategy
function testFuzz_Revert_WhenInvalidStrategy(address avs, uint startTimestamp, uint duration, uint amount)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
amount = bound(amount, 1, mockTokenInitialSupply);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
defaultStrategyAndMultipliers[0].strategy = IStrategy(address(999));
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration)
});
// 3. call createAVSRewardsSubmission() with expected event emitted
cheats.prank(avs);
cheats.expectRevert(StrategyNotWhitelisted.selector);
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
}
/**
* @notice test a single rewards submission asserting for the following
* - correct event emitted
* - submission nonce incrementation by 1, and rewards submission hash being set in storage.
* - rewards submission hash being set in storage
* - token balance before and after of avs and rewardsCoordinator
*/
function testFuzz_createAVSRewardsSubmission_SingleSubmission(address avs, uint startTimestamp, uint duration, uint amount)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
amount = bound(amount, 1, mockTokenInitialSupply);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration)
});
// 3. call createAVSRewardsSubmission() with expected event emitted
uint avsBalanceBefore = rewardToken.balanceOf(avs);
uint rewardsCoordinatorBalanceBefore = rewardToken.balanceOf(address(rewardsCoordinator));
cheats.startPrank(avs);
rewardToken.approve(address(rewardsCoordinator), amount);
uint currSubmissionNonce = rewardsCoordinator.submissionNonce(avs);
bytes32 rewardsSubmissionHash = keccak256(abi.encode(avs, currSubmissionNonce, rewardsSubmissions[0]));
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit AVSRewardsSubmissionCreated(avs, currSubmissionNonce, rewardsSubmissionHash, rewardsSubmissions[0]);
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
cheats.stopPrank();
assertTrue(rewardsCoordinator.isAVSRewardsSubmissionHash(avs, rewardsSubmissionHash), "rewards submission hash not submitted");
assertEq(currSubmissionNonce + 1, rewardsCoordinator.submissionNonce(avs), "submission nonce not incremented");
assertEq(avsBalanceBefore - amount, rewardToken.balanceOf(avs), "AVS balance not decremented by amount of rewards submission");
assertEq(
rewardsCoordinatorBalanceBefore + amount,
rewardToken.balanceOf(address(rewardsCoordinator)),
"RewardsCoordinator balance not incremented by amount of rewards submission"
);
}
/**
* @notice test multiple rewards submissions asserting for the following
* - correct event emitted
* - submission nonce incrementation by numSubmissions, and rewards submission hashes being set in storage.
* - rewards submission hash being set in storage
* - token balances before and after of avs and rewardsCoordinator
*/
function testFuzz_createAVSRewardsSubmission_MultipleSubmissions(FuzzAVSRewardsSubmission memory param, uint numSubmissions)
public
filterFuzzedAddressInputs(param.avs)
{
numSubmissions = bound(numSubmissions, 2, 10);
cheats.assume(param.avs != address(0));
cheats.prank(rewardsCoordinator.owner());
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](numSubmissions);
bytes32[] memory rewardsSubmissionHashes = new bytes32[](numSubmissions);
uint startSubmissionNonce = rewardsCoordinator.submissionNonce(param.avs);
_deployMockRewardTokens(param.avs, numSubmissions);
uint[] memory avsBalancesBefore = _getBalanceForTokens(rewardTokens, param.avs);
uint[] memory rewardsCoordinatorBalancesBefore = _getBalanceForTokens(rewardTokens, address(rewardsCoordinator));
uint[] memory amounts = new uint[](numSubmissions);
// Create multiple rewards submissions and their expected event
for (uint i = 0; i < numSubmissions; ++i) {
// 1. Bound fuzz inputs to valid ranges and amounts using randSeed for each
param.amount = bound(param.amount + i, 1, mockTokenInitialSupply);
amounts[i] = param.amount;
param.duration = bound(param.duration + i, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
param.duration = param.duration - (param.duration % CALCULATION_INTERVAL_SECONDS);
param.startTimestamp = bound(
param.startTimestamp + i,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH))
+ CALCULATION_INTERVAL_SECONDS - 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
param.startTimestamp = param.startTimestamp - (param.startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission memory rewardsSubmission = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardTokens[i],
amount: amounts[i],
startTimestamp: uint32(param.startTimestamp),
duration: uint32(param.duration)
});
rewardsSubmissions[i] = rewardsSubmission;
// 3. expected event emitted for this rewardsSubmission
rewardsSubmissionHashes[i] = keccak256(abi.encode(param.avs, startSubmissionNonce + i, rewardsSubmissions[i]));
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit AVSRewardsSubmissionCreated(param.avs, startSubmissionNonce + i, rewardsSubmissionHashes[i], rewardsSubmissions[i]);
}
// 4. call createAVSRewardsSubmission()
cheats.prank(param.avs);
rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions);
// 5. Check for submissionNonce() and rewardsSubmissionHashes being set
assertEq(
startSubmissionNonce + numSubmissions,
rewardsCoordinator.submissionNonce(param.avs),
"submission nonce not incremented properly"
);
for (uint i = 0; i < numSubmissions; ++i) {
assertTrue(
rewardsCoordinator.isAVSRewardsSubmissionHash(param.avs, rewardsSubmissionHashes[i]),
"rewards submission hash not submitted"
);
assertEq(
avsBalancesBefore[i] - amounts[i],
rewardTokens[i].balanceOf(param.avs),
"AVS balance not decremented by amount of rewards submission"
);
assertEq(
rewardsCoordinatorBalancesBefore[i] + amounts[i],
rewardTokens[i].balanceOf(address(rewardsCoordinator)),
"RewardsCoordinator balance not incremented by amount of rewards submission"
);
}
}
}
contract RewardsCoordinatorUnitTests_createRewardsForAllSubmission is RewardsCoordinatorUnitTests {
// Revert when paused
function test_Revert_WhenPaused() public {
cheats.prank(pauser);
rewardsCoordinator.pause(2 ** PAUSED_REWARDS_FOR_ALL_SUBMISSION);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
RewardsSubmission[] memory rewardsSubmissions;
rewardsCoordinator.createRewardsForAllSubmission(rewardsSubmissions);
}
// Revert from reentrancy
function test_Revert_WhenReentrancy(uint amount) public {
Reenterer reenterer = new Reenterer();
reenterer.prepareReturnData(abi.encode(amount));
address targetToUse = address(rewardsCoordinator);
uint msgValueToUse = 0;
_deployMockRewardTokens(address(this), 1);
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: IERC20(address(reenterer)),
amount: amount,
startTimestamp: uint32(block.timestamp),
duration: CALCULATION_INTERVAL_SECONDS
});
bytes memory calldataToUse = abi.encodeWithSelector(RewardsCoordinator.createAVSRewardsSubmission.selector, rewardsSubmissions);
reenterer.prepare(targetToUse, msgValueToUse, calldataToUse, bytes("ReentrancyGuard: reentrant call"));
cheats.prank(rewardsForAllSubmitter);
cheats.expectRevert();
rewardsCoordinator.createRewardsForAllSubmission(rewardsSubmissions);
}
function testFuzz_Revert_WhenNotRewardsForAllSubmitter(address invalidSubmitter) public filterFuzzedAddressInputs(invalidSubmitter) {
cheats.assume(invalidSubmitter != rewardsForAllSubmitter);
cheats.expectRevert(UnauthorizedCaller.selector);
RewardsSubmission[] memory rewardsSubmissions;
rewardsCoordinator.createRewardsForAllSubmission(rewardsSubmissions);
}
// Revert when duration is 0
function testFuzz_Revert_WhenDurationIsZero(uint startTimestamp, uint amount) public {
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, rewardsForAllSubmitter);
amount = bound(amount, 1, mockTokenInitialSupply);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: 0
});
// 3. call createRewardsForAllSubmission() with expected revert
cheats.prank(rewardsForAllSubmitter);
cheats.expectRevert(DurationIsZero.selector);
rewardsCoordinator.createRewardsForAllSubmission(rewardsSubmissions);
}
/**
* @notice test a single rewards submission asserting for the following
* - correct event emitted
* - submission nonce incrementation by 1, and rewards submission hash being set in storage.
* - rewards submission hash being set in storage
* - token balance before and after of RewardsForAllSubmitter and rewardsCoordinator
*/
function testFuzz_createRewardsForAllSubmission_SingleSubmission(uint startTimestamp, uint duration, uint amount) public {
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, rewardsForAllSubmitter);
amount = bound(amount, 1, mockTokenInitialSupply);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration)
});
// 3. call createAVSRewardsSubmission() with expected event emitted
uint submitterBalanceBefore = rewardToken.balanceOf(rewardsForAllSubmitter);
uint rewardsCoordinatorBalanceBefore = rewardToken.balanceOf(address(rewardsCoordinator));
cheats.startPrank(rewardsForAllSubmitter);
rewardToken.approve(address(rewardsCoordinator), amount);
uint currSubmissionNonce = rewardsCoordinator.submissionNonce(rewardsForAllSubmitter);
bytes32 rewardsSubmissionHash = keccak256(abi.encode(rewardsForAllSubmitter, currSubmissionNonce, rewardsSubmissions[0]));
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit RewardsSubmissionForAllCreated(rewardsForAllSubmitter, currSubmissionNonce, rewardsSubmissionHash, rewardsSubmissions[0]);
rewardsCoordinator.createRewardsForAllSubmission(rewardsSubmissions);
cheats.stopPrank();
assertTrue(
rewardsCoordinator.isRewardsSubmissionForAllHash(rewardsForAllSubmitter, rewardsSubmissionHash),
"rewards submission hash not submitted"
);
assertEq(currSubmissionNonce + 1, rewardsCoordinator.submissionNonce(rewardsForAllSubmitter), "submission nonce not incremented");
assertEq(
submitterBalanceBefore - amount,
rewardToken.balanceOf(rewardsForAllSubmitter),
"createRewardsForAllSubmission Submitter balance not decremented by amount of rewards submission"
);
assertEq(
rewardsCoordinatorBalanceBefore + amount,
rewardToken.balanceOf(address(rewardsCoordinator)),
"RewardsCoordinator balance not incremented by amount of rewards submission"
);
}
/**
* @notice test multiple rewards submissions asserting for the following
* - correct event emitted
* - submission nonce incrementation by numSubmissions, and rewards submission hashes being set in storage.
* - rewards submission hash being set in storage
* - token balances before and after of createRewardsForAllSubmission submitter and rewardsCoordinator
*/
function testFuzz_createRewardsForAllSubmission_MultipleSubmissions(FuzzAVSRewardsSubmission memory param, uint numSubmissions)
public
{
numSubmissions = bound(numSubmissions, 2, 10);
cheats.prank(rewardsCoordinator.owner());
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](numSubmissions);
bytes32[] memory rewardsSubmissionHashes = new bytes32[](numSubmissions);
uint startSubmissionNonce = rewardsCoordinator.submissionNonce(rewardsForAllSubmitter);
_deployMockRewardTokens(rewardsForAllSubmitter, numSubmissions);
uint[] memory submitterBalancesBefore = _getBalanceForTokens(rewardTokens, rewardsForAllSubmitter);
uint[] memory rewardsCoordinatorBalancesBefore = _getBalanceForTokens(rewardTokens, address(rewardsCoordinator));
uint[] memory amounts = new uint[](numSubmissions);
// Create multiple rewards submissions and their expected event
for (uint i = 0; i < numSubmissions; ++i) {
// 1. Bound fuzz inputs to valid ranges and amounts using randSeed for each
param.amount = bound(param.amount + i, 1, mockTokenInitialSupply);
amounts[i] = param.amount;
param.duration = bound(param.duration + i, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
param.duration = param.duration - (param.duration % CALCULATION_INTERVAL_SECONDS);
param.startTimestamp = bound(
param.startTimestamp + i,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH))
+ CALCULATION_INTERVAL_SECONDS - 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
param.startTimestamp = param.startTimestamp - (param.startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission memory rewardsSubmission = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardTokens[i],
amount: amounts[i],
startTimestamp: uint32(param.startTimestamp),
duration: uint32(param.duration)
});
rewardsSubmissions[i] = rewardsSubmission;
// 3. expected event emitted for this rewardsSubmission
rewardsSubmissionHashes[i] = keccak256(abi.encode(rewardsForAllSubmitter, startSubmissionNonce + i, rewardsSubmissions[i]));
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit RewardsSubmissionForAllCreated(
rewardsForAllSubmitter, startSubmissionNonce + i, rewardsSubmissionHashes[i], rewardsSubmissions[i]
);
}
// 4. call createAVSRewardsSubmission()
cheats.prank(rewardsForAllSubmitter);
rewardsCoordinator.createRewardsForAllSubmission(rewardsSubmissions);
// 5. Check for submissionNonce() and rewardsSubmissionHashes being set
assertEq(
startSubmissionNonce + numSubmissions,
rewardsCoordinator.submissionNonce(rewardsForAllSubmitter),
"submission nonce not incremented properly"
);
for (uint i = 0; i < numSubmissions; ++i) {
assertTrue(
rewardsCoordinator.isRewardsSubmissionForAllHash(rewardsForAllSubmitter, rewardsSubmissionHashes[i]),
"rewards submission hash not submitted"
);
assertEq(
submitterBalancesBefore[i] - amounts[i],
rewardTokens[i].balanceOf(rewardsForAllSubmitter),
"RewardsForAllSubmitter Submitter balance not decremented by amount of rewards submission"
);
assertEq(
rewardsCoordinatorBalancesBefore[i] + amounts[i],
rewardTokens[i].balanceOf(address(rewardsCoordinator)),
"RewardsCoordinator balance not incremented by amount of rewards submission"
);
}
}
}
contract RewardsCoordinatorUnitTests_createRewardsForAllEarners is RewardsCoordinatorUnitTests {
// Revert when paused
function test_Revert_WhenPaused() public {
cheats.prank(pauser);
rewardsCoordinator.pause(2 ** PAUSED_REWARD_ALL_STAKERS_AND_OPERATORS);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
RewardsSubmission[] memory rewardsSubmissions;
rewardsCoordinator.createRewardsForAllEarners(rewardsSubmissions);
}
// Revert from reentrancy
function test_Revert_WhenReentrancy(uint amount) public {
Reenterer reenterer = new Reenterer();
reenterer.prepareReturnData(abi.encode(amount));
address targetToUse = address(rewardsCoordinator);
uint msgValueToUse = 0;
_deployMockRewardTokens(address(this), 1);
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: IERC20(address(reenterer)),
amount: amount,
startTimestamp: uint32(block.timestamp),
duration: CALCULATION_INTERVAL_SECONDS
});
bytes memory calldataToUse = abi.encodeWithSelector(RewardsCoordinator.createAVSRewardsSubmission.selector, rewardsSubmissions);
reenterer.prepare(targetToUse, msgValueToUse, calldataToUse, bytes("ReentrancyGuard: reentrant call"));
cheats.prank(rewardsForAllSubmitter);
cheats.expectRevert();
rewardsCoordinator.createRewardsForAllEarners(rewardsSubmissions);
}
function testFuzz_Revert_WhenNotRewardsForAllSubmitter(address invalidSubmitter) public filterFuzzedAddressInputs(invalidSubmitter) {
cheats.assume(invalidSubmitter != rewardsForAllSubmitter);
cheats.expectRevert(UnauthorizedCaller.selector);
RewardsSubmission[] memory rewardsSubmissions;
rewardsCoordinator.createRewardsForAllEarners(rewardsSubmissions);
}
// Revert when duration is 0
function testFuzz_Revert_WhenDurationIsZero(uint startTimestamp, uint amount) public {
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, rewardsForAllSubmitter);
amount = bound(amount, 1, mockTokenInitialSupply);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: 0
});
// 3. call createRewardsForAllEarners() with expected revert
cheats.prank(rewardsForAllSubmitter);
cheats.expectRevert(DurationIsZero.selector);
rewardsCoordinator.createRewardsForAllEarners(rewardsSubmissions);
}
/**
* @notice test a single rewards submission asserting for the following
* - correct event emitted
* - submission nonce incrementation by 1, and rewards submission hash being set in storage.
* - rewards submission hash being set in storage
* - token balance before and after of RewardsForAllSubmitter and rewardsCoordinator
*/
function testFuzz_createRewardsForAllSubmission_SingleSubmission(uint startTimestamp, uint duration, uint amount) public {
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, rewardsForAllSubmitter);
amount = bound(amount, 1, mockTokenInitialSupply);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1);
rewardsSubmissions[0] = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
amount: amount,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration)
});
// 3. call createAVSRewardsSubmission() with expected event emitted
uint submitterBalanceBefore = rewardToken.balanceOf(rewardsForAllSubmitter);
uint rewardsCoordinatorBalanceBefore = rewardToken.balanceOf(address(rewardsCoordinator));
cheats.startPrank(rewardsForAllSubmitter);
rewardToken.approve(address(rewardsCoordinator), amount);
uint currSubmissionNonce = rewardsCoordinator.submissionNonce(rewardsForAllSubmitter);
bytes32 rewardsSubmissionHash = keccak256(abi.encode(rewardsForAllSubmitter, currSubmissionNonce, rewardsSubmissions[0]));
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit RewardsSubmissionForAllEarnersCreated(
rewardsForAllSubmitter, currSubmissionNonce, rewardsSubmissionHash, rewardsSubmissions[0]
);
rewardsCoordinator.createRewardsForAllEarners(rewardsSubmissions);
cheats.stopPrank();
assertTrue(
rewardsCoordinator.isRewardsSubmissionForAllEarnersHash(rewardsForAllSubmitter, rewardsSubmissionHash),
"rewards submission hash not submitted"
);
assertEq(currSubmissionNonce + 1, rewardsCoordinator.submissionNonce(rewardsForAllSubmitter), "submission nonce not incremented");
assertEq(
submitterBalanceBefore - amount,
rewardToken.balanceOf(rewardsForAllSubmitter),
"createRewardsForAllSubmission Submitter balance not decremented by amount of rewards submission"
);
assertEq(
rewardsCoordinatorBalanceBefore + amount,
rewardToken.balanceOf(address(rewardsCoordinator)),
"RewardsCoordinator balance not incremented by amount of rewards submission"
);
}
/**
* @notice test multiple rewards submissions asserting for the following
* - correct event emitted
* - submission nonce incrementation by numSubmissions, and rewards submission hashes being set in storage.
* - rewards submission hash being set in storage
* - token balances before and after of createRewardsForAllSubmission submitter and rewardsCoordinator
*/
function testFuzz_createRewardsForAllSubmission_MultipleSubmissions(FuzzAVSRewardsSubmission memory param, uint numSubmissions)
public
{
numSubmissions = bound(numSubmissions, 2, 10);
cheats.prank(rewardsCoordinator.owner());
RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](numSubmissions);
bytes32[] memory rewardsSubmissionHashes = new bytes32[](numSubmissions);
uint startSubmissionNonce = rewardsCoordinator.submissionNonce(rewardsForAllSubmitter);
_deployMockRewardTokens(rewardsForAllSubmitter, numSubmissions);
uint[] memory submitterBalancesBefore = _getBalanceForTokens(rewardTokens, rewardsForAllSubmitter);
uint[] memory rewardsCoordinatorBalancesBefore = _getBalanceForTokens(rewardTokens, address(rewardsCoordinator));
uint[] memory amounts = new uint[](numSubmissions);
// Create multiple rewards submissions and their expected event
for (uint i = 0; i < numSubmissions; ++i) {
// 1. Bound fuzz inputs to valid ranges and amounts using randSeed for each
param.amount = bound(param.amount + i, 1, mockTokenInitialSupply);
amounts[i] = param.amount;
param.duration = bound(param.duration + i, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
param.duration = param.duration - (param.duration % CALCULATION_INTERVAL_SECONDS);
param.startTimestamp = bound(
param.startTimestamp + i,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH))
+ CALCULATION_INTERVAL_SECONDS - 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
param.startTimestamp = param.startTimestamp - (param.startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
RewardsSubmission memory rewardsSubmission = RewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardTokens[i],
amount: amounts[i],
startTimestamp: uint32(param.startTimestamp),
duration: uint32(param.duration)
});
rewardsSubmissions[i] = rewardsSubmission;
// 3. expected event emitted for this rewardsSubmission
rewardsSubmissionHashes[i] = keccak256(abi.encode(rewardsForAllSubmitter, startSubmissionNonce + i, rewardsSubmissions[i]));
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit RewardsSubmissionForAllEarnersCreated(
rewardsForAllSubmitter, startSubmissionNonce + i, rewardsSubmissionHashes[i], rewardsSubmissions[i]
);
}
// 4. call createAVSRewardsSubmission()
cheats.prank(rewardsForAllSubmitter);
rewardsCoordinator.createRewardsForAllEarners(rewardsSubmissions);
// 5. Check for submissionNonce() and rewardsSubmissionHashes being set
assertEq(
startSubmissionNonce + numSubmissions,
rewardsCoordinator.submissionNonce(rewardsForAllSubmitter),
"submission nonce not incremented properly"
);
for (uint i = 0; i < numSubmissions; ++i) {
assertTrue(
rewardsCoordinator.isRewardsSubmissionForAllEarnersHash(rewardsForAllSubmitter, rewardsSubmissionHashes[i]),
"rewards submission hash not submitted"
);
assertEq(
submitterBalancesBefore[i] - amounts[i],
rewardTokens[i].balanceOf(rewardsForAllSubmitter),
"RewardsForAllSubmitter Submitter balance not decremented by amount of rewards submission"
);
assertEq(
rewardsCoordinatorBalancesBefore[i] + amounts[i],
rewardTokens[i].balanceOf(address(rewardsCoordinator)),
"RewardsCoordinator balance not incremented by amount of rewards submission"
);
}
}
}
contract RewardsCoordinatorUnitTests_createOperatorDirectedAVSRewardsSubmission is RewardsCoordinatorUnitTests {
// used for stack too deep
struct FuzzOperatorDirectedAVSRewardsSubmission {
address avs;
uint startTimestamp;
uint duration;
}
OperatorReward[] defaultOperatorRewards;
function setUp() public virtual override {
RewardsCoordinatorUnitTests.setUp();
address[] memory operators = new address[](3);
operators[0] = makeAddr("operator1");
operators[1] = makeAddr("operator2");
operators[2] = makeAddr("operator3");
operators = _sortAddressArrayAsc(operators);
defaultOperatorRewards.push(OperatorReward(operators[0], 1e18));
defaultOperatorRewards.push(OperatorReward(operators[1], 2e18));
defaultOperatorRewards.push(OperatorReward(operators[2], 3e18));
// Set the timestamp to when Rewards v2 will realisticly go out (i.e 6 months)
cheats.warp(GENESIS_REWARDS_TIMESTAMP + 168 days);
}
/// @dev Sort to ensure that the array is in ascending order for addresses
function _sortAddressArrayAsc(address[] memory arr) internal pure returns (address[] memory) {
uint l = arr.length;
for (uint i = 0; i < l; i++) {
for (uint j = i + 1; j < l; j++) {
if (arr[i] > arr[j]) {
address temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
function _getTotalRewardsAmount(OperatorReward[] memory operatorRewards) internal pure returns (uint) {
uint totalAmount = 0;
for (uint i = 0; i < operatorRewards.length; ++i) {
totalAmount += operatorRewards[i].amount;
}
return totalAmount;
}
// Revert when paused
function test_Revert_WhenPaused() public {
cheats.prank(pauser);
rewardsCoordinator.pause(2 ** PAUSED_OPERATOR_DIRECTED_AVS_REWARDS_SUBMISSION);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions;
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(address(this), operatorDirectedRewardsSubmissions);
}
// Revert from reentrancy
function testFuzz_Revert_WhenReentrancy(uint startTimestamp, uint duration) public {
// 1. Bound fuzz inputs to valid ranges and amounts
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Deploy Reenterer
Reenterer reenterer = new Reenterer();
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: IERC20(address(reenterer)),
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
address targetToUse = address(rewardsCoordinator);
uint msgValueToUse = 0;
bytes memory calldataToUse = abi.encodeWithSelector(
RewardsCoordinator.createOperatorDirectedAVSRewardsSubmission.selector, address(reenterer), operatorDirectedRewardsSubmissions
);
reenterer.prepare(targetToUse, msgValueToUse, calldataToUse);
cheats.prank(address(reenterer));
cheats.expectRevert("ReentrancyGuard: reentrant call");
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(address(reenterer), operatorDirectedRewardsSubmissions);
}
// Revert with 0 length strats and multipliers
function testFuzz_Revert_WhenEmptyStratsAndMultipliers(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
StrategyAndMultiplier[] memory emptyStratsAndMultipliers;
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: emptyStratsAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(InputArrayLengthZero.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert with 0 length operator rewards
function testFuzz_Revert_WhenEmptyOperatorRewards(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
OperatorReward[] memory emptyOperatorRewards;
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: emptyOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(InputArrayLengthZero.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert when operator is zero address
function testFuzz_Revert_WhenOperatorIsZeroAddress(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
defaultOperatorRewards[0].operator = address(0);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(InvalidAddressZero.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert when duplicate operators
function testFuzz_Revert_WhenDuplicateOperators(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
OperatorReward[] memory dupOperatorRewards = new OperatorReward[](2);
dupOperatorRewards[0] = defaultOperatorRewards[0];
dupOperatorRewards[1] = defaultOperatorRewards[0];
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: dupOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(OperatorsNotInAscendingOrder.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert when operator amount is zero
function testFuzz_Revert_WhenOperatorAmountIsZero(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
defaultOperatorRewards[0].amount = 0;
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(AmountIsZero.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert when operator amount is zero
function testFuzz_Revert_TotalAmountTooLarge(address avs, uint startTimestamp, uint duration, uint amount)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
amount = bound(amount, 1e38, type(uint).max - 5e18);
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
defaultOperatorRewards[0].amount = amount;
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(AmountExceedsMax.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert with exceeding max duration
function testFuzz_Revert_WhenExceedingMaxDuration(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, MAX_REWARDS_DURATION + 1, type(uint32).max);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(DurationExceedsMax.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert with invalid interval seconds
function testFuzz_Revert_WhenInvalidIntervalSeconds(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
cheats.assume(duration % CALCULATION_INTERVAL_SECONDS != 0);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(InvalidDurationRemainder.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert when duration is 0
function testFuzz_Revert_WhenDurationIsZero(address avs, uint startTimestamp) public filterFuzzedAddressInputs(avs) {
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: 0,
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(DurationIsZero.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert with invalid interval start timestamp
function testFuzz_Revert_WhenInvalidIntervalStartTimestamp(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
cheats.assume(startTimestamp % CALCULATION_INTERVAL_SECONDS != 0);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(InvalidStartTimestampRemainder.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert with retroactive rewards enabled and set too far in past
// - either before genesis rewards timestamp
// - before max retroactive length
function testFuzz_Revert_WhenRewardsSubmissionTooStale(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(startTimestamp, 0, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH - 1);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(StartTimestampTooFarInPast.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert when not retroactive
function testFuzz_Revert_WhenRewardsSubmissionNotRetroactive(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(startTimestamp, block.timestamp - duration + CALCULATION_INTERVAL_SECONDS, type(uint32).max - duration);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(SubmissionNotRetroactive.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert with non whitelisted strategy
function testFuzz_Revert_WhenInvalidStrategy(address avs, uint startTimestamp, uint duration) public filterFuzzedAddressInputs(avs) {
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
defaultStrategyAndMultipliers[0].strategy = IStrategy(address(999));
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(StrategyNotWhitelisted.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
// Revert when duplicate strategies
function testFuzz_Revert_WhenDuplicateStrategies(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
StrategyAndMultiplier[] memory dupStratsAndMultipliers = new StrategyAndMultiplier[](2);
dupStratsAndMultipliers[0] = defaultStrategyAndMultipliers[0];
dupStratsAndMultipliers[1] = defaultStrategyAndMultipliers[0];
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: dupStratsAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(StrategiesNotInAscendingOrder.selector);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
}
/**
* @notice test a single rewards submission asserting for the following
* - correct event emitted
* - submission nonce incrementation by 1, and rewards submission hash being set in storage.
* - rewards submission hash being set in storage
* - token balance before and after of avs and rewardsCoordinator
*/
function testFuzz_createOperatorDirectedAVSRewardsSubmission_SingleSubmission(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected event emitted
uint avsBalanceBefore = rewardToken.balanceOf(avs);
uint rewardsCoordinatorBalanceBefore = rewardToken.balanceOf(address(rewardsCoordinator));
cheats.startPrank(avs);
uint amount = _getTotalRewardsAmount(defaultOperatorRewards);
rewardToken.approve(address(rewardsCoordinator), amount);
uint currSubmissionNonce = rewardsCoordinator.submissionNonce(avs);
bytes32 rewardsSubmissionHash = keccak256(abi.encode(avs, currSubmissionNonce, operatorDirectedRewardsSubmissions[0]));
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorDirectedAVSRewardsSubmissionCreated(
avs, avs, rewardsSubmissionHash, currSubmissionNonce, operatorDirectedRewardsSubmissions[0]
);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
cheats.stopPrank();
assertTrue(
rewardsCoordinator.isOperatorDirectedAVSRewardsSubmissionHash(avs, rewardsSubmissionHash),
"rewards submission hash not submitted"
);
assertEq(currSubmissionNonce + 1, rewardsCoordinator.submissionNonce(avs), "submission nonce not incremented");
assertEq(avsBalanceBefore - amount, rewardToken.balanceOf(avs), "AVS balance not decremented by amount of rewards submission");
assertEq(
rewardsCoordinatorBalanceBefore + amount,
rewardToken.balanceOf(address(rewardsCoordinator)),
"RewardsCoordinator balance not incremented by amount of rewards submission"
);
}
/**
* @notice Same test as above, uses UAM
* - correct event emitted
* - submission nonce incrementation by 1, and rewards submission hash being set in storage.
* - rewards submission hash being set in storage
* - token balance before and after of avs and rewardsCoordinator
*/
function testFuzz_createOperatorDirectedAVSRewardsSubmission_SingleSubmission_UAM(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
// Set UAM
cheats.prank(avs);
permissionController.setAppointee(
avs, defaultAppointee, address(rewardsCoordinator), IRewardsCoordinator.createOperatorDirectedAVSRewardsSubmission.selector
);
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, defaultAppointee);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions =
new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedAVSRewardsSubmission() with expected event emitted
uint submitterBalanceBefore = rewardToken.balanceOf(defaultAppointee);
uint rewardsCoordinatorBalanceBefore = rewardToken.balanceOf(address(rewardsCoordinator));
cheats.startPrank(defaultAppointee);
uint amount = _getTotalRewardsAmount(defaultOperatorRewards);
rewardToken.approve(address(rewardsCoordinator), amount);
uint currSubmissionNonce = rewardsCoordinator.submissionNonce(avs);
bytes32 rewardsSubmissionHash = keccak256(abi.encode(avs, currSubmissionNonce, operatorDirectedRewardsSubmissions[0]));
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorDirectedAVSRewardsSubmissionCreated(
defaultAppointee, avs, rewardsSubmissionHash, currSubmissionNonce, operatorDirectedRewardsSubmissions[0]
);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions);
cheats.stopPrank();
assertTrue(
rewardsCoordinator.isOperatorDirectedAVSRewardsSubmissionHash(avs, rewardsSubmissionHash),
"rewards submission hash not submitted"
);
assertEq(currSubmissionNonce + 1, rewardsCoordinator.submissionNonce(avs), "submission nonce not incremented");
assertEq(
submitterBalanceBefore - amount,
rewardToken.balanceOf(defaultAppointee),
"Submitter balance not decremented by amount of rewards submission"
);
assertEq(
rewardsCoordinatorBalanceBefore + amount,
rewardToken.balanceOf(address(rewardsCoordinator)),
"RewardsCoordinator balance not incremented by amount of rewards submission"
);
}
/**
* @notice test a multiple rewards submission asserting for the following
* - correct event emitted
* - submission nonce incrementation by 1, and rewards submission hash being set in storage.
* - rewards submission hash being set in storage
* - token balance before and after of avs and rewardsCoordinator
*/
function testFuzz_createOperatorDirectedAVSRewardsSubmission_MultipleSubmissions(
FuzzOperatorDirectedAVSRewardsSubmission memory param,
uint numSubmissions
) public filterFuzzedAddressInputs(param.avs) {
numSubmissions = bound(numSubmissions, 2, 10);
cheats.assume(param.avs != address(0));
cheats.prank(rewardsCoordinator.owner());
OperatorDirectedRewardsSubmission[] memory rewardsSubmissions = new OperatorDirectedRewardsSubmission[](numSubmissions);
bytes32[] memory rewardsSubmissionHashes = new bytes32[](numSubmissions);
uint startSubmissionNonce = rewardsCoordinator.submissionNonce(param.avs);
_deployMockRewardTokens(param.avs, numSubmissions);
uint[] memory avsBalancesBefore = _getBalanceForTokens(rewardTokens, param.avs);
uint[] memory rewardsCoordinatorBalancesBefore = _getBalanceForTokens(rewardTokens, address(rewardsCoordinator));
uint[] memory amounts = new uint[](numSubmissions);
// Create multiple rewards submissions and their expected event
for (uint i = 0; i < numSubmissions; ++i) {
// 1. Bound fuzz inputs to valid ranges and amounts using randSeed for each
amounts[i] = _getTotalRewardsAmount(defaultOperatorRewards);
param.duration = bound(param.duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
param.duration = param.duration - (param.duration % CALCULATION_INTERVAL_SECONDS);
param.startTimestamp = bound(
param.startTimestamp + i,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH))
+ CALCULATION_INTERVAL_SECONDS - 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
param.startTimestamp = param.startTimestamp - (param.startTimestamp % CALCULATION_INTERVAL_SECONDS);
param.duration = bound(param.duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
param.duration = param.duration - (param.duration % CALCULATION_INTERVAL_SECONDS);
param.startTimestamp = bound(
param.startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH))
+ CALCULATION_INTERVAL_SECONDS - 1,
block.timestamp - param.duration - 1
);
param.startTimestamp = param.startTimestamp - (param.startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
OperatorDirectedRewardsSubmission memory rewardsSubmission = IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardTokens[i],
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(param.startTimestamp),
duration: uint32(param.duration),
description: ""
});
rewardsSubmissions[i] = rewardsSubmission;
// 3. expected event emitted for this rewardsSubmission
rewardsSubmissionHashes[i] = keccak256(abi.encode(param.avs, startSubmissionNonce + i, rewardsSubmissions[i]));
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorDirectedAVSRewardsSubmissionCreated(
param.avs, param.avs, rewardsSubmissionHashes[i], startSubmissionNonce + i, rewardsSubmissions[i]
);
}
// 4. call createAVSRewardsSubmission()
cheats.prank(param.avs);
rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(param.avs, rewardsSubmissions);
// 5. Check for submissionNonce() and rewardsSubmissionHashes being set
assertEq(
startSubmissionNonce + numSubmissions,
rewardsCoordinator.submissionNonce(param.avs),
"submission nonce not incremented properly"
);
for (uint i = 0; i < numSubmissions; ++i) {
assertTrue(
rewardsCoordinator.isOperatorDirectedAVSRewardsSubmissionHash(param.avs, rewardsSubmissionHashes[i]),
"rewards submission hash not submitted"
);
assertEq(
avsBalancesBefore[i] - amounts[i],
rewardTokens[i].balanceOf(param.avs),
"AVS balance not decremented by amount of rewards submission"
);
assertEq(
rewardsCoordinatorBalancesBefore[i] + amounts[i],
rewardTokens[i].balanceOf(address(rewardsCoordinator)),
"RewardsCoordinator balance not incremented by amount of rewards submission"
);
}
}
}
contract RewardsCoordinatorUnitTests_createOperatorDirectedOperatorSetRewardsSubmission is RewardsCoordinatorUnitTests {
OperatorSet operatorSet;
// used for stack too deep
struct FuzzOperatorDirectedAVSRewardsSubmission {
address avs;
uint startTimestamp;
uint duration;
}
OperatorReward[] defaultOperatorRewards;
function setUp() public virtual override {
RewardsCoordinatorUnitTests.setUp();
address[] memory operators = new address[](3);
operators[0] = makeAddr("operator1");
operators[1] = makeAddr("operator2");
operators[2] = makeAddr("operator3");
operators = _sortAddressArrayAsc(operators);
defaultOperatorRewards.push(OperatorReward(operators[0], 1e18));
defaultOperatorRewards.push(OperatorReward(operators[1], 2e18));
defaultOperatorRewards.push(OperatorReward(operators[2], 3e18));
// Set the timestamp to when Rewards v2 will realisticly go out (i.e 6 months)
cheats.warp(GENESIS_REWARDS_TIMESTAMP + 168 days);
operatorSet = OperatorSet(address(this), 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
}
/// @dev Sort to ensure that the array is in ascending order for addresses
function _sortAddressArrayAsc(address[] memory arr) internal pure returns (address[] memory) {
uint l = arr.length;
for (uint i = 0; i < l; i++) {
for (uint j = i + 1; j < l; j++) {
if (arr[i] > arr[j]) {
address temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
function _getTotalRewardsAmount(OperatorReward[] memory operatorRewards) internal pure returns (uint) {
uint totalAmount = 0;
for (uint i = 0; i < operatorRewards.length; ++i) {
totalAmount += operatorRewards[i].amount;
}
return totalAmount;
}
// Revert when paused
function test_Revert_WhenPaused() public {
cheats.prank(pauser);
rewardsCoordinator.pause(2 ** PAUSED_OPERATOR_DIRECTED_OPERATOR_SET_REWARDS_SUBMISSION);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions;
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
function testFuzz_Revert_InvalidOperatorSet(uint32 id) public {
cheats.assume(id != 1);
operatorSet.id = id;
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions;
cheats.prank(operatorSet.avs);
cheats.expectRevert(InvalidOperatorSet.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert from reentrancy
function testFuzz_Revert_WhenReentrancy(uint startTimestamp, uint duration) public {
// 1. Bound fuzz inputs to valid ranges and amounts
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Deploy Reenterer
Reenterer reenterer = new Reenterer();
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: IERC20(address(reenterer)),
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
operatorSet = OperatorSet(address(reenterer), 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
address targetToUse = address(rewardsCoordinator);
uint msgValueToUse = 0;
bytes memory calldataToUse = abi.encodeWithSelector(
RewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission.selector, operatorSet, operatorDirectedRewardsSubmissions
);
reenterer.prepare(targetToUse, msgValueToUse, calldataToUse);
cheats.prank(address(reenterer));
cheats.expectRevert("ReentrancyGuard: reentrant call");
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert with 0 length strats and multipliers
function testFuzz_Revert_WhenEmptyStratsAndMultipliers(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
StrategyAndMultiplier[] memory emptyStratsAndMultipliers;
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: emptyStratsAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(InputArrayLengthZero.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert with 0 length operator rewards
function testFuzz_Revert_WhenEmptyOperatorRewards(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
OperatorReward[] memory emptyOperatorRewards;
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: emptyOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(InputArrayLengthZero.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert when operator is zero address
function testFuzz_Revert_WhenOperatorIsZeroAddress(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
defaultOperatorRewards[0].operator = address(0);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(InvalidAddressZero.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert when duplicate operators
function testFuzz_Revert_WhenDuplicateOperators(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
OperatorReward[] memory dupOperatorRewards = new OperatorReward[](2);
dupOperatorRewards[0] = defaultOperatorRewards[0];
dupOperatorRewards[1] = defaultOperatorRewards[0];
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: dupOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(OperatorsNotInAscendingOrder.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert when operator amount is zero
function testFuzz_Revert_WhenOperatorAmountIsZero(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
defaultOperatorRewards[0].amount = 0;
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(AmountIsZero.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert when operator amount is zero
function testFuzz_Revert_TotalAmountTooLarge(address avs, uint startTimestamp, uint duration, uint amount)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
amount = bound(amount, 1e38, type(uint).max - 5e18);
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
defaultOperatorRewards[0].amount = amount;
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(AmountExceedsMax.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert with exceeding max duration
function testFuzz_Revert_WhenExceedingMaxDuration(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, MAX_REWARDS_DURATION + 1, type(uint32).max);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(DurationExceedsMax.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert with invalid interval seconds
function testFuzz_Revert_WhenInvalidIntervalSeconds(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
cheats.assume(duration % CALCULATION_INTERVAL_SECONDS != 0);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(InvalidDurationRemainder.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert when duration is 0
function testFuzz_Revert_WhenDurationIsZero(address avs, uint startTimestamp) public filterFuzzedAddressInputs(avs) {
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: 0,
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(DurationIsZero.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert with invalid interval start timestamp
function testFuzz_Revert_WhenInvalidIntervalStartTimestamp(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
cheats.assume(startTimestamp % CALCULATION_INTERVAL_SECONDS != 0);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(InvalidStartTimestampRemainder.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert with retroactive rewards enabled and set too far in past
// - either before genesis rewards timestamp
// - before max retroactive length
function testFuzz_Revert_WhenRewardsSubmissionTooStale(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(startTimestamp, 0, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH - 1);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(StartTimestampTooFarInPast.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert when not retroactive
function testFuzz_Revert_WhenRewardsSubmissionNotRetroactive(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(startTimestamp, block.timestamp - duration + CALCULATION_INTERVAL_SECONDS, type(uint32).max - duration);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(SubmissionNotRetroactive.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert with non whitelisted strategy
function testFuzz_Revert_WhenInvalidStrategy(address avs, uint startTimestamp, uint duration) public filterFuzzedAddressInputs(avs) {
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
defaultStrategyAndMultipliers[0].strategy = IStrategy(address(999));
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(StrategyNotWhitelisted.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
// Revert when duplicate strategies
function testFuzz_Revert_WhenDuplicateStrategies(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
StrategyAndMultiplier[] memory dupStratsAndMultipliers = new StrategyAndMultiplier[](2);
dupStratsAndMultipliers[0] = defaultStrategyAndMultipliers[0];
dupStratsAndMultipliers[1] = defaultStrategyAndMultipliers[0];
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: dupStratsAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert
cheats.prank(avs);
cheats.expectRevert(StrategiesNotInAscendingOrder.selector);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
}
/**
* @notice test a single rewards submission asserting for the following
* - correct event emitted
* - submission nonce incrementation by 1, and rewards submission hash being set in storage.
* - rewards submission hash being set in storage
* - token balance before and after of avs and rewardsCoordinator
*/
function testFuzz_createOperatorDirectedOperatorSetRewardsSubmission_SingleSubmission(address avs, uint startTimestamp, uint duration)
public
filterFuzzedAddressInputs(avs)
{
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected event emitted
uint avsBalanceBefore = rewardToken.balanceOf(avs);
uint rewardsCoordinatorBalanceBefore = rewardToken.balanceOf(address(rewardsCoordinator));
cheats.startPrank(avs);
uint amount = _getTotalRewardsAmount(defaultOperatorRewards);
rewardToken.approve(address(rewardsCoordinator), amount);
uint currSubmissionNonce = rewardsCoordinator.submissionNonce(avs);
bytes32 rewardsSubmissionHash = keccak256(abi.encode(avs, currSubmissionNonce, operatorDirectedRewardsSubmissions[0]));
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorDirectedOperatorSetRewardsSubmissionCreated(
avs, rewardsSubmissionHash, operatorSet, currSubmissionNonce, operatorDirectedRewardsSubmissions[0]
);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
cheats.stopPrank();
assertTrue(
rewardsCoordinator.isOperatorDirectedOperatorSetRewardsSubmissionHash(avs, rewardsSubmissionHash),
"rewards submission hash not submitted"
);
assertEq(currSubmissionNonce + 1, rewardsCoordinator.submissionNonce(avs), "submission nonce not incremented");
assertEq(avsBalanceBefore - amount, rewardToken.balanceOf(avs), "AVS balance not decremented by amount of rewards submission");
assertEq(
rewardsCoordinatorBalanceBefore + amount,
rewardToken.balanceOf(address(rewardsCoordinator)),
"RewardsCoordinator balance not incremented by amount of rewards submission"
);
}
/**
* @notice Same test as above, uses UAM
* - correct event emitted
* - submission nonce incrementation by 1, and rewards submission hash being set in storage.
* - rewards submission hash being set in storage
* - token balance before and after of avs and rewardsCoordinator
*/
function testFuzz_createOperatorDirectedOperatorSetRewardsSubmission_SingleSubmission_UAM(
address avs,
uint startTimestamp,
uint duration
) public filterFuzzedAddressInputs(avs) {
cheats.assume(avs != address(0));
operatorSet = OperatorSet(avs, 1);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
// Set UAM
cheats.prank(avs);
permissionController.setAppointee(
avs,
defaultAppointee,
address(rewardsCoordinator),
IRewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission.selector
);
// 1. Bound fuzz inputs to valid ranges and amounts
IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, defaultAppointee);
duration = bound(duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
duration = duration - (duration % CALCULATION_INTERVAL_SECONDS);
startTimestamp = bound(
startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + CALCULATION_INTERVAL_SECONDS
- 1,
block.timestamp - duration - 1
);
startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create operator directed rewards submission input param
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory operatorDirectedRewardsSubmissions =
new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1);
operatorDirectedRewardsSubmissions[0] = IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardToken,
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(startTimestamp),
duration: uint32(duration),
description: ""
});
// 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected event emitted
uint submitterBalanceBefore = rewardToken.balanceOf(defaultAppointee);
uint rewardsCoordinatorBalanceBefore = rewardToken.balanceOf(address(rewardsCoordinator));
cheats.startPrank(defaultAppointee);
uint amount = _getTotalRewardsAmount(defaultOperatorRewards);
rewardToken.approve(address(rewardsCoordinator), amount);
uint currSubmissionNonce = rewardsCoordinator.submissionNonce(avs);
bytes32 rewardsSubmissionHash = keccak256(abi.encode(avs, currSubmissionNonce, operatorDirectedRewardsSubmissions[0]));
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorDirectedOperatorSetRewardsSubmissionCreated(
defaultAppointee, rewardsSubmissionHash, operatorSet, currSubmissionNonce, operatorDirectedRewardsSubmissions[0]
);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, operatorDirectedRewardsSubmissions);
cheats.stopPrank();
assertTrue(
rewardsCoordinator.isOperatorDirectedOperatorSetRewardsSubmissionHash(avs, rewardsSubmissionHash),
"rewards submission hash not submitted"
);
assertEq(currSubmissionNonce + 1, rewardsCoordinator.submissionNonce(avs), "submission nonce not incremented");
assertEq(
submitterBalanceBefore - amount,
rewardToken.balanceOf(defaultAppointee),
"Submitter balance not decremented by amount of rewards submission"
);
assertEq(
rewardsCoordinatorBalanceBefore + amount,
rewardToken.balanceOf(address(rewardsCoordinator)),
"RewardsCoordinator balance not incremented by amount of rewards submission"
);
}
/**
* @notice test a multiple rewards submission asserting for the following
* - correct event emitted
* - submission nonce incrementation by 1, and rewards submission hash being set in storage.
* - rewards submission hash being set in storage
* - token balance before and after of avs and rewardsCoordinator
*/
function testFuzz_createOperatorDirectedOperatorSetRewardsSubmission_MultipleSubmissions(
FuzzOperatorDirectedAVSRewardsSubmission memory param,
uint numSubmissions
) public filterFuzzedAddressInputs(param.avs) {
numSubmissions = bound(numSubmissions, 2, 10);
cheats.assume(param.avs != address(0));
operatorSet = OperatorSet(param.avs, 2);
allocationManagerMock.setIsOperatorSet(operatorSet, true);
cheats.prank(rewardsCoordinator.owner());
OperatorDirectedRewardsSubmission[] memory rewardsSubmissions = new OperatorDirectedRewardsSubmission[](numSubmissions);
bytes32[] memory rewardsSubmissionHashes = new bytes32[](numSubmissions);
uint startSubmissionNonce = rewardsCoordinator.submissionNonce(param.avs);
_deployMockRewardTokens(param.avs, numSubmissions);
uint[] memory avsBalancesBefore = _getBalanceForTokens(rewardTokens, param.avs);
uint[] memory rewardsCoordinatorBalancesBefore = _getBalanceForTokens(rewardTokens, address(rewardsCoordinator));
uint[] memory amounts = new uint[](numSubmissions);
// Create multiple rewards submissions and their expected event
for (uint i = 0; i < numSubmissions; ++i) {
// 1. Bound fuzz inputs to valid ranges and amounts using randSeed for each
amounts[i] = _getTotalRewardsAmount(defaultOperatorRewards);
param.duration = bound(param.duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
param.duration = param.duration - (param.duration % CALCULATION_INTERVAL_SECONDS);
param.startTimestamp = bound(
param.startTimestamp + i,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH))
+ CALCULATION_INTERVAL_SECONDS - 1,
block.timestamp + uint(MAX_FUTURE_LENGTH)
);
param.startTimestamp = param.startTimestamp - (param.startTimestamp % CALCULATION_INTERVAL_SECONDS);
param.duration = bound(param.duration, CALCULATION_INTERVAL_SECONDS, MAX_REWARDS_DURATION);
param.duration = param.duration - (param.duration % CALCULATION_INTERVAL_SECONDS);
param.startTimestamp = bound(
param.startTimestamp,
uint(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH))
+ CALCULATION_INTERVAL_SECONDS - 1,
block.timestamp - param.duration - 1
);
param.startTimestamp = param.startTimestamp - (param.startTimestamp % CALCULATION_INTERVAL_SECONDS);
// 2. Create rewards submission input param
OperatorDirectedRewardsSubmission memory rewardsSubmission = IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: defaultStrategyAndMultipliers,
token: rewardTokens[i],
operatorRewards: defaultOperatorRewards,
startTimestamp: uint32(param.startTimestamp),
duration: uint32(param.duration),
description: ""
});
rewardsSubmissions[i] = rewardsSubmission;
// 3. expected event emitted for this rewardsSubmission
rewardsSubmissionHashes[i] = keccak256(abi.encode(param.avs, startSubmissionNonce + i, rewardsSubmissions[i]));
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit OperatorDirectedOperatorSetRewardsSubmissionCreated(
param.avs, rewardsSubmissionHashes[i], operatorSet, startSubmissionNonce + i, rewardsSubmissions[i]
);
}
// 4. call createAVSRewardsSubmission()
cheats.prank(param.avs);
rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, rewardsSubmissions);
// 5. Check for submissionNonce() and rewardsSubmissionHashes being set
assertEq(
startSubmissionNonce + numSubmissions,
rewardsCoordinator.submissionNonce(param.avs),
"submission nonce not incremented properly"
);
for (uint i = 0; i < numSubmissions; ++i) {
assertTrue(
rewardsCoordinator.isOperatorDirectedOperatorSetRewardsSubmissionHash(param.avs, rewardsSubmissionHashes[i]),
"rewards submission hash not submitted"
);
assertEq(
avsBalancesBefore[i] - amounts[i],
rewardTokens[i].balanceOf(param.avs),
"AVS balance not decremented by amount of rewards submission"
);
assertEq(
rewardsCoordinatorBalancesBefore[i] + amounts[i],
rewardTokens[i].balanceOf(address(rewardsCoordinator)),
"RewardsCoordinator balance not incremented by amount of rewards submission"
);
}
}
}
contract RewardsCoordinatorUnitTests_submitRoot is RewardsCoordinatorUnitTests {
// only callable by rewardsUpdater
function testFuzz_Revert_WhenNotRewardsUpdater(address invalidRewardsUpdater) public filterFuzzedAddressInputs(invalidRewardsUpdater) {
cheats.prank(invalidRewardsUpdater);
cheats.expectRevert(UnauthorizedCaller.selector);
rewardsCoordinator.submitRoot(bytes32(0), 0);
}
function test_Revert_WhenSubmitRootPaused() public {
cheats.prank(pauser);
rewardsCoordinator.pause(2 ** PAUSED_SUBMIT_ROOTS);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
rewardsCoordinator.submitRoot(bytes32(0), 0);
}
/// @notice submits root with correct values and adds to root storage array
/// - checks activatedAt has added activationDelay
function testFuzz_submitRoot(bytes32 root, uint32 rewardsCalculationEndTimestamp) public {
// fuzz avoiding overflows and valid activatedAt values
cheats.assume(rewardsCoordinator.currRewardsCalculationEndTimestamp() < rewardsCalculationEndTimestamp);
cheats.assume(rewardsCalculationEndTimestamp < block.timestamp);
uint32 expectedRootIndex = uint32(rewardsCoordinator.getDistributionRootsLength());
uint32 activatedAt = uint32(block.timestamp) + rewardsCoordinator.activationDelay();
cheats.expectEmit(true, true, true, true, address(rewardsCoordinator));
emit DistributionRootSubmitted(expectedRootIndex, root, rewardsCalculationEndTimestamp, activatedAt);
cheats.prank(rewardsUpdater);
rewardsCoordinator.submitRoot(root, rewardsCalculationEndTimestamp);
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(expectedRootIndex);
assertEq(expectedRootIndex, rewardsCoordinator.getDistributionRootsLength() - 1, "root not added to roots array");
assertEq(root, rewardsCoordinator.getCurrentDistributionRoot().root, "getCurrentDistributionRoot view function failed");
assertEq(
root, rewardsCoordinator.getDistributionRootAtIndex(expectedRootIndex).root, "getDistributionRootAtIndex view function failed"
);
assertEq(activatedAt, distributionRoot.activatedAt, "activatedAt not correct");
assertEq(root, distributionRoot.root, "root not set");
assertEq(rewardsCalculationEndTimestamp, distributionRoot.rewardsCalculationEndTimestamp, "rewardsCalculationEndTimestamp not set");
assertEq(
rewardsCoordinator.currRewardsCalculationEndTimestamp(),
rewardsCalculationEndTimestamp,
"currRewardsCalculationEndTimestamp not set"
);
}
/// @notice Submits multiple roots and checks root index from hash is correct
function testFuzz_getRootIndexFromHash(bytes32 root, uint16 numRoots, uint index) public {
numRoots = uint16(bound(numRoots, 1, 100));
index = bound(index, 0, uint(numRoots - 1));
bytes32[] memory roots = new bytes32[](numRoots);
cheats.startPrank(rewardsUpdater);
for (uint16 i = 0; i < numRoots; ++i) {
roots[i] = keccak256(abi.encodePacked(root, i));
uint32 activationDelay = uint32(block.timestamp) + rewardsCoordinator.activationDelay();
rewardsCoordinator.submitRoot(roots[i], uint32(block.timestamp - 1));
cheats.warp(activationDelay);
}
cheats.stopPrank();
assertEq(index, rewardsCoordinator.getRootIndexFromHash(roots[index]), "root index not found");
}
}
/// @notice Tests for sets of JSON data with different distribution roots
contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests {
using stdStorage for StdStorage;
/// @notice earner address used for proofs
address earner = 0xF2288D736d27C1584Ebf7be5f52f9E4d47251AeE;
/// @notice mock token bytecode
bytes mockTokenBytecode;
uint32 prevRootCalculationEndTimestamp;
// Temp storage for managing stack in _parseProofData
bytes32 merkleRoot;
uint32 earnerIndex;
bytes earnerTreeProof;
address proofEarner;
bytes32 earnerTokenRoot;
function setUp() public virtual override {
RewardsCoordinatorUnitTests.setUp();
// Create mock token to use bytecode later to etch
IERC20 mockToken = new ERC20Mock();
mockTokenBytecode = address(mockToken).code;
}
/// @notice Claim against latest submitted root, rootIndex 3 using batch claim.
/// Limit fuzz runs to speed up tests since these require reading from JSON
/// forge-config: default.fuzz.runs = 10
function testFuzz_processClaims_LatestRoot(bool setClaimerFor, address claimerFor) public filterFuzzedAddressInputs(claimerFor) {
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofs();
RewardsMerkleClaim memory claim = claims[2];
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Claim against root and check balances before/after, and check it matches the difference between
// cumulative claimed and earned.
cheats.startPrank(claimer);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
uint[] memory totalClaimedBefore = _getCumulativeClaimed(earner, claim);
uint[] memory earnings = _getCumulativeEarnings(claim);
uint[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim);
_assertRewardsClaimedEvents(distributionRoot.root, claim, claimer);
IRewardsCoordinator.RewardsMerkleClaim[] memory batchClaim = new IRewardsCoordinator.RewardsMerkleClaim[](1);
batchClaim[0] = claim;
rewardsCoordinator.processClaims(batchClaim, claimer);
uint[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim);
for (uint i = 0; i < totalClaimedBefore.length; ++i) {
assertEq(
earnings[i] - totalClaimedBefore[i],
tokenBalancesAfter[i] - tokenBalancesBefore[i],
"Token balance not incremented by earnings amount"
);
}
cheats.stopPrank();
}
/// @notice Claim against latest submitted root, rootIndex 3
/// Limit fuzz runs to speed up tests since these require reading from JSON
/// forge-config: default.fuzz.runs = 10
function testFuzz_processClaim_LatestRoot(bool setClaimerFor, address claimerFor) public filterFuzzedAddressInputs(claimerFor) {
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofs();
RewardsMerkleClaim memory claim = claims[2];
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Claim against root and check balances before/after, and check it matches the difference between
// cumulative claimed and earned.
cheats.startPrank(claimer);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
uint[] memory totalClaimedBefore = _getCumulativeClaimed(earner, claim);
uint[] memory earnings = _getCumulativeEarnings(claim);
uint[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim);
_assertRewardsClaimedEvents(distributionRoot.root, claim, claimer);
rewardsCoordinator.processClaim(claim, claimer);
uint[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim);
for (uint i = 0; i < totalClaimedBefore.length; ++i) {
assertEq(
earnings[i] - totalClaimedBefore[i],
tokenBalancesAfter[i] - tokenBalancesBefore[i],
"Token balance not incremented by earnings amount"
);
}
cheats.stopPrank();
}
/// @notice Claim against an old root that isn't the latest
/// forge-config: default.fuzz.runs = 10
function testFuzz_processClaim_OldRoot(bool setClaimerFor, address claimerFor) public filterFuzzedAddressInputs(claimerFor) {
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofs();
RewardsMerkleClaim memory claim = claims[0];
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Claim against root and check balances before/after, and check it matches the difference between
// cumulative claimed and earned.
cheats.startPrank(claimer);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
uint[] memory totalClaimedBefore = _getCumulativeClaimed(earner, claim);
uint[] memory earnings = _getCumulativeEarnings(claim);
uint[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim);
_assertRewardsClaimedEvents(distributionRoot.root, claim, claimer);
rewardsCoordinator.processClaim(claim, claimer);
uint[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim);
for (uint i = 0; i < totalClaimedBefore.length; ++i) {
assertEq(
earnings[i] - totalClaimedBefore[i],
tokenBalancesAfter[i] - tokenBalancesBefore[i],
"Token balance not incremented by earnings amount"
);
}
cheats.stopPrank();
}
/// @notice Claim against all roots in order, rootIndex 0, 1, 2
/// forge-config: default.fuzz.runs = 10
function testFuzz_processClaim_Sequential(bool setClaimerFor, address claimerFor) public filterFuzzedAddressInputs(claimerFor) {
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofs();
RewardsMerkleClaim memory claim = claims[0];
// 1. Claim against first root
{
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Claim against root and check balances before/after, and check it matches the difference between
// cumulative claimed and earned.
cheats.startPrank(claimer);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
uint[] memory totalClaimedBefore = _getCumulativeClaimed(earner, claim);
uint[] memory earnings = _getCumulativeEarnings(claim);
uint[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim);
_assertRewardsClaimedEvents(distributionRoot.root, claim, claimer);
rewardsCoordinator.processClaim(claim, claimer);
uint[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim);
for (uint i = 0; i < totalClaimedBefore.length; ++i) {
assertEq(
earnings[i] - totalClaimedBefore[i],
tokenBalancesAfter[i] - tokenBalancesBefore[i],
"Token balance not incremented by earnings amount"
);
}
cheats.stopPrank();
}
// 2. Claim against second root
claim = claims[1];
{
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Claim against root and check balances before/after, and check it matches the difference between
// cumulative claimed and earned.
cheats.startPrank(claimer);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
uint[] memory totalClaimedBefore = _getCumulativeClaimed(earner, claim);
uint[] memory earnings = _getCumulativeEarnings(claim);
uint[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim);
_assertRewardsClaimedEvents(distributionRoot.root, claim, claimer);
rewardsCoordinator.processClaim(claim, claimer);
uint[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim);
for (uint i = 0; i < totalClaimedBefore.length; ++i) {
assertEq(
earnings[i] - totalClaimedBefore[i],
tokenBalancesAfter[i] - tokenBalancesBefore[i],
"Token balance not incremented by earnings amount"
);
}
cheats.stopPrank();
}
// 3. Claim against third and latest root
claim = claims[2];
{
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Claim against root and check balances before/after, and check it matches the difference between
// cumulative claimed and earned.
cheats.startPrank(claimer);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
uint[] memory totalClaimedBefore = _getCumulativeClaimed(earner, claim);
uint[] memory earnings = _getCumulativeEarnings(claim);
uint[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim);
_assertRewardsClaimedEvents(distributionRoot.root, claim, claimer);
rewardsCoordinator.processClaim(claim, claimer);
uint[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim);
for (uint i = 0; i < totalClaimedBefore.length; ++i) {
assertEq(
earnings[i] - totalClaimedBefore[i],
tokenBalancesAfter[i] - tokenBalancesBefore[i],
"Token balance not incremented by earnings amount"
);
}
cheats.stopPrank();
}
}
function testFuzz_processClaim_Revert_WhenRootDisabled(bool setClaimerFor, address claimerFor, bytes32 root)
public
filterFuzzedAddressInputs(claimerFor)
{
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Submit a root and disable it
cheats.startPrank(rewardsUpdater);
rewardsCoordinator.submitRoot(root, 1);
uint32 rootIndex = 0;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
rewardsCoordinator.disableRoot(rootIndex);
cheats.stopPrank();
cheats.warp(distributionRoot.activatedAt);
cheats.startPrank(claimer);
// rootIndex in claim is 0, which is disabled
RewardsMerkleClaim memory claim;
cheats.expectRevert(RootDisabled.selector);
rewardsCoordinator.processClaim(claim, claimer);
cheats.stopPrank();
}
/// @notice Claim against rootIndex 0 and claim again. Balances should not increment.
/// forge-config: default.fuzz.runs = 10
function testFuzz_processClaim_Revert_WhenReuseSameClaimAgain(bool setClaimerFor, address claimerFor)
public
filterFuzzedAddressInputs(claimerFor)
{
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofs();
RewardsMerkleClaim memory claim = claims[0];
// 1. Claim against first root
{
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Claim against root and check balances before/after, and check it matches the difference between
// cumulative claimed and earned.
cheats.startPrank(claimer);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
uint[] memory totalClaimedBefore = _getCumulativeClaimed(earner, claim);
uint[] memory earnings = _getCumulativeEarnings(claim);
uint[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim);
_assertRewardsClaimedEvents(distributionRoot.root, claim, claimer);
rewardsCoordinator.processClaim(claim, claimer);
uint[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim);
for (uint i = 0; i < totalClaimedBefore.length; ++i) {
assertEq(
earnings[i] - totalClaimedBefore[i],
tokenBalancesAfter[i] - tokenBalancesBefore[i],
"Token balance not incremented by earnings amount"
);
}
cheats.stopPrank();
}
// 2. Claim against first root again, expect a revert
{
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Claim against root and check balances before/after, and check it matches the difference between
// cumulative claimed and earned.
cheats.startPrank(claimer);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
cheats.expectRevert(EarningsNotGreaterThanClaimed.selector);
rewardsCoordinator.processClaim(claim, claimer);
cheats.stopPrank();
}
}
/// @notice Claim against latest submitted root, rootIndex 3 but modify some of the leaf values
/// forge-config: default.fuzz.runs = 10
function testFuzz_processClaim_Revert_WhenInvalidTokenClaim(bool setClaimerFor, address claimerFor)
public
filterFuzzedAddressInputs(claimerFor)
{
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofs();
RewardsMerkleClaim memory claim = claims[2];
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Modify Earnings
claim.tokenLeaves[0].cumulativeEarnings = 1e20;
claim.tokenLeaves[1].cumulativeEarnings = 1e20;
// Check claim is not valid from both checkClaim() and processClaim() throwing a revert
cheats.startPrank(claimer);
cheats.expectRevert(InvalidClaimProof.selector);
assertFalse(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
cheats.expectRevert(InvalidClaimProof.selector);
rewardsCoordinator.processClaim(claim, claimer);
cheats.stopPrank();
}
/// @notice Claim against latest submitted root, rootIndex 3 but modify some of the leaf values
/// forge-config: default.fuzz.runs = 10
function testFuzz_processClaim_Revert_WhenInvalidEarnerClaim(bool setClaimerFor, address claimerFor, address invalidEarner)
public
filterFuzzedAddressInputs(claimerFor)
{
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofs();
RewardsMerkleClaim memory claim = claims[2];
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Modify Earner
claim.earnerLeaf.earner = invalidEarner;
// Check claim is not valid from both checkClaim() and processClaim() throwing a revert
cheats.startPrank(claimer);
cheats.expectRevert(InvalidClaimProof.selector);
assertFalse(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
cheats.expectRevert(InvalidClaimProof.selector);
rewardsCoordinator.processClaim(claim, claimer);
cheats.stopPrank();
}
/// @notice Claim against latest submitted root, rootIndex 3 but write to cumulativeClaimed storage.
/// Expects a revert when calling processClaim()
function testFuzz_processClaim_Revert_WhenCumulativeClaimedUnderflow(bool setClaimerFor, address claimerFor)
public
filterFuzzedAddressInputs(claimerFor)
{
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofs();
RewardsMerkleClaim memory claim = claims[2];
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
// Set cumulativeClaimed to be max uint256, should revert when attempting to claim
stdstore.target(address(rewardsCoordinator)).sig("cumulativeClaimed(address,address)").with_key(claim.earnerLeaf.earner).with_key(
address(claim.tokenLeaves[0].token)
).checked_write(type(uint).max);
cheats.startPrank(claimer);
cheats.expectRevert(EarningsNotGreaterThanClaimed.selector);
rewardsCoordinator.processClaim(claim, claimer);
cheats.stopPrank();
}
/// @notice Claim against latest submitted root, rootIndex 3 but with larger tokenIndex used that could pass the proofs.
/// Expects a revert as we check for this in processClaim()
function testFuzz_processClaim_Revert_WhenTokenIndexBitwiseAddedTo(bool setClaimerFor, address claimerFor, uint8 numShift)
public
filterFuzzedAddressInputs(claimerFor)
{
cheats.assume(0 < numShift && numShift < 16);
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofs();
RewardsMerkleClaim memory claim = claims[2];
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
// Take the tokenIndex and add a significant bit so that the actual index number is increased
// but still with the same least significant bits for valid proofs
uint8 proofLength = uint8(claim.tokenTreeProofs[0].length);
claim.tokenIndices[0] = claim.tokenIndices[0] | uint32(1 << (numShift + proofLength / 32));
cheats.startPrank(claimer);
cheats.expectRevert(InvalidTokenLeafIndex.selector);
rewardsCoordinator.processClaim(claim, claimer);
cheats.stopPrank();
}
/// @notice Claim against latest submitted root, rootIndex 3 but with larger earnerIndex used that could pass the proofs.
/// Expects a revert as we check for this in processClaim()
function testFuzz_processClaim_Revert_WhenEarnerIndexBitwiseAddedTo(bool setClaimerFor, address claimerFor, uint8 numShift)
public
filterFuzzedAddressInputs(claimerFor)
{
cheats.assume(0 < numShift && numShift < 16);
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofs();
RewardsMerkleClaim memory claim = claims[2];
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
// Take the tokenIndex and add a significant bit so that the actual index number is increased
// but still with the same least significant bits for valid proofs
uint8 proofLength = uint8(claim.earnerTreeProof.length);
claim.earnerIndex = claim.earnerIndex | uint32(1 << (numShift + proofLength / 32));
cheats.startPrank(claimer);
cheats.expectRevert(InvalidEarnerLeafIndex.selector);
rewardsCoordinator.processClaim(claim, claimer);
cheats.stopPrank();
}
/// @notice tests with earnerIndex and tokenIndex set to max value and using alternate claim proofs
function testFuzz_processClaim_WhenMaxEarnerIndexAndTokenIndex(bool setClaimerFor, address claimerFor)
public
filterFuzzedAddressInputs(claimerFor)
{
// Hardcode earner address to earner in alternate claim proofs
earner = 0x25A1B7322f9796B26a4Bec125913b34C292B28D6;
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofsMaxEarnerAndLeafIndices();
RewardsMerkleClaim memory claim = claims[0];
// 1. Claim against first root where earner tree is full tree and earner and token index is last index of that tree height
{
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Claim against root and check balances before/after, and check it matches the difference between
// cumulative claimed and earned.
cheats.startPrank(claimer);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
uint[] memory totalClaimedBefore = _getCumulativeClaimed(earner, claim);
uint[] memory earnings = _getCumulativeEarnings(claim);
uint[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim);
// +1 since earnerIndex is 0-indexed
// Here the earnerIndex is 7 in a full binary tree and the number of bytes32 hash proofs is 3
assertEq(claim.earnerIndex + 1, (1 << ((claim.earnerTreeProof.length / 32))), "EarnerIndex not set to max value");
// +1 since tokenIndex is 0-indexed
// Here the tokenIndex is also 7 in a full binary tree and the number of bytes32 hash proofs is 3
assertEq(claim.tokenIndices[0] + 1, (1 << ((claim.tokenTreeProofs[0].length / 32))), "TokenIndex not set to max value");
_assertRewardsClaimedEvents(distributionRoot.root, claim, claimer);
rewardsCoordinator.processClaim(claim, claimer);
uint[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim);
for (uint i = 0; i < totalClaimedBefore.length; ++i) {
assertEq(
earnings[i] - totalClaimedBefore[i],
tokenBalancesAfter[i] - tokenBalancesBefore[i],
"Token balance not incremented by earnings amount"
);
}
cheats.stopPrank();
}
}
/// @notice tests with single token leaf for the earner's subtree. tokenTreeProof for the token in the claim should be empty
function testFuzz_processClaim_WhenSingleTokenLeaf(bool setClaimerFor, address claimerFor)
public
filterFuzzedAddressInputs(claimerFor)
{
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofsSingleTokenLeaf();
RewardsMerkleClaim memory claim = claims[0];
// 1. Claim against first root where earner tree is full tree and earner and token index is last index of that tree height
{
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Claim against root and check balances before/after, and check it matches the difference between
// cumulative claimed and earned.
cheats.startPrank(claimer);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
uint[] memory totalClaimedBefore = _getCumulativeClaimed(earner, claim);
uint[] memory earnings = _getCumulativeEarnings(claim);
uint[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim);
// Single tokenLeaf in earner's subtree, should be 0 index
assertEq(claim.tokenIndices[0], 0, "TokenIndex should be 0");
assertEq(claim.tokenTreeProofs[0].length, 0, "TokenTreeProof should be empty");
_assertRewardsClaimedEvents(distributionRoot.root, claim, claimer);
rewardsCoordinator.processClaim(claim, claimer);
uint[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim);
for (uint i = 0; i < totalClaimedBefore.length; ++i) {
assertEq(
earnings[i] - totalClaimedBefore[i],
tokenBalancesAfter[i] - tokenBalancesBefore[i],
"Token balance not incremented by earnings amount"
);
}
cheats.stopPrank();
}
}
/// @notice tests with single earner leaf in the merkle tree. earnerTreeProof in claim should be empty
function testFuzz_processClaim_WhenSingleEarnerLeaf(bool setClaimerFor, address claimerFor)
public
filterFuzzedAddressInputs(claimerFor)
{
// Hardcode earner address to earner in alternate claim proofs
earner = 0x0D6bA28b9919CfCDb6b233469Cc5Ce30b979e08E;
// if setClaimerFor is true, set the earners claimer to the fuzzed address
address claimer;
if (setClaimerFor) {
cheats.prank(earner);
rewardsCoordinator.setClaimerFor(claimerFor);
claimer = claimerFor;
} else {
claimer = earner;
}
// Parse all 3 claim proofs for distributionRoots 0,1,2 respectively
RewardsMerkleClaim[] memory claims = _parseAllProofsSingleEarnerLeaf();
RewardsMerkleClaim memory claim = claims[0];
// 1. Claim against first root where earner tree is full tree and earner and token index is last index of that tree height
{
uint32 rootIndex = claim.rootIndex;
IRewardsCoordinator.DistributionRoot memory distributionRoot = rewardsCoordinator.getDistributionRootAtIndex(rootIndex);
cheats.warp(distributionRoot.activatedAt);
// Claim against root and check balances before/after, and check it matches the difference between
// cumulative claimed and earned.
cheats.startPrank(claimer);
assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid");
uint[] memory totalClaimedBefore = _getCumulativeClaimed(earner, claim);
uint[] memory earnings = _getCumulativeEarnings(claim);
uint[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim);
// Earner Leaf in merkle tree, should be 0 index
assertEq(claim.earnerIndex, 0, "EarnerIndex should be 0");
assertEq(claim.earnerTreeProof.length, 0, "EarnerTreeProof should be empty");
_assertRewardsClaimedEvents(distributionRoot.root, claim, claimer);
rewardsCoordinator.processClaim(claim, claimer);
uint[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim);
for (uint i = 0; i < totalClaimedBefore.length; ++i) {
assertEq(
earnings[i] - totalClaimedBefore[i],
tokenBalancesAfter[i] - tokenBalancesBefore[i],
"Token balance not incremented by earnings amount"
);
}
cheats.stopPrank();
}
}
/// @notice Set address with ERC20Mock bytecode and mint amount to rewardsCoordinator for
/// balance for testing processClaim()
function _setAddressAsERC20(address randAddress, uint mintAmount) internal {
cheats.etch(randAddress, mockTokenBytecode);
ERC20Mock(randAddress).mint(address(rewardsCoordinator), mintAmount);
}
/// @notice parse proofs from json file and submitRoot()
function _parseProofData(string memory filePath) internal returns (RewardsMerkleClaim memory) {
cheats.readFile(filePath);
string memory claimProofData = cheats.readFile(filePath);
// Parse RewardsMerkleClaim
merkleRoot = abi.decode(stdJson.parseRaw(claimProofData, ".Root"), (bytes32));
earnerIndex = abi.decode(stdJson.parseRaw(claimProofData, ".EarnerIndex"), (uint32));
earnerTreeProof = abi.decode(stdJson.parseRaw(claimProofData, ".EarnerTreeProof"), (bytes));
proofEarner = stdJson.readAddress(claimProofData, ".EarnerLeaf.Earner");
require(earner == proofEarner, "earner in test and json file do not match");
earnerTokenRoot = abi.decode(stdJson.parseRaw(claimProofData, ".EarnerLeaf.EarnerTokenRoot"), (bytes32));
uint numTokenLeaves = stdJson.readUint(claimProofData, ".TokenLeavesNum");
uint numTokenTreeProofs = stdJson.readUint(claimProofData, ".TokenTreeProofsNum");
TokenTreeMerkleLeaf[] memory tokenLeaves = new TokenTreeMerkleLeaf[](numTokenLeaves);
uint32[] memory tokenIndices = new uint32[](numTokenLeaves);
for (uint i = 0; i < numTokenLeaves; ++i) {
string memory tokenKey = string.concat(".TokenLeaves[", cheats.toString(i), "].Token");
string memory amountKey = string.concat(".TokenLeaves[", cheats.toString(i), "].CumulativeEarnings");
string memory leafIndicesKey = string.concat(".LeafIndices[", cheats.toString(i), "]");
IERC20 token = IERC20(stdJson.readAddress(claimProofData, tokenKey));
uint cumulativeEarnings = stdJson.readUint(claimProofData, amountKey);
tokenLeaves[i] = TokenTreeMerkleLeaf({token: token, cumulativeEarnings: cumulativeEarnings});
tokenIndices[i] = uint32(stdJson.readUint(claimProofData, leafIndicesKey));
/// DeployCode ERC20 to Token Address
// deployCodeTo("ERC20PresetFixedSupply.sol", address(tokenLeaves[i].token));
_setAddressAsERC20(address(token), cumulativeEarnings);
}
bytes[] memory tokenTreeProofs = new bytes[](numTokenTreeProofs);
for (uint i = 0; i < numTokenTreeProofs; ++i) {
string memory tokenTreeProofKey = string.concat(".TokenTreeProofs[", cheats.toString(i), "]");
tokenTreeProofs[i] = abi.decode(stdJson.parseRaw(claimProofData, tokenTreeProofKey), (bytes));
}
uint32 rootCalculationEndTimestamp = uint32(block.timestamp);
uint32 activatedAt = uint32(block.timestamp) + rewardsCoordinator.activationDelay();
prevRootCalculationEndTimestamp = rootCalculationEndTimestamp;
cheats.warp(activatedAt);
uint32 rootIndex = uint32(rewardsCoordinator.getDistributionRootsLength());
cheats.prank(rewardsUpdater);
rewardsCoordinator.submitRoot(merkleRoot, prevRootCalculationEndTimestamp);
RewardsMerkleClaim memory newClaim = RewardsMerkleClaim({
rootIndex: rootIndex,
earnerIndex: earnerIndex,
earnerTreeProof: earnerTreeProof,
earnerLeaf: EarnerTreeMerkleLeaf({earner: earner, earnerTokenRoot: earnerTokenRoot}),
tokenIndices: tokenIndices,
tokenTreeProofs: tokenTreeProofs,
tokenLeaves: tokenLeaves
});
return newClaim;
}
function _parseAllProofs() internal virtual returns (RewardsMerkleClaim[] memory) {
RewardsMerkleClaim[] memory claims = new RewardsMerkleClaim[](3);
claims[0] = _parseProofData("src/test/test-data/rewardsCoordinator/processClaimProofs_Root1.json");
claims[1] = _parseProofData("src/test/test-data/rewardsCoordinator/processClaimProofs_Root2.json");
claims[2] = _parseProofData("src/test/test-data/rewardsCoordinator/processClaimProofs_Root3.json");
return claims;
}
function _parseAllProofsMaxEarnerAndLeafIndices() internal virtual returns (RewardsMerkleClaim[] memory) {
RewardsMerkleClaim[] memory claims = new RewardsMerkleClaim[](1);
claims[0] = _parseProofData("src/test/test-data/rewardsCoordinator/processClaimProofs_MaxEarnerAndLeafIndices.json");
return claims;
}
function _parseAllProofsSingleTokenLeaf() internal virtual returns (RewardsMerkleClaim[] memory) {
RewardsMerkleClaim[] memory claims = new RewardsMerkleClaim[](1);
claims[0] = _parseProofData("src/test/test-data/rewardsCoordinator/processClaimProofs_SingleTokenLeaf.json");
return claims;
}
function _parseAllProofsSingleEarnerLeaf() internal virtual returns (RewardsMerkleClaim[] memory) {
RewardsMerkleClaim[] memory claims = new RewardsMerkleClaim[](1);
claims[0] = _parseProofData("src/test/test-data/rewardsCoordinator/processClaimProofs_SingleEarnerLeaf.json");
return claims;
}
}
````
## File: src/test/unit/StrategyBaseTVLLimitsUnit.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "./StrategyBaseUnit.t.sol";
import "../../contracts/strategies/StrategyBaseTVLLimits.sol";
contract StrategyBaseTVLLimitsUnitTests is StrategyBaseUnitTests {
StrategyBaseTVLLimits public strategyBaseTVLLimitsImplementation;
StrategyBaseTVLLimits public strategyWithTVLLimits;
// defaults for tests, used in setup
uint maxTotalDeposits = 3200e18;
uint maxPerDeposit = 32e18;
/// @notice Emitted when `maxPerDeposit` value is updated from `previousValue` to `newValue`
event MaxPerDepositUpdated(uint previousValue, uint newValue);
/// @notice Emitted when `maxTotalDeposits` value is updated from `previousValue` to `newValue`
event MaxTotalDepositsUpdated(uint previousValue, uint newValue);
function setUp() public virtual override {
// copy setup for StrategyBaseUnitTests
StrategyBaseUnitTests.setUp();
// depoloy the TVL-limited strategy
strategyBaseTVLLimitsImplementation = new StrategyBaseTVLLimits(strategyManager, pauserRegistry, "v9.9.9");
strategyWithTVLLimits = StrategyBaseTVLLimits(
address(
new TransparentUpgradeableProxy(
address(strategyBaseTVLLimitsImplementation),
address(proxyAdmin),
abi.encodeWithSelector(
StrategyBaseTVLLimits.initialize.selector, maxPerDeposit, maxTotalDeposits, underlyingToken, pauserRegistry
)
)
)
);
// verify that limits were set up correctly
require(strategyWithTVLLimits.maxTotalDeposits() == maxTotalDeposits, "bad test setup");
require(strategyWithTVLLimits.maxPerDeposit() == maxPerDeposit, "bad test setup");
// replace the strategy from the non-TVL-limited tests with the TVL-limited strategy
// this should result in all the StrategyBaseUnitTests also running against the `StrategyBaseTVLLimits` contract
strategy = strategyWithTVLLimits;
}
function testSetTVLLimits(uint maxPerDepositFuzzedInput, uint maxTotalDepositsFuzzedInput) public {
_setTVLLimits(maxPerDepositFuzzedInput, maxTotalDepositsFuzzedInput);
(uint _maxPerDeposit, uint _maxDeposits) = strategyWithTVLLimits.getTVLLimits();
assertEq(_maxPerDeposit, maxPerDepositFuzzedInput);
assertEq(_maxDeposits, maxTotalDepositsFuzzedInput);
}
function testSetTVLLimitsFailsWhenNotCalledByUnpauser(
uint maxPerDepositFuzzedInput,
uint maxTotalDepositsFuzzedInput,
address notUnpauser
) public {
cheats.assume(notUnpauser != address(proxyAdmin));
cheats.assume(notUnpauser != unpauser);
cheats.prank(notUnpauser);
cheats.expectRevert(IPausable.OnlyUnpauser.selector);
strategyWithTVLLimits.setTVLLimits(maxPerDepositFuzzedInput, maxTotalDepositsFuzzedInput);
}
function testSetInvalidMaxPerDepositAndMaxDeposits(uint maxPerDepositFuzzedInput, uint maxTotalDepositsFuzzedInput) public {
cheats.assume(maxTotalDepositsFuzzedInput < maxPerDepositFuzzedInput);
cheats.prank(unpauser);
cheats.expectRevert(IStrategyErrors.MaxPerDepositExceedsMax.selector);
strategyWithTVLLimits.setTVLLimits(maxPerDepositFuzzedInput, maxTotalDepositsFuzzedInput);
}
function testDepositMoreThanMaxPerDeposit(uint maxPerDepositFuzzedInput, uint maxTotalDepositsFuzzedInput, uint amount) public {
cheats.assume(amount > maxPerDepositFuzzedInput);
_setTVLLimits(maxPerDepositFuzzedInput, maxTotalDepositsFuzzedInput);
cheats.prank(address(strategyManager));
cheats.expectRevert(IStrategyErrors.MaxPerDepositExceedsMax.selector);
strategyWithTVLLimits.deposit(underlyingToken, amount);
}
function testDepositMorethanMaxDeposits() public {
maxTotalDeposits = 1e12;
maxPerDeposit = 3e11;
uint numDeposits = maxTotalDeposits / maxPerDeposit;
_setTVLLimits(maxPerDeposit, maxTotalDeposits);
underlyingToken.transfer(address(strategyManager), maxTotalDeposits);
cheats.startPrank(address(strategyManager));
for (uint i = 0; i < numDeposits; i++) {
underlyingToken.transfer(address(strategyWithTVLLimits), maxPerDeposit);
strategyWithTVLLimits.deposit(underlyingToken, maxPerDeposit);
}
cheats.stopPrank();
underlyingToken.transfer(address(strategyWithTVLLimits), maxPerDeposit);
require(underlyingToken.balanceOf(address(strategyWithTVLLimits)) > maxTotalDeposits, "bad test setup");
cheats.prank(address(strategyManager));
cheats.expectRevert(IStrategyErrors.BalanceExceedsMaxTotalDeposits.selector);
strategyWithTVLLimits.deposit(underlyingToken, maxPerDeposit);
}
function testDepositValidAmount(uint depositAmount) public {
maxTotalDeposits = 1e12;
maxPerDeposit = 3e11;
cheats.assume(depositAmount > 0);
cheats.assume(depositAmount < maxPerDeposit);
_setTVLLimits(maxPerDeposit, maxTotalDeposits);
// we need to actually transfer the tokens to the strategy to avoid underflow in the `deposit` calculation
underlyingToken.transfer(address(strategyWithTVLLimits), depositAmount);
uint sharesBefore = strategyWithTVLLimits.totalShares();
cheats.prank(address(strategyManager));
strategyWithTVLLimits.deposit(underlyingToken, depositAmount);
require(strategyWithTVLLimits.totalShares() == depositAmount + sharesBefore, "total shares not updated correctly");
}
function testDepositTVLLimit_ThenChangeTVLLimit(uint maxTotalDepositsFuzzedInput, uint newMaxTotalDepositsFuzzedInput) public {
cheats.assume(maxTotalDepositsFuzzedInput > 0);
cheats.assume(newMaxTotalDepositsFuzzedInput > maxTotalDepositsFuzzedInput);
cheats.assume(newMaxTotalDepositsFuzzedInput < initialSupply);
cheats.prank(unpauser);
strategyWithTVLLimits.setTVLLimits(maxTotalDepositsFuzzedInput, maxTotalDepositsFuzzedInput);
underlyingToken.transfer(address(strategyWithTVLLimits), maxTotalDepositsFuzzedInput);
uint sharesBefore = strategyWithTVLLimits.totalShares();
cheats.prank(address(strategyManager));
strategyWithTVLLimits.deposit(underlyingToken, maxTotalDepositsFuzzedInput);
require(strategyWithTVLLimits.totalShares() == maxTotalDepositsFuzzedInput + sharesBefore, "total shares not updated correctly");
cheats.prank(unpauser);
strategyWithTVLLimits.setTVLLimits(newMaxTotalDepositsFuzzedInput, newMaxTotalDepositsFuzzedInput);
underlyingToken.transfer(address(strategyWithTVLLimits), newMaxTotalDepositsFuzzedInput - maxTotalDepositsFuzzedInput);
sharesBefore = strategyWithTVLLimits.totalShares();
cheats.prank(address(strategyManager));
strategyWithTVLLimits.deposit(underlyingToken, newMaxTotalDepositsFuzzedInput - maxTotalDepositsFuzzedInput);
require(strategyWithTVLLimits.totalShares() == newMaxTotalDepositsFuzzedInput, "total shares not updated correctly");
}
/// @notice General-purpose test, re-useable, handles whether the deposit should revert or not and returns 'true' if it did revert.
function testDeposit_WithTVLLimits(uint maxPerDepositFuzzedInput, uint maxTotalDepositsFuzzedInput, uint depositAmount)
public
returns (bool depositReverted)
{
cheats.assume(maxPerDepositFuzzedInput < maxTotalDepositsFuzzedInput);
// need to filter this to make sure the deposit amounts can actually be transferred
cheats.assume(depositAmount <= initialSupply);
// set TVL limits
_setTVLLimits(maxPerDepositFuzzedInput, maxTotalDepositsFuzzedInput);
// we need to calculate this before transferring tokens to the strategy
uint expectedSharesOut = strategyWithTVLLimits.underlyingToShares(depositAmount);
// we need to actually transfer the tokens to the strategy to avoid underflow in the `deposit` calculation
underlyingToken.transfer(address(strategyWithTVLLimits), depositAmount);
if (depositAmount > maxPerDepositFuzzedInput) {
cheats.prank(address(strategyManager));
cheats.expectRevert(IStrategyErrors.MaxPerDepositExceedsMax.selector);
strategyWithTVLLimits.deposit(underlyingToken, depositAmount);
// transfer the tokens back from the strategy to not mess up the state
cheats.prank(address(strategyWithTVLLimits));
underlyingToken.transfer(address(this), depositAmount);
// return 'true' since the call to `deposit` reverted
return true;
} else if (underlyingToken.balanceOf(address(strategyWithTVLLimits)) > maxTotalDepositsFuzzedInput) {
cheats.prank(address(strategyManager));
cheats.expectRevert(IStrategyErrors.MaxPerDepositExceedsMax.selector);
strategyWithTVLLimits.deposit(underlyingToken, depositAmount);
// transfer the tokens back from the strategy to not mess up the state
cheats.prank(address(strategyWithTVLLimits));
underlyingToken.transfer(address(this), depositAmount);
// return 'true' since the call to `deposit` reverted
return true;
} else {
uint totalSharesBefore = strategyWithTVLLimits.totalShares();
if (expectedSharesOut == 0) {
cheats.prank(address(strategyManager));
cheats.expectRevert(IStrategyErrors.NewSharesZero.selector);
strategyWithTVLLimits.deposit(underlyingToken, depositAmount);
// transfer the tokens back from the strategy to not mess up the state
cheats.prank(address(strategyWithTVLLimits));
underlyingToken.transfer(address(this), depositAmount);
// return 'true' since the call to `deposit` reverted
return true;
} else {
cheats.prank(address(strategyManager));
strategyWithTVLLimits.deposit(underlyingToken, depositAmount);
require(strategyWithTVLLimits.totalShares() == expectedSharesOut + totalSharesBefore, "total shares not updated correctly");
// return 'false' since the call to `deposit` succeeded
return false;
}
}
}
// sets the TVL Limits and checks that events were emitted correctly
function _setTVLLimits(uint _maxPerDeposit, uint _maxTotalDeposits) internal {
cheats.assume(_maxPerDeposit < _maxTotalDeposits);
(uint _maxPerDepositBefore, uint _maxTotalDepositsBefore) = strategyWithTVLLimits.getTVLLimits();
cheats.expectEmit(true, true, true, true, address(strategyWithTVLLimits));
cheats.prank(unpauser);
emit MaxPerDepositUpdated(_maxPerDepositBefore, _maxPerDeposit);
emit MaxTotalDepositsUpdated(_maxTotalDepositsBefore, _maxTotalDeposits);
strategyWithTVLLimits.setTVLLimits(_maxPerDeposit, _maxTotalDeposits);
}
/// OVERRIDING EXISTING TESTS TO FILTER INPUTS THAT WOULD FAIL DUE TO DEPOSIT-LIMITING
modifier filterToValidDepositAmounts(uint amountToDeposit) {
(uint _maxPerDeposit, uint _maxTotalDeposits) = strategyWithTVLLimits.getTVLLimits();
cheats.assume(amountToDeposit <= _maxPerDeposit && amountToDeposit <= _maxTotalDeposits);
_;
}
function testCanWithdrawDownToSmallShares(uint amountToDeposit, uint32 sharesToLeave)
public
virtual
override
filterToValidDepositAmounts(amountToDeposit)
{
StrategyBaseUnitTests.testCanWithdrawDownToSmallShares(amountToDeposit, sharesToLeave);
}
function testDepositWithNonzeroPriorBalanceAndNonzeroPriorShares(uint priorTotalShares, uint amountToDeposit)
public
virtual
override
filterToValidDepositAmounts(priorTotalShares)
filterToValidDepositAmounts(amountToDeposit)
{
StrategyBaseUnitTests.testDepositWithNonzeroPriorBalanceAndNonzeroPriorShares(priorTotalShares, amountToDeposit);
}
function testDepositWithZeroPriorBalanceAndZeroPriorShares(uint amountToDeposit)
public
virtual
override
filterToValidDepositAmounts(amountToDeposit)
{
StrategyBaseUnitTests.testDepositWithZeroPriorBalanceAndZeroPriorShares(amountToDeposit);
}
function testIntegrityOfSharesToUnderlyingWithNonzeroTotalShares(
uint amountToDeposit,
uint amountToTransfer,
uint96 amountSharesToQuery
) public virtual override filterToValidDepositAmounts(amountToDeposit) {
StrategyBaseUnitTests.testIntegrityOfSharesToUnderlyingWithNonzeroTotalShares(
amountToDeposit, amountToTransfer, amountSharesToQuery
);
}
function testIntegrityOfUnderlyingToSharesWithNonzeroTotalShares(
uint amountToDeposit,
uint amountToTransfer,
uint96 amountUnderlyingToQuery
) public virtual override filterToValidDepositAmounts(amountToDeposit) {
StrategyBaseUnitTests.testIntegrityOfUnderlyingToSharesWithNonzeroTotalShares(
amountToDeposit, amountToTransfer, amountUnderlyingToQuery
);
}
function testWithdrawFailsWhenSharesGreaterThanTotalShares(uint amountToDeposit, uint sharesToWithdraw)
public
virtual
override
filterToValidDepositAmounts(amountToDeposit)
{
StrategyBaseUnitTests.testWithdrawFailsWhenSharesGreaterThanTotalShares(amountToDeposit, sharesToWithdraw);
}
function testWithdrawFailsWhenWithdrawalsPaused(uint amountToDeposit)
public
virtual
override
filterToValidDepositAmounts(amountToDeposit)
{
StrategyBaseUnitTests.testWithdrawFailsWhenWithdrawalsPaused(amountToDeposit);
}
function testWithdrawWithPriorTotalSharesAndAmountSharesEqual(uint amountToDeposit)
public
virtual
override
filterToValidDepositAmounts(amountToDeposit)
{
StrategyBaseUnitTests.testWithdrawWithPriorTotalSharesAndAmountSharesEqual(amountToDeposit);
}
function testWithdrawWithPriorTotalSharesAndAmountSharesNotEqual(uint96 amountToDeposit, uint96 sharesToWithdraw)
public
virtual
override
filterToValidDepositAmounts(amountToDeposit)
{
StrategyBaseUnitTests.testWithdrawWithPriorTotalSharesAndAmountSharesNotEqual(amountToDeposit, sharesToWithdraw);
}
}
````
## File: src/test/unit/StrategyBaseUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "../../contracts/strategies/StrategyBase.sol";
import "../../contracts/permissions/PauserRegistry.sol";
import "../mocks/StrategyManagerMock.sol";
import "../mocks/ERC20_SetTransferReverting_Mock.sol";
import "forge-std/Test.sol";
contract StrategyBaseUnitTests is Test {
Vm cheats = Vm(VM_ADDRESS);
ProxyAdmin public proxyAdmin;
PauserRegistry public pauserRegistry;
IStrategyManager public strategyManager;
IERC20 public underlyingToken;
StrategyBase public strategyImplementation;
StrategyBase public strategy;
address public pauser = address(555);
address public unpauser = address(999);
uint initialSupply = 1e36;
address initialOwner = address(this);
/**
* @notice virtual shares used as part of the mitigation of the common 'share inflation' attack vector.
* Constant value chosen to reasonably reduce attempted share inflation by the first depositor, while still
* incurring reasonably small losses to depositors
*/
uint internal constant SHARES_OFFSET = 1e3;
/**
* @notice virtual balance used as part of the mitigation of the common 'share inflation' attack vector
* Constant value chosen to reasonably reduce attempted share inflation by the first depositor, while still
* incurring reasonably small losses to depositors
*/
uint internal constant BALANCE_OFFSET = 1e3;
event ExchangeRateEmitted(uint rate);
function setUp() public virtual {
proxyAdmin = new ProxyAdmin();
address[] memory pausers = new address[](1);
pausers[0] = pauser;
pauserRegistry = new PauserRegistry(pausers, unpauser);
strategyManager = IStrategyManager(address(new StrategyManagerMock(IDelegationManager(address(0)))));
underlyingToken = new ERC20PresetFixedSupply("Test Token", "TEST", initialSupply, initialOwner);
strategyImplementation = new StrategyBase(strategyManager, pauserRegistry, "v9.9.9");
strategy = StrategyBase(
address(
new TransparentUpgradeableProxy(
address(strategyImplementation),
address(proxyAdmin),
abi.encodeWithSelector(StrategyBase.initialize.selector, underlyingToken)
)
)
);
}
function testCannotReinitialize() public {
cheats.expectRevert(bytes("Initializable: contract is already initialized"));
strategy.initialize(underlyingToken);
}
function testCannotReceiveZeroShares() public {
uint amountToDeposit = 0;
cheats.startPrank(address(strategyManager));
cheats.expectRevert(IStrategyErrors.NewSharesZero.selector);
strategy.deposit(underlyingToken, amountToDeposit);
cheats.stopPrank();
}
function testDepositWithZeroPriorBalanceAndZeroPriorShares(uint amountToDeposit) public virtual {
// sanity check / filter
cheats.assume(amountToDeposit <= underlyingToken.balanceOf(address(this)));
cheats.assume(amountToDeposit >= 1);
uint totalSharesBefore = strategy.totalShares();
underlyingToken.transfer(address(strategy), amountToDeposit);
cheats.prank(address(strategyManager));
cheats.expectEmit(true, true, true, true, address(strategy));
emit ExchangeRateEmitted(1e18);
uint newShares = strategy.deposit(underlyingToken, amountToDeposit);
require(newShares == amountToDeposit, "newShares != amountToDeposit");
uint totalSharesAfter = strategy.totalShares();
require(totalSharesAfter - totalSharesBefore == newShares, "totalSharesAfter - totalSharesBefore != newShares");
require(strategy.sharesToUnderlying(1e18) == 1e18);
}
function testDepositWithNonzeroPriorBalanceAndNonzeroPriorShares(uint priorTotalShares, uint amountToDeposit) public virtual {
cheats.assume(priorTotalShares >= 1 && amountToDeposit > 0);
testDepositWithZeroPriorBalanceAndZeroPriorShares(priorTotalShares);
// sanity check / filter
cheats.assume(amountToDeposit <= underlyingToken.balanceOf(address(this)));
uint totalSharesBefore = strategy.totalShares();
underlyingToken.transfer(address(strategy), amountToDeposit);
cheats.prank(address(strategyManager));
uint newShares = strategy.deposit(underlyingToken, amountToDeposit);
require(newShares == amountToDeposit, "newShares != amountToDeposit");
uint totalSharesAfter = strategy.totalShares();
require(totalSharesAfter - totalSharesBefore == newShares, "totalSharesAfter - totalSharesBefore != newShares");
}
function testDepositFailsWhenDepositsPaused() public {
// pause deposits
cheats.prank(pauser);
strategy.pause(1);
uint amountToDeposit = 1e18;
underlyingToken.transfer(address(strategy), amountToDeposit);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
cheats.prank(address(strategyManager));
strategy.deposit(underlyingToken, amountToDeposit);
}
function testDepositFailsWhenCallingFromNotStrategyManager(address caller) public {
cheats.assume(caller != address(strategy.strategyManager()) && caller != address(proxyAdmin));
uint amountToDeposit = 1e18;
underlyingToken.transfer(address(strategy), amountToDeposit);
cheats.expectRevert(IStrategyErrors.OnlyStrategyManager.selector);
cheats.prank(caller);
strategy.deposit(underlyingToken, amountToDeposit);
}
function testDepositFailsWhenNotUsingUnderlyingToken(address notUnderlyingToken) public {
cheats.assume(notUnderlyingToken != address(underlyingToken));
uint amountToDeposit = 1e18;
cheats.expectRevert(IStrategyErrors.OnlyUnderlyingToken.selector);
cheats.prank(address(strategyManager));
strategy.deposit(IERC20(notUnderlyingToken), amountToDeposit);
}
function testDepositFailForTooManyShares() public {
// Deploy token with 1e39 total supply
underlyingToken = new ERC20PresetFixedSupply("Test Token", "TEST", 1e39, initialOwner);
strategyImplementation = new StrategyBase(strategyManager, pauserRegistry, "v9.9.9");
strategy = StrategyBase(
address(
new TransparentUpgradeableProxy(
address(strategyImplementation),
address(proxyAdmin),
abi.encodeWithSelector(StrategyBase.initialize.selector, underlyingToken)
)
)
);
// Transfer underlying token to strategy
uint amountToDeposit = 1e39;
underlyingToken.transfer(address(strategy), amountToDeposit);
// Deposit
cheats.prank(address(strategyManager));
cheats.expectRevert(IStrategyErrors.TotalSharesExceedsMax.selector);
strategy.deposit(underlyingToken, amountToDeposit);
}
function testWithdrawWithPriorTotalSharesAndAmountSharesEqual(uint amountToDeposit) public virtual {
cheats.assume(amountToDeposit >= 1);
testDepositWithZeroPriorBalanceAndZeroPriorShares(amountToDeposit);
uint sharesToWithdraw = strategy.totalShares();
uint strategyBalanceBefore = underlyingToken.balanceOf(address(strategy));
uint tokenBalanceBefore = underlyingToken.balanceOf(address(this));
cheats.prank(address(strategyManager));
cheats.expectEmit(true, true, true, true, address(strategy));
emit ExchangeRateEmitted(1e18);
strategy.withdraw(address(this), underlyingToken, sharesToWithdraw);
uint tokenBalanceAfter = underlyingToken.balanceOf(address(this));
uint totalSharesAfter = strategy.totalShares();
require(totalSharesAfter == 0, "shares did not decrease appropriately");
require(
tokenBalanceAfter - tokenBalanceBefore == strategyBalanceBefore,
"tokenBalanceAfter - tokenBalanceBefore != strategyBalanceBefore"
);
}
function testWithdrawWithPriorTotalSharesAndAmountSharesNotEqual(uint96 amountToDeposit, uint96 sharesToWithdraw) public virtual {
cheats.assume(amountToDeposit >= 1);
testDepositWithZeroPriorBalanceAndZeroPriorShares(amountToDeposit);
uint totalSharesBefore = strategy.totalShares();
cheats.assume(sharesToWithdraw <= totalSharesBefore);
uint strategyBalanceBefore = underlyingToken.balanceOf(address(strategy));
uint tokenBalanceBefore = underlyingToken.balanceOf(address(this));
cheats.prank(address(strategyManager));
strategy.withdraw(address(this), underlyingToken, sharesToWithdraw);
uint tokenBalanceAfter = underlyingToken.balanceOf(address(this));
uint totalSharesAfter = strategy.totalShares();
require(totalSharesBefore - totalSharesAfter == sharesToWithdraw, "shares did not decrease appropriately");
require(
tokenBalanceAfter - tokenBalanceBefore == (strategyBalanceBefore * sharesToWithdraw) / totalSharesBefore,
"token balance did not increase appropriately"
);
}
function testWithdrawZeroAmount(uint amountToDeposit) public {
cheats.assume(amountToDeposit >= 1);
testDepositWithZeroPriorBalanceAndZeroPriorShares(amountToDeposit);
uint amountToWithdraw = 0;
uint sharesBefore = strategy.totalShares();
uint tokenBalanceBefore = underlyingToken.balanceOf(address(this));
cheats.prank(address(strategyManager));
strategy.withdraw(address(this), underlyingToken, amountToWithdraw);
require(sharesBefore == strategy.totalShares(), "shares changed");
require(tokenBalanceBefore == underlyingToken.balanceOf(address(this)), "token balance changed");
}
function testWithdrawFailsWhenWithdrawalsPaused(uint amountToDeposit) public virtual {
cheats.assume(amountToDeposit >= 1);
testDepositWithZeroPriorBalanceAndZeroPriorShares(amountToDeposit);
// pause withdrawals
cheats.prank(pauser);
strategy.pause(2);
uint amountToWithdraw = 1e18;
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
cheats.prank(address(strategyManager));
strategy.withdraw(address(this), underlyingToken, amountToWithdraw);
}
function testWithdrawalFailsWhenCallingFromNotStrategyManager(address caller) public {
cheats.assume(caller != address(strategy.strategyManager()) && caller != address(proxyAdmin));
uint amountToDeposit = 1e18;
testDepositWithZeroPriorBalanceAndZeroPriorShares(amountToDeposit);
uint amountToWithdraw = 1e18;
cheats.expectRevert(IStrategyErrors.OnlyStrategyManager.selector);
cheats.prank(caller);
strategy.withdraw(address(this), underlyingToken, amountToWithdraw);
}
function testWithdrawalFailsWhenNotUsingUnderlyingToken(address notUnderlyingToken) public {
cheats.assume(notUnderlyingToken != address(underlyingToken));
uint amountToWithdraw = 1e18;
cheats.expectRevert(IStrategyErrors.OnlyUnderlyingToken.selector);
cheats.prank(address(strategyManager));
strategy.withdraw(address(this), IERC20(notUnderlyingToken), amountToWithdraw);
}
function testWithdrawFailsWhenSharesGreaterThanTotalShares(uint amountToDeposit, uint sharesToWithdraw) public virtual {
cheats.assume(amountToDeposit >= 1);
testDepositWithZeroPriorBalanceAndZeroPriorShares(amountToDeposit);
uint totalSharesBefore = strategy.totalShares();
// since we are checking strictly greater than in this test
cheats.assume(sharesToWithdraw > totalSharesBefore);
cheats.expectRevert(IStrategyErrors.WithdrawalAmountExceedsTotalDeposits.selector);
cheats.prank(address(strategyManager));
strategy.withdraw(address(this), underlyingToken, sharesToWithdraw);
}
function testWithdrawalFailsWhenTokenTransferFails() public {
underlyingToken = new ERC20_SetTransferReverting_Mock(initialSupply, initialOwner);
strategy = StrategyBase(
address(
new TransparentUpgradeableProxy(
address(strategyImplementation),
address(proxyAdmin),
abi.encodeWithSelector(StrategyBase.initialize.selector, underlyingToken)
)
)
);
uint amountToDeposit = 1e18;
testDepositWithZeroPriorBalanceAndZeroPriorShares(amountToDeposit);
uint amountToWithdraw = 1e18;
ERC20_SetTransferReverting_Mock(address(underlyingToken)).setTransfersRevert(true);
cheats.expectRevert();
cheats.prank(address(strategyManager));
strategy.withdraw(address(this), underlyingToken, amountToWithdraw);
}
// uint240 input to prevent overflow
function testIntegrityOfSharesToUnderlyingWithZeroTotalShares(uint240 amountSharesToQuery) public view {
uint underlyingFromShares = strategy.sharesToUnderlying(amountSharesToQuery);
require(underlyingFromShares == amountSharesToQuery, "underlyingFromShares != amountSharesToQuery");
uint underlyingFromSharesView = strategy.sharesToUnderlyingView(amountSharesToQuery);
require(underlyingFromSharesView == amountSharesToQuery, "underlyingFromSharesView != amountSharesToQuery");
}
function testDeposit_ZeroAmount() public {
cheats.prank(address(strategyManager));
cheats.expectRevert(IStrategyErrors.NewSharesZero.selector);
strategy.deposit(underlyingToken, 0);
}
// amountSharesToQuery input is uint96 to prevent overflow
function testIntegrityOfSharesToUnderlyingWithNonzeroTotalShares(
uint amountToDeposit,
uint amountToTransfer,
uint96 amountSharesToQuery
) public virtual {
// sanity check / filter
cheats.assume(amountToDeposit <= underlyingToken.balanceOf(address(this)));
cheats.assume(amountToDeposit >= 1);
testDepositWithZeroPriorBalanceAndZeroPriorShares(amountToDeposit);
cheats.assume(amountToTransfer <= underlyingToken.balanceOf(address(this)));
underlyingToken.transfer(address(strategy), amountToTransfer);
uint strategyBalance = underlyingToken.balanceOf(address(strategy));
uint virtualBalance = strategyBalance + BALANCE_OFFSET;
uint expectedValueOut = (virtualBalance * amountSharesToQuery) / (strategy.totalShares() + SHARES_OFFSET);
uint underlyingFromShares = strategy.sharesToUnderlying(amountSharesToQuery);
require(underlyingFromShares == expectedValueOut, "underlyingFromShares != expectedValueOut");
uint underlyingFromSharesView = strategy.sharesToUnderlyingView(amountSharesToQuery);
require(underlyingFromSharesView == expectedValueOut, "underlyingFromSharesView != expectedValueOut");
}
// amountUnderlyingToQuery input is uint96 to prevent overflow
function testIntegrityOfUnderlyingToSharesWithNonzeroTotalShares(
uint amountToDeposit,
uint amountToTransfer,
uint96 amountUnderlyingToQuery
) public virtual {
// sanity check / filter
cheats.assume(amountToDeposit <= underlyingToken.balanceOf(address(this)));
cheats.assume(amountToDeposit >= 1);
testDepositWithZeroPriorBalanceAndZeroPriorShares(amountToDeposit);
cheats.assume(amountToTransfer <= underlyingToken.balanceOf(address(this)));
underlyingToken.transfer(address(strategy), amountToTransfer);
uint strategyBalance = underlyingToken.balanceOf(address(strategy));
uint expectedValueOut = ((strategy.totalShares() + SHARES_OFFSET) * amountUnderlyingToQuery) / (strategyBalance + BALANCE_OFFSET);
uint sharesFromUnderlying = strategy.underlyingToShares(amountUnderlyingToQuery);
require(sharesFromUnderlying == expectedValueOut, "sharesFromUnderlying != expectedValueOut");
uint sharesFromUnderlyingView = strategy.underlyingToSharesView(amountUnderlyingToQuery);
require(sharesFromUnderlyingView == expectedValueOut, "sharesFromUnderlyingView != expectedValueOut");
}
// verify that small remaining share amounts (nonzero in particular) are allowed
function testCanWithdrawDownToSmallShares(uint amountToDeposit, uint32 sharesToLeave) public virtual {
cheats.assume(amountToDeposit >= 1);
testDepositWithZeroPriorBalanceAndZeroPriorShares(amountToDeposit);
uint totalSharesBefore = strategy.totalShares();
// filter out underflow
cheats.assume(sharesToLeave <= totalSharesBefore);
uint sharesToWithdraw = totalSharesBefore - sharesToLeave;
cheats.prank(address(strategyManager));
strategy.withdraw(address(this), underlyingToken, sharesToWithdraw);
}
}
````
## File: src/test/unit/StrategyFactoryUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol";
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "src/contracts/strategies/StrategyFactory.sol";
import "src/test/utils/EigenLayerUnitTestSetup.sol";
import "../../contracts/permissions/PauserRegistry.sol";
/**
* @notice Unit testing of the StrategyFactory contract.
* Contracts tested: StrategyFactory
*/
contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup {
// Contract under test
StrategyFactory public strategyFactory;
StrategyFactory public strategyFactoryImplementation;
// Contract dependencies
StrategyBase public strategyImplementation;
UpgradeableBeacon public strategyBeacon;
ERC20PresetFixedSupply public underlyingToken;
uint initialSupply = 1e36;
address initialOwner = address(this);
address beaconProxyOwner = address(this);
address notOwner = address(7_777_777);
uint initialPausedStatus = 0;
/// @notice Emitted when the `strategyBeacon` is changed
event StrategyBeaconModified(IBeacon previousBeacon, IBeacon newBeacon);
/// @notice Emitted whenever a slot is set in the `deployedStrategies` mapping
event StrategySetForToken(IERC20 token, IStrategy strategy);
event TokenBlacklisted(IERC20 token);
function setUp() public virtual override {
EigenLayerUnitTestSetup.setUp();
address[] memory pausers = new address[](1);
pausers[0] = pauser;
pauserRegistry = new PauserRegistry(pausers, unpauser);
underlyingToken = new ERC20PresetFixedSupply("Test Token", "TEST", initialSupply, initialOwner);
strategyImplementation = new StrategyBase(IStrategyManager(address(strategyManagerMock)), pauserRegistry, "v9.9.9");
strategyBeacon = new UpgradeableBeacon(address(strategyImplementation));
strategyBeacon.transferOwnership(beaconProxyOwner);
strategyFactoryImplementation = new StrategyFactory(IStrategyManager(address(strategyManagerMock)), pauserRegistry, "v9.9.9");
strategyFactory = StrategyFactory(
address(
new TransparentUpgradeableProxy(
address(strategyFactoryImplementation),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(StrategyFactory.initialize.selector, initialOwner, initialPausedStatus, strategyBeacon)
)
)
);
}
function test_initialization() public view {
assertEq(
address(strategyFactory.strategyManager()),
address(strategyManagerMock),
"constructor / initializer incorrect, strategyManager set wrong"
);
assertEq(
address(strategyFactory.strategyBeacon().implementation()),
address(strategyImplementation),
"constructor / initializer incorrect, strategyImplementation set wrong"
);
assertEq(
address(strategyFactory.strategyBeacon()),
address(strategyBeacon),
"constructor / initializer incorrect, strategyBeacon set wrong"
);
assertEq(
address(strategyFactory.pauserRegistry()),
address(pauserRegistry),
"constructor / initializer incorrect, pauserRegistry set wrong"
);
assertEq(strategyFactory.owner(), initialOwner, "constructor / initializer incorrect, owner set wrong");
assertEq(strategyFactory.paused(), initialPausedStatus, "constructor / initializer incorrect, paused status set wrong");
assertEq(strategyBeacon.owner(), beaconProxyOwner, "constructor / initializer incorrect, beaconProxyOwner set wrong");
}
function test_initialize_revert_reinitialization() public {
cheats.expectRevert("Initializable: contract is already initialized");
strategyFactory.initialize({_initialOwner: initialOwner, _initialPausedStatus: initialPausedStatus, _strategyBeacon: strategyBeacon});
}
function test_deployNewStrategy() public {
// cheats.expectEmit(true, true, true, true, address(strategyFactory));
// StrategySetForToken(underlyingToken, newStrategy);
StrategyBase newStrategy = StrategyBase(address(strategyFactory.deployNewStrategy(underlyingToken)));
require(strategyFactory.deployedStrategies(underlyingToken) == newStrategy, "deployedStrategies mapping not set correctly");
require(address(newStrategy.strategyManager()) == address(strategyManagerMock), "strategyManager not set correctly");
require(strategyBeacon.implementation() == address(strategyImplementation), "strategyImplementation not set correctly");
require(newStrategy.pauserRegistry() == pauserRegistry, "pauserRegistry not set correctly");
require(newStrategy.underlyingToken() == underlyingToken, "underlyingToken not set correctly");
require(strategyManagerMock.strategyIsWhitelistedForDeposit(newStrategy), "underlyingToken is not whitelisted");
}
function test_deployNewStrategy_revert_StrategyAlreadyExists() public {
test_deployNewStrategy();
cheats.expectRevert(IStrategyFactory.StrategyAlreadyExists.selector);
strategyFactory.deployNewStrategy(underlyingToken);
}
function test_blacklistTokens(IERC20 token) public {
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = token;
vm.prank(strategyFactory.owner());
cheats.expectEmit(true, false, false, false, address(strategyFactory));
emit TokenBlacklisted(token);
strategyFactory.blacklistTokens(tokens);
cheats.expectRevert(IStrategyFactory.BlacklistedToken.selector);
strategyFactory.deployNewStrategy(token);
}
function test_blacklistTokens_RemovesFromWhitelist() public {
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = underlyingToken;
IStrategy newStrat = strategyFactory.deployNewStrategy(underlyingToken);
IStrategy[] memory toRemove = new IStrategy[](1);
toRemove[0] = newStrat;
vm.prank(strategyFactory.owner());
cheats.expectEmit(true, false, false, false, address(strategyFactory));
emit TokenBlacklisted(underlyingToken);
cheats.expectCall(
address(strategyManagerMock),
abi.encodeWithSelector(strategyManagerMock.removeStrategiesFromDepositWhitelist.selector, toRemove)
);
strategyFactory.blacklistTokens(tokens);
}
function test_whitelistStrategies() public {
StrategyBase strategy = _deployStrategy();
IStrategy[] memory strategiesToWhitelist = new IStrategy[](1);
strategiesToWhitelist[0] = strategy;
strategyFactory.whitelistStrategies(strategiesToWhitelist);
assertTrue(strategyManagerMock.strategyIsWhitelistedForDeposit(strategy), "Strategy not whitelisted");
}
function test_whitelistStrategies_revert_notOwner() public {
IStrategy[] memory strategiesToWhitelist = new IStrategy[](1);
cheats.expectRevert("Ownable: caller is not the owner");
cheats.prank(notOwner);
strategyFactory.whitelistStrategies(strategiesToWhitelist);
}
function test_removeStrategiesFromWhitelist_revert_notOwner() public {
IStrategy[] memory strategiesToRemove = new IStrategy[](1);
cheats.expectRevert("Ownable: caller is not the owner");
cheats.prank(notOwner);
strategyFactory.removeStrategiesFromWhitelist(strategiesToRemove);
}
function test_removeStrategiesFromWhitelist() public {
IStrategy[] memory strategiesToRemove = new IStrategy[](1);
strategiesToRemove[0] = IStrategy(_deployStrategy());
strategyFactory.removeStrategiesFromWhitelist(strategiesToRemove);
assertFalse(strategyManagerMock.strategyIsWhitelistedForDeposit(strategiesToRemove[0]), "Strategy not removed from whitelist");
}
function _deployStrategy() internal returns (StrategyBase) {
return StrategyBase(
address(
new TransparentUpgradeableProxy(
address(strategyImplementation),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(StrategyBase.initialize.selector, underlyingToken)
)
)
);
}
}
````
## File: src/test/unit/StrategyManagerUnit.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/mocks/ERC1271WalletMock.sol";
import "src/contracts/core/StrategyManager.sol";
import "src/contracts/strategies/StrategyBase.sol";
import "src/contracts/permissions/PauserRegistry.sol";
import "src/test/mocks/ERC20Mock.sol";
import "src/test/mocks/ERC20_SetTransferReverting_Mock.sol";
import "src/test/mocks/Reverter.sol";
import "src/test/mocks/Reenterer.sol";
import "src/test/mocks/MockDecimals.sol";
import "src/test/utils/EigenLayerUnitTestSetup.sol";
/**
* @notice Unit testing of the StrategyManager contract, entire withdrawal tests related to the
* DelegationManager are not tested here but callable functions by the DelegationManager are mocked and tested here.
* Contracts tested: StrategyManager.sol
* Contracts not mocked: StrategyBase, PauserRegistry
*/
contract StrategyManagerUnitTests is EigenLayerUnitTestSetup, IStrategyManagerEvents {
StrategyManager public strategyManagerImplementation;
StrategyManager public strategyManager;
IERC20 public dummyToken;
ERC20_SetTransferReverting_Mock public revertToken;
StrategyBase public dummyStrat;
StrategyBase public dummyStrat2;
StrategyBase public dummyStrat3;
Reenterer public reenterer;
address initialOwner = address(this);
uint public privateKey = 111_111;
address constant dummyAdmin = address(uint160(uint(keccak256("DummyAdmin"))));
uint constant MAX_STRATEGY_TOTAL_SHARES = 1e38 - 1;
function setUp() public override {
EigenLayerUnitTestSetup.setUp();
strategyManagerImplementation = new StrategyManager(IDelegationManager(address(delegationManagerMock)), pauserRegistry, "v9.9.9");
strategyManager = StrategyManager(
address(
new TransparentUpgradeableProxy(
address(strategyManagerImplementation),
address(eigenLayerProxyAdmin),
abi.encodeWithSelector(StrategyManager.initialize.selector, initialOwner, initialOwner, 0 /*initialPausedStatus*/ )
)
)
);
dummyToken = new ERC20PresetFixedSupply("mock token", "MOCK", MAX_STRATEGY_TOTAL_SHARES, address(this));
revertToken = new ERC20_SetTransferReverting_Mock(1000e18, address(this));
revertToken.setTransfersRevert(true);
dummyStrat = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
dummyStrat2 = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
dummyStrat3 = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
// whitelist the strategy for deposit
cheats.prank(strategyManager.owner());
IStrategy[] memory _strategies = new IStrategy[](3);
_strategies[0] = dummyStrat;
_strategies[1] = dummyStrat2;
_strategies[2] = dummyStrat3;
for (uint i = 0; i < _strategies.length; ++i) {
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit StrategyAddedToDepositWhitelist(_strategies[i]);
}
strategyManager.addStrategiesToDepositWhitelist(_strategies);
isExcludedFuzzAddress[address(reenterer)] = true;
}
// INTERNAL / HELPER FUNCTIONS
function _deployNewStrategy(IERC20 _token, IStrategyManager _strategyManager, IPauserRegistry _pauserRegistry, address admin)
public
returns (StrategyBase)
{
StrategyBase newStrategyImplementation = new StrategyBase(_strategyManager, _pauserRegistry, "v9.9.9");
StrategyBase newStrategy =
StrategyBase(address(new TransparentUpgradeableProxy(address(newStrategyImplementation), address(admin), "")));
newStrategy.initialize(_token);
return newStrategy;
}
function _depositIntoStrategySuccessfully(IStrategy strategy, address staker, uint amount) internal filterFuzzedAddressInputs(staker) {
IERC20 token = dummyToken;
// filter out zero case since it will revert with "StrategyManager._addShares: shares should not be zero!"
cheats.assume(amount != 0);
// filter out zero address because the mock ERC20 we are using will revert on using it
cheats.assume(staker != address(0));
// sanity check / filter
cheats.assume(amount <= token.balanceOf(address(this)));
token.transfer(staker, amount);
uint depositSharesBefore = strategyManager.stakerDepositShares(staker, strategy);
uint stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker);
// needed for expecting an event with the right parameters
uint expectedDepositShares = amount;
cheats.startPrank(staker);
token.approve(address(strategyManager), amount);
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit Deposit(staker, strategy, expectedDepositShares);
uint shares = strategyManager.depositIntoStrategy(strategy, token, amount);
cheats.stopPrank();
uint depositSharesAfter = strategyManager.stakerDepositShares(staker, strategy);
uint stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker);
assertEq(depositSharesAfter, depositSharesBefore + shares, "depositSharesAfter != depositSharesBefore + shares");
if (depositSharesBefore == 0) {
assertEq(
stakerStrategyListLengthAfter,
stakerStrategyListLengthBefore + 1,
"stakerStrategyListLengthAfter != stakerStrategyListLengthBefore + 1"
);
assertEq(
address(strategyManager.stakerStrategyList(staker, stakerStrategyListLengthAfter - 1)),
address(strategy),
"strategyManager.stakerStrategyList(staker, stakerStrategyListLengthAfter - 1) != strategy"
);
}
}
// internal function for de-duping code. expects success if `expectedRevertMessage` is empty and expiry is valid.
function _depositIntoStrategyWithSignature(address staker, uint amount, uint expiry, bytes4 expectedRevertMessage)
internal
returns (bytes memory)
{
// filter out zero case since it will revert with "StrategyManager._addShares: shares should not be zero!"
cheats.assume(amount != 0);
// sanity check / filter
cheats.assume(amount <= dummyToken.balanceOf(address(this)));
dummyToken.approve(address(strategyManager), amount);
uint nonceBefore = strategyManager.nonces(staker);
bytes memory signature;
{
bytes32 structHash =
keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), staker, dummyStrat, dummyToken, amount, nonceBefore, expiry));
bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash));
(uint8 v, bytes32 r, bytes32 s) = cheats.sign(privateKey, digestHash);
signature = abi.encodePacked(r, s, v);
}
uint depositSharesBefore = strategyManager.stakerDepositShares(staker, dummyStrat);
bool expectedRevertMessageIsempty = expectedRevertMessage == bytes4(0x00000000);
if (!expectedRevertMessageIsempty) {
cheats.expectRevert(expectedRevertMessage);
} else if (expiry < block.timestamp) {
cheats.expectRevert(ISignatureUtilsMixinErrors.SignatureExpired.selector);
} else {
// needed for expecting an event with the right parameters
uint expectedDepositShares = amount;
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit Deposit(staker, dummyStrat, expectedDepositShares);
}
uint shares = strategyManager.depositIntoStrategyWithSignature(dummyStrat, dummyToken, amount, staker, expiry, signature);
uint depositSharesAfter = strategyManager.stakerDepositShares(staker, dummyStrat);
uint nonceAfter = strategyManager.nonces(staker);
if (expiry >= block.timestamp && expectedRevertMessageIsempty) {
assertEq(depositSharesAfter, depositSharesBefore + shares, "depositSharesAfter != depositSharesBefore + shares");
assertEq(nonceAfter, nonceBefore + 1, "nonceAfter != nonceBefore + 1");
}
return signature;
}
/**
* @notice internal function to help check if a strategy is part of list of deposited strategies for a staker
* Used to check if removed correctly after withdrawing all shares for a given strategy
*/
function _isDepositedStrategy(address staker, IStrategy strategy) internal view returns (bool) {
uint stakerStrategyListLength = strategyManager.stakerStrategyListLength(staker);
for (uint i = 0; i < stakerStrategyListLength; ++i) {
if (strategyManager.stakerStrategyList(staker, i) == strategy) return true;
}
return false;
}
/**
* @notice Deploys numberOfStrategiesToAdd new strategies and adds them to the whitelist
*/
function _addStrategiesToWhitelist(uint8 numberOfStrategiesToAdd) internal returns (IStrategy[] memory) {
IStrategy[] memory strategyArray = new IStrategy[](numberOfStrategiesToAdd);
// loop that deploys a new strategy and adds it to the array
for (uint i = 0; i < numberOfStrategiesToAdd; ++i) {
IStrategy _strategy = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
strategyArray[i] = _strategy;
assertFalse(strategyManager.strategyIsWhitelistedForDeposit(_strategy), "strategy improperly whitelisted?");
}
cheats.prank(strategyManager.strategyWhitelister());
for (uint i = 0; i < numberOfStrategiesToAdd; ++i) {
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit StrategyAddedToDepositWhitelist(strategyArray[i]);
}
strategyManager.addStrategiesToDepositWhitelist(strategyArray);
for (uint i = 0; i < numberOfStrategiesToAdd; ++i) {
assertTrue(strategyManager.strategyIsWhitelistedForDeposit(strategyArray[i]), "strategy not whitelisted");
}
return strategyArray;
}
}
contract StrategyManagerUnitTests_initialize is StrategyManagerUnitTests {
function test_CannotReinitialize() public {
cheats.expectRevert("Initializable: contract is already initialized");
strategyManager.initialize(initialOwner, initialOwner, 0);
}
function test_InitializedStorageProperly() public view {
assertTrue(strategyManager.domainSeparator() != bytes32(0), "sanity check");
assertEq(strategyManager.owner(), initialOwner, "strategyManager.owner() != initialOwner");
assertEq(strategyManager.strategyWhitelister(), initialOwner, "strategyManager.strategyWhitelister() != initialOwner");
assertEq(address(strategyManager.pauserRegistry()), address(pauserRegistry), "strategyManager.pauserRegistry() != pauserRegistry");
bytes memory v = bytes(strategyManager.version());
bytes32 expectedDomainSeparator = keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(bytes("EigenLayer")),
keccak256(bytes(bytes.concat(v[0], v[1]))),
block.chainid,
address(strategyManager)
)
);
assertEq(strategyManager.domainSeparator(), expectedDomainSeparator, "sanity check");
}
}
contract StrategyManagerUnitTests_depositIntoStrategy is StrategyManagerUnitTests {
function testFuzz_depositIntoStrategySuccessfully(address staker, uint amount) public filterFuzzedAddressInputs(staker) {
IERC20 token = dummyToken;
IStrategy strategy = dummyStrat;
// filter out zero case since it will revert with "StrategyManager._addShares: shares should not be zero!"
cheats.assume(amount != 0);
// filter out zero address because the mock ERC20 we are using will revert on using it
cheats.assume(staker != address(0));
// filter out the strategy itself from fuzzed inputs
cheats.assume(staker != address(strategy));
// sanity check / filter
cheats.assume(amount <= token.balanceOf(address(this)));
cheats.assume(amount >= 1);
uint depositSharesBefore = strategyManager.stakerDepositShares(staker, strategy);
uint stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker);
// needed for expecting an event with the right parameters
uint expectedDepositShares = strategy.underlyingToShares(amount);
uint strategyBalanceBefore = token.balanceOf(address(strategy));
token.transfer(staker, amount);
cheats.startPrank(staker);
token.approve(address(strategyManager), amount);
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit Deposit(staker, strategy, expectedDepositShares);
uint depositedShares = strategyManager.depositIntoStrategy(strategy, token, amount);
cheats.stopPrank();
uint strategyBalanceAfter = token.balanceOf(address(strategy));
uint depositSharesAfter = strategyManager.stakerDepositShares(staker, strategy);
uint stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker);
assertEq(strategyBalanceBefore + amount, strategyBalanceAfter, "balance of strategy not increased by deposit amount");
assertEq(depositSharesAfter, depositSharesBefore + depositedShares, "depositSharesAfter != depositSharesBefore + depositedShares");
if (depositSharesBefore == 0) {
assertEq(
stakerStrategyListLengthAfter,
stakerStrategyListLengthBefore + 1,
"stakerStrategyListLengthAfter != stakerStrategyListLengthBefore + 1"
);
assertEq(
address(strategyManager.stakerStrategyList(staker, stakerStrategyListLengthAfter - 1)),
address(strategy),
"strategyManager.stakerStrategyList(staker, stakerStrategyListLengthAfter - 1) != strategy"
);
} else {
assertEq(
stakerStrategyListLengthAfter,
stakerStrategyListLengthBefore,
"stakerStrategyListLengthAfter != stakerStrategyListLengthBefore"
);
}
}
function test_DepositWhenStrategySharesExist() public {
address staker = address(this);
uint amount = 1e18;
testFuzz_depositIntoStrategySuccessfully(staker, amount);
testFuzz_depositIntoStrategySuccessfully(staker, amount);
}
// TODO: test depositing into multiple strategies
// function testFuzz_depositIntoStrategy_MultipleStrategies()
// /// @notice deploys 'numStratsToAdd' strategies using '_testAddStrategy' and then deposits '1e18' to each of them from 'getOperatorAddress(0)'
// /// @param numStratsToAdd is the number of strategies being added and deposited into
// function testDepositStrategies(uint8 numStratsToAdd) public {
// _testDepositStrategies(getOperatorAddress(0), 1e18, numStratsToAdd);
// }
// TODO: fix old stETH fork test
// /// @notice Shadow-forks mainnet and tests depositing stETH tokens into a "StrategyBase" contract.
// function testForkMainnetDepositSteth() public {
// // hard-coded inputs
// // address sender = address(this);
// uint64 amountToDeposit = 1e12;
// // shadow-fork mainnet
// try cheats.createFork("mainnet") returns (uint256 forkId) {
// cheats.selectFork(forkId);
// // If RPC_MAINNET ENV not set, default to this mainnet RPC endpoint
// } catch {
// cheats.createSelectFork("https://eth.llamarpc.com");
// }
// // cast mainnet stETH address to IERC20 interface
// // IERC20 steth = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);
// IERC20 underlyingToken = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);
// // deploy necessary contracts on the shadow-forked network
// // deploy proxy admin for ability to upgrade proxy contracts
// eigenLayerProxyAdmin = new ProxyAdmin();
// //deploy pauser registry
// address[] memory pausers = new address[](1);
// pausers[0] = pauser;
// eigenLayerPauserReg = new PauserRegistry(pausers, unpauser);
// /**
// * First, deploy upgradeable proxy contracts that **will point** to the implementations. Since the implementation contracts are
// * not yet deployed, we give these proxies an empty contract as the initial implementation, to act as if they have no code.
// */
// emptyContract = new EmptyContract();
// delegation = DelegationManager(
// address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))
// );
// strategyManager = StrategyManager(
// address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))
// );
// eigenPodManager = EigenPodManager(
// address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))
// );
// ethPOSDeposit = new ETHPOSDepositMock();
// pod = new EigenPod(ethPOSDeposit, eigenPodManager, GOERLI_GENESIS_TIME);
// eigenPodBeacon = new UpgradeableBeacon(address(pod));
// // Second, deploy the *implementation* contracts, using the *proxy contracts* as inputs
// DelegationManager delegationImplementation = new DelegationManager(avsDirectory, strategyManager, eigenPodManager, allocationManager, MIN_WITHDRAWAL_DELAY);
// StrategyManager strategyManagerImplementation = new StrategyManager(delegation);
// EigenPodManager eigenPodManagerImplementation = new EigenPodManager(ethPOSDeposit, eigenPodBeacon, strategyManager, delegation);
// // Third, upgrade the proxy contracts to use the correct implementation contracts and initialize them.
// eigenLayerProxyAdmin.upgradeAndCall(
// ITransparentUpgradeableProxy(payable(address(delegation))),
// address(delegationImplementation),
// abi.encodeWithSelector(
// DelegationManager.initialize.selector,
// eigenLayerReputedMultisig,
// eigenLayerPauserReg,
// 0 /*initialPausedStatus*/,
// minWithdrawalDelayBlocks,
// initializeStrategiesToSetDelayBlocks,
// initializeWithdrawalDelayBlocks
// )
// );
// eigenLayerProxyAdmin.upgradeAndCall(
// ITransparentUpgradeableProxy(payable(address(strategyManager))),
// address(strategyManagerImplementation),
// abi.encodeWithSelector(
// StrategyManager.initialize.selector,
// eigenLayerReputedMultisig,
// eigenLayerReputedMultisig,
// eigenLayerPauserReg,
// 0/*initialPausedStatus*/
// )
// );
// eigenLayerProxyAdmin.upgradeAndCall(
// ITransparentUpgradeableProxy(payable(address(eigenPodManager))),
// address(eigenPodManagerImplementation),
// abi.encodeWithSelector(
// EigenPodManager.initialize.selector,
// eigenLayerReputedMultisig,
// eigenLayerPauserReg,
// 0/*initialPausedStatus*/
// )
// );
// // cheat a bunch of ETH to this address
// cheats.deal(address(this), 1e20);
// // deposit a huge amount of ETH to get ample stETH
// {
// (bool success, bytes memory returnData) = address(underlyingToken).call{value: 1e20}("");
// require(success, "depositing stETH failed");
// returnData;
// }
// // deploy StrategyBase contract implementation, then create upgradeable proxy that points to implementation and initialize it
// baseStrategyImplementation = new StrategyBase(strategyManager);
// IStrategy stethStrategy = StrategyBase(
// address(
// new TransparentUpgradeableProxy(
// address(baseStrategyImplementation),
// address(eigenLayerProxyAdmin),
// abi.encodeWithSelector(StrategyBase.initialize.selector, underlyingToken, eigenLayerPauserReg)
// )
// )
// );
// // REMAINDER OF CODE ADAPTED FROM `_testDepositToStrategy`
// // _testDepositToStrategy(sender, amountToDeposit, underlyingToken, stethStrategy);
// // whitelist the strategy for deposit, in case it wasn't before
// {
// cheats.startPrank(strategyManager.strategyWhitelister());
// IStrategy[] memory _strategy = new IStrategy[](1);
// _strategy[0] = stethStrategy;
// strategyManager.addStrategiesToDepositWhitelist(_strategy);
// cheats.stopPrank();
// }
// uint256 operatorSharesBefore = strategyManager.stakerDepositShares(address(this), stethStrategy);
// // check the expected output
// uint256 expectedSharesOut = stethStrategy.underlyingToShares(amountToDeposit);
// underlyingToken.transfer(address(this), amountToDeposit);
// cheats.startPrank(address(this));
// underlyingToken.approve(address(strategyManager), type(uint256).max);
// strategyManager.depositIntoStrategy(stethStrategy, underlyingToken, amountToDeposit);
// //check if depositor has never used this strat, that it is added correctly to stakerStrategyList array.
// if (operatorSharesBefore == 0) {
// // check that strategy is appropriately added to dynamic array of all of sender's strategies
// assertTrue(
// strategyManager.stakerStrategyList(address(this), strategyManager.stakerStrategyListLength(address(this)) - 1)
// == stethStrategy,
// "_testDepositToStrategy: stakerStrategyList array updated incorrectly"
// );
// }
// // check that the shares out match the expected amount out
// // the actual transfer in will be lower by 1-2 wei than expected due to stETH's internal rounding
// // to account for this we check approximate rather than strict equivalence here
// {
// uint256 actualSharesOut = strategyManager.stakerDepositShares(address(this), stethStrategy) - operatorSharesBefore;
// require(actualSharesOut >= expectedSharesOut, "too few shares");
// require((actualSharesOut * 1000) / expectedSharesOut < 1003, "too many shares");
// // additional sanity check for deposit not increasing in value
// require(stethStrategy.sharesToUnderlying(actualSharesOut) <= amountToDeposit, "value cannot have increased");
// // slippage check
// require((stethStrategy.sharesToUnderlying(actualSharesOut) * 1e6) / amountToDeposit >= (1e6 - 1), "bad slippage on first deposit");
// }
// cheats.stopPrank();
// }
// TODO: fix old frontrun depositor test
// function testFrontrunFirstDepositor(/*uint256 depositAmount*/) public {
// //setup addresses
// address attacker = address(100);
// address user = address(200);
// //give 2 ether to attacker and user
// weth.transfer(attacker,2 ether);
// weth.transfer(user,2 ether);
// //attacker FRONTRUN: deposit 1 wei (receive 1 share)
// StrategyManager _strategyManager = _whitelistStrategy(strategyManager, wethStrat);
// cheats.startPrank(attacker);
// weth.approve(address(strategyManager), type(uint256).max);
// _strategyManager.depositIntoStrategy(wethStrat, weth, 1 wei);
// cheats.stopPrank();
// //attacker FRONTRUN: transfer 1 ether into strategy directly to manipulate the value of shares
// cheats.prank(attacker);
// weth.transfer(address(wethStrat),1 ether);
// //user deposits 2 eth into strategy - only gets 1 share due to rounding
// cheats.startPrank(user);
// weth.approve(address(_strategyManager), type(uint256).max);
// _strategyManager.depositIntoStrategy(wethStrat, weth, 2 ether);
// cheats.stopPrank();
// //attacker deposited 1 ether and 1 wei - received 1 share
// //user deposited 2 ether - received X shares
// //user has lost 0.5 ether?
// (, uint256[] memory shares) = _strategyManager.getDeposits(attacker);
// uint256 attackerValueWeth = wethStrat.sharesToUnderlyingView(shares[0]);
// require(attackerValueWeth >= (1), "attacker got zero shares");
// (, shares) = _strategyManager.getDeposits(user);
// uint256 userValueWeth = wethStrat.sharesToUnderlyingView(shares[0]);
// require(userValueWeth >= (1900000000000000000), "user has lost more than 0.1 eth from frontrunning");
// uint256 attackerLossesWeth = (2 ether + 1 wei) - attackerValueWeth;
// uint256 userLossesWeth = 2 ether - userValueWeth;
// require(attackerLossesWeth > userLossesWeth, "griefing attack deals more damage than cost");
// }
// TODO: fix old testFrontrunFirstDepositorFuzzed
// function testFrontrunFirstDepositorFuzzed(uint96 firstDepositAmount, uint96 donationAmount, uint96 secondDepositAmount) public {
// // want to only use nonzero amounts or else we'll get reverts
// cheats.assume(firstDepositAmount != 0 && secondDepositAmount != 0);
// // setup addresses
// address attacker = address(100);
// address user = address(200);
// // attacker makes first deposit
// _testDepositToStrategy(attacker, firstDepositAmount, weth, wethStrat);
// // transfer tokens into strategy directly to manipulate the value of shares
// weth.transfer(address(wethStrat), donationAmount);
// // filter out calls that would revert for minting zero shares
// cheats.assume(wethStrat.underlyingToShares(secondDepositAmount) != 0);
// // user makes 2nd deposit into strategy - gets diminished shares due to rounding
// _testDepositToStrategy(user, secondDepositAmount, weth, wethStrat);
// // check for griefing
// (, uint256[] memory shares) = strategyManager.getDeposits(attacker);
// uint256 attackerValueWeth = wethStrat.sharesToUnderlyingView(shares[0]);
// (, shares) = strategyManager.getDeposits(user);
// uint256 userValueWeth = wethStrat.sharesToUnderlyingView(shares[0]);
// uint256 attackerCost = uint256(firstDepositAmount) + uint256(donationAmount);
// require(attackerCost >= attackerValueWeth, "attacker gained value?");
// // uint256 attackerLossesWeth = attackerValueWeth > attackerCost ? 0 : (attackerCost - attackerValueWeth);
// uint256 attackerLossesWeth = attackerCost - attackerValueWeth;
// uint256 userLossesWeth = secondDepositAmount - userValueWeth;
// emit log_named_uint("attackerLossesWeth", attackerLossesWeth);
// emit log_named_uint("userLossesWeth", userLossesWeth);
// // use '+1' here to account for rounding. given the attack will cost ETH in the form of gas, this is fine.
// require(attackerLossesWeth + 1 >= userLossesWeth, "griefing attack deals more damage than cost");
// }
// TODO: testDepositTokenWithOneWeiFeeOnTransfer
// function testDepositTokenWithOneWeiFeeOnTransfer(address sender, uint64 amountToDeposit) public fuzzedAddress(sender) {
// cheats.assume(amountToDeposit != 0);
// IERC20 underlyingToken;
// {
// uint256 initSupply = 1e50;
// address initOwner = address(this);
// ERC20_OneWeiFeeOnTransfer oneWeiFeeOnTransferToken = new ERC20_OneWeiFeeOnTransfer(initSupply, initOwner);
// underlyingToken = IERC20(address(oneWeiFeeOnTransferToken));
// }
// // need to transfer extra here because otherwise the `sender` won't have enough tokens
// underlyingToken.transfer(sender, 1000);
// IStrategy oneWeiFeeOnTransferTokenStrategy = StrategyBase(
// address(
// new TransparentUpgradeableProxy(
// address(baseStrategyImplementation),
// address(eigenLayerProxyAdmin),
// abi.encodeWithSelector(StrategyBase.initialize.selector, underlyingToken, eigenLayerPauserReg)
// )
// )
// );
// // REMAINDER OF CODE ADAPTED FROM `_testDepositToStrategy`
// // _testDepositToStrategy(sender, amountToDeposit, underlyingToken, oneWeiFeeOnTransferTokenStrategy);
// // whitelist the strategy for deposit, in case it wasn't before
// {
// cheats.startPrank(strategyManager.strategyWhitelister());
// IStrategy[] memory _strategy = new IStrategy[](1);
// _strategy[0] = oneWeiFeeOnTransferTokenStrategy;
// strategyManager.addStrategiesToDepositWhitelist(_strategy);
// cheats.stopPrank();
// }
// uint256 operatorSharesBefore = strategyManager.stakerDepositShares(sender, oneWeiFeeOnTransferTokenStrategy);
// // check the expected output
// uint256 expectedSharesOut = oneWeiFeeOnTransferTokenStrategy.underlyingToShares(amountToDeposit);
// underlyingToken.transfer(sender, amountToDeposit);
// cheats.startPrank(sender);
// underlyingToken.approve(address(strategyManager), type(uint256).max);
// strategyManager.depositIntoStrategy(oneWeiFeeOnTransferTokenStrategy, underlyingToken, amountToDeposit);
// //check if depositor has never used this strat, that it is added correctly to stakerStrategyList array.
// if (operatorSharesBefore == 0) {
// // check that strategy is appropriately added to dynamic array of all of sender's strategies
// assertTrue(
// strategyManager.stakerStrategyList(sender, strategyManager.stakerStrategyListLength(sender) - 1)
// == oneWeiFeeOnTransferTokenStrategy,
// "_testDepositToStrategy: stakerStrategyList array updated incorrectly"
// );
// }
// // check that the shares out match the expected amount out
// // the actual transfer in will be lower by 1 wei than expected due to stETH's internal rounding
// // to account for this we check approximate rather than strict equivalence here
// {
// uint256 actualSharesOut = strategyManager.stakerDepositShares(sender, oneWeiFeeOnTransferTokenStrategy) - operatorSharesBefore;
// require((actualSharesOut * 1000) / expectedSharesOut > 998, "too few shares");
// require((actualSharesOut * 1000) / expectedSharesOut < 1002, "too many shares");
// // additional sanity check for deposit not increasing in value
// require(oneWeiFeeOnTransferTokenStrategy.sharesToUnderlying(actualSharesOut) <= amountToDeposit, "value cannot have increased");
// }
// cheats.stopPrank();
// }
function test_Revert_WhenDepositsPaused() public {
uint amount = 1e18;
// pause deposits
cheats.prank(pauser);
strategyManager.pause(1);
cheats.expectRevert(IPausable.CurrentlyPaused.selector);
strategyManager.depositIntoStrategy(dummyStrat, dummyToken, amount);
}
function test_Revert_WhenReentering() public {
uint amount = 1e18;
reenterer = new Reenterer();
dummyToken.approve(address(strategyManager), MAX_STRATEGY_TOTAL_SHARES);
// whitelist the strategy for deposit
cheats.prank(strategyManager.owner());
IStrategy[] memory _strategy = new IStrategy[](1);
_strategy[0] = IStrategy(address(reenterer));
for (uint i = 0; i < _strategy.length; ++i) {
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit StrategyAddedToDepositWhitelist(_strategy[i]);
}
strategyManager.addStrategiesToDepositWhitelist(_strategy);
reenterer.prepareReturnData(abi.encode(amount));
address targetToUse = address(strategyManager);
uint msgValueToUse = 0;
bytes memory calldataToUse =
abi.encodeWithSelector(StrategyManager.depositIntoStrategy.selector, address(reenterer), dummyToken, amount);
reenterer.prepare(targetToUse, msgValueToUse, calldataToUse, bytes("ReentrancyGuard: reentrant call"));
strategyManager.depositIntoStrategy(IStrategy(address(reenterer)), dummyToken, amount);
}
function test_Revert_WhenTokenSafeTransferFromReverts() external {
// replace 'dummyStrat' with one that uses a reverting token
dummyToken = IERC20(address(new ReverterWithDecimals()));
dummyStrat = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
// whitelist the strategy for deposit
cheats.prank(strategyManager.owner());
IStrategy[] memory _strategy = new IStrategy[](1);
_strategy[0] = dummyStrat;
strategyManager.addStrategiesToDepositWhitelist(_strategy);
address staker = address(this);
IERC20 token = dummyToken;
uint amount = 1e18;
IStrategy strategy = dummyStrat;
cheats.prank(staker);
cheats.expectRevert("Reverter: I am a contract that always reverts");
strategyManager.depositIntoStrategy(strategy, token, amount);
}
function test_Revert_WhenTokenDoesNotExist() external {
// replace 'dummyStrat' with one that uses a non-existent token, but will pass the initializer decimals check
dummyToken = IERC20(address(new MockDecimals()));
dummyStrat = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
// whitelist the strategy for deposit
cheats.prank(strategyManager.owner());
IStrategy[] memory _strategy = new IStrategy[](1);
_strategy[0] = dummyStrat;
strategyManager.addStrategiesToDepositWhitelist(_strategy);
address staker = address(this);
IERC20 token = dummyToken;
uint amount = 1e18;
IStrategy strategy = dummyStrat;
cheats.prank(staker);
cheats.expectRevert("SafeERC20: low-level call failed");
strategyManager.depositIntoStrategy(strategy, token, amount);
}
function test_Revert_WhenStrategyDepositFunctionReverts() external {
// replace 'dummyStrat' with one that always reverts
dummyStrat = StrategyBase(address(new Reverter()));
// whitelist the strategy for deposit
cheats.prank(strategyManager.owner());
IStrategy[] memory _strategy = new IStrategy[](1);
_strategy[0] = dummyStrat;
strategyManager.addStrategiesToDepositWhitelist(_strategy);
address staker = address(this);
dummyToken.approve(address(strategyManager), MAX_STRATEGY_TOTAL_SHARES);
IERC20 token = dummyToken;
uint amount = 1e18;
IStrategy strategy = dummyStrat;
cheats.prank(staker);
cheats.expectRevert("Reverter: I am a contract that always reverts");
strategyManager.depositIntoStrategy(strategy, token, amount);
}
function test_Revert_WhenStrategyDoesNotExist() external {
// replace 'dummyStrat' with one that does not exist
dummyStrat = StrategyBase(address(5678));
// whitelist the strategy for deposit
cheats.prank(strategyManager.owner());
IStrategy[] memory _strategy = new IStrategy[](1);
_strategy[0] = dummyStrat;
strategyManager.addStrategiesToDepositWhitelist(_strategy);
address staker = address(this);
IERC20 token = dummyToken;
uint amount = 1e18;
IStrategy strategy = dummyStrat;
cheats.prank(staker);
cheats.expectRevert();
strategyManager.depositIntoStrategy(strategy, token, amount);
}
function test_Revert_WhenStrategyNotWhitelisted() external {
// replace 'dummyStrat' with one that is not whitelisted
dummyStrat = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
address staker = address(this);
IERC20 token = dummyToken;
uint amount = 1e18;
IStrategy strategy = dummyStrat;
cheats.prank(staker);
cheats.expectRevert(IStrategyManagerErrors.StrategyNotWhitelisted.selector);
strategyManager.depositIntoStrategy(strategy, token, amount);
}
function test_addShares_Revert_WhenSharesIsZero() external {
// replace dummyStrat with Reenterer contract
reenterer = new Reenterer();
dummyStrat = StrategyBase(address(reenterer));
// whitelist the strategy for deposit
cheats.prank(strategyManager.owner());
IStrategy[] memory _strategy = new IStrategy[](1);
_strategy[0] = dummyStrat;
strategyManager.addStrategiesToDepositWhitelist(_strategy);
address staker = address(this);
dummyToken.approve(address(strategyManager), MAX_STRATEGY_TOTAL_SHARES);
IStrategy strategy = dummyStrat;
IERC20 token = dummyToken;
uint amount = 1e18;
reenterer.prepareReturnData(abi.encode(uint(0)));
cheats.prank(staker);
cheats.expectRevert(IStrategyManagerErrors.SharesAmountZero.selector);
strategyManager.depositIntoStrategy(strategy, token, amount);
}
}
contract StrategyManagerUnitTests_depositIntoStrategyWithSignature is StrategyManagerUnitTests {
function test_Revert_WhenSignatureInvalid() public {
address staker = cheats.addr(privateKey);
IStrategy strategy = dummyStrat;
IERC20 token = dummyToken;
uint amount = 1e18;
uint nonceBefore = strategyManager.nonces(staker);
uint expiry = block.timestamp;
bytes memory signature;
{
bytes32 structHash =
keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), staker, strategy, token, amount, nonceBefore, expiry));
bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash));
(uint8 v, bytes32 r, bytes32 s) = cheats.sign(privateKey, digestHash);
signature = abi.encodePacked(r, s, v);
}
uint depositSharesBefore = strategyManager.stakerDepositShares(staker, strategy);
cheats.expectRevert(ISignatureUtilsMixinErrors.InvalidSignature.selector);
// call with `notStaker` as input instead of `staker` address
address notStaker = address(3333);
strategyManager.depositIntoStrategyWithSignature(strategy, token, amount, notStaker, expiry, signature);
uint depositSharesAfter = strategyManager.stakerDepositShares(staker, strategy);
uint nonceAfter = strategyManager.nonces(staker);
assertEq(depositSharesAfter, depositSharesBefore, "depositSharesAfter != depositSharesBefore");
assertEq(nonceAfter, nonceBefore, "nonceAfter != nonceBefore");
}
function testFuzz_DepositSuccessfully(uint amount, uint expiry) public {
// min shares must be minted on strategy
cheats.assume(amount >= 1);
address staker = cheats.addr(privateKey);
// not expecting a revert, so input an empty string
_depositIntoStrategyWithSignature(staker, amount, expiry, bytes4(0x00000000));
}
function testFuzz_Revert_SignatureReplay(uint amount, uint expiry) public {
// min shares must be minted on strategy
cheats.assume(amount >= 1);
cheats.assume(expiry > block.timestamp);
address staker = cheats.addr(privateKey);
// not expecting a revert, so input an empty string
bytes memory signature = _depositIntoStrategyWithSignature(staker, amount, expiry, "");
cheats.expectRevert(ISignatureUtilsMixinErrors.InvalidSignature.selector);
strategyManager.depositIntoStrategyWithSignature(dummyStrat, dummyToken, amount, staker, expiry, signature);
}
// tries depositing using a signature and an EIP 1271 compliant wallet, *but* providing a bad signature
function testFuzz_Revert_WithContractWallet_BadSignature(uint amount) public {
// min shares must be minted on strategy
cheats.assume(amount >= 1);
address staker = cheats.addr(privateKey);
IStrategy strategy = dummyStrat;
IERC20 token = dummyToken;
// deploy ERC1271WalletMock for staker to use
cheats.prank(staker);
ERC1271WalletMock wallet = new ERC1271WalletMock(staker);
staker = address(wallet);
// filter out zero case since it will revert with "StrategyManager._addShares: shares should not be zero!"
cheats.assume(amount != 0);
// sanity check / filter
cheats.assume(amount <= token.balanceOf(address(this)));
uint nonceBefore = strategyManager.nonces(staker);
uint expiry = type(uint).max;
bytes memory signature;
{
bytes32 structHash =
keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), staker, strategy, token, amount, nonceBefore, expiry));
bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash));
(uint8 v, bytes32 r, bytes32 s) = cheats.sign(privateKey, digestHash);
// mess up the signature by flipping v's parity
v = (v == 27 ? 28 : 27);
signature = abi.encodePacked(r, s, v);
}
cheats.expectRevert(ISignatureUtilsMixinErrors.InvalidSignature.selector);
strategyManager.depositIntoStrategyWithSignature(strategy, token, amount, staker, expiry, signature);
}
// tries depositing using a wallet that does not comply with EIP 1271
function testFuzz_Revert_WithContractWallet_NonconformingWallet(uint amount, uint8 v, bytes32 r, bytes32 s) public {
// min shares must be minted on strategy
cheats.assume(amount >= 1);
address staker = cheats.addr(privateKey);
IStrategy strategy = dummyStrat;
IERC20 token = dummyToken;
// deploy ERC1271WalletMock for staker to use
cheats.prank(staker);
ERC1271MaliciousMock wallet = new ERC1271MaliciousMock();
staker = address(wallet);
// filter out zero case since it will revert with "StrategyManager._addShares: shares should not be zero!"
cheats.assume(amount != 0);
// sanity check / filter
cheats.assume(amount <= token.balanceOf(address(this)));
uint expiry = type(uint).max;
bytes memory signature = abi.encodePacked(r, s, v);
cheats.expectRevert();
strategyManager.depositIntoStrategyWithSignature(strategy, token, amount, staker, expiry, signature);
}
// Tries depositing without token approval and transfer fails. deposit function should also revert
function test_Revert_WithContractWallet_TokenTransferFails() external {
address staker = cheats.addr(privateKey);
uint amount = 1e18;
uint nonceBefore = strategyManager.nonces(staker);
uint expiry = block.timestamp + 100;
bytes memory signature;
{
bytes32 structHash =
keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), staker, dummyStrat, revertToken, amount, nonceBefore, expiry));
bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash));
(uint8 v, bytes32 r, bytes32 s) = cheats.sign(privateKey, digestHash);
signature = abi.encodePacked(r, s, v);
}
cheats.expectRevert("ERC20: insufficient allowance");
strategyManager.depositIntoStrategyWithSignature(dummyStrat, revertToken, amount, staker, expiry, signature);
}
// tries depositing using a signature and an EIP 1271 compliant wallet
function testFuzz_WithContractWallet_Successfully(uint amount, uint expiry) public {
// min shares must be minted on strategy
cheats.assume(amount >= 1);
address staker = cheats.addr(privateKey);
// deploy ERC1271WalletMock for staker to use
cheats.prank(staker);
ERC1271WalletMock wallet = new ERC1271WalletMock(staker);
staker = address(wallet);
// not expecting a revert, so input an empty string
_depositIntoStrategyWithSignature(staker, amount, expiry, bytes4(0x00000000));
}
function test_Revert_WhenDepositsPaused() public {
address staker = cheats.addr(privateKey);
// pause deposits
cheats.prank(pauser);
strategyManager.pause(1);
_depositIntoStrategyWithSignature(staker, 1e18, type(uint).max, IPausable.CurrentlyPaused.selector);
}
/**
* @notice reenterer contract which is configured as the strategy contract
* is configured to call depositIntoStrategy after reenterer.deposit() is called from the
* depositIntoStrategyWithSignature() is called from the StrategyManager. Situation is not likely to occur given
* the strategy has to be whitelisted but it at least protects from reentrant attacks
*/
function test_Revert_WhenReentering() public {
reenterer = new Reenterer();
// whitelist the strategy for deposit
cheats.prank(strategyManager.owner());
IStrategy[] memory _strategy = new IStrategy[](1);
dummyToken.approve(address(strategyManager), MAX_STRATEGY_TOTAL_SHARES);
_strategy[0] = IStrategy(address(reenterer));
for (uint i = 0; i < _strategy.length; ++i) {
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit StrategyAddedToDepositWhitelist(_strategy[i]);
}
strategyManager.addStrategiesToDepositWhitelist(_strategy);
address staker = cheats.addr(privateKey);
IStrategy strategy = IStrategy(address(reenterer));
IERC20 token = dummyToken;
uint amount = 1e18;
uint nonceBefore = strategyManager.nonces(staker);
uint expiry = type(uint).max;
bytes memory signature;
{
bytes32 structHash =
keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), staker, strategy, token, amount, nonceBefore, expiry));
bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash));
(uint8 v, bytes32 r, bytes32 s) = cheats.sign(privateKey, digestHash);
signature = abi.encodePacked(r, s, v);
}
uint shareAmountToReturn = amount;
reenterer.prepareReturnData(abi.encode(shareAmountToReturn));
{
address targetToUse = address(strategyManager);
uint msgValueToUse = 0;
bytes memory calldataToUse =
abi.encodeWithSelector(StrategyManager.depositIntoStrategy.selector, address(reenterer), dummyToken, amount);
reenterer.prepare(targetToUse, msgValueToUse, calldataToUse, bytes("ReentrancyGuard: reentrant call"));
}
strategyManager.depositIntoStrategyWithSignature(strategy, token, amount, staker, expiry, signature);
}
function test_Revert_WhenSignatureExpired() public {
address staker = cheats.addr(privateKey);
IStrategy strategy = dummyStrat;
IERC20 token = dummyToken;
uint amount = 1e18;
uint nonceBefore = strategyManager.nonces(staker);
uint expiry = 5555;
// warp to 1 second after expiry
cheats.warp(expiry + 1);
bytes memory signature;
{
bytes32 structHash =
keccak256(abi.encode(strategyManager.DEPOSIT_TYPEHASH(), staker, strategy, token, amount, nonceBefore, expiry));
bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash));
(uint8 v, bytes32 r, bytes32 s) = cheats.sign(privateKey, digestHash);
signature = abi.encodePacked(r, s, v);
}
uint depositSharesBefore = strategyManager.stakerDepositShares(staker, strategy);
cheats.expectRevert(ISignatureUtilsMixinErrors.SignatureExpired.selector);
strategyManager.depositIntoStrategyWithSignature(strategy, token, amount, staker, expiry, signature);
uint depositSharesAfter = strategyManager.stakerDepositShares(staker, strategy);
uint nonceAfter = strategyManager.nonces(staker);
assertEq(depositSharesAfter, depositSharesBefore, "depositSharesAfter != depositSharesBefore");
assertEq(nonceAfter, nonceBefore, "nonceAfter != nonceBefore");
}
function test_Revert_WhenStrategyNotWhitelisted() external {
// replace 'dummyStrat' with one that is not whitelisted
dummyStrat = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
dummyToken = dummyStrat.underlyingToken();
address staker = cheats.addr(privateKey);
uint amount = 1e18;
_depositIntoStrategyWithSignature(staker, amount, type(uint).max, IStrategyManagerErrors.StrategyNotWhitelisted.selector);
}
}
contract StrategyManagerUnitTests_removeDepositShares is StrategyManagerUnitTests {
/**
* @notice Should revert if not called by DelegationManager
*/
function test_Revert_DelegationManagerModifier() external {
DelegationManagerMock invalidDelegationManager = new DelegationManagerMock();
cheats.expectRevert(IStrategyManagerErrors.OnlyDelegationManager.selector);
invalidDelegationManager.removeDepositShares(strategyManager, address(this), dummyStrat, 1);
}
/**
* @notice deposits a single strategy and tests removeDepositShares() function reverts when sharesAmount is 0
*/
function testFuzz_Revert_ZeroShares(address staker, uint depositAmount) external filterFuzzedAddressInputs(staker) {
cheats.assume(staker != address(0));
cheats.assume(depositAmount > 0 && depositAmount < dummyToken.totalSupply());
IStrategy strategy = dummyStrat;
_depositIntoStrategySuccessfully(strategy, staker, depositAmount);
cheats.expectRevert(IStrategyManagerErrors.SharesAmountZero.selector);
delegationManagerMock.removeDepositShares(strategyManager, staker, strategy, 0);
}
/**
* @notice deposits a single strategy and tests removeDepositShares() function reverts when sharesAmount is
* higher than depositAmount
*/
function testFuzz_Revert_ShareAmountTooHigh(address staker, uint depositAmount, uint removeSharesAmount)
external
filterFuzzedAddressInputs(staker)
{
cheats.assume(staker != address(0));
cheats.assume(depositAmount > 0 && depositAmount < dummyToken.totalSupply());
cheats.assume(removeSharesAmount > depositAmount);
IStrategy strategy = dummyStrat;
_depositIntoStrategySuccessfully(strategy, staker, depositAmount);
cheats.expectRevert(IStrategyManagerErrors.SharesAmountTooHigh.selector);
delegationManagerMock.removeDepositShares(strategyManager, staker, strategy, removeSharesAmount);
}
/**
* @notice deposit single strategy and removeDepositShares() for less than the deposited amount
* Shares should be updated correctly with stakerStrategyListLength unchanged
*/
function testFuzz_RemoveSharesLessThanDeposit(address staker, uint depositAmount, uint removeSharesAmount)
external
filterFuzzedAddressInputs(staker)
{
cheats.assume(staker != address(0));
cheats.assume(depositAmount > 0 && depositAmount < dummyToken.totalSupply());
cheats.assume(removeSharesAmount > 0 && removeSharesAmount < depositAmount);
IStrategy strategy = dummyStrat;
_depositIntoStrategySuccessfully(strategy, staker, depositAmount);
uint stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker);
uint depositSharesBefore = strategyManager.stakerDepositShares(staker, strategy);
delegationManagerMock.removeDepositShares(strategyManager, staker, strategy, removeSharesAmount);
uint stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker);
uint depositSharesAfter = strategyManager.stakerDepositShares(staker, strategy);
assertEq(depositSharesBefore, depositSharesAfter + removeSharesAmount, "Remove incorrect amount of shares");
assertEq(stakerStrategyListLengthBefore, stakerStrategyListLengthAfter, "stakerStrategyListLength shouldn't have changed");
}
/**
* @notice testing removeDepositShares()
* deposits 1 strategy and tests it is removed from staker strategy list after removing all shares
*/
function testFuzz_RemovesStakerStrategyListSingleStrat(address staker, uint sharesAmount) external filterFuzzedAddressInputs(staker) {
cheats.assume(staker != address(0));
cheats.assume(sharesAmount > 0 && sharesAmount < dummyToken.totalSupply());
IStrategy strategy = dummyStrat;
_depositIntoStrategySuccessfully(strategy, staker, sharesAmount);
uint stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker);
uint depositSharesBefore = strategyManager.stakerDepositShares(staker, strategy);
assertEq(depositSharesBefore, sharesAmount, "Staker has not deposited amount into strategy");
delegationManagerMock.removeDepositShares(strategyManager, staker, strategy, sharesAmount);
uint stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker);
uint depositSharesAfter = strategyManager.stakerDepositShares(staker, strategy);
assertEq(
stakerStrategyListLengthAfter,
stakerStrategyListLengthBefore - 1,
"stakerStrategyListLengthAfter != stakerStrategyListLengthBefore - 1"
);
assertEq(depositSharesAfter, 0, "depositSharesAfter != 0");
assertFalse(_isDepositedStrategy(staker, strategy), "strategy should not be part of staker strategy list");
}
/**
* @notice testing removeDepositShares() function with 3 strategies deposited.
* Randomly selects one of the 3 strategies to be fully removed from staker strategy list.
* Only callable by DelegationManager
*/
function testFuzz_RemovesStakerStrategyListMultipleStrat(address staker, uint[3] memory amounts, uint8 randStrategy)
external
filterFuzzedAddressInputs(staker)
{
cheats.assume(staker != address(0));
IStrategy[] memory strategies = new IStrategy[](3);
strategies[0] = dummyStrat;
strategies[1] = dummyStrat2;
strategies[2] = dummyStrat3;
for (uint i = 0; i < 3; ++i) {
amounts[i] = bound(amounts[i], 1, dummyToken.totalSupply() - 1);
_depositIntoStrategySuccessfully(strategies[i], staker, amounts[i]);
}
IStrategy removeStrategy = strategies[randStrategy % 3];
uint removeAmount = amounts[randStrategy % 3];
uint stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker);
uint[] memory depositSharesBefore = new uint[](3);
for (uint i = 0; i < 3; ++i) {
depositSharesBefore[i] = strategyManager.stakerDepositShares(staker, strategies[i]);
assertEq(depositSharesBefore[i], amounts[i], "Staker has not deposited amount into strategy");
assertTrue(_isDepositedStrategy(staker, strategies[i]), "strategy should be deposited");
}
delegationManagerMock.removeDepositShares(strategyManager, staker, removeStrategy, removeAmount);
uint stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker);
uint depositSharesAfter = strategyManager.stakerDepositShares(staker, removeStrategy);
assertEq(
stakerStrategyListLengthAfter,
stakerStrategyListLengthBefore - 1,
"stakerStrategyListLengthAfter != stakerStrategyListLengthBefore - 1"
);
assertEq(depositSharesAfter, 0, "depositSharesAfter != 0");
assertFalse(_isDepositedStrategy(staker, removeStrategy), "strategy should not be part of staker strategy list");
}
/**
* @notice testing removeDepositShares() function with 3 strategies deposited.
* Removing Shares could result in removing from staker strategy list if depositAmounts[i] == sharesAmounts[i].
* Only callable by DelegationManager
*/
function testFuzz_RemoveShares(uint[3] memory depositAmounts, uint[3] memory sharesAmounts) external {
address staker = address(this);
IStrategy[] memory strategies = new IStrategy[](3);
strategies[0] = dummyStrat;
strategies[1] = dummyStrat2;
strategies[2] = dummyStrat3;
uint[] memory depositSharesBefore = new uint[](3);
for (uint i = 0; i < 3; ++i) {
depositAmounts[i] = bound(depositAmounts[i], 1, strategies[i].underlyingToken().totalSupply());
sharesAmounts[i] = bound(sharesAmounts[i], 1, depositAmounts[i]);
_depositIntoStrategySuccessfully(strategies[i], staker, depositAmounts[i]);
depositSharesBefore[i] = strategyManager.stakerDepositShares(staker, strategies[i]);
assertEq(depositSharesBefore[i], depositAmounts[i], "Staker has not deposited amount into strategy");
assertTrue(_isDepositedStrategy(staker, strategies[i]), "strategy should be deposited");
}
uint stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker);
uint numPoppedStrategies = 0;
uint[] memory depositSharesAfter = new uint[](3);
for (uint i = 0; i < 3; ++i) {
delegationManagerMock.removeDepositShares(strategyManager, staker, strategies[i], sharesAmounts[i]);
}
for (uint i = 0; i < 3; ++i) {
depositSharesAfter[i] = strategyManager.stakerDepositShares(staker, strategies[i]);
if (sharesAmounts[i] == depositAmounts[i]) {
++numPoppedStrategies;
assertFalse(_isDepositedStrategy(staker, strategies[i]), "strategy should not be part of staker strategy list");
assertEq(depositSharesAfter[i], 0, "depositSharesAfter != 0");
} else {
assertTrue(_isDepositedStrategy(staker, strategies[i]), "strategy should be part of staker strategy list");
assertEq(
depositSharesAfter[i],
depositSharesBefore[i] - sharesAmounts[i],
"depositSharesAfter != depositSharesBefore - sharesAmounts"
);
}
}
assertEq(
stakerStrategyListLengthBefore - numPoppedStrategies,
strategyManager.stakerStrategyListLength(staker),
"stakerStrategyListLengthBefore - numPoppedStrategies != strategyManager.stakerStrategyListLength(staker)"
);
}
}
contract StrategyManagerUnitTests_addShares is StrategyManagerUnitTests {
function test_Revert_DelegationManagerModifier() external {
DelegationManagerMock invalidDelegationManager = new DelegationManagerMock();
cheats.expectRevert(IStrategyManagerErrors.OnlyDelegationManager.selector);
invalidDelegationManager.addShares(strategyManager, address(this), dummyStrat, 1);
}
function testFuzz_Revert_StakerZeroAddress(uint amount) external {
cheats.expectRevert(IStrategyManagerErrors.StakerAddressZero.selector);
delegationManagerMock.addShares(strategyManager, address(0), dummyStrat, amount);
}
function testFuzz_Revert_ZeroShares(address staker) external filterFuzzedAddressInputs(staker) {
cheats.assume(staker != address(0));
cheats.expectRevert(IStrategyManagerErrors.SharesAmountZero.selector);
delegationManagerMock.addShares(strategyManager, staker, dummyStrat, 0);
}
function testFuzz_AppendsStakerStrategyList(address staker, uint amount) external filterFuzzedAddressInputs(staker) {
cheats.assume(staker != address(0) && amount != 0);
uint stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker);
uint depositSharesBefore = strategyManager.stakerDepositShares(staker, dummyStrat);
assertEq(depositSharesBefore, 0, "Staker has already deposited into this strategy");
assertFalse(_isDepositedStrategy(staker, dummyStrat), "strategy should not be deposited");
delegationManagerMock.addShares(strategyManager, staker, dummyStrat, amount);
uint stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker);
uint depositSharesAfter = strategyManager.stakerDepositShares(staker, dummyStrat);
assertEq(
stakerStrategyListLengthAfter,
stakerStrategyListLengthBefore + 1,
"stakerStrategyListLengthAfter != stakerStrategyListLengthBefore + 1"
);
assertEq(depositSharesAfter, amount, "depositSharesAfter != amount");
assertTrue(_isDepositedStrategy(staker, dummyStrat), "strategy should be deposited");
}
function testFuzz_AddSharesToExistingShares(address staker, uint sharesAmount) external filterFuzzedAddressInputs(staker) {
cheats.assume(staker != address(0) && 0 < sharesAmount && sharesAmount <= dummyToken.totalSupply());
uint initialAmount = 1e18;
IStrategy strategy = dummyStrat;
_depositIntoStrategySuccessfully(strategy, staker, initialAmount);
uint stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker);
uint depositSharesBefore = strategyManager.stakerDepositShares(staker, dummyStrat);
assertEq(depositSharesBefore, initialAmount, "Staker has not deposited amount into strategy");
assertTrue(_isDepositedStrategy(staker, strategy), "strategy should be deposited");
delegationManagerMock.addShares(strategyManager, staker, dummyStrat, sharesAmount);
uint stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker);
uint depositSharesAfter = strategyManager.stakerDepositShares(staker, dummyStrat);
assertEq(
stakerStrategyListLengthAfter, stakerStrategyListLengthBefore, "stakerStrategyListLengthAfter != stakerStrategyListLengthBefore"
);
assertEq(depositSharesAfter, depositSharesBefore + sharesAmount, "depositSharesAfter != depositSharesBefore + sharesAmount");
assertTrue(_isDepositedStrategy(staker, strategy), "strategy should be deposited");
}
/**
* @notice When _addShares() called either by depositIntoStrategy or addShares() results in appending to
* stakerStrategyListLength when the staker has MAX_STAKER_STRATEGY_LIST_LENGTH strategies, it should revert
*/
function test_Revert_WhenMaxStrategyListLength() external {
address staker = address(this);
IERC20 token = dummyToken;
uint amount = 1e18;
IStrategy strategy = dummyStrat;
uint MAX_STAKER_STRATEGY_LIST_LENGTH = 32;
cheats.prank(staker);
token.approve(address(strategyManager), MAX_STRATEGY_TOTAL_SHARES);
// loop that deploys a new strategy and deposits into it
for (uint i = 0; i < MAX_STAKER_STRATEGY_LIST_LENGTH; ++i) {
cheats.prank(staker);
strategyManager.depositIntoStrategy(strategy, token, amount);
dummyStrat = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
strategy = dummyStrat;
// whitelist the strategy for deposit
cheats.prank(strategyManager.owner());
IStrategy[] memory _strategy = new IStrategy[](1);
_strategy[0] = dummyStrat;
strategyManager.addStrategiesToDepositWhitelist(_strategy);
}
assertEq(
strategyManager.stakerStrategyListLength(staker),
MAX_STAKER_STRATEGY_LIST_LENGTH,
"strategyManager.stakerStrategyListLength(staker) != MAX_STAKER_STRATEGY_LIST_LENGTH"
);
cheats.prank(staker);
cheats.expectRevert(IStrategyManagerErrors.MaxStrategiesExceeded.selector);
delegationManagerMock.addShares(strategyManager, staker, strategy, amount);
cheats.expectRevert(IStrategyManagerErrors.MaxStrategiesExceeded.selector);
strategyManager.depositIntoStrategy(strategy, token, amount);
}
}
contract StrategyManagerUnitTests_withdrawSharesAsTokens is StrategyManagerUnitTests {
function test_Revert_DelegationManagerModifier() external {
DelegationManagerMock invalidDelegationManager = new DelegationManagerMock();
cheats.expectRevert(IStrategyManagerErrors.OnlyDelegationManager.selector);
invalidDelegationManager.removeDepositShares(strategyManager, address(this), dummyStrat, 1);
}
/**
* @notice deposits a single strategy and withdrawSharesAsTokens() function reverts when sharesAmount is
* higher than depositAmount
*/
function testFuzz_Revert_ShareAmountTooHigh(address staker, uint depositAmount, uint sharesAmount)
external
filterFuzzedAddressInputs(staker)
{
cheats.assume(staker != address(this));
cheats.assume(staker != address(0));
cheats.assume(staker != address(dummyStrat));
cheats.assume(depositAmount > 0 && depositAmount < dummyToken.totalSupply() && depositAmount < sharesAmount);
IStrategy strategy = dummyStrat;
IERC20 token = dummyToken;
_depositIntoStrategySuccessfully(strategy, staker, depositAmount);
cheats.expectRevert(IStrategyErrors.WithdrawalAmountExceedsTotalDeposits.selector);
delegationManagerMock.withdrawSharesAsTokens(strategyManager, staker, strategy, sharesAmount, token);
}
function testFuzz_SingleStrategyDeposited(address staker, uint depositAmount, uint sharesAmount)
external
filterFuzzedAddressInputs(staker)
{
cheats.assume(staker != address(this));
cheats.assume(staker != address(0));
cheats.assume(staker != address(dummyStrat));
cheats.assume(sharesAmount > 0 && sharesAmount < dummyToken.totalSupply() && depositAmount >= sharesAmount);
IStrategy strategy = dummyStrat;
IERC20 token = dummyToken;
_depositIntoStrategySuccessfully(strategy, staker, depositAmount);
uint balanceBefore = token.balanceOf(staker);
delegationManagerMock.withdrawSharesAsTokens(strategyManager, staker, strategy, sharesAmount, token);
uint balanceAfter = token.balanceOf(staker);
assertEq(balanceAfter, balanceBefore + sharesAmount, "balanceAfter != balanceBefore + sharesAmount");
}
}
contract StrategyManagerUnitTests_increaseBurnableShares is StrategyManagerUnitTests {
function test_Revert_DelegationManagerModifier() external {
DelegationManagerMock invalidDelegationManager = new DelegationManagerMock();
cheats.prank(address(invalidDelegationManager));
cheats.expectRevert(IStrategyManagerErrors.OnlyDelegationManager.selector);
strategyManager.increaseBurnableShares(dummyStrat, 1);
}
function testFuzz_increaseBurnableShares(uint addedSharesToBurn) external {
IStrategy strategy = dummyStrat;
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit BurnableSharesIncreased(strategy, addedSharesToBurn);
cheats.prank(address(delegationManagerMock));
strategyManager.increaseBurnableShares(strategy, addedSharesToBurn);
assertEq(
strategyManager.getBurnableShares(strategy), addedSharesToBurn, "strategyManager.burnableShares(strategy) != addedSharesToBurn"
);
}
function testFuzz_increaseBurnableShares_existingShares(uint existingBurnableShares, uint addedSharesToBurn) external {
// preventing fuzz overflow, in practice StrategyBase has a 1e38 - 1 maxShares limit so this won't
// be an issue on mainnet/testnet environments
existingBurnableShares = bound(existingBurnableShares, 1, type(uint).max / 2);
addedSharesToBurn = bound(addedSharesToBurn, 1, type(uint).max / 2);
IStrategy strategy = dummyStrat;
cheats.prank(address(delegationManagerMock));
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit BurnableSharesIncreased(strategy, existingBurnableShares);
strategyManager.increaseBurnableShares(strategy, existingBurnableShares);
assertEq(
strategyManager.getBurnableShares(strategy),
existingBurnableShares,
"strategyManager.burnableShares(strategy) != existingBurnableShares"
);
cheats.prank(address(delegationManagerMock));
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit BurnableSharesIncreased(strategy, addedSharesToBurn);
strategyManager.increaseBurnableShares(strategy, addedSharesToBurn);
assertEq(
strategyManager.getBurnableShares(strategy),
existingBurnableShares + addedSharesToBurn,
"strategyManager.burnableShares(strategy) != existingBurnableShares + addedSharesToBurn"
);
}
}
contract StrategyManagerUnitTests_burnShares is StrategyManagerUnitTests {
function testFuzz_SingleStrategyDeposited(address staker, uint depositAmount, uint sharesToBurn)
external
filterFuzzedAddressInputs(staker)
{
cheats.assume(staker != address(0));
cheats.assume(staker != address(dummyStrat));
cheats.assume(sharesToBurn > 0 && sharesToBurn < dummyToken.totalSupply() && depositAmount >= sharesToBurn);
IStrategy strategy = dummyStrat;
IERC20 token = dummyToken;
_depositIntoStrategySuccessfully(strategy, staker, depositAmount);
// slash shares and increase amount to burn from DelegationManager
cheats.prank(address(delegationManagerMock));
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit BurnableSharesIncreased(strategy, sharesToBurn);
strategyManager.increaseBurnableShares(strategy, sharesToBurn);
uint strategyBalanceBefore = token.balanceOf(address(strategy));
uint burnAddressBalanceBefore = token.balanceOf(strategyManager.DEFAULT_BURN_ADDRESS());
cheats.prank(address(delegationManagerMock));
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit BurnableSharesDecreased(strategy, sharesToBurn);
strategyManager.burnShares(strategy);
uint strategyBalanceAfter = token.balanceOf(address(strategy));
uint burnAddressBalanceAfter = token.balanceOf(strategyManager.DEFAULT_BURN_ADDRESS());
assertEq(strategyBalanceBefore - sharesToBurn, strategyBalanceAfter, "strategyBalanceBefore - sharesToBurn != strategyBalanceAfter");
assertEq(burnAddressBalanceAfter, burnAddressBalanceBefore + sharesToBurn, "balanceAfter != balanceBefore + sharesAmount");
// Verify strategy was removed from burnable shares
(address[] memory strategiesAfterBurn,) = strategyManager.getStrategiesWithBurnableShares();
assertEq(strategiesAfterBurn.length, 0, "Should have no strategies after burning");
assertEq(strategyManager.getBurnableShares(strategy), 0, "getBurnableShares should return 0 after burning");
}
/// @notice check that balances are unchanged with a reverting token but burnShares doesn't revert
function testFuzz_BurnableSharesUnchangedWithRevertToken(address staker, uint depositAmount, uint 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);
// slash shares and increase amount to burn from DelegationManager
cheats.prank(address(delegationManagerMock));
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit BurnableSharesIncreased(strategy, sharesToBurn);
strategyManager.increaseBurnableShares(strategy, sharesToBurn);
// Now set token to be contract that reverts simulating an upgrade
cheats.etch(address(token), address(revertToken).code);
ERC20_SetTransferReverting_Mock(address(token)).setTransfersRevert(true);
cheats.expectRevert("SafeERC20: low-level call failed");
cheats.prank(address(delegationManagerMock));
strategyManager.burnShares(strategy);
assertEq(strategyManager.getBurnableShares(strategy), sharesToBurn, "burnable shares should be unchanged");
}
}
contract StrategyManagerUnitTests_setStrategyWhitelister is StrategyManagerUnitTests {
function testFuzz_SetStrategyWhitelister(address newWhitelister) external filterFuzzedAddressInputs(newWhitelister) {
address previousStrategyWhitelister = strategyManager.strategyWhitelister();
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit StrategyWhitelisterChanged(previousStrategyWhitelister, newWhitelister);
strategyManager.setStrategyWhitelister(newWhitelister);
assertEq(strategyManager.strategyWhitelister(), newWhitelister, "strategyManager.strategyWhitelister() != newWhitelister");
}
function testFuzz_Revert_WhenCalledByNotOwner(address notOwner) external filterFuzzedAddressInputs(notOwner) {
cheats.assume(notOwner != strategyManager.owner());
address newWhitelister = address(this);
cheats.prank(notOwner);
cheats.expectRevert("Ownable: caller is not the owner");
strategyManager.setStrategyWhitelister(newWhitelister);
}
}
contract StrategyManagerUnitTests_addStrategiesToDepositWhitelist is StrategyManagerUnitTests {
function testFuzz_Revert_WhenCalledByNotStrategyWhitelister(address notStrategyWhitelister)
external
filterFuzzedAddressInputs(notStrategyWhitelister)
{
cheats.assume(notStrategyWhitelister != strategyManager.strategyWhitelister());
IStrategy[] memory strategyArray = new IStrategy[](1);
IStrategy _strategy = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
strategyArray[0] = _strategy;
cheats.prank(notStrategyWhitelister);
cheats.expectRevert(IStrategyManagerErrors.OnlyStrategyWhitelister.selector);
strategyManager.addStrategiesToDepositWhitelist(strategyArray);
}
function test_AddSingleStrategyToWhitelist() external {
IStrategy[] memory strategyArray = new IStrategy[](1);
IStrategy strategy = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
strategyArray[0] = strategy;
assertFalse(strategyManager.strategyIsWhitelistedForDeposit(strategy), "strategy should not be whitelisted");
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit StrategyAddedToDepositWhitelist(strategy);
strategyManager.addStrategiesToDepositWhitelist(strategyArray);
assertTrue(strategyManager.strategyIsWhitelistedForDeposit(strategy), "strategy should be whitelisted");
}
function test_AddAlreadyWhitelistedStrategy() external {
IStrategy[] memory strategyArray = new IStrategy[](1);
IStrategy strategy = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
strategyArray[0] = strategy;
assertFalse(strategyManager.strategyIsWhitelistedForDeposit(strategy), "strategy should not be whitelisted");
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit StrategyAddedToDepositWhitelist(strategy);
strategyManager.addStrategiesToDepositWhitelist(strategyArray);
assertTrue(strategyManager.strategyIsWhitelistedForDeposit(strategy), "strategy should be whitelisted");
// Make sure event not emitted by checking logs length
cheats.recordLogs();
uint numLogsBefore = cheats.getRecordedLogs().length;
strategyManager.addStrategiesToDepositWhitelist(strategyArray);
uint numLogsAfter = cheats.getRecordedLogs().length;
assertEq(numLogsBefore, numLogsAfter, "event emitted when strategy already whitelisted");
assertTrue(strategyManager.strategyIsWhitelistedForDeposit(strategy), "strategy should still be whitelisted");
}
function testFuzz_AddStrategiesToDepositWhitelist(uint8 numberOfStrategiesToAdd) external {
// sanity filtering on fuzzed input
cheats.assume(numberOfStrategiesToAdd <= 16);
_addStrategiesToWhitelist(numberOfStrategiesToAdd);
}
}
contract StrategyManagerUnitTests_removeStrategiesFromDepositWhitelist is StrategyManagerUnitTests {
function testFuzz_Revert_WhenCalledByNotStrategyWhitelister(address notStrategyWhitelister)
external
filterFuzzedAddressInputs(notStrategyWhitelister)
{
cheats.assume(notStrategyWhitelister != strategyManager.strategyWhitelister());
IStrategy[] memory strategyArray = _addStrategiesToWhitelist(1);
cheats.prank(notStrategyWhitelister);
cheats.expectRevert(IStrategyManagerErrors.OnlyStrategyWhitelister.selector);
strategyManager.removeStrategiesFromDepositWhitelist(strategyArray);
}
/**
* @notice testing that mapping is still false and no event emitted
*/
function test_RemoveNonWhitelistedStrategy() external {
IStrategy[] memory strategyArray = new IStrategy[](1);
IStrategy strategy = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
strategyArray[0] = strategy;
assertFalse(strategyManager.strategyIsWhitelistedForDeposit(strategy), "strategy should not be whitelisted");
// Make sure event not emitted by checking logs length
cheats.recordLogs();
uint numLogsBefore = cheats.getRecordedLogs().length;
strategyManager.removeStrategiesFromDepositWhitelist(strategyArray);
uint numLogsAfter = cheats.getRecordedLogs().length;
assertEq(numLogsBefore, numLogsAfter, "event emitted when strategy already not whitelisted");
assertFalse(strategyManager.strategyIsWhitelistedForDeposit(strategy), "strategy still should not be whitelisted");
}
/**
* @notice testing that strategy is removed from whitelist and event is emitted
*/
function test_RemoveWhitelistedStrategy() external {
IStrategy[] memory strategyArray = new IStrategy[](1);
IStrategy strategy = _deployNewStrategy(dummyToken, strategyManager, pauserRegistry, dummyAdmin);
strategyArray[0] = strategy;
assertFalse(strategyManager.strategyIsWhitelistedForDeposit(strategy), "strategy should not be whitelisted");
// Add strategy to whitelist first
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit StrategyAddedToDepositWhitelist(strategy);
strategyManager.addStrategiesToDepositWhitelist(strategyArray);
assertTrue(strategyManager.strategyIsWhitelistedForDeposit(strategy), "strategy should be whitelisted");
// Now remove strategy from whitelist
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit StrategyRemovedFromDepositWhitelist(strategy);
strategyManager.removeStrategiesFromDepositWhitelist(strategyArray);
assertFalse(strategyManager.strategyIsWhitelistedForDeposit(strategy), "strategy should no longer be whitelisted");
}
function testFuzz_RemoveStrategiesFromDepositWhitelist(uint8 numberOfStrategiesToAdd, uint8 numberOfStrategiesToRemove) external {
// sanity filtering on fuzzed input
cheats.assume(numberOfStrategiesToAdd <= 16);
cheats.assume(numberOfStrategiesToRemove <= 16);
cheats.assume(numberOfStrategiesToRemove <= numberOfStrategiesToAdd);
IStrategy[] memory strategiesAdded = _addStrategiesToWhitelist(numberOfStrategiesToAdd);
IStrategy[] memory strategiesToRemove = new IStrategy[](numberOfStrategiesToRemove);
// loop that selectively copies from array to other array
for (uint i = 0; i < numberOfStrategiesToRemove; ++i) {
strategiesToRemove[i] = strategiesAdded[i];
}
cheats.prank(strategyManager.strategyWhitelister());
for (uint i = 0; i < strategiesToRemove.length; ++i) {
cheats.expectEmit(true, true, true, true, address(strategyManager));
emit StrategyRemovedFromDepositWhitelist(strategiesToRemove[i]);
}
strategyManager.removeStrategiesFromDepositWhitelist(strategiesToRemove);
for (uint i = 0; i < numberOfStrategiesToAdd; ++i) {
if (i < numberOfStrategiesToRemove) {
assertFalse(
strategyManager.strategyIsWhitelistedForDeposit(strategiesToRemove[i]), "strategy not properly removed from whitelist"
);
} else {
assertTrue(
strategyManager.strategyIsWhitelistedForDeposit(strategiesAdded[i]), "strategy improperly removed from whitelist?"
);
}
}
}
}
contract StrategyManagerUnitTests_getStrategiesWithBurnableShares is StrategyManagerUnitTests {
function test_getStrategiesWithBurnableShares_Empty() public view {
(address[] memory strategies, uint[] memory shares) = strategyManager.getStrategiesWithBurnableShares();
assertEq(strategies.length, 0, "Should have no strategies when empty");
assertEq(shares.length, 0, "Should have no shares when empty");
}
function testFuzz_getStrategiesWithBurnableShares_Single(uint sharesToAdd) public {
//ensure non-zero
cheats.assume(sharesToAdd > 0);
// Add burnable shares
cheats.prank(address(delegationManagerMock));
strategyManager.increaseBurnableShares(dummyStrat, sharesToAdd);
// Get strategies with burnable shares
(address[] memory strategies, uint[] memory shares) = strategyManager.getStrategiesWithBurnableShares();
// Verify results
assertEq(strategies.length, 1, "Should have one strategy");
assertEq(shares.length, 1, "Should have one share amount");
assertEq(strategies[0], address(dummyStrat), "Wrong strategy address");
assertEq(shares[0], sharesToAdd, "Wrong shares amount");
}
function testFuzz_getStrategiesWithBurnableShares_Multiple(uint[3] calldata sharesToAdd) public {
IStrategy[] memory strategies = new IStrategy[](3);
strategies[0] = dummyStrat;
strategies[1] = dummyStrat2;
strategies[2] = dummyStrat3;
uint[3] memory expectedShares;
uint expectedLength = 0;
// Add non-zero shares to strategies
for (uint i = 0; i < 3; i++) {
expectedShares[i] = sharesToAdd[i];
if (sharesToAdd[i] > 0) {
expectedLength++;
cheats.prank(address(delegationManagerMock));
strategyManager.increaseBurnableShares(strategies[i], sharesToAdd[i]);
}
}
// Get strategies with burnable shares
(address[] memory returnedStrategies, uint[] memory returnedShares) = strategyManager.getStrategiesWithBurnableShares();
// Verify lengths match
assertEq(returnedStrategies.length, expectedLength, "Wrong number of strategies returned");
assertEq(returnedShares.length, expectedLength, "Wrong number of share amounts returned");
// For all strategies with non-zero shares, verify they are in the returned arrays
uint foundCount = 0;
for (uint i = 0; i < 3; i++) {
if (expectedShares[i] > 0) {
bool found = false;
for (uint j = 0; j < returnedStrategies.length; j++) {
if (returnedStrategies[j] == address(strategies[i])) {
assertEq(returnedShares[j], expectedShares[i], "Wrong share amount");
found = true;
foundCount++;
break;
}
}
assertTrue(found, "Strategy with non-zero shares not found in returned array");
}
}
assertEq(foundCount, expectedLength, "Number of found strategies doesn't match expected length");
}
}
````
## File: src/test/utils/ArrayLib.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/contracts/interfaces/IAllocationManager.sol";
import "src/contracts/interfaces/IDelegationManager.sol";
library ArrayLib {
using ArrayLib for *;
using ArrayLib for uint[];
using ArrayLib for address[];
/// -----------------------------------------------------------------------
/// Single Item Arrays
/// -----------------------------------------------------------------------
function toArrayU16(uint16 x) internal pure returns (uint16[] memory array) {
array = new uint16[](1);
array[0] = x;
}
function toArrayU32(uint32 x) internal pure returns (uint32[] memory array) {
array = new uint32[](1);
array[0] = x;
}
function toArrayU64(uint64 x) internal pure returns (uint64[] memory array) {
array = new uint64[](1);
array[0] = x;
}
function toArrayU256(uint x) internal pure returns (uint[] memory array) {
array = new uint[](1);
array[0] = x;
}
function toArrayU16(uint16 x, uint len) internal pure returns (uint16[] memory array) {
array = new uint16[](len);
for (uint i; i < len; ++i) {
array[i] = x;
}
}
function toArrayU32(uint32 x, uint len) internal pure returns (uint32[] memory array) {
array = new uint32[](len);
for (uint i; i < len; ++i) {
array[i] = x;
}
}
function toArrayU64(uint64 x, uint len) internal pure returns (uint64[] memory array) {
array = new uint64[](len);
for (uint i; i < len; ++i) {
array[i] = x;
}
}
function toArrayU256(uint x, uint len) internal pure returns (uint[] memory array) {
array = new uint[](len);
for (uint i; i < len; ++i) {
array[i] = x;
}
}
function toArray(address x) internal pure returns (address[] memory array) {
array = new address[](1);
array[0] = x;
}
function toArray(bool x) internal pure returns (bool[] memory array) {
array = new bool[](1);
array[0] = x;
}
function toArray(bool x, uint len) internal pure returns (bool[] memory array) {
array = new bool[](len);
for (uint i; i < len; ++i) {
array[i] = x;
}
}
/// -----------------------------------------------------------------------
/// EigenLayer Types
/// -----------------------------------------------------------------------
function toArray(IERC20 token) internal pure returns (IERC20[] memory array) {
array = new IERC20[](1);
array[0] = token;
}
function toArray(IStrategy strategy) internal pure returns (IStrategy[] memory array) {
array = new IStrategy[](1);
array[0] = strategy;
}
function toArray(OperatorSet memory operatorSet) internal pure returns (OperatorSet[] memory array) {
array = new OperatorSet[](1);
array[0] = operatorSet;
}
function toArray(IAllocationManagerTypes.CreateSetParams memory createSetParams)
internal
pure
returns (IAllocationManagerTypes.CreateSetParams[] memory array)
{
array = new IAllocationManagerTypes.CreateSetParams[](1);
array[0] = createSetParams;
}
function toArray(IAllocationManagerTypes.AllocateParams memory allocateParams)
internal
pure
returns (IAllocationManagerTypes.AllocateParams[] memory array)
{
array = new IAllocationManagerTypes.AllocateParams[](1);
array[0] = allocateParams;
}
function toArray(IDelegationManagerTypes.Withdrawal memory withdrawal)
internal
pure
returns (IDelegationManagerTypes.Withdrawal[] memory array)
{
array = new IDelegationManagerTypes.Withdrawal[](1);
array[0] = withdrawal;
}
function toArray(IDelegationManagerTypes.QueuedWithdrawalParams memory queuedWithdrawalParams)
internal
pure
returns (IDelegationManagerTypes.QueuedWithdrawalParams[] memory array)
{
array = new IDelegationManagerTypes.QueuedWithdrawalParams[](1);
array[0] = queuedWithdrawalParams;
}
/// -----------------------------------------------------------------------
/// Length Updates
/// -----------------------------------------------------------------------
function setLength(uint16[] memory array, uint len) internal pure returns (uint16[] memory) {
assembly {
mstore(array, len)
}
return array;
}
function setLength(uint32[] memory array, uint len) internal pure returns (uint32[] memory) {
assembly {
mstore(array, len)
}
return array;
}
function setLength(uint64[] memory array, uint len) internal pure returns (uint64[] memory) {
assembly {
mstore(array, len)
}
return array;
}
function setLength(uint[] memory array, uint len) internal pure returns (uint[] memory) {
assembly {
mstore(array, len)
}
return array;
}
function setLength(address[] memory array, uint len) internal pure returns (address[] memory) {
assembly {
mstore(array, len)
}
return array;
}
function setLength(IERC20[] memory array, uint len) internal pure returns (IERC20[] memory) {
assembly {
mstore(array, len)
}
return array;
}
function setLength(IStrategy[] memory array, uint len) internal pure returns (IStrategy[] memory) {
assembly {
mstore(array, len)
}
return array;
}
function setLength(OperatorSet[] memory array, uint len) internal pure returns (OperatorSet[] memory) {
assembly {
mstore(array, len)
}
return array;
}
function setLength(IDelegationManagerTypes.Withdrawal[] memory array, uint len)
internal
pure
returns (IDelegationManagerTypes.Withdrawal[] memory)
{
assembly {
mstore(array, len)
}
return array;
}
/// -----------------------------------------------------------------------
/// Contains
/// -----------------------------------------------------------------------
function contains(uint16[] memory array, uint16 x) internal pure returns (bool) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return true;
}
return false;
}
function contains(uint32[] memory array, uint32 x) internal pure returns (bool) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return true;
}
return false;
}
function contains(uint64[] memory array, uint64 x) internal pure returns (bool) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return true;
}
return false;
}
function contains(uint[] memory array, uint x) internal pure returns (bool) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return true;
}
return false;
}
function contains(address[] memory array, address x) internal pure returns (bool) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return true;
}
return false;
}
function contains(IERC20[] memory array, IERC20 x) internal pure returns (bool) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return true;
}
return false;
}
function contains(IStrategy[] memory array, IStrategy x) internal pure returns (bool) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return true;
}
return false;
}
function contains(OperatorSet[] memory array, OperatorSet memory x) internal pure returns (bool) {
for (uint i; i < array.length; ++i) {
if (keccak256(abi.encode(array[i])) == keccak256(abi.encode(x))) return true;
}
return false;
}
function contains(IDelegationManagerTypes.Withdrawal[] memory array, IDelegationManagerTypes.Withdrawal memory x)
internal
pure
returns (bool)
{
for (uint i; i < array.length; ++i) {
if (keccak256(abi.encode(array[i])) == keccak256(abi.encode(x))) return true;
}
return false;
}
/// -----------------------------------------------------------------------
/// indexOf
/// -----------------------------------------------------------------------
function indexOf(uint16[] memory array, uint16 x) internal pure returns (uint) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return i;
}
return type(uint).max;
}
function indexOf(uint32[] memory array, uint32 x) internal pure returns (uint) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return i;
}
return type(uint).max;
}
function indexOf(uint64[] memory array, uint64 x) internal pure returns (uint) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return i;
}
return type(uint).max;
}
function indexOf(uint[] memory array, uint x) internal pure returns (uint) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return i;
}
return type(uint).max;
}
function indexOf(address[] memory array, address x) internal pure returns (uint) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return i;
}
return type(uint).max;
}
function indexOf(IERC20[] memory array, IERC20 x) internal pure returns (uint) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return i;
}
return type(uint).max;
}
function indexOf(IStrategy[] memory array, IStrategy x) internal pure returns (uint) {
for (uint i; i < array.length; ++i) {
if (array[i] == x) return i;
}
return type(uint).max;
}
function indexOf(OperatorSet[] memory array, OperatorSet memory x) internal pure returns (uint) {
for (uint i; i < array.length; ++i) {
if (keccak256(abi.encode(array[i])) == keccak256(abi.encode(x))) return i;
}
return type(uint).max;
}
function indexOf(IDelegationManagerTypes.Withdrawal[] memory array, IDelegationManagerTypes.Withdrawal memory x)
internal
pure
returns (uint)
{
for (uint i; i < array.length; ++i) {
if (keccak256(abi.encode(array[i])) == keccak256(abi.encode(x))) return i;
}
return type(uint).max;
}
/// -----------------------------------------------------------------------
/// Sorting
/// -----------------------------------------------------------------------
function sort(IStrategy[] memory array) internal pure returns (IStrategy[] memory) {
if (array.length <= 1) return array;
for (uint i = 1; i < array.length; i++) {
IStrategy key = array[i];
uint j = i - 1;
while (j > 0 && uint(uint160(address(array[j]))) > uint(uint160(address(key)))) {
array[j + 1] = array[j];
j--;
}
// Special case for the first element
if (j == 0 && uint(uint160(address(array[j]))) > uint(uint160(address(key)))) {
array[j + 1] = array[j];
array[j] = key;
} else if (j < i - 1) {
array[j + 1] = key;
}
}
return array;
}
}
````
## File: src/test/utils/EigenLayerUnitTestSetup.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol";
import "src/contracts/permissions/PauserRegistry.sol";
import "src/contracts/permissions/PermissionController.sol";
import "src/contracts/strategies/StrategyBase.sol";
import "src/test/mocks/AVSDirectoryMock.sol";
import "src/test/mocks/AllocationManagerMock.sol";
import "src/test/mocks/StrategyManagerMock.sol";
import "src/test/mocks/DelegationManagerMock.sol";
import "src/test/mocks/EigenPodManagerMock.sol";
import "src/test/mocks/EmptyContract.sol";
import "src/test/utils/ArrayLib.sol";
import "src/test/utils/Random.sol";
import "src/test/utils/ArrayLib.sol";
abstract contract EigenLayerUnitTestSetup is Test {
using ArrayLib for *;
uint internal constant MAX_PRIVATE_KEY = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140;
Vm cheats = Vm(VM_ADDRESS);
address constant pauser = address(555);
address constant unpauser = address(556);
PauserRegistry pauserRegistry;
ProxyAdmin eigenLayerProxyAdmin;
PermissionController permissionControllerImplementation;
PermissionController permissionController;
AVSDirectoryMock avsDirectoryMock;
AllocationManagerMock allocationManagerMock;
StrategyManagerMock strategyManagerMock;
DelegationManagerMock delegationManagerMock;
EigenPodManagerMock eigenPodManagerMock;
EmptyContract emptyContract;
mapping(address => bool) public isExcludedFuzzAddress;
modifier filterFuzzedAddressInputs(address addr) {
cheats.assume(!isExcludedFuzzAddress[addr]);
_;
}
modifier rand(Randomness r) {
r.set();
_;
}
function random() internal returns (Randomness) {
return Randomness.wrap(Random.SEED).shuffle();
}
function setUp() public virtual {
address[] memory pausers = new address[](2);
pausers[0] = pauser;
pausers[1] = address(this);
pauserRegistry = new PauserRegistry(pausers, unpauser);
eigenLayerProxyAdmin = new ProxyAdmin();
// Deploy permission controller
permissionControllerImplementation = new PermissionController("v9.9.9");
permissionController = PermissionController(
address(new TransparentUpgradeableProxy(address(permissionControllerImplementation), address(eigenLayerProxyAdmin), ""))
);
avsDirectoryMock = AVSDirectoryMock(payable(address(new AVSDirectoryMock())));
allocationManagerMock = AllocationManagerMock(payable(address(new AllocationManagerMock())));
strategyManagerMock =
StrategyManagerMock(payable(address(new StrategyManagerMock(IDelegationManager(address(delegationManagerMock))))));
delegationManagerMock = DelegationManagerMock(payable(address(new DelegationManagerMock())));
eigenPodManagerMock = EigenPodManagerMock(payable(address(new EigenPodManagerMock(pauserRegistry))));
emptyContract = new EmptyContract();
isExcludedFuzzAddress[address(0)] = true;
isExcludedFuzzAddress[address(pauserRegistry)] = true;
isExcludedFuzzAddress[address(permissionController)] = true;
isExcludedFuzzAddress[address(eigenLayerProxyAdmin)] = true;
isExcludedFuzzAddress[address(avsDirectoryMock)] = true;
isExcludedFuzzAddress[address(allocationManagerMock)] = true;
isExcludedFuzzAddress[address(strategyManagerMock)] = true;
isExcludedFuzzAddress[address(delegationManagerMock)] = true;
isExcludedFuzzAddress[address(eigenPodManagerMock)] = true;
}
}
````
## File: src/test/utils/EigenPodUser.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "src/contracts/pods/EigenPodManager.sol";
import "src/contracts/pods/EigenPod.sol";
import "src/contracts/interfaces/IStrategy.sol";
import "src/test/integration/TimeMachine.t.sol";
import "src/test/integration/mocks/BeaconChainMock.t.sol";
import "src/test/utils/Logger.t.sol";
struct Validator {
uint40 index;
}
interface IUserDeployer {
function timeMachine() external view returns (TimeMachine);
function beaconChain() external view returns (BeaconChainMock);
function eigenPodBeacon() external view returns (IBeacon);
}
contract EigenPodUser is Logger {
TimeMachine timeMachine;
BeaconChainMock beaconChain;
IBeacon public eigenPodBeacon;
string _NAME;
// User's EigenPod and each of their validator indices within that pod
EigenPod public pod;
uint40[] validators;
bytes internal constant beaconProxyBytecode =
hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564";
constructor(string memory name) {
IUserDeployer deployer = IUserDeployer(msg.sender);
timeMachine = deployer.timeMachine();
beaconChain = deployer.beaconChain();
eigenPodBeacon = deployer.eigenPodBeacon();
pod = EigenPod(
payable(
Create2.deploy(
0,
bytes32(uint(uint160(address(this)))),
// set the beacon address to the eigenPodBeacon
abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))
)
)
);
pod.initialize(address(this));
_NAME = name;
}
modifier createSnapshot() virtual {
timeMachine.createSnapshot();
_;
}
receive() external payable {}
function NAME() public view override returns (string memory) {
return _NAME;
}
/**
*
* BEACON CHAIN METHODS
*
*/
/// @dev Uses any ETH held by the User to start validators on the beacon chain
/// @return A list of created validator indices
/// @return The amount of wei sent to the beacon chain
/// @return The number of validators that have the MaxEB
/// Note: If the user does not have enough ETH to start a validator, this method reverts
/// Note: This method also advances one epoch forward on the beacon chain, so that
/// withdrawal credential proofs are generated for each validator.
function startValidators() public virtual createSnapshot returns (uint40[] memory, uint64, uint) {
print.method("startValidators");
return _startValidators();
}
function exitValidators(uint40[] memory _validators) public virtual createSnapshot returns (uint64 exitedBalanceGwei) {
print.method("exitValidators");
return _exitValidators(_validators);
}
/**
*
* EIGENPOD METHODS
*
*/
function verifyWithdrawalCredentials(uint40[] memory _validators) public virtual createSnapshot {
print.method("verifyWithdrawalCredentials");
_verifyWithdrawalCredentials(_validators);
}
function startCheckpoint() public virtual createSnapshot {
print.method("startCheckpoint");
_startCheckpoint();
}
function completeCheckpoint() public virtual createSnapshot {
print.method("completeCheckpoint");
_completeCheckpoint();
}
/**
*
* INTERNAL METHODS
*
*/
/// @dev Uses any ETH held by the User to start validators on the beacon chain
/// @dev Creates validators between 32 and 2048 ETH
/// @return A list of created validator indices
/// @return The amount of wei sent to the beacon chain
/// @return The number of validators that have the MaxEB
/// Note: If the user does not have enough ETH to start a validator, this method reverts
/// Note: This method also advances one epoch forward on the beacon chain, so that
/// withdrawal credential proofs are generated for each validator.
function _startValidators() internal virtual returns (uint40[] memory, uint64, uint) {
uint originalBalance = address(this).balance;
uint balanceWei = address(this).balance;
uint numValidators = 0;
uint maxEBValidators = 0;
// Get maximum possible number of validators. Add 1 to account for a validator with < 32 ETH
uint maxValidators = balanceWei / 32 ether;
uint40[] memory newValidators = new uint40[](maxValidators + 1);
// Create validators between 32 and 2048 ETH until we can't create more
while (balanceWei >= 32 ether) {
// Generate random validator balance between 32 and 2048 ETH
uint validatorEth = uint(keccak256(abi.encodePacked(block.timestamp, balanceWei, numValidators))) % 64 + 1; // 1-64 multiplier
validatorEth *= 32 ether; // Results in 32-2048 ETH
// If we don't have enough ETH for the random amount, use remaining balance
// as long as it's >= 32 ETH
if (balanceWei < validatorEth) {
if (balanceWei >= 32 ether) validatorEth = balanceWei - (balanceWei % 32 ether);
else break;
}
// Track validators with maximum effective balance
if (validatorEth == 2048 ether) maxEBValidators++;
// Create the validator
bytes memory withdrawalCredentials =
validatorEth == 32 ether ? _podWithdrawalCredentials() : _podCompoundingWithdrawalCredentials();
uint40 validatorIndex = beaconChain.newValidator{value: validatorEth}(withdrawalCredentials);
newValidators[numValidators] = validatorIndex;
validators.push(validatorIndex);
balanceWei -= validatorEth;
numValidators++;
}
// If we still have at least 1 ETH left over, we can create another (non-full) validator
// Note that in the mock beacon chain this validator will generate rewards like any other.
// The main point is to ensure pods are able to handle validators that have less than 32 ETH
if (balanceWei >= 1 ether) {
uint lastValidatorBalance = balanceWei - (balanceWei % 1 gwei);
uint40 validatorIndex = beaconChain.newValidator{value: lastValidatorBalance}(_podWithdrawalCredentials());
newValidators[numValidators] = validatorIndex;
validators.push(validatorIndex);
balanceWei -= lastValidatorBalance;
numValidators++;
}
// Resize the array to actual number of validators created
assembly {
mstore(newValidators, numValidators)
}
require(numValidators != 0, "startValidators: not enough ETH to start a validator");
uint64 totalBeaconBalanceGwei = uint64((originalBalance - balanceWei) / GWEI_TO_WEI);
console.log("- created new validators", newValidators.length);
console.log("- deposited balance to beacon chain (gwei)", totalBeaconBalanceGwei);
// Advance forward one epoch and generate withdrawal and balance proofs for each validator
beaconChain.advanceEpoch_NoRewards();
return (newValidators, totalBeaconBalanceGwei, maxEBValidators);
}
function _exitValidators(uint40[] memory _validators) internal returns (uint64 exitedBalanceGwei) {
console.log("- exiting num validators", _validators.length);
for (uint i = 0; i < _validators.length; i++) {
exitedBalanceGwei += beaconChain.exitValidator(_validators[i]);
}
console.log("- exited balance to pod (gwei)", exitedBalanceGwei);
return exitedBalanceGwei;
}
function _startCheckpoint() internal {
pod.startCheckpoint(false);
}
function _completeCheckpoint() internal {
console.log("- active validator count", pod.activeValidatorCount());
console.log("- proofs remaining", pod.currentCheckpoint().proofsRemaining);
uint64 checkpointTimestamp = pod.currentCheckpointTimestamp();
if (checkpointTimestamp == 0) revert("User._completeCheckpoint: no existing checkpoint");
CheckpointProofs memory proofs = beaconChain.getCheckpointProofs(validators, checkpointTimestamp);
console.log("- submitting num checkpoint proofs", proofs.balanceProofs.length);
pod.verifyCheckpointProofs({balanceContainerProof: proofs.balanceContainerProof, proofs: proofs.balanceProofs});
}
function _verifyWithdrawalCredentials(uint40[] memory _validators) internal {
CredentialProofs memory proofs = beaconChain.getCredentialProofs(_validators);
pod.verifyWithdrawalCredentials({
beaconTimestamp: proofs.beaconTimestamp,
stateRootProof: proofs.stateRootProof,
validatorIndices: _validators,
validatorFieldsProofs: proofs.validatorFieldsProofs,
validatorFields: proofs.validatorFields
});
}
function _podWithdrawalCredentials() internal view returns (bytes memory) {
return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(pod));
}
function _podCompoundingWithdrawalCredentials() internal view returns (bytes memory) {
return abi.encodePacked(bytes1(uint8(2)), bytes11(0), address(pod));
}
function getActiveValidators() public view returns (uint40[] memory) {
uint40[] memory activeValidators = new uint40[](validators.length);
uint numActive;
uint pos;
for (uint i = 0; i < validators.length; i++) {
if (beaconChain.isActive(validators[i])) {
activeValidators[pos] = validators[i];
numActive++;
pos++;
}
}
// Manually update length
assembly {
mstore(activeValidators, numActive)
}
return activeValidators;
}
}
````
## File: src/test/utils/Logger.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "src/contracts/interfaces/IStrategy.sol";
import {IAllocationManagerTypes} from "src/contracts/interfaces/IAllocationManager.sol";
Vm constant cheats = Vm(address(uint160(uint(keccak256("hevm cheat code")))));
IStrategy constant BEACONCHAIN_ETH_STRAT = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);
IERC20 constant NATIVE_ETH = IERC20(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);
uint constant MIN_BALANCE = 1e6;
uint constant MAX_BALANCE = 5e6;
uint constant GWEI_TO_WEI = 1e9;
uint constant FLAG = 1;
/// @dev Types representing the different types of assets a ranomized users can hold.
uint constant NO_ASSETS = (FLAG << 0); // will have no assets
uint constant HOLDS_LST = (FLAG << 1); // will hold some random amount of LSTs
uint constant HOLDS_ETH = (FLAG << 2); // will hold some random amount of ETH
uint constant HOLDS_ALL = (FLAG << 3); // will always hold ETH, and some LSTs
uint constant HOLDS_MAX = (FLAG << 4); // will hold every LST and ETH (used for testing max strategies)
/// @dev Types representing the different types of users that can be created.
uint constant DEFAULT = (FLAG << 0);
uint constant ALT_METHODS = (FLAG << 1);
/// @dev Types representing the different types of forks that can be simulated.
uint constant LOCAL = (FLAG << 0);
uint constant MAINNET = (FLAG << 1);
uint constant HOLESKY = (FLAG << 2);
abstract contract Logger is Test {
using StdStyle for *;
/// -----------------------------------------------------------------------
/// Storage
/// -----------------------------------------------------------------------
bool public logging = true;
/// -----------------------------------------------------------------------
/// Modifiers
/// -----------------------------------------------------------------------
// Address used to store a trace counter to allow us to use noTracing
// across any contract that inherits Logger
address constant LOG_STATE_ADDR = address(0xDEADBEEF);
bytes32 constant LOG_STATE_SLOT = bytes32(0);
modifier noTracing() {
uint traceCounter = _getTraceCounter();
if (traceCounter == 0) cheats.pauseTracing();
traceCounter++;
_setTraceCounter(traceCounter);
_;
traceCounter = _getTraceCounter();
traceCounter--;
_setTraceCounter(traceCounter);
if (traceCounter == 0) cheats.resumeTracing();
}
modifier noLogging() {
logging = false;
_;
logging = true;
}
/// -----------------------------------------------------------------------
/// Must Override
/// -----------------------------------------------------------------------
/// @dev Provide a name for the inheriting contract.
function NAME() public view virtual returns (string memory);
/// -----------------------------------------------------------------------
/// Colored Names
/// -----------------------------------------------------------------------
/// @dev Returns `NAME` colored based logging the inheriting contract's role.
function NAME_COLORED() public view returns (string memory) {
return colorByRole(NAME());
}
/// @dev Returns `name` colored based logging its role.
function colorByRole(string memory name) public pure returns (string memory colored) {
bool isOperator = _contains(name, "operator");
bool isStaker = _contains(name, "staker");
bool isAVS = _contains(name, "avs");
if (isOperator) colored = name.blue();
else if (isStaker) colored = name.cyan();
else if (isAVS) colored = name.magenta();
else colored = name.yellow();
}
/// @dev Returns `true` if `needle` is found in `haystack`.
function _contains(string memory haystack, string memory needle) internal pure returns (bool) {
return cheats.indexOf(haystack, needle) != type(uint).max;
}
/// -----------------------------------------------------------------------
/// Cheats
/// -----------------------------------------------------------------------
function rollForward(uint blocks) internal {
cheats.roll(block.number + blocks);
console.log("%s.roll(+ %d blocks)", colorByRole("cheats"), blocks);
}
function rollBackward(uint blocks) internal {
cheats.roll(block.number - blocks);
console.log("%s.roll(- %d blocks)", colorByRole("cheats"), blocks);
}
/// -----------------------------------------------------------------------
/// Logging
/// -----------------------------------------------------------------------
function _toggleLog() internal {
logging = !logging;
console.log("\n%s logging %s...", NAME_COLORED(), logging ? "enabled" : "disabled");
}
/// -----------------------------------------------------------------------
/// Trace Counter get/set
/// -----------------------------------------------------------------------
function _getTraceCounter() internal view returns (uint) {
return uint(cheats.load(LOG_STATE_ADDR, LOG_STATE_SLOT));
}
function _setTraceCounter(uint _newValue) internal {
cheats.store(LOG_STATE_ADDR, LOG_STATE_SLOT, bytes32(_newValue));
}
}
/// @dev Assumes the user is a `Logger`.
library print {
using print for *;
using StdStyle for *;
/// -----------------------------------------------------------------------
/// Logging
/// -----------------------------------------------------------------------
function method(string memory m) internal view {
if (!_logging()) return;
console.log("%s.%s()", _name(), m.italic());
}
function method(string memory m, string memory args) internal view {
if (!_logging()) return;
console.log("%s.%s(%s)", _name(), m.italic(), args);
}
function user(string memory name, uint assetType, uint userType, IStrategy[] memory strategies, uint[] memory tokenBalances)
internal
view
{
if (!_logging()) return;
console.log("\nCreated %s %s who holds %s.", userType.asUserType(), _logger().colorByRole(name), assetType.asAssetType());
console.log(" Balances:");
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
if (strat == BEACONCHAIN_ETH_STRAT) {
console.log(" Native ETH: %s", print.asWad(tokenBalances[i]));
} else {
IERC20 underlyingToken = strat.underlyingToken();
console.log(" %s: %s", IERC20Metadata(address(underlyingToken)).name(), print.asGwei(tokenBalances[i]));
}
}
}
function gasUsed() internal {
uint used = cheats.snapshotGasLastCall("gasUsed");
if (!_logging()) return;
console.log(" Gas used: %d".dim().bold(), used);
}
/// -----------------------------------------------------------------------
/// Logging
/// -----------------------------------------------------------------------
function createOperatorSets(IAllocationManagerTypes.CreateSetParams[] memory p) internal pure {
console.log("Creating operator sets:");
for (uint i; i < p.length; ++i) {
console.log(" operatorSet%d:".yellow(), p[i].operatorSetId);
for (uint j; j < p[i].strategies.length; ++j) {
console.log(" strategy%s: %s", cheats.toString(j), address(p[i].strategies[j]));
}
}
}
function deregisterFromOperatorSets(IAllocationManagerTypes.DeregisterParams memory p) internal pure {
console.log("Deregistering operator: %s", address(p.operator));
console.log(" from operator sets:");
for (uint i; i < p.operatorSetIds.length; ++i) {
console.log(" operatorSet%d:".yellow(), p.operatorSetIds[i]);
}
}
/// -----------------------------------------------------------------------
/// Strings
/// -----------------------------------------------------------------------
function asAssetType(uint t) internal pure returns (string memory s) {
if (t == HOLDS_ALL) s = "ALL_ASSETS";
else if (t == HOLDS_LST) s = "LST";
else if (t == HOLDS_ETH) s = "ETH";
else if (t == NO_ASSETS) s = "NO_ASSETS";
}
function asUserType(uint t) internal pure returns (string memory s) {
if (t == DEFAULT) s = "DEFAULT";
else if (t == ALT_METHODS) s = "ALT_METHODS";
}
function asForkType(uint t) internal pure returns (string memory s) {
if (t == LOCAL) s = "LOCAL";
else if (t == MAINNET) s = "MAINNET";
else if (t == HOLESKY) s = "HOLESKY";
}
function asGwei(uint x) internal pure returns (string memory) {
return x.asDecimal(9, " gwei");
}
function asWad(uint x) internal pure returns (string memory) {
return x.asDecimal(18, " wad");
}
function asDecimal(uint x, uint8 decimals, string memory denomination) internal pure returns (string memory s) {
if (x == 0) return string.concat("0.0", denomination);
s = cheats.toString(x);
while (bytes(s).length < decimals) s = string.concat("0", s);
uint len = bytes(s).length;
bytes memory b = bytes(s);
bytes memory left = new bytes(len > decimals ? len - decimals : 0);
bytes memory right = new bytes(decimals);
for (uint i; i < left.length; ++i) {
left[i] = b[i];
}
for (uint i; i < decimals; ++i) {
right[i] = b[len - decimals + i];
}
return string.concat(left.length > 0 ? string(left) : "0", ".", string(right), denomination);
}
/// -----------------------------------------------------------------------
/// Helpers
/// -----------------------------------------------------------------------
function _name() internal view returns (string memory) {
try _logger().NAME_COLORED() returns (string memory name) {
return name;
} catch {
revert("This contract is not a `Logger`.");
}
}
function _logger() internal view returns (Logger) {
return Logger(address(this));
}
function _logging() internal view returns (bool) {
return _logger().logging();
}
}
````
## File: src/test/utils/ProofParsing.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
import "forge-std/StdJson.sol";
contract ProofParsing is Test {
string internal proofConfigJson;
string prefix;
bytes32[18] blockHeaderProof;
bytes32[3] slotProof;
bytes32[10] withdrawalProofDeneb;
bytes32[9] withdrawalProofCapella;
bytes32[46] validatorProof;
bytes32[44] historicalSummaryProof;
bytes32[7] executionPayloadProof;
bytes32[5] timestampProofsCapella;
bytes32[4] timestampProofsDeneb;
bytes32 slotRoot;
bytes32 executionPayloadRoot;
function setJSON(string memory path) public {
proofConfigJson = vm.readFile(path);
}
function getSlot() public view returns (uint) {
return stdJson.readUint(proofConfigJson, ".slot");
}
function getValidatorIndex() public view returns (uint) {
return stdJson.readUint(proofConfigJson, ".validatorIndex");
}
function getValidatorPubkeyHash() public view returns (bytes32) {
return stdJson.readBytes32(proofConfigJson, ".ValidatorFields[0]");
}
function getWithdrawalIndex() public view returns (uint) {
return stdJson.readUint(proofConfigJson, ".withdrawalIndex");
}
function getBlockRootIndex() public view returns (uint) {
return stdJson.readUint(proofConfigJson, ".blockHeaderRootIndex");
}
function getHistoricalSummaryIndex() public view returns (uint) {
return stdJson.readUint(proofConfigJson, ".historicalSummaryIndex");
}
function getBeaconStateRoot() public view returns (bytes32) {
return stdJson.readBytes32(proofConfigJson, ".beaconStateRoot");
}
function getBlockRoot() public view returns (bytes32) {
return stdJson.readBytes32(proofConfigJson, ".blockHeaderRoot");
}
function getSlotRoot() public view returns (bytes32) {
return stdJson.readBytes32(proofConfigJson, ".slotRoot");
}
function getTimestampRoot() public view returns (bytes32) {
return stdJson.readBytes32(proofConfigJson, ".timestampRoot");
}
function getExecutionPayloadRoot() public view returns (bytes32) {
return stdJson.readBytes32(proofConfigJson, ".executionPayloadRoot");
}
function getLatestBlockRoot() public view returns (bytes32) {
return stdJson.readBytes32(proofConfigJson, ".latestBlockHeaderRoot");
}
function getExecutionPayloadProof() public returns (bytes32[7] memory) {
for (uint i = 0; i < 7; i++) {
prefix = string.concat(".ExecutionPayloadProof[", string.concat(vm.toString(i), "]"));
executionPayloadProof[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return executionPayloadProof;
}
function getTimestampProofDeneb() public returns (bytes32[5] memory) {
for (uint i = 0; i < 5; i++) {
prefix = string.concat(".TimestampProof[", string.concat(vm.toString(i), "]"));
timestampProofsCapella[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return timestampProofsCapella;
}
function getTimestampProofCapella() public returns (bytes32[4] memory) {
for (uint i = 0; i < 4; i++) {
prefix = string.concat(".TimestampProof[", string.concat(vm.toString(i), "]"));
timestampProofsDeneb[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return timestampProofsDeneb;
}
function getBlockHeaderProof() public returns (bytes32[18] memory) {
for (uint i = 0; i < 18; i++) {
prefix = string.concat(".BlockHeaderProof[", string.concat(vm.toString(i), "]"));
blockHeaderProof[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return blockHeaderProof;
}
function getSlotProof() public returns (bytes32[3] memory) {
for (uint i = 0; i < 3; i++) {
prefix = string.concat(".SlotProof[", string.concat(vm.toString(i), "]"));
slotProof[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return slotProof;
}
function getStateRootProof() public returns (bytes memory) {
bytes32[] memory stateRootProof = new bytes32[](3);
for (uint i = 0; i < 3; i++) {
prefix = string.concat(".StateRootAgainstLatestBlockHeaderProof[", string.concat(vm.toString(i), "]"));
stateRootProof[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return abi.encodePacked(stateRootProof);
}
function getWithdrawalProofDeneb() public returns (bytes32[10] memory) {
for (uint i = 0; i < 10; i++) {
prefix = string.concat(".WithdrawalProof[", string.concat(vm.toString(i), "]"));
withdrawalProofDeneb[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return withdrawalProofDeneb;
}
function getWithdrawalProofCapella() public returns (bytes32[9] memory) {
for (uint i = 0; i < 9; i++) {
prefix = string.concat(".WithdrawalProof[", string.concat(vm.toString(i), "]"));
withdrawalProofCapella[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return withdrawalProofCapella;
}
function getValidatorProof() public returns (bytes32[46] memory) {
for (uint i = 0; i < 46; i++) {
prefix = string.concat(".ValidatorProof[", string.concat(vm.toString(i), "]"));
validatorProof[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return validatorProof;
}
function getHistoricalSummaryProof() public returns (bytes32[44] memory) {
for (uint i = 0; i < 44; i++) {
prefix = string.concat(".HistoricalSummaryProof[", string.concat(vm.toString(i), "]"));
historicalSummaryProof[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return historicalSummaryProof;
}
function getWithdrawalFields() public returns (bytes32[] memory) {
bytes32[] memory withdrawalFields = new bytes32[](4);
for (uint i = 0; i < 4; i++) {
prefix = string.concat(".WithdrawalFields[", string.concat(vm.toString(i), "]"));
withdrawalFields[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return withdrawalFields;
}
function getValidatorFields() public returns (bytes32[] memory) {
bytes32[] memory validatorFields = new bytes32[](8);
for (uint i = 0; i < 8; i++) {
prefix = string.concat(".ValidatorFields[", string.concat(vm.toString(i), "]"));
validatorFields[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return validatorFields;
}
function getBalanceUpdateProof() public returns (bytes memory) {
// Balance update proofs are the same as withdrawal credential proofs
return getWithdrawalCredentialProof();
}
function getWithdrawalCredentialProof() public returns (bytes memory) {
bytes32[] memory withdrawalCredentialProof = new bytes32[](46);
for (uint i = 0; i < 46; i++) {
prefix = string.concat(".WithdrawalCredentialProof[", string.concat(vm.toString(i), "]"));
withdrawalCredentialProof[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return abi.encodePacked(withdrawalCredentialProof);
}
function getValidatorFieldsProof() public returns (bytes32[] memory) {
bytes32[] memory validatorFieldsProof = new bytes32[](46);
for (uint i = 0; i < 46; i++) {
prefix = string.concat(".ValidatorFieldsProof[", string.concat(vm.toString(i), "]"));
validatorFieldsProof[i] = (stdJson.readBytes32(proofConfigJson, prefix));
}
return validatorFieldsProof;
}
}
````
## File: src/test/utils/Random.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/contracts/interfaces/IAllocationManager.sol";
import "src/contracts/interfaces/IStrategy.sol";
import "src/contracts/libraries/OperatorSetLib.sol";
type Randomness is uint;
using Random for Randomness global;
library Random {
/// -----------------------------------------------------------------------
/// Constants
/// -----------------------------------------------------------------------
/// @dev Equivalent to: `uint256(keccak256("RANDOMNESS.SEED"))`.
uint constant SEED = 0x93bfe7cafd9427243dc4fe8c6e706851eb6696ba8e48960dd74ecc96544938ce;
/// @dev Equivalent to: `uint256(keccak256("RANDOMNESS.SLOT"))`.
uint constant SLOT = 0xd0660badbab446a974e6a19901c78a2ad88d7e4f1710b85e1cfc0878477344fd;
/// -----------------------------------------------------------------------
/// Helpers
/// -----------------------------------------------------------------------
function set(Randomness r) internal returns (Randomness) {
/// @solidity memory-safe-assembly
assembly {
sstore(SLOT, r)
}
return r;
}
function shuffle(Randomness r) internal returns (Randomness) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, sload(SLOT))
mstore(0x20, r)
r := keccak256(0x00, 0x20)
}
return r.set();
}
/// -----------------------------------------------------------------------
/// Native Types
/// -----------------------------------------------------------------------
function Int256(Randomness r, int min, int max) internal returns (int) {
return max <= min ? min : r.Int256() % (max - min) + min;
}
function Int256(Randomness r) internal returns (int) {
return r.unwrap() % 2 == 0 ? int(r.Uint256()) : -int(r.Uint256());
}
function Int128(Randomness r, int128 min, int128 max) internal returns (int128) {
return int128(Int256(r, min, max));
}
function Int128(Randomness r) internal returns (int128) {
return int128(Int256(r));
}
function Int64(Randomness r, int64 min, int64 max) internal returns (int64) {
return int64(Int256(r, min, max));
}
function Int64(Randomness r) internal returns (int64) {
return int64(Int256(r));
}
function Int32(Randomness r, int32 min, int32 max) internal returns (int32) {
return int32(Int256(r, min, max));
}
function Uint256(Randomness r, uint min, uint max) internal returns (uint) {
return max <= min ? min : r.Uint256() % (max - min) + min;
}
function Uint256(Randomness r) internal returns (uint) {
return r.shuffle().unwrap();
}
function Uint128(Randomness r, uint128 min, uint128 max) internal returns (uint128) {
return uint128(Uint256(r, min, max));
}
function Uint128(Randomness r) internal returns (uint128) {
return uint128(Uint256(r));
}
function Uint64(Randomness r, uint64 min, uint64 max) internal returns (uint64) {
return uint64(Uint256(r, min, max));
}
function Uint64(Randomness r) internal returns (uint64) {
return uint64(Uint256(r));
}
function Uint32(Randomness r, uint32 min, uint32 max) internal returns (uint32) {
return uint32(Uint256(r, min, max));
}
function Uint32(Randomness r) internal returns (uint32) {
return uint32(Uint256(r));
}
function Bytes32(Randomness r) internal returns (bytes32) {
return bytes32(r.Uint256());
}
function Address(Randomness r) internal returns (address) {
return address(uint160(r.Uint256(1, type(uint160).max)));
}
function Boolean(Randomness r) internal returns (bool) {
return r.Uint256() % 2 == 0;
}
/// -----------------------------------------------------------------------
/// Arrays
/// -----------------------------------------------------------------------
function Int256Array(Randomness r, uint len, int min, int max) internal returns (int[] memory arr) {
arr = new int[](len);
for (uint i; i < len; ++i) {
arr[i] = r.Int256(min, max);
}
}
function Int128Array(Randomness r, uint len, int128 min, int128 max) internal returns (int128[] memory arr) {
arr = new int128[](len);
for (uint i; i < len; ++i) {
arr[i] = r.Int128(min, max);
}
}
function Int64Array(Randomness r, uint len, int64 min, int64 max) internal returns (int64[] memory arr) {
arr = new int64[](len);
for (uint i; i < len; ++i) {
arr[i] = r.Int64(min, max);
}
}
function Int32Array(Randomness r, uint len, int32 min, int32 max) internal returns (int32[] memory arr) {
arr = new int32[](len);
for (uint i; i < len; ++i) {
arr[i] = r.Int32(min, max);
}
}
function Uint256Array(Randomness r, uint len, uint min, uint max) internal returns (uint[] memory arr) {
arr = new uint[](len);
for (uint i; i < len; ++i) {
arr[i] = uint(r.Uint256(min, max));
}
}
/// -----------------------------------------------------------------------
/// General Types
/// -----------------------------------------------------------------------
function StakerArray(Randomness r, uint len) internal returns (address[] memory stakers) {
stakers = new address[](len);
for (uint i; i < len; ++i) {
stakers[i] = r.Address();
}
}
function StrategyArray(Randomness r, uint len) internal returns (IStrategy[] memory strategies) {
strategies = new IStrategy[](len);
for (uint i; i < len; ++i) {
strategies[i] = IStrategy(r.Address());
}
}
function OperatorSetArray(Randomness r, address avs, uint len) internal returns (OperatorSet[] memory operatorSets) {
operatorSets = new OperatorSet[](len);
for (uint i; i < len; ++i) {
operatorSets[i] = OperatorSet(avs, r.Uint32());
}
}
/// -----------------------------------------------------------------------
/// `AllocationManager` Types
/// -----------------------------------------------------------------------
/// @dev Usage: `r.createSetParams(r, numOpSets, numStrats)`.
function CreateSetParams(Randomness r, uint numOpSets, uint numStrats)
internal
returns (IAllocationManagerTypes.CreateSetParams[] memory params)
{
params = new IAllocationManagerTypes.CreateSetParams[](numOpSets);
for (uint i; i < numOpSets; ++i) {
params[i].operatorSetId = r.Uint32(1, type(uint32).max);
params[i].strategies = r.StrategyArray(numStrats);
}
}
/// @dev Usage:
/// ```
/// AllocateParams[] memory allocateParams = r.allocateParams(avs, numAllocations, numStrats);
/// cheats.prank(avs);
/// allocationManager.createOperatorSets(r.createSetParams(allocateParams));
/// ```
function CreateSetParams(Randomness, IAllocationManagerTypes.AllocateParams[] memory allocateParams)
internal
pure
returns (IAllocationManagerTypes.CreateSetParams[] memory params)
{
params = new IAllocationManagerTypes.CreateSetParams[](allocateParams.length);
for (uint i; i < allocateParams.length; ++i) {
params[i] = IAllocationManagerTypes.CreateSetParams(allocateParams[i].operatorSet.id, allocateParams[i].strategies);
}
}
/// @dev Usage:
/// ```
/// AllocateParams[] memory allocateParams = r.allocateParams(avs, numAllocations, numStrats);
/// CreateSetParams[] memory createSetParams = r.createSetParams(allocateParams);
///
/// cheats.prank(avs);
/// allocationManager.createOperatorSets(createSetParams);
///
/// cheats.prank(operator);
/// allocationManager.modifyAllocations(allocateParams);
/// ```
function AllocateParams(Randomness r, address avs, uint numAllocations, uint numStrats)
internal
returns (IAllocationManagerTypes.AllocateParams[] memory allocateParams)
{
allocateParams = new IAllocationManagerTypes.AllocateParams[](numAllocations);
// TODO: Randomize magnitudes such that they sum to 1e18 (100%).
uint64 magnitudePerSet = uint64(WAD / numStrats);
for (uint i; i < numAllocations; ++i) {
allocateParams[i].operatorSet = OperatorSet(avs, r.Uint32());
allocateParams[i].strategies = r.StrategyArray(numStrats);
allocateParams[i].newMagnitudes = new uint64[](numStrats);
for (uint j; j < numStrats; ++j) {
allocateParams[i].newMagnitudes[j] = magnitudePerSet;
}
}
}
/// @dev Usage:
/// ```
/// AllocateParams[] memory allocateParams = r.allocateParams(avs, numAllocations, numStrats);
/// AllocateParams[] memory deallocateParams = r.deallocateParams(allocateParams);
/// CreateSetParams[] memory createSetParams = r.createSetParams(allocateParams);
///
/// cheats.prank(avs);
/// allocationManager.createOperatorSets(createSetParams);
///
/// cheats.prank(operator);
/// allocationManager.modifyAllocations(allocateParams);
///
/// cheats.prank(operator)
/// allocationManager.modifyAllocations(deallocateParams);
/// ```
function DeallocateParams(Randomness r, IAllocationManagerTypes.AllocateParams[] memory allocateParams)
internal
returns (IAllocationManagerTypes.AllocateParams[] memory deallocateParams)
{
uint numDeallocations = allocateParams.length;
deallocateParams = new IAllocationManagerTypes.AllocateParams[](numDeallocations);
for (uint i; i < numDeallocations; ++i) {
deallocateParams[i].operatorSet = allocateParams[i].operatorSet;
deallocateParams[i].strategies = allocateParams[i].strategies;
deallocateParams[i].newMagnitudes = new uint64[](allocateParams[i].strategies.length);
for (uint j; j < allocateParams[i].strategies.length; ++j) {
deallocateParams[i].newMagnitudes[j] = r.Uint64(0, allocateParams[i].newMagnitudes[j] - 1);
}
}
}
/// @dev Usage:
/// ```
/// AllocateParams[] memory allocateParams = r.allocateParams(avs, numAllocations, numStrats);
/// CreateSetParams[] memory createSetParams = r.createSetParams(allocateParams);
/// RegisterParams memory registerParams = r.registerParams(allocateParams);
///
/// cheats.prank(avs);
/// allocationManager.createOperatorSets(createSetParams);
///
/// cheats.prank(operator);
/// allocationmanager.registerForOperatorSets(registerParams);
/// ```
function RegisterParams(Randomness r, IAllocationManagerTypes.AllocateParams[] memory allocateParams)
internal
returns (IAllocationManagerTypes.RegisterParams memory params)
{
params.avs = allocateParams[0].operatorSet.avs;
params.operatorSetIds = new uint32[](allocateParams.length);
for (uint i; i < allocateParams.length; ++i) {
params.operatorSetIds[i] = allocateParams[i].operatorSet.id;
}
params.data = abi.encode(r.Bytes32());
}
function SlashingParams(Randomness r, address operator, IAllocationManagerTypes.AllocateParams memory allocateParams)
internal
returns (IAllocationManagerTypes.SlashingParams memory params)
{
IStrategy[] memory strategies = allocateParams.strategies;
params.operator = operator;
params.operatorSetId = allocateParams.operatorSet.id;
// Randomly select a subset of strategies to slash.
uint len = r.Uint256({min: 1, max: strategies.length});
// Update length of strategies array.
assembly {
mstore(strategies, len)
}
params.strategies = strategies;
params.wadsToSlash = new uint[](len);
// Randomly select a `wadToSlash` for each strategy.
for (uint i; i < len; ++i) {
params.wadsToSlash[i] = r.Uint256({min: 0.001 ether, max: 1 ether});
}
}
/// -----------------------------------------------------------------------
/// Helpers
/// -----------------------------------------------------------------------
function wrap(uint r) internal pure returns (Randomness) {
return Randomness.wrap(r);
}
function unwrap(Randomness r) internal pure returns (uint) {
return Randomness.unwrap(r);
}
}
````
## File: src/test/DevnetLifecycle.t.sol
````
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
// Contracts
import "../../src/contracts/core/DelegationManager.sol";
import "../../src/contracts/core/StrategyManager.sol";
import "../../src/contracts/core/AVSDirectory.sol";
import "../../src/contracts/core/AllocationManager.sol";
import "../../src/contracts/strategies/StrategyBase.sol";
import "src/test/utils/ArrayLib.sol";
// Test
import "forge-std/Test.sol";
/// @notice Tests deployed contracts as part of the public devnet
/// Run with: forge test --mc Devnet_Lifecycle_Test --rpc-url $RPC_HOLESKY
contract Devnet_Lifecycle_Test is Test, IAllocationManagerTypes {
using ArrayLib for *;
// Contracts
DelegationManager public delegationManager;
StrategyManager public strategyManager;
AVSDirectory public avsDirectory;
AllocationManager public allocationManager;
StrategyBase public wethStrategy;
IERC20 public weth;
Vm cheats = Vm(VM_ADDRESS);
// Addresses
address public staker = address(0x1);
address public operator;
uint operatorPk = 420;
address public avs = address(0x3);
uint32 public operatorSetId = 1;
uint public wethAmount = 100 ether;
uint public wethShares = 100 ether;
OperatorSet public operatorSet;
// Values
uint64 public magnitudeToSet = 1e18;
function setUp() public {
// Set contracts
delegationManager = DelegationManager(0x3391eBafDD4b2e84Eeecf1711Ff9FC06EF9Ed182);
strategyManager = StrategyManager(0x70f8bC2Da145b434de66114ac539c9756eF64fb3);
avsDirectory = AVSDirectory(0xCa839541648D3e23137457b1Fd4A06bccEADD33a);
allocationManager = AllocationManager(0xAbD5Dd30CaEF8598d4EadFE7D45Fd582EDEade15);
wethStrategy = StrategyBase(0x4f812633943022fA97cb0881683aAf9f318D5Caa);
weth = IERC20(0x94373a4919B3240D86eA41593D5eBa789FEF3848);
// Set operator
operator = cheats.addr(operatorPk);
operatorSet = OperatorSet({avs: avs, id: operatorSetId});
}
function _getOperatorSetArray() internal view returns (uint32[] memory) {
return operatorSetId.toArrayU32();
}
function _getOperatorSetsArray() internal view returns (OperatorSet[] memory) {
return OperatorSet({avs: avs, id: operatorSetId}).toArray();
}
function test() public {
if (block.chainid == 17_000) {
// Seed staker with WETH
StdCheats.deal(address(weth), address(staker), wethAmount);
_run_lifecycle();
}
}
function _run_lifecycle() internal {
// Staker <> Operator Relationship
_depositIntoStrategy();
_registerOperator();
_delegateToOperator();
// Operator <> AVS Relationship
_registerAVS();
_registerOperatorToAVS();
_setMagnitude();
// Slash operator
_slashOperator();
// Withdraw staker
_withdrawStaker();
}
function _depositIntoStrategy() internal {
// Approve WETH
cheats.startPrank(staker);
weth.approve(address(strategyManager), wethAmount);
// Deposit WETH into strategy
strategyManager.depositIntoStrategy(wethStrategy, weth, wethAmount);
cheats.stopPrank();
// Check staker balance
assertEq(weth.balanceOf(staker), 0);
// Check staker shares
assertEq(strategyManager.stakerDepositShares(staker, wethStrategy), wethAmount);
}
function _registerOperator() internal {
// Register operator
string memory emptyStringForMetadataURI;
cheats.prank(operator);
delegationManager.registerAsOperator(address(0), 1, emptyStringForMetadataURI);
// Warp passed configuration delay
cheats.roll(block.number + delegationManager.minWithdrawalDelayBlocks());
// Validate storage
assertTrue(delegationManager.isOperator(operator));
}
function _delegateToOperator() internal {
// Delegate to operator
ISignatureUtilsMixinTypes.SignatureWithExpiry memory signatureWithExpiry;
cheats.prank(staker);
delegationManager.delegateTo(operator, signatureWithExpiry, bytes32(0));
// Validate storage
assertTrue(delegationManager.isDelegated(staker));
assertEq(delegationManager.delegatedTo(staker), operator);
// Validate operator shares
assertEq(delegationManager.operatorShares(operator, wethStrategy), wethShares);
}
function _registerAVS() internal {
cheats.startPrank(avs);
CreateSetParams memory createSetParams = CreateSetParams({operatorSetId: operatorSetId, strategies: wethStrategy.toArray()});
allocationManager.createOperatorSets(avs, createSetParams.toArray());
cheats.stopPrank();
}
function _registerOperatorToAVS() public {
cheats.prank(operator);
allocationManager.registerForOperatorSets(operator, RegisterParams(avs, operatorSetId.toArrayU32(), ""));
assertEq(allocationManager.getMembers(OperatorSet(avs, operatorSetId))[0], operator);
}
function _setMagnitude() public {
AllocateParams[] memory allocations = new AllocateParams[](1);
allocations[0] =
AllocateParams({operatorSet: operatorSet, strategies: wethStrategy.toArray(), newMagnitudes: magnitudeToSet.toArrayU64()});
cheats.prank(operator);
allocationManager.modifyAllocations(operator, allocations);
// Assert storage
Allocation memory info = allocationManager.getAllocation(operator, operatorSet, wethStrategy);
assertEq(info.currentMagnitude, 0);
assertEq(info.pendingDiff, int128(uint128(magnitudeToSet)));
assertEq(info.effectBlock, block.number + 1);
// Warp to effect block
cheats.roll(block.number + 1);
// Check allocation
info = allocationManager.getAllocation(operator, operatorSet, wethStrategy);
assertEq(info.currentMagnitude, magnitudeToSet);
}
function _slashOperator() public {
// Get slashing params
SlashingParams memory slashingParams = SlashingParams({
operator: operator,
operatorSetId: 1,
strategies: wethStrategy.toArray(),
wadsToSlash: 5e17.toArrayU256(),
description: "test"
});
// Slash operator
cheats.prank(avs);
allocationManager.slashOperator(avs, slashingParams);
// Assert storage
Allocation memory info = allocationManager.getAllocation(operator, operatorSet, wethStrategy);
assertEq(info.currentMagnitude, magnitudeToSet - 5e17);
}
function _withdrawStaker() public {
// Generate queued withdrawal params
IStrategy[] memory strategies = wethStrategy.toArray();
(uint[] memory withdrawableShares,) = delegationManager.getWithdrawableShares(staker, strategies);
IDelegationManagerTypes.QueuedWithdrawalParams[] memory queuedWithdrawals = new IDelegationManagerTypes.QueuedWithdrawalParams[](1);
queuedWithdrawals[0] = IDelegationManagerTypes.QueuedWithdrawalParams({
strategies: strategies,
depositShares: withdrawableShares,
__deprecated_withdrawer: address(0)
});
// Generate withdrawal params
IDelegationManagerTypes.Withdrawal memory withdrawal = IDelegationManagerTypes.Withdrawal({
staker: staker,
delegatedTo: operator,
withdrawer: staker,
nonce: delegationManager.cumulativeWithdrawalsQueued(staker),
startBlock: uint32(block.number),
strategies: strategies,
scaledShares: 100e18.toArrayU256()
});
// bytes32 withdrawalRoot = delegationManager.calculateWithdrawalRoot(withdrawal);
// Generate complete withdrawal params
cheats.startPrank(staker);
delegationManager.queueWithdrawals(queuedWithdrawals);
// Roll passed withdrawal delay
cheats.roll(block.number + delegationManager.minWithdrawalDelayBlocks());
// Complete withdrawal
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = weth;
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, true);
// Assert tokens
assertEq(weth.balanceOf(staker), wethAmount / 2);
}
}
````