diff --git a/.github/workflows/forge-test-intense.yml b/.github/workflows/forge-test-intense.yml new file mode 100644 index 00000000..61e3ca15 --- /dev/null +++ b/.github/workflows/forge-test-intense.yml @@ -0,0 +1,49 @@ +name: Forge Test (Intense) + +on: + workflow_dispatch: + push: + branches: + - mainnet + - testnet-holesky + - dev + +env: + FOUNDRY_PROFILE: ci + RPC_MAINNET: ${{ secrets.RPC_MAINNET }} + RPC_HOLESKY: ${{ secrets.RPC_HOLESKY }} + CHAIN_ID: ${{ secrets.CHAIN_ID }} + +jobs: + # ----------------------------------------------------------------------- + # Forge Test (Intense) + # ----------------------------------------------------------------------- + + forge-test-intense: + name: Test (Intense) + runs-on: ubuntu-latest + steps: + # Check out repository with all submodules for complete codebase access. + - uses: actions/checkout@v4 + with: + submodules: recursive + + # Install the Foundry toolchain. + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + + # Build the project and display contract sizes. + - name: Forge Build + run: | + forge --version + forge build --sizes + id: build + + # Run Forge Test (Intense) + - name: Forge Test (Intense) + run: | + echo -e "\033[1;33mWarning: This workflow may take several hours to complete.\033[0m" + echo -e "\033[1;33mThis intense fuzzing workflow is optional but helps catch edge cases through extended testing.\033[0m" + FOUNDRY_PROFILE=intense forge test -vvv \ No newline at end of file diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index af4cb7f0..52f86809 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -4,9 +4,8 @@ on: workflow_dispatch: push: branches: - - master - mainnet - - testnet-goerli + - testnet-holesky - dev pull_request: @@ -24,8 +23,6 @@ jobs: test: name: Test runs-on: ubuntu-latest - strategy: - fail-fast: true steps: # Check out repository with all submodules for complete codebase access. - uses: actions/checkout@v4 @@ -33,67 +30,32 @@ jobs: submodules: recursive # Install the Foundry toolchain. - - name: "Install Foundry" + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: version: stable # Run Forge's formatting checker to ensure consistent code style. - - name: "Forge Fmt" + - name: Forge Fmt run: | forge fmt --check id: fmt # Build the project and display contract sizes. - - name: "Forge Build" + - name: Forge Build run: | forge --version forge build --sizes id: build # Run local tests (unit and integration). - - name: "Forge Test (Local)" + - name: Forge Test (Local) run: forge test -vvv # Run integration tests using a mainnet fork. - - name: "Forge Test Integration (Fork)" + - name: Forge Test Integration (Fork) run: FOUNDRY_PROFILE=forktest forge test --match-contract Integration -vvv - # ----------------------------------------------------------------------- - # Forge Test (Intense) - # ----------------------------------------------------------------------- - - continuous-fuzzing: - name: Test (Intense) - runs-on: ubuntu-latest - strategy: - fail-fast: true - steps: - # Check out repository with all submodules for complete codebase access. - - uses: actions/checkout@v4 - with: - submodules: recursive - - # Install the Foundry toolchain. - - name: "Install Foundry" - uses: foundry-rs/foundry-toolchain@v1 - with: - version: stable - - # Build the project and display contract sizes. - - name: "Forge Build" - run: | - forge --version - forge build --sizes - id: build - - # Run Forge Test (Intense) - - name: Forge Test (Intense) - run: | - echo -e "\033[1;33mWarning: This workflow may take several hours to complete.\033[0m" - echo -e "\033[1;33mThis intense fuzzing workflow is optional but helps catch edge cases through extended testing.\033[0m" - FOUNDRY_PROFILE=intense forge test -vvv - # ----------------------------------------------------------------------- # Forge Coverage # ----------------------------------------------------------------------- @@ -101,16 +63,6 @@ jobs: run-coverage: name: Coverage runs-on: ubuntu-latest - # Only run coverage checks on dev, testnet-holesky, and mainnet branches, or PRs targeting these branches - if: | - github.ref == 'refs/heads/dev' || - github.ref == 'refs/heads/testnet-holesky' || - github.ref == 'refs/heads/mainnet' || - github.base_ref == 'dev' || - github.base_ref == 'testnet-holesky' || - github.base_ref == 'mainnet' - strategy: - fail-fast: true steps: # Check out repository with all submodules for complete codebase access. - uses: actions/checkout@v4 @@ -118,7 +70,7 @@ jobs: submodules: recursive # Install the Foundry toolchain. - - name: "Install Foundry" + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: version: stable @@ -130,7 +82,7 @@ jobs: id: lcov # Build the project and display contract sizes. - - name: "Forge Build" + - name: Forge Build run: | forge --version forge build --sizes @@ -150,7 +102,7 @@ jobs: path: report/* # Check coverage threshold after uploading report - - name: Check Coverage Threshold + - name: Check Coverage Threshold for >=90% run: | LINES_PCT=$(lcov --summary lcov.info | grep "lines" | cut -d ':' -f 2 | cut -d '%' -f 1 | tr -d '[:space:]') FUNCTIONS_PCT=$(lcov --summary lcov.info | grep "functions" | cut -d ':' -f 2 | cut -d '%' -f 1 | tr -d '[:space:]') @@ -188,7 +140,7 @@ jobs: submodules: recursive # Install the Foundry toolchain. - - name: "Install Foundry" + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: version: stable diff --git a/foundry.toml b/foundry.toml index 8d51a3cb..b2e0400a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -16,10 +16,12 @@ # Defines paths for Solidity imports. remappings = [ - "@openzeppelin/=lib/openzeppelin-contracts-v4.9.0/", - "@openzeppelin-upgrades/=lib/openzeppelin-contracts-upgradeable-v4.9.0/", + "forge-std/=lib/forge-std/src/", "ds-test/=lib/ds-test/src/", - "forge-std/=lib/forge-std/src/" + "@openzeppelin/=lib/openzeppelin-contracts/", + "@openzeppelin-upgrades/=lib/openzeppelin-contracts-upgradeable/", + "eigenlayer-contracts/=lib/eigenlayer-contracts/", + ] # Specifies the exact version of Solidity to use, overriding auto-detection. solc_version = '0.8.27' diff --git a/lib/eigenlayer-contracts b/lib/eigenlayer-contracts index 5341ef83..851978db 160000 --- a/lib/eigenlayer-contracts +++ b/lib/eigenlayer-contracts @@ -1 +1 @@ -Subproject commit 5341ef83500476c62a4406ff00cdde7f5c2cc11f +Subproject commit 851978db8658072a07fe5ec669bf6be1aaad425f diff --git a/remappings.txt b/remappings.txt deleted file mode 100644 index 8c001fd0..00000000 --- a/remappings.txt +++ /dev/null @@ -1,5 +0,0 @@ -@openzeppelin-upgrades/=lib/openzeppelin-contracts-upgradeable/ -@openzeppelin/=lib/openzeppelin-contracts/ -ds-test/=lib/ds-test/src/ -eigenlayer-contracts/=lib/eigenlayer-contracts/ -forge-std/=lib/forge-std/src/ diff --git a/src/BLSApkRegistry.sol b/src/BLSApkRegistry.sol index 831210ea..2ec13071 100644 --- a/src/BLSApkRegistry.sol +++ b/src/BLSApkRegistry.sol @@ -189,9 +189,7 @@ contract BLSApkRegistry is BLSApkRegistryStorage { quorumApkUpdatesLength == 0 || blockNumber < apkHistory[quorumNumber][0].updateBlockNumber ) { - revert( - "BLSApkRegistry.getApkIndicesAtBlockNumber: blockNumber is before the first update" - ); + revert BlockNumberBeforeFirstUpdate(); } // Loop backward through apkHistory until we find an entry that preceeds `blockNumber` diff --git a/src/BLSSignatureChecker.sol b/src/BLSSignatureChecker.sol index 5743be64..3f5b2444 100644 --- a/src/BLSSignatureChecker.sol +++ b/src/BLSSignatureChecker.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.27; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + import {BitmapUtils} from "./libraries/BitmapUtils.sol"; import {BN254} from "./libraries/BN254.sol"; @@ -18,7 +20,10 @@ contract BLSSignatureChecker is BLSSignatureCheckerStorage { /// MODIFIERS modifier onlyCoordinatorOwner() { - require(msg.sender == registryCoordinator.owner(), OnlyRegistryCoordinatorOwner()); + require( + msg.sender == Ownable(address(registryCoordinator)).owner(), + OnlyRegistryCoordinatorOwner() + ); _; } diff --git a/src/EjectionManager.sol b/src/EjectionManager.sol index 2d695f27..234b6f1d 100644 --- a/src/EjectionManager.sol +++ b/src/EjectionManager.sol @@ -5,21 +5,19 @@ import {OwnableUpgradeable} from "@openzeppelin-upgrades/contracts/access/Ownabl import { EjectionManagerStorage, IEjectionManager, - IRegistryCoordinator, + ISlashingRegistryCoordinator, IStakeRegistry } from "./EjectionManagerStorage.sol"; -// TODO: double check order of inheritance since we separated storage from logic... - /** - * @title Used for automated ejection of operators from the RegistryCoordinator under a ratelimit + * @title Used for automated ejection of operators from the SlashingRegistryCoordinator under a ratelimit * @author Layr Labs, Inc. */ contract EjectionManager is OwnableUpgradeable, EjectionManagerStorage { constructor( - IRegistryCoordinator _registryCoordinator, + ISlashingRegistryCoordinator _slashingRegistryCoordinator, IStakeRegistry _stakeRegistry - ) EjectionManagerStorage(_registryCoordinator, _stakeRegistry) { + ) EjectionManagerStorage(_slashingRegistryCoordinator, _stakeRegistry) { _disableInitializers(); } @@ -67,8 +65,8 @@ contract EjectionManager is OwnableUpgradeable, EjectionManagerStorage { stakeForEjection += operatorStake; ++ejectedOperators; - registryCoordinator.ejectOperator( - registryCoordinator.getOperatorFromId(operatorIds[i][j]), + slashingRegistryCoordinator.ejectOperator( + slashingRegistryCoordinator.getOperatorFromId(operatorIds[i][j]), abi.encodePacked(quorumNumber) ); @@ -80,8 +78,8 @@ contract EjectionManager is OwnableUpgradeable, EjectionManagerStorage { stakeForEjection += operatorStake; ++ejectedOperators; - registryCoordinator.ejectOperator( - registryCoordinator.getOperatorFromId(operatorIds[i][j]), + slashingRegistryCoordinator.ejectOperator( + slashingRegistryCoordinator.getOperatorFromId(operatorIds[i][j]), abi.encodePacked(quorumNumber) ); diff --git a/src/EjectionManagerStorage.sol b/src/EjectionManagerStorage.sol index e8996cd6..03ba446e 100644 --- a/src/EjectionManagerStorage.sol +++ b/src/EjectionManagerStorage.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import {IRegistryCoordinator} from "./interfaces/IRegistryCoordinator.sol"; +import {ISlashingRegistryCoordinator} from "./interfaces/ISlashingRegistryCoordinator.sol"; import {IStakeRegistry} from "./interfaces/IStakeRegistry.sol"; import {IEjectionManager} from "./interfaces/IEjectionManager.sol"; @@ -12,7 +12,7 @@ abstract contract EjectionManagerStorage is IEjectionManager { uint8 internal constant MAX_QUORUM_COUNT = 192; /// @inheritdoc IEjectionManager - IRegistryCoordinator public immutable registryCoordinator; + ISlashingRegistryCoordinator public immutable slashingRegistryCoordinator; /// @inheritdoc IEjectionManager IStakeRegistry public immutable stakeRegistry; @@ -23,8 +23,11 @@ abstract contract EjectionManagerStorage is IEjectionManager { /// @inheritdoc IEjectionManager mapping(uint8 => QuorumEjectionParams) public quorumEjectionParams; - constructor(IRegistryCoordinator _registryCoordinator, IStakeRegistry _stakeRegistry) { - registryCoordinator = _registryCoordinator; + constructor( + ISlashingRegistryCoordinator _slashingRegistryCoordinator, + IStakeRegistry _stakeRegistry + ) { + slashingRegistryCoordinator = _slashingRegistryCoordinator; stakeRegistry = _stakeRegistry; } diff --git a/src/RegistryCoordinator.sol b/src/RegistryCoordinator.sol index 325722ad..831a36d0 100644 --- a/src/RegistryCoordinator.sol +++ b/src/RegistryCoordinator.sol @@ -2,8 +2,10 @@ pragma solidity ^0.8.27; import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol"; -import {IAllocationManager} from - "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import { + IAllocationManager, + OperatorSet +} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; import {IBLSApkRegistry, IBLSApkRegistryTypes} from "./interfaces/IBLSApkRegistry.sol"; import {IStakeRegistry} from "./interfaces/IStakeRegistry.sol"; import {IIndexRegistry} from "./interfaces/IIndexRegistry.sol"; @@ -15,21 +17,20 @@ import {BitmapUtils} from "./libraries/BitmapUtils.sol"; import {SlashingRegistryCoordinator} from "./SlashingRegistryCoordinator.sol"; import {ISlashingRegistryCoordinator} from "./interfaces/ISlashingRegistryCoordinator.sol"; import {OwnableUpgradeable} from "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import {RegistryCoordinatorStorage} from "./RegistryCoordinatorStorage.sol"; /** - * @title A `RegistryCoordinator` that has three registries: + * @title A `RegistryCoordinator` that has four registries: * 1) a `StakeRegistry` that keeps track of operators' stakes * 2) a `BLSApkRegistry` that keeps track of operators' BLS public keys and aggregate BLS public keys for each quorum * 3) an `IndexRegistry` that keeps track of an ordered list of operators for each quorum + * 4) a `SocketRegistry` that keeps track of operators' sockets (arbitrary strings) * * @author Layr Labs, Inc. */ -contract RegistryCoordinator is IRegistryCoordinator, SlashingRegistryCoordinator { +contract RegistryCoordinator is RegistryCoordinatorStorage { using BitmapUtils for *; - /// @notice the ServiceManager for this AVS, which forwards calls onto EigenLayer's core contracts - IServiceManager public immutable serviceManager; - constructor( IServiceManager _serviceManager, IStakeRegistry _stakeRegistry, @@ -39,7 +40,8 @@ contract RegistryCoordinator is IRegistryCoordinator, SlashingRegistryCoordinato IAllocationManager _allocationManager, IPauserRegistry _pauserRegistry ) - SlashingRegistryCoordinator( + RegistryCoordinatorStorage( + _serviceManager, _stakeRegistry, _blsApkRegistry, _indexRegistry, @@ -47,9 +49,7 @@ contract RegistryCoordinator is IRegistryCoordinator, SlashingRegistryCoordinato _allocationManager, _pauserRegistry ) - { - serviceManager = _serviceManager; - } + {} /** * @@ -64,44 +64,28 @@ contract RegistryCoordinator is IRegistryCoordinator, SlashingRegistryCoordinato IBLSApkRegistryTypes.PubkeyRegistrationParams memory params, SignatureWithSaltAndExpiry memory operatorSignature ) external onlyWhenNotPaused(PAUSED_REGISTER_OPERATOR) { - require(!m2QuorumsDisabled, M2QuorumsAlreadyDisabled()); - /** - * If the operator has NEVER registered a pubkey before, use `params` to register - * their pubkey in blsApkRegistry - * - * If the operator HAS registered a pubkey, `params` is ignored and the pubkey hash - * (operatorId) is fetched instead - */ - bytes32 operatorId = _getOrCreateOperatorId(msg.sender, params); - - // Register the operator in each of the registry contracts and update the operator's - // quorum bitmap and registration status - uint32[] memory numOperatorsPerQuorum = _registerOperator({ + require(!isM2QuorumRegistrationDisabled, M2QuorumRegistrationIsDisabled()); + require( + quorumNumbers.orderedBytesArrayToBitmap().isSubsetOf(m2QuorumBitmap()), + OnlyM2QuorumsAllowed() + ); + + // Check if the operator has registered before + bool operatorRegisteredBefore = + _operatorInfo[msg.sender].status == OperatorStatus.REGISTERED; + + // register the operator with the registry coordinator + _registerOperator({ operator: msg.sender, - operatorId: operatorId, + operatorId: _getOrCreateOperatorId(msg.sender, params), quorumNumbers: quorumNumbers, - socket: socket - }).numOperatorsPerQuorum; - - // For each quorum, validate that the new operator count does not exceed the maximum - // (If it does, an operator needs to be replaced -- see `registerOperatorWithChurn`) - for (uint256 i = 0; i < quorumNumbers.length; i++) { - uint8 quorumNumber = uint8(quorumNumbers[i]); - - require( - numOperatorsPerQuorum[i] <= _quorumParams[quorumNumber].maxOperatorCount, - MaxQuorumsReached() - ); - } - - // If the operator wasn't registered for any quorums, update their status - // and register them with this AVS in EigenLayer core (DelegationManager) - if (_operatorInfo[msg.sender].status != OperatorStatus.REGISTERED) { - _operatorInfo[msg.sender] = - OperatorInfo({operatorId: operatorId, status: OperatorStatus.REGISTERED}); + socket: socket, + checkMaxOperatorCount: true + }); + // If the operator has never registered before, register them with the AVSDirectory + if (!operatorRegisteredBefore) { serviceManager.registerOperatorToAVS(msg.sender, operatorSignature); - emit OperatorRegistered(msg.sender, operatorId); } } @@ -114,34 +98,29 @@ contract RegistryCoordinator is IRegistryCoordinator, SlashingRegistryCoordinato SignatureWithSaltAndExpiry memory churnApproverSignature, SignatureWithSaltAndExpiry memory operatorSignature ) external onlyWhenNotPaused(PAUSED_REGISTER_OPERATOR) { - require(!m2QuorumsDisabled, M2QuorumsAlreadyDisabled()); + require(!isM2QuorumRegistrationDisabled, M2QuorumRegistrationIsDisabled()); + require( + quorumNumbers.orderedBytesArrayToBitmap().isSubsetOf(m2QuorumBitmap()), + OnlyM2QuorumsAllowed() + ); - /** - * If the operator has NEVER registered a pubkey before, use `params` to register - * their pubkey in blsApkRegistry - * - * If the operator HAS registered a pubkey, `params` is ignored and the pubkey hash - * (operatorId) is fetched instead - */ - bytes32 operatorId = _getOrCreateOperatorId(msg.sender, params); + // Check if the operator has registered before + bool operatorRegisteredBefore = + _operatorInfo[msg.sender].status == OperatorStatus.REGISTERED; + // register the operator with the registry coordinator with churn _registerOperatorWithChurn({ operator: msg.sender, - operatorId: operatorId, + operatorId: _getOrCreateOperatorId(msg.sender, params), quorumNumbers: quorumNumbers, socket: socket, operatorKickParams: operatorKickParams, churnApproverSignature: churnApproverSignature }); - // If the operator wasn't registered for any quorums, update their status - // and register them with this AVS in EigenLayer core (DelegationManager) - if (_operatorInfo[msg.sender].status != OperatorStatus.REGISTERED) { - _operatorInfo[msg.sender] = - OperatorInfo({operatorId: operatorId, status: OperatorStatus.REGISTERED}); - + // If the operator has never registered before, register them with the AVSDirectory + if (!operatorRegisteredBefore) { serviceManager.registerOperatorToAVS(msg.sender, operatorSignature); - emit OperatorRegistered(msg.sender, operatorId); } } @@ -150,44 +129,95 @@ contract RegistryCoordinator is IRegistryCoordinator, SlashingRegistryCoordinato bytes memory quorumNumbers ) external override onlyWhenNotPaused(PAUSED_DEREGISTER_OPERATOR) { // Check that the quorum numbers are M2 quorums - for (uint256 i = 0; i < quorumNumbers.length; i++) { - require( - !operatorSetsEnabled || _isM2Quorum(uint8(quorumNumbers[i])), OperatorSetQuorum() - ); - } + require( + quorumNumbers.orderedBytesArrayToBitmap().isSubsetOf(m2QuorumBitmap()), + OnlyM2QuorumsAllowed() + ); + _deregisterOperator({operator: msg.sender, quorumNumbers: quorumNumbers}); } /// @inheritdoc IRegistryCoordinator - function enableOperatorSets() external onlyOwner { - require(!operatorSetsEnabled, OperatorSetsAlreadyEnabled()); - - // Set the bitmap for M2 quorums - M2quorumBitmap = _getQuorumBitmap(quorumCount); + function disableM2QuorumRegistration() external onlyOwner { + require(!isM2QuorumRegistrationDisabled, M2QuorumRegistrationIsDisabled()); - // Enable operator sets mode - operatorSetsEnabled = true; + isM2QuorumRegistrationDisabled = true; - emit OperatorSetsEnabled(); + emit M2QuorumRegistrationDisabled(); } - /// @inheritdoc IRegistryCoordinator - function disableM2QuorumRegistration() external onlyOwner { - require(operatorSetsEnabled, OperatorSetsNotEnabled()); + /** + * + * INTERNAL FUNCTIONS + * + */ + + /// @dev override the _kickOperator function to handle M2 quorum forced deregistration + function _kickOperator( + address operator, + bytes memory quorumNumbers + ) internal virtual override { + OperatorInfo storage operatorInfo = _operatorInfo[operator]; + uint192 quorumsToRemove = + uint192(BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers, quorumCount)); + if (operatorInfo.status == OperatorStatus.REGISTERED && !quorumsToRemove.isEmpty()) { + // Allocate memory once outside the loop + bytes memory singleQuorumNumber = new bytes(1); + // For each quorum number, check if it's an M2 quorum + for (uint256 i = 0; i < quorumNumbers.length; i++) { + singleQuorumNumber[0] = quorumNumbers[i]; - m2QuorumsDisabled = true; + if (_isM2Quorum(uint8(quorumNumbers[i]))) { + // For M2 quorums, use _deregisterOperator + _deregisterOperator({operator: operator, quorumNumbers: singleQuorumNumber}); + } else { + // For non-M2 quorums, use _forceDeregisterOperator + _forceDeregisterOperator(operator, singleQuorumNumber); + } + } + } + } + + /// @dev override the _forceDeregisterOperator function to handle M2 quorum deregistration + function _forceDeregisterOperator( + address operator, + bytes memory quorumNumbers + ) internal virtual override { + // filter out M2 quorums from the quorum numbers + uint256 operatorSetBitmap = + quorumNumbers.orderedBytesArrayToBitmap().minus(m2QuorumBitmap()); + if (!operatorSetBitmap.isEmpty()) { + // call the parent _forceDeregisterOperator function for operator sets quorums + super._forceDeregisterOperator(operator, operatorSetBitmap.bitmapToBytesArray()); + } + } - emit M2QuorumsDisabled(); + /// @dev Hook to prevent any new quorums from being created if operator sets are not enabled + function _beforeCreateQuorum( + uint8 + ) internal virtual override { + // If operator sets are not enabled, set the m2 quorum bitmap to the current m2 quorum bitmap + // and enable operator sets + if (!operatorSetsEnabled) { + _m2QuorumBitmap = m2QuorumBitmap(); + operatorSetsEnabled = true; + } } /// @dev Hook to allow for any post-deregister logic function _afterDeregisterOperator( address operator, - bytes32 operatorId, - bytes memory quorumNumbers, + bytes32, + bytes memory, uint192 newBitmap ) internal virtual override { - uint256 operatorM2QuorumBitmap = newBitmap.minus(M2quorumBitmap); + // Bitmap representing all quorums including M2 and OperatorSet quorums + uint256 totalQuorumBitmap = _getTotalQuorumBitmap(); + // Bitmap representing only OperatorSet quorums. Equal to 0 if operatorSets not enabled + uint256 operatorSetQuorumBitmap = totalQuorumBitmap.minus(m2QuorumBitmap()); + // Operators updated M2 quorum bitmap, clear all the bits of operatorSetQuorumBitmap which gives the + // operator's M2 quorum bitmap. + uint256 operatorM2QuorumBitmap = newBitmap.minus(operatorSetQuorumBitmap); // If the operator is no longer registered for any M2 quorums, update their status and deregister // them from the AVS via the EigenLayer core contracts if (operatorM2QuorumBitmap.isEmpty()) { @@ -195,11 +225,35 @@ contract RegistryCoordinator is IRegistryCoordinator, SlashingRegistryCoordinato } } - /// @dev Returns a bitmap with all bits set up to `quorumCount`. Used for bit-masking quorum numbers - /// and differentiating between operator sets and M2 quorums - function _getQuorumBitmap( - uint256 quorumCount - ) internal pure returns (uint256) { + /** + * @dev Helper function to update operator stakes and deregister operators with insufficient stake + * This function handles two cases: + * 1. Operators who no longer meet the minimum stake requirement for a quorum + * 2. Operators who have been force-deregistered from the AllocationManager but not from this contract + * (e.g. due to out of gas errors in the deregistration callback) + * @param operators The list of operators to check and update + * @param operatorIds The corresponding operator IDs + * @param quorumNumber The quorum number to check stakes for + */ + function _updateOperatorsStakes( + address[] memory operators, + bytes32[] memory operatorIds, + uint8 quorumNumber + ) internal virtual override { + bytes memory singleQuorumNumber = new bytes(1); + singleQuorumNumber[0] = bytes1(quorumNumber); + bool[] memory doesNotMeetStakeThreshold = + stakeRegistry.updateOperatorsStake(operators, operatorIds, quorumNumber); + + for (uint256 i = 0; i < operators.length; ++i) { + if (doesNotMeetStakeThreshold[i]) { + _kickOperator(operators[i], singleQuorumNumber); + } + } + } + + /// @notice Return bitmap representing all quorums(Legacy M2 and OperatorSet) quorums + function _getTotalQuorumBitmap() internal view returns (uint256) { // This creates a number where all bits up to quorumCount are set to 1 // For example: // quorumCount = 3 -> 0111 (7 in decimal) @@ -208,13 +262,35 @@ contract RegistryCoordinator is IRegistryCoordinator, SlashingRegistryCoordinato return (1 << quorumCount) - 1; } - /// @dev need to override function here since its defined in both these contracts - function owner() - public - view - override(SlashingRegistryCoordinator, ISlashingRegistryCoordinator) - returns (address) - { - return OwnableUpgradeable.owner(); + /// @notice Returns true if the quorum number is an M2 quorum + /// @dev We use bitwise and to check if the quorum number is an M2 quorum + function _isM2Quorum( + uint8 quorumNumber + ) internal view returns (bool) { + return m2QuorumBitmap().isSet(quorumNumber); + } + + /** + * + * VIEW FUNCTIONS + * + */ + + /// @dev Returns a bitmap with all bits set up to `quorumCount`. Used for bit-masking quorum numbers + /// and differentiating between operator sets and M2 quorums + function m2QuorumBitmap() public view returns (uint256) { + // If operator sets are enabled, return the current m2 quorum bitmap + if (operatorSetsEnabled) { + return _m2QuorumBitmap; + } + + return _getTotalQuorumBitmap(); + } + + /// @notice Returns true if the quorum number is an M2 quorum + function isM2Quorum( + uint8 quorumNumber + ) external view returns (bool) { + return _isM2Quorum(quorumNumber); } } diff --git a/src/RegistryCoordinatorStorage.sol b/src/RegistryCoordinatorStorage.sol new file mode 100644 index 00000000..8124cd95 --- /dev/null +++ b/src/RegistryCoordinatorStorage.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IBLSApkRegistry, IBLSApkRegistryTypes} from "./interfaces/IBLSApkRegistry.sol"; +import {IStakeRegistry} from "./interfaces/IStakeRegistry.sol"; +import {IIndexRegistry} from "./interfaces/IIndexRegistry.sol"; +import {IServiceManager} from "./interfaces/IServiceManager.sol"; +import {IRegistryCoordinator} from "./interfaces/IRegistryCoordinator.sol"; +import {ISocketRegistry} from "./interfaces/ISocketRegistry.sol"; + +import {SlashingRegistryCoordinator} from "./SlashingRegistryCoordinator.sol"; + +abstract contract RegistryCoordinatorStorage is + IRegistryCoordinator, + SlashingRegistryCoordinator +{ + /** + * + * CONSTANTS AND IMMUTABLES + * + */ + + /// @notice the ServiceManager for this AVS, which forwards calls onto EigenLayer's core contracts + IServiceManager public immutable serviceManager; + + /** + * + * STATE + * + */ + + /// @notice Whether this AVS allows operator sets for creation/registration + /// @dev If true, then operator sets may be created and operators may register to operator sets via the AllocationManager + bool public operatorSetsEnabled; + + /// @notice Whether this AVS allows M2 quorums for registration + /// @dev If true, operators may **not** register to M2 quorums. Deregistration is still allowed. + bool public isM2QuorumRegistrationDisabled; + + /// @notice The bitmap containing all M2 quorums. This is only used for existing AVS middlewares that have M2 quorums + /// and need to call `enableOperatorSets()` to enable operator sets mode. + uint256 internal _m2QuorumBitmap; + + constructor( + IServiceManager _serviceManager, + IStakeRegistry _stakeRegistry, + IBLSApkRegistry _blsApkRegistry, + IIndexRegistry _indexRegistry, + ISocketRegistry _socketRegistry, + IAllocationManager _allocationManager, + IPauserRegistry _pauserRegistry + ) + SlashingRegistryCoordinator( + _stakeRegistry, + _blsApkRegistry, + _indexRegistry, + _socketRegistry, + _allocationManager, + _pauserRegistry + ) + { + serviceManager = _serviceManager; + } + + uint256[48] private __GAP; +} diff --git a/src/SlashingRegistryCoordinator.sol b/src/SlashingRegistryCoordinator.sol index 101fad15..a1710853 100644 --- a/src/SlashingRegistryCoordinator.sol +++ b/src/SlashingRegistryCoordinator.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.27; import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol"; import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; import { IAllocationManager, OperatorSet, @@ -29,10 +30,11 @@ import {Pausable} from "eigenlayer-contracts/src/contracts/permissions/Pausable. import {SlashingRegistryCoordinatorStorage} from "./SlashingRegistryCoordinatorStorage.sol"; /** - * @title A `RegistryCoordinator` that has three registries: + * @title A `RegistryCoordinator` that has four registries: * 1) a `StakeRegistry` that keeps track of operators' stakes * 2) a `BLSApkRegistry` that keeps track of operators' BLS public keys and aggregate BLS public keys for each quorum * 3) an `IndexRegistry` that keeps track of an ordered list of operators for each quorum + * 4) a `SocketRegistry` that keeps track of operators' sockets (arbitrary strings) * * @author Layr Labs, Inc. */ @@ -97,36 +99,22 @@ contract SlashingRegistryCoordinator is address _churnApprover, address _ejector, uint256 _initialPausedStatus, - address _accountIdentifier + address _avs ) external initializer { _transferOwnership(_initialOwner); _setChurnApprover(_churnApprover); _setPausedStatus(_initialPausedStatus); _setEjector(_ejector); - _setAccountIdentifier(_accountIdentifier); + _setAVS(_avs); + // Add registry contracts to the registries array registries.push(address(stakeRegistry)); registries.push(address(blsApkRegistry)); registries.push(address(indexRegistry)); - - // Set the AVS to be OperatorSets compatible - operatorSetsEnabled = true; - - // Set the AVS to not accept M2 quorums - m2QuorumsDisabled = true; + registries.push(address(socketRegistry)); } - /** - * @notice Creates a quorum and initializes it in each registry contract - * @param operatorSetParams configures the quorum's max operator count and churn parameters - * @param minimumStake sets the minimum stake required for an operator to register or remain - * registered - * @param strategyParams a list of strategies and multipliers used by the StakeRegistry to - * calculate an operator's stake weight for the quorum - * @dev For m2 AVS this function has the same behavior as createQuorum before - * For migrated AVS that enable operator sets this will create a quorum that measures total delegated stake for operator set - * - */ + /// @inheritdoc ISlashingRegistryCoordinator function createTotalDelegatedStakeQuorum( OperatorSetParam memory operatorSetParams, uint96 minimumStake, @@ -141,13 +129,13 @@ contract SlashingRegistryCoordinator is ); } + /// @inheritdoc ISlashingRegistryCoordinator function createSlashableStakeQuorum( OperatorSetParam memory operatorSetParams, uint96 minimumStake, IStakeRegistryTypes.StrategyParams[] memory strategyParams, uint32 lookAheadPeriod ) external virtual onlyOwner { - require(operatorSetsEnabled, OperatorSetsNotEnabled()); _createQuorum( operatorSetParams, minimumStake, @@ -157,12 +145,14 @@ contract SlashingRegistryCoordinator is ); } + /// @inheritdoc IAVSRegistrar function registerOperator( address operator, + address avs, uint32[] memory operatorSetIds, bytes calldata data ) external override onlyAllocationManager onlyWhenNotPaused(PAUSED_REGISTER_OPERATOR) { - require(operatorSetsEnabled, OperatorSetsNotEnabled()); + require(supportsAVS(avs), InvalidAVS()); bytes memory quorumNumbers = _getQuorumNumbers(operatorSetIds); ( @@ -187,7 +177,8 @@ contract SlashingRegistryCoordinator is operator: operator, operatorId: operatorId, quorumNumbers: quorumNumbers, - socket: socket + socket: socket, + checkMaxOperatorCount: true }).numOperatorsPerQuorum; // For each quorum, validate that the new operator count does not exceed the maximum @@ -229,59 +220,40 @@ contract SlashingRegistryCoordinator is } else { revert InvalidRegistrationType(); } - - // If the operator wasn't registered for any quorums, update their status - // and register them with this AVS in EigenLayer core (DelegationManager) - if (_operatorInfo[operator].status != OperatorStatus.REGISTERED) { - _operatorInfo[operator] = OperatorInfo(operatorId, OperatorStatus.REGISTERED); - emit OperatorRegistered(operator, operatorId); - } } + /// @inheritdoc IAVSRegistrar function deregisterOperator( address operator, + address avs, uint32[] memory operatorSetIds ) external override onlyAllocationManager onlyWhenNotPaused(PAUSED_DEREGISTER_OPERATOR) { - require(operatorSetsEnabled, OperatorSetsNotEnabled()); + require(supportsAVS(avs), InvalidAVS()); bytes memory quorumNumbers = _getQuorumNumbers(operatorSetIds); - _deregisterOperator(operator, quorumNumbers); + _deregisterOperator({operator: operator, quorumNumbers: quorumNumbers}); } - /** - * @notice Updates the StakeRegistry's view of one or more operators' stakes. If any operator - * is found to be below the minimum stake for the quorum, they are deregistered. - * @dev stakes are queried from the Eigenlayer core DelegationManager contract - * @param operators a list of operator addresses to update - */ + /// @inheritdoc ISlashingRegistryCoordinator function updateOperators( address[] memory operators - ) external onlyWhenNotPaused(PAUSED_UPDATE_OPERATOR) { + ) external override onlyWhenNotPaused(PAUSED_UPDATE_OPERATOR) { for (uint256 i = 0; i < operators.length; i++) { - address operator = operators[i]; - OperatorInfo memory operatorInfo = _operatorInfo[operator]; - bytes32 operatorId = operatorInfo.operatorId; - - // Update the operator's stake for their active quorums - uint192 currentBitmap = _currentOperatorBitmap(operatorId); - bytes memory quorumsToUpdate = BitmapUtils.bitmapToBytesArray(currentBitmap); - _updateOperator(operator, operatorInfo, quorumsToUpdate); + // create single-element arrays for the operator and operatorId + address[] memory singleOperator = new address[](1); + singleOperator[0] = operators[i]; + bytes32[] memory singleOperatorId = new bytes32[](1); + singleOperatorId[0] = _operatorInfo[operators[i]].operatorId; + + uint192 currentBitmap = _currentOperatorBitmap(singleOperatorId[0]); + bytes memory quorumNumbers = currentBitmap.bitmapToBytesArray(); + for (uint256 j = 0; j < quorumNumbers.length; j++) { + // update the operator's stake for each quorum + _updateOperatorsStakes(singleOperator, singleOperatorId, uint8(quorumNumbers[j])); + } } } - /** - * @notice For each quorum in `quorumNumbers`, updates the StakeRegistry's view of ALL its registered operators' stakes. - * Each quorum's `quorumUpdateBlockNumber` is also updated, which tracks the most recent block number when ALL registered - * operators were updated. - * @dev stakes are queried from the Eigenlayer core DelegationManager contract - * @param operatorsPerQuorum for each quorum in `quorumNumbers`, this has a corresponding list of operators to update. - * @dev Each list of operator addresses MUST be sorted in ascending order - * @dev Each list of operator addresses MUST represent the entire list of registered operators for the corresponding quorum - * @param quorumNumbers is an ordered byte array containing the quorum numbers being updated - * @dev invariant: Each list of `operatorsPerQuorum` MUST be a sorted version of `IndexRegistry.getOperatorListAtBlockNumber` - * for the corresponding quorum. - * @dev note on race condition: if an operator registers/deregisters for any quorum in `quorumNumbers` after a txn to - * this method is broadcast (but before it is executed), the method will fail - */ + /// @inheritdoc ISlashingRegistryCoordinator function updateOperatorsForQuorum( address[][] memory operatorsPerQuorum, bytes calldata quorumNumbers @@ -304,6 +276,7 @@ contract SlashingRegistryCoordinator is QuorumOperatorCountMismatch() ); + bytes32[] memory operatorIds = new bytes32[](currQuorumOperators.length); address prevOperatorAddress = address(0); // For each operator: // - check that they are registered for this quorum @@ -312,11 +285,9 @@ contract SlashingRegistryCoordinator is for (uint256 j = 0; j < currQuorumOperators.length; ++j) { address operator = currQuorumOperators[j]; - OperatorInfo memory operatorInfo = _operatorInfo[operator]; - bytes32 operatorId = operatorInfo.operatorId; - + operatorIds[j] = _operatorInfo[operator].operatorId; { - uint192 currentBitmap = _currentOperatorBitmap(operatorId); + uint192 currentBitmap = _currentOperatorBitmap(operatorIds[j]); // Check that the operator is registered require( BitmapUtils.isSet(currentBitmap, quorumNumber), NotRegisteredForQuorum() @@ -325,21 +296,18 @@ contract SlashingRegistryCoordinator is require(operator > prevOperatorAddress, NotSorted()); } - // Update the operator - _updateOperator(operator, operatorInfo, quorumNumbers[i:i + 1]); prevOperatorAddress = operator; } + _updateOperatorsStakes(currQuorumOperators, operatorIds, quorumNumber); + // Update timestamp that all operators in quorum have been updated all at once quorumUpdateBlockNumber[quorumNumber] = block.number; emit QuorumBlockNumberUpdated(quorumNumber, block.number); } } - /** - * @notice Updates the socket of the msg.sender given they are a registered operator - * @param socket is the new socket of the operator - */ + /// @inheritdoc ISlashingRegistryCoordinator function updateSocket( string memory socket ) external { @@ -353,30 +321,13 @@ contract SlashingRegistryCoordinator is * */ - /** - * @notice Forcibly deregisters an operator from one or more quorums - * @param operator the operator to eject - * @param quorumNumbers the quorum numbers to eject the operator from - * @dev possible race condition if prior to being ejected for a set of quorums the operator self deregisters from a subset - */ - function ejectOperator(address operator, bytes memory quorumNumbers) external onlyEjector { + /// @inheritdoc ISlashingRegistryCoordinator + function ejectOperator( + address operator, + bytes memory quorumNumbers + ) public virtual onlyEjector { lastEjectionTimestamp[operator] = block.timestamp; - - OperatorInfo storage operatorInfo = _operatorInfo[operator]; - bytes32 operatorId = operatorInfo.operatorId; - uint192 quorumsToRemove = - uint192(BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers, quorumCount)); - uint192 currentBitmap = _currentOperatorBitmap(operatorId); - if ( - operatorInfo.status == OperatorStatus.REGISTERED && !quorumsToRemove.isEmpty() - && quorumsToRemove.isSubsetOf(currentBitmap) - ) { - _deregisterOperator({operator: operator, quorumNumbers: quorumNumbers}); - - if (operatorSetsEnabled) { - _forceDeregisterOperator(operator, quorumNumbers); - } - } + _kickOperator(operator, quorumNumbers); } /** @@ -385,13 +336,7 @@ contract SlashingRegistryCoordinator is * */ - /** - * @notice Updates an existing quorum's configuration with a new max operator count - * and operator churn parameters - * @param quorumNumber the quorum number to update - * @param operatorSetParams the new config - * @dev only callable by the owner - */ + /// @inheritdoc ISlashingRegistryCoordinator function setOperatorSetParams( uint8 quorumNumber, OperatorSetParam memory operatorSetParams @@ -411,34 +356,21 @@ contract SlashingRegistryCoordinator is _setChurnApprover(_churnApprover); } - /** - * @notice Sets the ejector, which can force-deregister operators from quorums - * @param _ejector the new ejector - * @dev only callable by the owner - */ + /// @inheritdoc ISlashingRegistryCoordinator function setEjector( address _ejector ) external onlyOwner { _setEjector(_ejector); } - /** - * @notice Sets the account identifier for this AVS (used for UAM integration in EigenLayer) - * @param _accountIdentifier the new account identifier - * @dev only callable by the owner - */ - function setAccountIdentifier( - address _accountIdentifier + /// @inheritdoc ISlashingRegistryCoordinator + function setAVS( + address _avs ) external onlyOwner { - _setAccountIdentifier(_accountIdentifier); + _setAVS(_avs); } - /** - * @notice Sets the ejection cooldown, which is the time an operator must wait in - * seconds afer ejection before registering for any quorum - * @param _ejectionCooldown the new ejection cooldown in seconds - * @dev only callable by the owner - */ + /// @inheritdoc ISlashingRegistryCoordinator function setEjectionCooldown( uint256 _ejectionCooldown ) external onlyOwner { @@ -451,6 +383,25 @@ contract SlashingRegistryCoordinator is * */ + /** + * @notice Internal function to handle operator ejection logic + * @param operator The operator to force deregister from the avs + * @param quorumNumbers The quorum numbers to eject the operator from + */ + function _kickOperator(address operator, bytes memory quorumNumbers) internal virtual { + OperatorInfo storage operatorInfo = _operatorInfo[operator]; + bytes32 operatorId = operatorInfo.operatorId; + uint192 quorumsToRemove = + uint192(BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers, quorumCount)); + uint192 currentBitmap = _currentOperatorBitmap(operatorId); + if ( + operatorInfo.status == OperatorStatus.REGISTERED && !quorumsToRemove.isEmpty() + && quorumsToRemove.isSubsetOf(currentBitmap) + ) { + _forceDeregisterOperator(operator, quorumNumbers); + } + } + /** * @notice Register the operator for one or more quorums. This method updates the * operator's quorum bitmap, socket, and status, then registers them with each registry. @@ -459,7 +410,8 @@ contract SlashingRegistryCoordinator is address operator, bytes32 operatorId, bytes memory quorumNumbers, - string memory socket + string memory socket, + bool checkMaxOperatorCount ) internal virtual returns (RegisterResults memory results) { /** * Get bitmap of quorums to register for and operator's current bitmap. Validate that: @@ -491,7 +443,14 @@ contract SlashingRegistryCoordinator is */ _updateOperatorBitmap({operatorId: operatorId, newBitmap: newBitmap}); - emit OperatorSocketUpdate(operatorId, socket); + _setOperatorSocket(operatorId, socket); + + // If the operator wasn't registered for any quorums, update their status + // and register them with this AVS in EigenLayer core (DelegationManager) + if (_operatorInfo[operator].status != OperatorStatus.REGISTERED) { + _operatorInfo[operator] = OperatorInfo(operatorId, OperatorStatus.REGISTERED); + emit OperatorRegistered(operator, operatorId); + } // Register the operator with the BLSApkRegistry, StakeRegistry, and IndexRegistry blsApkRegistry.registerOperator(operator, quorumNumbers); @@ -499,6 +458,16 @@ contract SlashingRegistryCoordinator is stakeRegistry.registerOperator(operator, operatorId, quorumNumbers); results.numOperatorsPerQuorum = indexRegistry.registerOperator(operatorId, quorumNumbers); + if (checkMaxOperatorCount) { + for (uint256 i = 0; i < quorumNumbers.length; i++) { + OperatorSetParam memory operatorSetParams = _quorumParams[uint8(quorumNumbers[i])]; + require( + results.numOperatorsPerQuorum[i] <= operatorSetParams.maxOperatorCount, + MaxQuorumsReached() + ); + } + } + // call hook to allow for any post-register logic _afterRegisterOperator(operator, operatorId, quorumNumbers, newBitmap); @@ -525,8 +494,13 @@ contract SlashingRegistryCoordinator is // Register the operator in each of the registry contracts and update the operator's // quorum bitmap and registration status - RegisterResults memory results = - _registerOperator(operator, operatorId, quorumNumbers, socket); + RegisterResults memory results = _registerOperator({ + operator: operator, + operatorId: operatorId, + quorumNumbers: quorumNumbers, + socket: socket, + checkMaxOperatorCount: false + }); // Check that each quorum's operator count is below the configured maximum. If the max // is exceeded, use `operatorKickParams` to deregister an existing operator to make space @@ -549,11 +523,7 @@ contract SlashingRegistryCoordinator is bytes memory singleQuorumNumber = new bytes(1); singleQuorumNumber[0] = quorumNumbers[i]; - _deregisterOperator(operatorKickParams[i].operator, singleQuorumNumber); - - if (operatorSetsEnabled) { - _forceDeregisterOperator(operatorKickParams[i].operator, singleQuorumNumber); - } + _kickOperator(operatorKickParams[i].operator, singleQuorumNumber); } } } @@ -562,6 +532,9 @@ contract SlashingRegistryCoordinator is * @dev Deregister the operator from one or more quorums * This method updates the operator's quorum bitmap and status, then deregisters * the operator with the BLSApkRegistry, IndexRegistry, and StakeRegistry + * @param operator the operator to deregister + * @param quorumNumbers the quorum numbers to deregister from + * the core EigenLayer contract AllocationManager */ function _deregisterOperator(address operator, bytes memory quorumNumbers) internal virtual { // Fetch the operator's info and ensure they are registered @@ -608,36 +581,50 @@ contract SlashingRegistryCoordinator is /** * @notice Helper function to handle operator set deregistration for OperatorSets quorums. This is used - * when an operator is force-deregistered from a set of quorums. For any of the quorums that are - * OperatorSets quorums, we need to deregister the operator in the AllocationManager. + * when an operator is force-deregistered from a set of quorums. + * Due to deregistration being possible in the AllocationManager but not in the AVS as a result of the + * try/catch in `AllocationManager.deregisterFromOperatorSets`, we need to first check that the operator + * is not already deregistered from the OperatorSet in the AllocationManager. * @param operator The operator to deregister * @param quorumNumbers The quorum numbers the operator is force-deregistered from */ - function _forceDeregisterOperator(address operator, bytes memory quorumNumbers) internal { - uint32[] memory operatorSetIds = new uint32[](quorumNumbers.length); - uint256 numOperatorSetQuorums; - - // Check each quorum's stake type - for (uint256 i = 0; i < quorumNumbers.length; i++) { - uint8 quorumNumber = uint8(quorumNumbers[i]); - if (_isM2Quorum(quorumNumber)) { - operatorSetIds[numOperatorSetQuorums++] = quorumNumber; - } - } + function _forceDeregisterOperator( + address operator, + bytes memory quorumNumbers + ) internal virtual { + allocationManager.deregisterFromOperatorSets( + IAllocationManagerTypes.DeregisterParams({ + operator: operator, + avs: avs, + operatorSetIds: _getOperatorSetIds(quorumNumbers) + }) + ); + } - // If any OperatorSet quorums found, deregister from AVS in the AllocationManager - if (numOperatorSetQuorums > 0) { - // Resize array to exact size needed - assembly { - mstore(operatorSetIds, numOperatorSetQuorums) + /** + * @dev Helper function to update operator stakes and deregister operators with insufficient stake + * This function handles two cases: + * 1. Operators who no longer meet the minimum stake requirement for a quorum + * 2. Operators who have been force-deregistered from the AllocationManager but not from this contract + * (e.g. due to out of gas errors in the deregistration callback) + * @param operators The list of operators to check and update + * @param operatorIds The corresponding operator IDs + * @param quorumNumber The quorum number to check stakes for + */ + function _updateOperatorsStakes( + address[] memory operators, + bytes32[] memory operatorIds, + uint8 quorumNumber + ) internal virtual { + bytes memory singleQuorumNumber = new bytes(1); + singleQuorumNumber[0] = bytes1(quorumNumber); + bool[] memory doesNotMeetStakeThreshold = + stakeRegistry.updateOperatorsStake(operators, operatorIds, quorumNumber); + for (uint256 j = 0; j < operators.length; ++j) { + // If the operator does not have the minimum stake, they need to be force deregistered. + if (doesNotMeetStakeThreshold[j]) { + _kickOperator(operators[j], singleQuorumNumber); } - allocationManager.deregisterFromOperatorSets( - IAllocationManagerTypes.DeregisterParams({ - operator: operator, - avs: accountIdentifier, - operatorSetIds: operatorSetIds - }) - ); } } @@ -728,32 +715,6 @@ contract SlashingRegistryCoordinator is ); } - /** - * @notice Updates the StakeRegistry's view of the operator's stake in one or more quorums. - * For any quorums where the StakeRegistry finds the operator is under the configured minimum - * stake, `quorumsToRemove` is returned and used to deregister the operator from those quorums - * @dev does nothing if operator is not registered for any quorums. - */ - function _updateOperator( - address operator, - OperatorInfo memory operatorInfo, - bytes memory quorumsToUpdate - ) internal { - if (operatorInfo.status != OperatorStatus.REGISTERED) { - return; - } - bytes32 operatorId = operatorInfo.operatorId; - uint192 quorumsToRemove = - stakeRegistry.updateOperatorStake(operator, operatorId, quorumsToUpdate); - - if (!quorumsToRemove.isEmpty()) { - _deregisterOperator({ - operator: operator, - quorumNumbers: BitmapUtils.bitmapToBytesArray(quorumsToRemove) - }); - } - } - /** * @notice Returns the stake threshold required for an incoming operator to replace an existing operator * The incoming operator must have more stake than the return value. @@ -830,36 +791,37 @@ contract SlashingRegistryCoordinator is IStakeRegistryTypes.StakeType stakeType, uint32 lookAheadPeriod ) internal { - // Increment the total quorum count. Fails if we're already at the max - uint8 prevQuorumCount = quorumCount; - require(prevQuorumCount < MAX_QUORUM_COUNT, MaxQuorumsReached()); - quorumCount = prevQuorumCount + 1; + // The previous quorum count is the new quorum's number, + // this is because quorum numbers begin from index 0. + uint8 quorumNumber = quorumCount; - // The previous count is the new quorum's number - uint8 quorumNumber = prevQuorumCount; + // Hook to allow for any pre-create quorum logic + _beforeCreateQuorum(quorumNumber); + + // Increment the total quorum count. Fails if we're already at the max + require(quorumNumber < MAX_QUORUM_COUNT, MaxQuorumsReached()); + quorumCount += 1; // Initialize the quorum here and in each registry _setOperatorSetParams(quorumNumber, operatorSetParams); - /// Update the AllocationManager if operatorSetQuorum - if (operatorSetsEnabled && !_isM2Quorum(quorumNumber)) { - // Create array of CreateSetParams for the new quorum - IAllocationManagerTypes.CreateSetParams[] memory createSetParams = - new IAllocationManagerTypes.CreateSetParams[](1); + // Create array of CreateSetParams for the new quorum + IAllocationManagerTypes.CreateSetParams[] memory createSetParams = + new IAllocationManagerTypes.CreateSetParams[](1); - // Extract strategies from strategyParams - IStrategy[] memory strategies = new IStrategy[](strategyParams.length); - for (uint256 i = 0; i < strategyParams.length; i++) { - strategies[i] = strategyParams[i].strategy; - } - - // Initialize CreateSetParams with quorumNumber as operatorSetId - createSetParams[0] = IAllocationManagerTypes.CreateSetParams({ - operatorSetId: quorumNumber, - strategies: strategies - }); - allocationManager.createOperatorSets({avs: accountIdentifier, params: createSetParams}); + // Extract strategies from strategyParams + IStrategy[] memory strategies = new IStrategy[](strategyParams.length); + for (uint256 i = 0; i < strategyParams.length; i++) { + strategies[i] = strategyParams[i].strategy; } + + // Initialize CreateSetParams with quorumNumber as operatorSetId + createSetParams[0] = IAllocationManagerTypes.CreateSetParams({ + operatorSetId: quorumNumber, + strategies: strategies + }); + allocationManager.createOperatorSets({avs: avs, params: createSetParams}); + // Initialize stake registry based on stake type if (stakeType == IStakeRegistryTypes.StakeType.TOTAL_DELEGATED) { stakeRegistry.initializeDelegatedStakeQuorum(quorumNumber, minimumStake, strategyParams); @@ -871,6 +833,9 @@ contract SlashingRegistryCoordinator is indexRegistry.initializeQuorum(quorumNumber); blsApkRegistry.initializeQuorum(quorumNumber); + + // Hook to allow for any post-create quorum logic + _afterCreateQuorum(quorumNumber); } /** @@ -915,12 +880,14 @@ contract SlashingRegistryCoordinator is return quorumNumbers; } - /// @notice Returns true if the quorum number is an M2 quorum - /// @dev We use bitwise and to check if the quorum number is an M2 quorum - function _isM2Quorum( - uint8 quorumNumber - ) internal view returns (bool) { - return M2quorumBitmap.isSet(quorumNumber); + function _getOperatorSetIds( + bytes memory quorumNumbers + ) internal pure returns (uint32[] memory) { + uint32[] memory operatorSetIds = new uint32[](quorumNumbers.length); + for (uint256 i = 0; i < quorumNumbers.length; i++) { + operatorSetIds[i] = uint32(uint8(quorumNumbers[i])); + } + return operatorSetIds; } function _setOperatorSetParams( @@ -945,12 +912,22 @@ contract SlashingRegistryCoordinator is ejector = newEjector; } - function _setAccountIdentifier( - address _accountIdentifier + function _setAVS( + address _avs ) internal { - accountIdentifier = _accountIdentifier; + avs = _avs; } + /// @dev Hook to allow for any pre-create quorum logic + function _beforeCreateQuorum( + uint8 quorumNumber + ) internal virtual {} + + /// @dev Hook to allow for any post-create quorum logic + function _afterCreateQuorum( + uint8 quorumNumber + ) internal virtual {} + /// @dev Hook to allow for any pre-register logic in `_registerOperator` function _beforeRegisterOperator( address operator, @@ -1024,13 +1001,6 @@ contract SlashingRegistryCoordinator is return _operatorInfo[operator].status; } - /// @notice Returns true if the quorum number is an M2 quorum - function isM2Quorum( - uint8 quorumNumber - ) external view returns (bool) { - return _isM2Quorum(quorumNumber); - } - /** * @notice Returns the indices of the quorumBitmaps for the provided `operatorIds` at the given `blockNumber` * @dev Reverts if any of the `operatorIds` was not (yet) registered at `blockNumber` @@ -1129,14 +1099,19 @@ contract SlashingRegistryCoordinator is ); } - /// @dev need to override function here since its defined in both these contracts - function owner() - public - view - virtual - override(OwnableUpgradeable, ISlashingRegistryCoordinator) - returns (address) - { - return OwnableUpgradeable.owner(); + /** + * @notice Returns the message hash that an operator must sign to register their BLS public key. + * @param operator is the address of the operator registering their BLS public key + */ + function calculatePubkeyRegistrationMessageHash( + address operator + ) public view returns (bytes32) { + return _hashTypedDataV4(keccak256(abi.encode(PUBKEY_REGISTRATION_TYPEHASH, operator))); + } + + function supportsAVS( + address _avs + ) public view virtual returns (bool) { + return _avs == address(avs); } } diff --git a/src/SlashingRegistryCoordinatorStorage.sol b/src/SlashingRegistryCoordinatorStorage.sol index 14946dd9..2f455a6b 100644 --- a/src/SlashingRegistryCoordinatorStorage.sol +++ b/src/SlashingRegistryCoordinatorStorage.sol @@ -41,7 +41,7 @@ abstract contract SlashingRegistryCoordinatorStorage is ISlashingRegistryCoordin /// @notice The maximum number of quorums this contract supports uint8 internal constant MAX_QUORUM_COUNT = 192; - /// @notice + /// @notice the Socket Registry contract that will keep track of operators' sockets (arbitrary strings) ISocketRegistry public immutable socketRegistry; /// @notice the BLS Aggregate Pubkey Registry contract that will keep track of operators' aggregate BLS public keys per quorum IBLSApkRegistry public immutable blsApkRegistry; @@ -85,22 +85,10 @@ abstract contract SlashingRegistryCoordinatorStorage is ISlashingRegistryCoordin /// @notice the delay in seconds before an operator can reregister after being ejected uint256 public ejectionCooldown; - /// @notice Whether this AVS allows operator sets for registration - /// @dev If true, operators may register to operator sets via the AllocationManager - bool public operatorSetsEnabled; - - /// @notice Whether this AVS allows M2 quorums for registration - /// @dev If true, operators may **not** register to M2 quorums. Deregistration is still allowed. - bool public m2QuorumsDisabled; - - /// @notice The account identifier for this AVS (used for UAM integration in EigenLayer) + /// @notice The avs address for this AVS (used for UAM integration in EigenLayer) /// @dev NOTE: Updating this value will break existing OperatorSets and UAM integration. /// This value should only be set once. - address public accountIdentifier; - - /// @notice The bitmap containing all M2 quorums. This is only used for existing AVS middlewares that have M2 quorums - /// and need to call `enableOperatorSets()` to enable operator sets mode. - uint256 internal M2quorumBitmap; + address public avs; constructor( IStakeRegistry _stakeRegistry, @@ -118,5 +106,5 @@ abstract contract SlashingRegistryCoordinatorStorage is ISlashingRegistryCoordin // storage gap for upgradeability // slither-disable-next-line shadowing-state - uint256[37] private __GAP; + uint256[38] private __GAP; } diff --git a/src/SocketRegistry.sol b/src/SocketRegistry.sol index 691af84b..919ee5e5 100644 --- a/src/SocketRegistry.sol +++ b/src/SocketRegistry.sol @@ -1,46 +1,35 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.12; -import {IRegistryCoordinator} from "./interfaces/IRegistryCoordinator.sol"; +import {ISlashingRegistryCoordinator} from "./interfaces/ISlashingRegistryCoordinator.sol"; import {ISocketRegistry} from "./interfaces/ISocketRegistry.sol"; import {SocketRegistryStorage} from "./SocketRegistryStorage.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; /** - * @title A `Registry` that keeps track of operator sockets. + * @title A `Registry` that keeps track of operator sockets (arbitrary strings). * @author Layr Labs, Inc. */ -contract SocketRegistry is ISocketRegistry, SocketRegistryStorage { - /// @notice A modifier that only allows the RegistryCoordinator to call a function - modifier onlyRegistryCoordinator() { - require( - msg.sender == address(registryCoordinator), - "SocketRegistry.onlyRegistryCoordinator: caller is not the RegistryCoordinator" - ); - _; - } - - /// @notice A modifier that only allows the owner of the RegistryCoordinator to call a function - modifier onlyCoordinatorOwner() { - require( - msg.sender == IRegistryCoordinator(registryCoordinator).owner(), - "SocketRegistry.onlyCoordinatorOwner: caller is not the owner of the registryCoordinator" - ); +contract SocketRegistry is SocketRegistryStorage { + /// @notice A modifier that only allows the SlashingRegistryCoordinator to call a function + modifier onlySlashingRegistryCoordinator() { + require(msg.sender == slashingRegistryCoordinator, OnlySlashingRegistryCoordinator()); _; } constructor( - IRegistryCoordinator _registryCoordinator - ) SocketRegistryStorage(address(_registryCoordinator)) {} + ISlashingRegistryCoordinator _slashingRegistryCoordinator + ) SocketRegistryStorage(address(_slashingRegistryCoordinator)) {} - /// @notice sets the socket for an operator only callable by the RegistryCoordinator + /// @inheritdoc ISocketRegistry function setOperatorSocket( bytes32 _operatorId, string memory _socket - ) external onlyRegistryCoordinator { + ) external onlySlashingRegistryCoordinator { operatorIdToSocket[_operatorId] = _socket; } - /// @notice gets the stored socket for an operator + /// @inheritdoc ISocketRegistry function getOperatorSocket( bytes32 _operatorId ) external view returns (string memory) { diff --git a/src/SocketRegistryStorage.sol b/src/SocketRegistryStorage.sol index 4f700173..41d7d3b3 100644 --- a/src/SocketRegistryStorage.sol +++ b/src/SocketRegistryStorage.sol @@ -1,22 +1,36 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.12; +import {ISocketRegistry} from "./interfaces/ISocketRegistry.sol"; + /** - * @title Storage contract for SocketRegistry + * @title Storage variables for the `SocketRegistry` contract. * @author Layr Labs, Inc. */ -contract SocketRegistryStorage { - /// @notice The address of the RegistryCoordinator - address public immutable registryCoordinator; +abstract contract SocketRegistryStorage is ISocketRegistry { + /** + * + * CONSTANTS AND IMMUTABLES + * + */ + + /// @notice The address of the SlashingRegistryCoordinator + address public immutable slashingRegistryCoordinator; + + /** + * + * STATE + * + */ /// @notice A mapping from operator IDs to their sockets mapping(bytes32 => string) public operatorIdToSocket; constructor( - address _registryCoordinator + address _slashingRegistryCoordinator ) { - registryCoordinator = _registryCoordinator; + slashingRegistryCoordinator = _slashingRegistryCoordinator; } - uint256[48] private __GAP; + uint256[49] private __GAP; } diff --git a/src/StakeRegistry.sol b/src/StakeRegistry.sol index 4cf3a286..79c1540c 100644 --- a/src/StakeRegistry.sol +++ b/src/StakeRegistry.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.27; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; @@ -123,12 +125,12 @@ contract StakeRegistry is StakeRegistryStorage { } /// @inheritdoc IStakeRegistry - function updateOperatorStake( - address operator, - bytes32 operatorId, - bytes calldata quorumNumbers - ) external onlySlashingRegistryCoordinator returns (uint192) { - uint192 quorumsToRemove; + function updateOperatorsStake( + address[] memory operators, + bytes32[] memory operatorIds, + uint8 quorumNumber + ) external onlySlashingRegistryCoordinator returns (bool[] memory) { + bool[] memory shouldBeDeregistered = new bool[](operators.length); /** * For each quorum, update the operator's stake and record the delta @@ -138,34 +140,37 @@ contract StakeRegistry is StakeRegistryStorage { * in the quorum, the quorum number is added to `quorumsToRemove`, which * is returned to the registry coordinator. */ - for (uint256 i = 0; i < quorumNumbers.length; i++) { - uint8 quorumNumber = uint8(quorumNumbers[i]); - _checkQuorumExists(quorumNumber); + _checkQuorumExists(quorumNumber); - // Fetch the operator's current stake, applying weighting parameters and checking - // against the minimum stake requirements for the quorum. - (uint96 stakeWeight, bool hasMinimumStake) = - _weightOfOperatorForQuorum(quorumNumber, operator); - // If the operator no longer meets the minimum stake, set their stake to zero and mark them for removal - /// also handle setting the operator's stake to 0 and remove them from the quorum - if (!hasMinimumStake) { - stakeWeight = 0; - quorumsToRemove = uint192(quorumsToRemove.setBit(quorumNumber)); + // Fetch the operators' current stake, applying weighting parameters and checking + // against the minimum stake requirements for the quorum. + (uint96[] memory stakeWeights, bool[] memory hasMinimumStakes) = + _weightOfOperatorsForQuorum(quorumNumber, operators); + + int256 totalStakeDelta = 0; + // If the operator no longer meets the minimum stake, set their stake to zero and mark them for removal + /// also handle setting the operator's stake to 0 and remove them from the quorum + for (uint256 i = 0; i < operators.length; i++) { + if (!hasMinimumStakes[i]) { + stakeWeights[i] = 0; + shouldBeDeregistered[i] = true; } // Update the operator's stake and retrieve the delta // If we're deregistering them, their weight is set to 0 int256 stakeDelta = _recordOperatorStakeUpdate({ - operatorId: operatorId, + operatorId: operatorIds[i], quorumNumber: quorumNumber, - newStake: stakeWeight + newStake: stakeWeights[i] }); - // Apply the delta to the quorum's total stake - _recordTotalStakeUpdate(quorumNumber, stakeDelta); + totalStakeDelta += stakeDelta; } - return quorumsToRemove; + // Apply the delta to the quorum's total stake + _recordTotalStakeUpdate(quorumNumber, totalStakeDelta); + + return shouldBeDeregistered; } /// @inheritdoc IStakeRegistry @@ -235,13 +240,14 @@ contract StakeRegistry is StakeRegistryStorage { uint256 numStratsToAdd = _strategyParams.length; - if (isOperatorSetQuorum(quorumNumber)) { + address avs = registryCoordinator.avs(); + if (allocationManager.isOperatorSet(OperatorSet(avs, quorumNumber))) { IStrategy[] memory strategiesToAdd = new IStrategy[](numStratsToAdd); for (uint256 i = 0; i < numStratsToAdd; i++) { strategiesToAdd[i] = _strategyParams[i].strategy; } allocationManager.addStrategiesToOperatorSet({ - avs: ISlashingRegistryCoordinator(registryCoordinator).accountIdentifier(), + avs: avs, operatorSetId: quorumNumber, strategies: strategiesToAdd }); @@ -277,9 +283,10 @@ contract StakeRegistry is StakeRegistryStorage { _strategiesPerQuorum.pop(); } - if (isOperatorSetQuorum(quorumNumber)) { + address avs = registryCoordinator.avs(); + if (allocationManager.isOperatorSet(OperatorSet(avs, quorumNumber))) { allocationManager.removeStrategiesFromOperatorSet({ - avs: ISlashingRegistryCoordinator(registryCoordinator).accountIdentifier(), + avs: avs, operatorSetId: quorumNumber, strategies: _strategiesToRemove }); @@ -500,73 +507,82 @@ contract StakeRegistry is StakeRegistryStorage { ); } - /// Returns total Slashable stake for an operator per strategy that can have the weights applied based on strategy multipliers + /// Returns total Slashable stake for a list of operators per strategy that can have the weights applied based on strategy multipliers function _getSlashableStakePerStrategy( uint8 quorumNumber, - address operator - ) internal view returns (uint256[] memory) { - address[] memory operators = new address[](1); - operators[0] = operator; - uint32 beforeTimestamp = - uint32(block.number + slashableStakeLookAheadPerQuorum[quorumNumber]); + address[] memory operators + ) internal view returns (uint256[][] memory) { + uint32 beforeBlock = uint32(block.number + slashableStakeLookAheadPerQuorum[quorumNumber]); uint256[][] memory slashableShares = allocationManager.getMinimumSlashableStake( - OperatorSet( - ISlashingRegistryCoordinator(registryCoordinator).accountIdentifier(), quorumNumber - ), + OperatorSet(registryCoordinator.avs(), quorumNumber), operators, strategiesPerQuorum[quorumNumber], - beforeTimestamp + beforeBlock ); - return slashableShares[0]; + return slashableShares; } /** - * @notice This function computes the total weight of the @param operator in the quorum @param quorumNumber. + * @notice This function computes the total weight of the @param operators in the quorum @param quorumNumber. * @dev this method DOES NOT check that the quorum exists - * @return `uint96` The weighted sum of the operator's shares across each strategy considered by the quorum - * @return `bool` True if the operator meets the quorum's minimum stake + * @return `uint96[] memory` The weighted sum of the operators' shares across each strategy considered by the quorum + * @return `bool[] memory` True if the respective operator meets the quorum's minimum stake */ - function _weightOfOperatorForQuorum( + function _weightOfOperatorsForQuorum( uint8 quorumNumber, - address operator - ) internal view virtual returns (uint96, bool) { - uint96 weight; + address[] memory operators + ) internal view virtual returns (uint96[] memory, bool[] memory) { + uint96[] memory weights = new uint96[](operators.length); + bool[] memory hasMinimumStakes = new bool[](operators.length); + uint256 stratsLength = strategyParamsLength(quorumNumber); - StrategyParams memory strategyAndMultiplier; - uint256[] memory strategyShares; + StrategyParams[] memory stratsAndMultipliers = strategyParams[quorumNumber]; + uint256[][] memory strategyShares; if (stakeTypePerQuorum[quorumNumber] == IStakeRegistryTypes.StakeType.TOTAL_SLASHABLE) { - strategyShares = _getSlashableStakePerStrategy(quorumNumber, operator); - for (uint256 i = 0; i < stratsLength; i++) { - strategyAndMultiplier = strategyParams[quorumNumber][i]; - if (strategyShares[i] > 0) { - weight += uint96( - strategyShares[i] * strategyAndMultiplier.multiplier / WEIGHTING_DIVISOR - ); - } - } + // get slashable stake for the operators from AllocationManager + strategyShares = _getSlashableStakePerStrategy(quorumNumber, operators); } else { - /// M2 Concept of delegated stake + // get delegated stake for the operators from DelegationManager strategyShares = - delegation.getOperatorShares(operator, strategiesPerQuorum[quorumNumber]); - for (uint256 i = 0; i < stratsLength; i++) { - // accessing i^th StrategyParams struct for the quorumNumber - strategyAndMultiplier = strategyParams[quorumNumber][i]; - - // add the weight from the shares for this strategy to the total weight - if (strategyShares[i] > 0) { - weight += uint96( - strategyShares[i] * strategyAndMultiplier.multiplier / WEIGHTING_DIVISOR + delegation.getOperatorsShares(operators, strategiesPerQuorum[quorumNumber]); + } + + // Calculate weight of each operator and whether they contain minimum stake for the quorum + for (uint256 opIndex = 0; opIndex < operators.length; opIndex++) { + // 1. For the given operator, loop through the strategies and calculate the operator's + // weight for the quorum + for (uint256 stratIndex = 0; stratIndex < stratsLength; stratIndex++) { + // get multiplier for strategy + StrategyParams memory strategyAndMultiplier = stratsAndMultipliers[stratIndex]; + + // calculate added weight for strategy and multiplier + if (strategyShares[opIndex][stratIndex] > 0) { + weights[opIndex] += uint96( + strategyShares[opIndex][stratIndex] * strategyAndMultiplier.multiplier + / WEIGHTING_DIVISOR ); } } + + // 2. Check whether operator is above minimum stake threshold + hasMinimumStakes[opIndex] = weights[opIndex] >= minimumStakeForQuorum[quorumNumber]; } - // Return the weight, and `true` if the operator meets the quorum's minimum stake - bool hasMinimumStake = weight >= minimumStakeForQuorum[quorumNumber]; - return (weight, hasMinimumStake); + return (weights, hasMinimumStakes); + } + + function _weightOfOperatorForQuorum( + uint8 quorumNumber, + address operator + ) internal view virtual returns (uint96, bool) { + address[] memory operators = new address[](1); + operators[0] = operator; + (uint96[] memory weights, bool[] memory hasMinimumStakes) = + _weightOfOperatorsForQuorum(quorumNumber, operators); + return (weights[0], hasMinimumStakes[0]); } /// @notice Returns `true` if the quorum has been initialized @@ -582,15 +598,6 @@ contract StakeRegistry is StakeRegistryStorage { * */ - /// @inheritdoc IStakeRegistry - function isOperatorSetQuorum( - uint8 quorumNumber - ) public view returns (bool) { - bool isM2 = ISlashingRegistryCoordinator(registryCoordinator).isM2Quorum(quorumNumber); - bool isOperatorSet = ISlashingRegistryCoordinator(registryCoordinator).operatorSetsEnabled(); - return isOperatorSet && !isM2; - } - /// @inheritdoc IStakeRegistry function weightOfOperatorForQuorum( uint8 quorumNumber, @@ -785,18 +792,22 @@ contract StakeRegistry is StakeRegistryStorage { * @param _lookAheadBlocks The number of blocks to look ahead when checking shares */ function _setLookAheadPeriod(uint8 quorumNumber, uint32 _lookAheadBlocks) internal { + require( + stakeTypePerQuorum[quorumNumber] == IStakeRegistryTypes.StakeType.TOTAL_SLASHABLE, + QuorumNotSlashable() + ); uint32 oldLookAheadDays = slashableStakeLookAheadPerQuorum[quorumNumber]; slashableStakeLookAheadPerQuorum[quorumNumber] = _lookAheadBlocks; emit LookAheadPeriodChanged(oldLookAheadDays, _lookAheadBlocks); } function _checkSlashingRegistryCoordinator() internal view { - require(msg.sender == registryCoordinator, OnlySlashingRegistryCoordinator()); + require(msg.sender == address(registryCoordinator), OnlySlashingRegistryCoordinator()); } function _checkSlashingRegistryCoordinatorOwner() internal view { require( - msg.sender == ISlashingRegistryCoordinator(registryCoordinator).owner(), + msg.sender == Ownable(address(registryCoordinator)).owner(), OnlySlashingRegistryCoordinatorOwner() ); } diff --git a/src/StakeRegistryStorage.sol b/src/StakeRegistryStorage.sol index 2c00aec2..24700aee 100644 --- a/src/StakeRegistryStorage.sol +++ b/src/StakeRegistryStorage.sol @@ -37,7 +37,7 @@ abstract contract StakeRegistryStorage is IStakeRegistry { IAllocationManager public immutable allocationManager; /// @notice the coordinator contract that this registry is associated with - address public immutable registryCoordinator; + ISlashingRegistryCoordinator public immutable registryCoordinator; /// @notice In order to register for a quorum i, an operator must have at least `minimumStakeForQuorum[i]` /// evaluated by this contract's 'VoteWeigher' logic. @@ -70,7 +70,7 @@ abstract contract StakeRegistryStorage is IStakeRegistry { IAVSDirectory _avsDirectory, IAllocationManager _allocationManager ) { - registryCoordinator = address(_slashingRegistryCoordinator); + registryCoordinator = _slashingRegistryCoordinator; delegation = _delegationManager; avsDirectory = _avsDirectory; allocationManager = _allocationManager; diff --git a/src/interfaces/IBLSApkRegistry.sol b/src/interfaces/IBLSApkRegistry.sol index 05a5c229..5449098c 100644 --- a/src/interfaces/IBLSApkRegistry.sol +++ b/src/interfaces/IBLSApkRegistry.sol @@ -24,6 +24,8 @@ interface IBLSApkRegistryErrors { error BlockNumberTooRecent(); /// @notice Thrown when blocknumber and index provided is not the latest apk update. error BlockNumberNotLatest(); + /// @notice Thrown when the block number is before the first update. + error BlockNumberBeforeFirstUpdate(); } interface IBLSApkRegistryTypes { diff --git a/src/interfaces/IEjectionManager.sol b/src/interfaces/IEjectionManager.sol index 5ead07cb..56db0dd1 100644 --- a/src/interfaces/IEjectionManager.sol +++ b/src/interfaces/IEjectionManager.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import {IRegistryCoordinator} from "./IRegistryCoordinator.sol"; +import {ISlashingRegistryCoordinator} from "./ISlashingRegistryCoordinator.sol"; import {IStakeRegistry} from "./IStakeRegistry.sol"; interface IEjectionManagerErrors { @@ -66,11 +66,11 @@ interface IEjectionManager is IEjectionManagerErrors, IEjectionManagerEvents { /* STATE */ /* - * @notice Returns the address of the registry coordinator contract. - * @return The address of the registry coordinator. + * @notice Returns the address of the slashing registry coordinator contract. + * @return The address of the slashing registry coordinator. * @dev This value is immutable and set during contract construction. */ - function registryCoordinator() external view returns (IRegistryCoordinator); + function slashingRegistryCoordinator() external view returns (ISlashingRegistryCoordinator); /* * @notice Returns the address of the stake registry contract. diff --git a/src/interfaces/IInstantSlasher.sol b/src/interfaces/IInstantSlasher.sol new file mode 100644 index 00000000..f1326005 --- /dev/null +++ b/src/interfaces/IInstantSlasher.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {ISlasher} from "./ISlasher.sol"; + +/// @title IInstantSlasher +/// @notice A slashing contract that immediately executes slashing requests without any delay or veto period +/// @dev Extends base interfaces to provide access controlled slashing functionality +interface IInstantSlasher is ISlasher { + /// @notice Immediately executes a slashing request + /// @param _slashingParams Parameters defining the slashing request including operator and amount + /// @dev Can only be called by the authorized slasher + function fulfillSlashingRequest( + IAllocationManager.SlashingParams memory _slashingParams + ) external; +} diff --git a/src/interfaces/IRegistryCoordinator.sol b/src/interfaces/IRegistryCoordinator.sol index 4c9397dc..e25f1477 100644 --- a/src/interfaces/IRegistryCoordinator.sol +++ b/src/interfaces/IRegistryCoordinator.sol @@ -17,7 +17,11 @@ interface IRegistryCoordinatorErrors is ISlashingRegistryCoordinatorErrors { /// @notice Thrown when a quorum is an operator set quorum. error OperatorSetQuorum(); /// @notice Thrown when M2 quorums are already disabled. - error M2QuorumsAlreadyDisabled(); + error M2QuorumRegistrationIsDisabled(); + /// @notice Thrown when operator set operations are attempted while not enabled. + error OperatorSetsNotEnabled(); + /// @notice Thrown when only M2 quorums are allowed. + error OnlyM2QuorumsAllowed(); } interface IRegistryCoordinatorTypes is ISlashingRegistryCoordinatorTypes {} @@ -33,10 +37,10 @@ interface IRegistryCoordinatorEvents is event OperatorSetsEnabled(); /** - * @notice Emitted when M2 quorums are disabled. + * @notice Emitted when M2 quorum registration is disabled. * @dev Emitted in disableM2QuorumRegistration(). */ - event M2QuorumsDisabled(); + event M2QuorumRegistrationDisabled(); } interface IRegistryCoordinator is @@ -107,11 +111,13 @@ interface IRegistryCoordinator is ) external; /** - * @notice Enables operator sets mode for the AVS. Once enabled, this cannot be disabled. - * @dev When enabled, all existing quorums are marked as M2 quorums and future quorums must be explicitly - * created as either M2 or operator set quorums. + * @notice Checks if a quorum is an M2 quorum. + * @param quorumNumber The quorum identifier. + * @return True if the quorum is M2, false otherwise. */ - function enableOperatorSets() external; + function isM2Quorum( + uint8 quorumNumber + ) external view returns (bool); /** * @notice Disables M2 quorum registration for the AVS. Once disabled, this cannot be enabled. diff --git a/src/interfaces/IServiceManager.sol b/src/interfaces/IServiceManager.sol index a93f03f0..29f50501 100644 --- a/src/interfaces/IServiceManager.sol +++ b/src/interfaces/IServiceManager.sol @@ -19,8 +19,6 @@ interface IServiceManagerErrors { error OnlyRewardsInitiator(); /// @notice Thrown when a function is called by an address that is not the StakeRegistry. error OnlyStakeRegistry(); - /// @notice Thrown when a function is called by an address that is not the Slasher. - error OnlySlasher(); /// @notice Thrown when a slashing proposal delay has not been met yet. error DelayPeriodNotPassed(); } diff --git a/src/interfaces/ISlasher.sol b/src/interfaces/ISlasher.sol index fe388afb..7785b537 100644 --- a/src/interfaces/ISlasher.sol +++ b/src/interfaces/ISlasher.sol @@ -1,85 +1,24 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.27; -import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; import {IAllocationManager} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; interface ISlasherErrors { - /// @notice Thrown when a caller without veto committee privileges attempts a restricted operation. - error OnlyVetoCommittee(); - /// @notice Thrown when a caller without slasher privileges attempts a restricted operation. + /// @notice Thrown when a caller without slasher privileges attempts a restricted operation error OnlySlasher(); - /// @notice Thrown when attempting to veto a slashing request after the veto period has expired. - error VetoPeriodPassed(); - /// @notice Thrown when attempting to execute a slashing request before the veto period has ended. - error VetoPeriodNotPassed(); - /// @notice Thrown when attempting to interact with a slashing request that has been cancelled. - error SlashingRequestIsCancelled(); - /// @notice Thrown when attempting to modify a slashing request that does not exist. - error SlashingRequestNotRequested(); } interface ISlasherTypes { - /** - * @notice Represents the current status of a slashing request. - * @dev The status of a slashing request can be one of the following: - * - Null: Default state, no request exists. - * - Requested: Slashing has been requested but not yet executed. - * - Completed: Slashing has been successfully executed. - * - Cancelled: Slashing request was cancelled by veto committee. - */ - enum SlashingStatus { - Null, - Requested, - Completed, - Cancelled - } - - /** - * @notice Contains all information related to a slashing request. - * @param params The slashing parameters from the allocation manager. - * @param requestTimestamp The timestamp when the slashing request was created. - * @param status The current status of the slashing request. - */ + /// @notice Structure containing details about a slashing request struct SlashingRequest { IAllocationManager.SlashingParams params; uint256 requestTimestamp; - SlashingStatus status; } } interface ISlasherEvents is ISlasherTypes { - /** - * @notice Emitted when a new slashing request is created. - * @param requestId The unique identifier for the slashing request (indexed). - * @param operator The address of the operator to be slashed (indexed). - * @param operatorSetId The ID of the operator set involved (indexed). - * @param wadsToSlash The amounts to slash from each strategy. - * @param description A human-readable description of the slashing reason. - */ - event SlashingRequested( - uint256 indexed requestId, - address indexed operator, - uint32 indexed operatorSetId, - uint256[] wadsToSlash, - string description - ); - - /** - * @notice Emitted when a slashing request is cancelled by the veto committee. - * @param requestId The unique identifier of the cancelled request (indexed). - */ - event SlashingRequestCancelled(uint256 indexed requestId); - - /** - * @notice Emitted when an operator is successfully slashed. - * @param slashingRequestId The ID of the executed slashing request (indexed). - * @param operator The address of the slashed operator (indexed). - * @param operatorSetId The ID of the operator set involved (indexed). - * @param wadsToSlash The amounts slashed from each strategy. - * @param description A human-readable description of why the operator was slashed. - */ + /// @notice Emitted when an operator is successfully slashed event OperatorSlashed( uint256 indexed slashingRequestId, address indexed operator, @@ -89,4 +28,12 @@ interface ISlasherEvents is ISlasherTypes { ); } -interface ISlasher is ISlasherErrors, ISlasherEvents {} +/// @title ISlasher +/// @notice Base interface containing shared functionality for all slasher implementations +interface ISlasher is ISlasherErrors, ISlasherEvents { + /// @notice Returns the address authorized to create and fulfill slashing requests + function slasher() external view returns (address); + + /// @notice Returns the next slashing request ID + function nextRequestId() external view returns (uint256); +} diff --git a/src/interfaces/ISlashingRegistryCoordinator.sol b/src/interfaces/ISlashingRegistryCoordinator.sol index 70b74af8..307ad5cb 100644 --- a/src/interfaces/ISlashingRegistryCoordinator.sol +++ b/src/interfaces/ISlashingRegistryCoordinator.sol @@ -10,6 +10,7 @@ import {IAllocationManager} from import {IBLSApkRegistry} from "./IBLSApkRegistry.sol"; import {IStakeRegistry, IStakeRegistryTypes} from "./IStakeRegistry.sol"; import {IIndexRegistry} from "./IIndexRegistry.sol"; +import {ISocketRegistry} from "./ISocketRegistry.sol"; import {BN254} from "../libraries/BN254.sol"; import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; @@ -50,8 +51,8 @@ interface ISlashingRegistryCoordinatorErrors { error NotSorted(); /// @notice Thrown when maximum quorum count is reached. error MaxQuorumsReached(); - /// @notice Thrown when operator set operations are attempted while not enabled. - error OperatorSetsNotEnabled(); + /// @notice Thrown when the provided AVS address does not match the expected one. + error InvalidAVS(); } interface ISlashingRegistryCoordinatorTypes { @@ -234,6 +235,12 @@ interface ISlashingRegistryCoordinator is */ function allocationManager() external view returns (IAllocationManager); + /** + * @notice Reference to the SocketRegistry contract. + * @return The SocketRegistry contract interface. + */ + function socketRegistry() external view returns (ISocketRegistry); + /// STORAGE /** @@ -296,53 +303,14 @@ interface ISlashingRegistryCoordinator is */ function ejectionCooldown() external view returns (uint256); - /** - * @notice Checks if a quorum is an M2 quorum. - * @param quorumNumber The quorum identifier. - * @return True if the quorum is M2, false otherwise. - */ - function isM2Quorum( - uint8 quorumNumber - ) external view returns (bool); - - /** - * @notice Whether operator sets mode is enabled. - * @return True if operator sets mode is enabled, false otherwise. - */ - function operatorSetsEnabled() external view returns (bool); - /// ACTIONS - /** - * @notice Registers an operator through the allocation manager for operator set quorums. - * @param operator The operator address to register. - * @param operatorSetIds The operator set IDs to register for (corresponds to quorum numbers). - * @param data Additional registration data containing the operator's socket and BLS public key parameters. - * @dev Can only be called by the allocation manager. - * @dev Will revert if operator sets are not enabled or if registering for M2 quorums. - * @dev This function implements the Slashing registration pathway specified by the IAVSRegistrar interface. - */ - function registerOperator( - address operator, - uint32[] memory operatorSetIds, - bytes memory data - ) external; - - /** - * @notice Deregisters an operator through the allocation manager from operator set quorums. - * @param operator The operator address to deregister. - * @param operatorSetIds The operator set IDs to deregister from (corresponds to quorum numbers). - * @dev Can only be called by the allocation manager. - * @dev Will revert if operator sets are not enabled or if deregistering from M2 quorums. - * @dev This function implements the Slashing deregistration pathway specified by the IAVSRegistrar interface. - */ - function deregisterOperator(address operator, uint32[] memory operatorSetIds) external; - /** * @notice Updates stake weights for specified operators. If any operator is found to be below * the minimum stake for their registered quorums, they are deregistered from those quorums. * @param operators The operators whose stakes should be updated. * @dev Stakes are queried from the Eigenlayer core DelegationManager contract. + * @dev WILL BE DEPRECATED IN FAVOR OF updateOperatorsForQuorum */ function updateOperators( address[] memory operators @@ -352,12 +320,15 @@ interface ISlashingRegistryCoordinator is * @notice For each quorum in `quorumNumbers`, updates the StakeRegistry's view of ALL its registered operators' stakes. * Each quorum's `quorumUpdateBlockNumber` is also updated, which tracks the most recent block number when ALL registered * operators were updated. + * @dev stakes are queried from the Eigenlayer core DelegationManager contract * @param operatorsPerQuorum for each quorum in `quorumNumbers`, this has a corresponding list of operators to update. - * @param quorumNumbers is an ordered byte array containing the quorum numbers being updated. - * @dev Each list of operator addresses MUST be sorted in ascending order. - * @dev Each list of operator addresses MUST represent the entire list of registered operators for the corresponding quorum. - * @dev Stakes are queried from the Eigenlayer core DelegationManager contract. - * @dev Will revert if an operator registers/deregisters for any quorum in `quorumNumbers` after transaction broadcast but before execution. + * @dev Each list of operator addresses MUST be sorted in ascending order + * @dev Each list of operator addresses MUST represent the entire list of registered operators for the corresponding quorum + * @param quorumNumbers is an ordered byte array containing the quorum numbers being updated + * @dev invariant: Each list of `operatorsPerQuorum` MUST be a sorted version of `IndexRegistry.getOperatorListAtBlockNumber` + * for the corresponding quorum. + * @dev note on race condition: if an operator registers/deregisters for any quorum in `quorumNumbers` after a txn to + * this method is broadcast (but before it is executed), the method will fail */ function updateOperatorsForQuorum( address[][] memory operatorsPerQuorum, @@ -453,8 +424,26 @@ interface ISlashingRegistryCoordinator is uint256 _ejectionCooldown ) external; + /** + * @notice Updates the avs address for this AVS (used for UAM integration in EigenLayer) + * @param _avs The new avs address + * @dev Can only be called by the contract owner + * @dev NOTE: Updating this value will break existing OperatorSets and UAM integration. This value should only be set once. + */ + function setAVS( + address _avs + ) external; + /// VIEW + /** + * @notice Returns the hash of the message that operators must sign with their BLS key to register + * @param operator The operator's Ethereum address + */ + function calculatePubkeyRegistrationMessageHash( + address operator + ) external view returns (bytes32); + /** * @notice Returns the operator set parameters for a given quorum. * @param quorumNumber The identifier of the quorum to query. @@ -591,16 +580,9 @@ interface ISlashingRegistryCoordinator is ) external view returns (BN254.G1Point memory); /** - * @notice Returns the address of the contract owner. - * @return The owner's address. - * @dev The owner can update contract configuration and create new quorums. - */ - function owner() external view returns (address); - - /** - * @notice Returns the account identifier for this AVS (used for UAM integration in EigenLayer) + * @notice Returns the avs address for this AVS (used for UAM integration in EigenLayer) * @dev NOTE: Updating this value will break existing OperatorSets and UAM integration. This value should only be set once. - * @return The account identifier address + * @return The avs address */ - function accountIdentifier() external view returns (address); + function avs() external view returns (address); } diff --git a/src/interfaces/ISocketRegistry.sol b/src/interfaces/ISocketRegistry.sol index c456a4f9..d94b1dba 100644 --- a/src/interfaces/ISocketRegistry.sol +++ b/src/interfaces/ISocketRegistry.sol @@ -1,11 +1,25 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; -interface ISocketRegistry { - /// @notice sets the socket for an operator only callable by the RegistryCoordinator +interface ISocketRegistryErrors { + /// @notice Thrown when the caller is not the SlashingRegistryCoordinator + error OnlySlashingRegistryCoordinator(); +} + +interface ISocketRegistry is ISocketRegistryErrors { + /** + * @notice Sets the socket for an operator. + * @param _operatorId The id of the operator to set the socket for. + * @param _socket The socket (any arbitrary string as deemed useful by an AVS) to set. + * @dev Only callable by the SlashingRegistryCoordinator. + */ function setOperatorSocket(bytes32 _operatorId, string memory _socket) external; - /// @notice gets the stored socket for an operator + /** + * @notice Gets the stored socket for an operator. + * @param _operatorId The id of the operator to query. + * @return The stored socket associated with the operator. + */ function getOperatorSocket( bytes32 _operatorId ) external view returns (string memory); diff --git a/src/interfaces/IStakeRegistry.sol b/src/interfaces/IStakeRegistry.sol index b7e74644..5e47deb5 100644 --- a/src/interfaces/IStakeRegistry.sol +++ b/src/interfaces/IStakeRegistry.sol @@ -29,6 +29,8 @@ interface IStakeRegistryErrors { error InvalidBlockNumber(); /// @notice Thrown when attempting to access stake history that doesn't exist for a quorum. error EmptyStakeHistory(); + /// @notice Thrown when the quorum is not slashable and the caller attempts to set the look ahead period. + error QuorumNotSlashable(); } interface IStakeRegistryTypes { @@ -164,17 +166,17 @@ interface IStakeRegistry is IStakeRegistryErrors, IStakeRegistryEvents { function deregisterOperator(bytes32 operatorId, bytes memory quorumNumbers) external; /** - * @notice Called by the registry coordinator to update an operator's stake for one or more quorums. - * @param operator The address of the operator to update. - * @param operatorId The id of the operator to update. - * @param quorumNumbers The quorum numbers to update the stake for. - * @return A bitmap of quorums where the operator no longer meets the minimum stake and should be deregistered. + * @notice Called by the registry coordinator to update the stake of a list of operators for a specific quorum. + * @param operators The addresses of the operators to update. + * @param operatorIds The ids of the operators to update. + * @param quorumNumber The quorum number to update the stake for. + * @return A list of bools, true if the corresponding operator should be deregistered since they no longer meet the minimum stake requirement. */ - function updateOperatorStake( - address operator, - bytes32 operatorId, - bytes calldata quorumNumbers - ) external returns (uint192); + function updateOperatorsStake( + address[] memory operators, + bytes32[] memory operatorIds, + uint8 quorumNumber + ) external returns (bool[] memory); /** * @notice Initialize a new quorum created by the registry coordinator by setting strategies, weights, and minimum stake. @@ -249,15 +251,6 @@ interface IStakeRegistry is IStakeRegistryErrors, IStakeRegistryEvents { /// VIEW - /** - * @notice Returns whether a quorum is an operator set quorum. - * @param quorumNumber The quorum number to query. - * @return Whether the quorum is an operator set quorum. - */ - function isOperatorSetQuorum( - uint8 quorumNumber - ) external view returns (bool); - /** * @notice Returns the minimum stake requirement for a quorum `quorumNumber`. * @dev In order to register for a quorum i, an operator must have at least `minimumStakeForQuorum[i]`. diff --git a/src/interfaces/IVetoableSlasher.sol b/src/interfaces/IVetoableSlasher.sol new file mode 100644 index 00000000..796b4265 --- /dev/null +++ b/src/interfaces/IVetoableSlasher.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {ISlasher} from "./ISlasher.sol"; + +interface IVetoableSlasherErrors { + /// @notice Thrown when a caller without veto committee privileges attempts a restricted operation + error OnlyVetoCommittee(); + /// @notice Thrown when attempting to veto a slashing request after the veto period has expired + error VetoPeriodPassed(); + /// @notice Thrown when attempting to execute a slashing request before the veto period has ended + error VetoPeriodNotPassed(); + /// @notice Thrown when attempting to interact with a slashing request that has been cancelled + error SlashingRequestIsCancelled(); + /// @notice Thrown when attempting to modify a slashing request that does not exist + error SlashingRequestNotRequested(); +} + +interface IVetoableSlasherTypes { + /// @notice Represents the status of a slashing request + enum SlashingStatus { + Requested, + Cancelled, + Completed + } + + /// @notice Structure containing details about a vetoable slashing request + struct VetoableSlashingRequest { + IAllocationManager.SlashingParams params; + uint256 requestBlock; + SlashingStatus status; + } +} + +interface IVetoableSlasherEvents { + /// @notice Emitted when a new slashing request is created + event SlashingRequested( + uint256 indexed requestId, + address indexed operator, + uint32 operatorSetId, + uint256[] wadsToSlash, + string description + ); + + /// @notice Emitted when a slashing request is cancelled by the veto committee + event SlashingRequestCancelled(uint256 indexed requestId); +} + +/// @title IVetoableSlasher +/// @notice A slashing contract that implements a veto mechanism allowing a designated committee to cancel slashing requests +/// @dev Extends base interfaces and adds a veto period during which slashing requests can be cancelled +interface IVetoableSlasher is + ISlasher, + IVetoableSlasherErrors, + IVetoableSlasherTypes, + IVetoableSlasherEvents +{ + /// @notice Duration of the veto period during which the veto committee can cancel slashing requests + function vetoWindowBlocks() external view returns (uint32); + + /// @notice Address of the committee that has veto power over slashing requests + function vetoCommittee() external view returns (address); + + /// @notice Queues a new slashing request + /// @param params Parameters defining the slashing request including operator and amount + /// @dev Can only be called by the authorized slasher + function queueSlashingRequest( + IAllocationManager.SlashingParams calldata params + ) external; + + /// @notice Cancels a pending slashing request + /// @param requestId The ID of the slashing request to cancel + /// @dev Can only be called by the veto committee during the veto period + function cancelSlashingRequest( + uint256 requestId + ) external; + + /// @notice Executes a slashing request after the veto period has passed + /// @param requestId The ID of the slashing request to fulfill + /// @dev Can only be called by the authorized slasher after the veto period + function fulfillSlashingRequest( + uint256 requestId + ) external; +} diff --git a/src/libraries/BitmapUtils.sol b/src/libraries/BitmapUtils.sol index 4e4c9373..cbe670a3 100644 --- a/src/libraries/BitmapUtils.sol +++ b/src/libraries/BitmapUtils.sol @@ -215,4 +215,12 @@ library BitmapUtils { function minus(uint256 a, uint256 b) internal pure returns (uint256) { return a & ~b; } + + /** + * @notice Returns a new bitmap that contains only bits set in both `a` and `b` + * @dev Result is the intersection of `a` and `b` + */ + function and(uint256 a, uint256 b) internal pure returns (uint256) { + return a & b; + } } diff --git a/src/slashers/InstantSlasher.sol b/src/slashers/InstantSlasher.sol index 10276bff..08d4d59f 100644 --- a/src/slashers/InstantSlasher.sol +++ b/src/slashers/InstantSlasher.sol @@ -4,25 +4,24 @@ pragma solidity ^0.8.27; import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; import {IAllocationManager} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; -import {ISlashingRegistryCoordinator} from "../interfaces/ISlashingRegistryCoordinator.sol"; import {SlasherBase} from "./base/SlasherBase.sol"; +import {ISlashingRegistryCoordinator} from "../interfaces/ISlashingRegistryCoordinator.sol"; +import {IInstantSlasher} from "../interfaces/IInstantSlasher.sol"; -contract InstantSlasher is SlasherBase { +/// @title InstantSlasher +/// @notice A slashing contract that immediately executes slashing requests without any delay or veto period +/// @dev Extends SlasherBase to provide access controlled slashing functionality +contract InstantSlasher is IInstantSlasher, SlasherBase { constructor( IAllocationManager _allocationManager, ISlashingRegistryCoordinator _slashingRegistryCoordinator, address _slasher - ) SlasherBase(_allocationManager, _slashingRegistryCoordinator) {} - - function initialize( - address _slasher - ) external initializer { - __SlasherBase_init(_slasher); - } + ) SlasherBase(_allocationManager, _slashingRegistryCoordinator, _slasher) {} + /// @inheritdoc IInstantSlasher function fulfillSlashingRequest( - IAllocationManager.SlashingParams memory _slashingParams - ) external virtual onlySlasher { + IAllocationManager.SlashingParams calldata _slashingParams + ) external virtual override(IInstantSlasher) onlySlasher { uint256 requestId = nextRequestId++; _fulfillSlashingRequest(requestId, _slashingParams); } diff --git a/src/slashers/VetoableSlasher.sol b/src/slashers/VetoableSlasher.sol index 949c46c2..eca72579 100644 --- a/src/slashers/VetoableSlasher.sol +++ b/src/slashers/VetoableSlasher.sol @@ -6,13 +6,22 @@ import {IAllocationManager} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; import {SlasherBase} from "./base/SlasherBase.sol"; import {ISlashingRegistryCoordinator} from "../interfaces/ISlashingRegistryCoordinator.sol"; +import {IVetoableSlasher, IVetoableSlasherTypes} from "../interfaces/IVetoableSlasher.sol"; -contract VetoableSlasher is SlasherBase { - uint256 public constant VETO_PERIOD = 3 days; - address public vetoCommittee; +/// @title VetoableSlasher +/// @notice A slashing contract that implements a veto mechanism allowing a designated committee to cancel slashing requests +/// @dev Extends SlasherBase and adds a veto period during which slashing requests can be cancelled +contract VetoableSlasher is IVetoableSlasher, SlasherBase { + /// @inheritdoc IVetoableSlasher + uint32 public immutable override vetoWindowBlocks; - mapping(uint256 => SlashingRequest) public slashingRequests; + /// @inheritdoc IVetoableSlasher + address public immutable override vetoCommittee; + /// @notice Mapping of request IDs to their corresponding slashing request details + mapping(uint256 => IVetoableSlasherTypes.VetoableSlashingRequest) public slashingRequests; + + /// @notice Modifier to restrict function access to only the veto committee modifier onlyVetoCommittee() { _checkVetoCommittee(msg.sender); _; @@ -21,55 +30,45 @@ contract VetoableSlasher is SlasherBase { constructor( IAllocationManager _allocationManager, ISlashingRegistryCoordinator _slashingRegistryCoordinator, - address _slasher - ) SlasherBase(_allocationManager, _slashingRegistryCoordinator) {} - - function initialize(address _vetoCommittee, address _slasher) external virtual initializer { - __SlasherBase_init(_slasher); + address _slasher, + address _vetoCommittee, + uint32 _vetoWindowBlocks + ) SlasherBase(_allocationManager, _slashingRegistryCoordinator, _slasher) { + vetoWindowBlocks = _vetoWindowBlocks; vetoCommittee = _vetoCommittee; } + /// @inheritdoc IVetoableSlasher function queueSlashingRequest( - IAllocationManager.SlashingParams memory params - ) external virtual onlySlasher { + IAllocationManager.SlashingParams calldata params + ) external virtual override onlySlasher { _queueSlashingRequest(params); } + /// @inheritdoc IVetoableSlasher function cancelSlashingRequest( uint256 requestId - ) external virtual onlyVetoCommittee { - require( - block.timestamp < slashingRequests[requestId].requestTimestamp + VETO_PERIOD, - VetoPeriodPassed() - ); - require( - slashingRequests[requestId].status == SlashingStatus.Requested, - SlashingRequestNotRequested() - ); - + ) external virtual override onlyVetoCommittee { _cancelSlashingRequest(requestId); } + /// @inheritdoc IVetoableSlasher function fulfillSlashingRequest( uint256 requestId - ) external virtual onlySlasher { - SlashingRequest storage request = slashingRequests[requestId]; - require(block.timestamp >= request.requestTimestamp + VETO_PERIOD, VetoPeriodNotPassed()); - require(request.status == SlashingStatus.Requested, SlashingRequestIsCancelled()); - - request.status = SlashingStatus.Completed; - - _fulfillSlashingRequest(requestId, request.params); + ) external virtual override onlySlasher { + _fulfillSlashingRequestAndMarkAsCompleted(requestId); } + /// @notice Internal function to create and store a new slashing request + /// @param params Parameters defining the slashing request function _queueSlashingRequest( IAllocationManager.SlashingParams memory params ) internal virtual { uint256 requestId = nextRequestId++; - slashingRequests[requestId] = SlashingRequest({ + slashingRequests[requestId] = IVetoableSlasherTypes.VetoableSlashingRequest({ params: params, - requestTimestamp: block.timestamp, - status: SlashingStatus.Requested + requestBlock: block.number, + status: IVetoableSlasherTypes.SlashingStatus.Requested }); emit SlashingRequested( @@ -77,13 +76,44 @@ contract VetoableSlasher is SlasherBase { ); } + /// @notice Internal function to mark a slashing request as cancelled + /// @param requestId The ID of the slashing request to cancel function _cancelSlashingRequest( uint256 requestId ) internal virtual { - slashingRequests[requestId].status = SlashingStatus.Cancelled; + require( + block.number < slashingRequests[requestId].requestBlock + vetoWindowBlocks, + VetoPeriodPassed() + ); + require( + slashingRequests[requestId].status == IVetoableSlasherTypes.SlashingStatus.Requested, + SlashingRequestNotRequested() + ); + + slashingRequests[requestId].status = IVetoableSlasherTypes.SlashingStatus.Cancelled; emit SlashingRequestCancelled(requestId); } + /// @notice Internal function to fullfill a slashing request and mark it as completed + /// @param requestId The ID of the slashing request to fulfill + function _fulfillSlashingRequestAndMarkAsCompleted( + uint256 requestId + ) internal virtual { + IVetoableSlasherTypes.VetoableSlashingRequest storage request = slashingRequests[requestId]; + require(block.number >= request.requestBlock + vetoWindowBlocks, VetoPeriodNotPassed()); + require( + request.status == IVetoableSlasherTypes.SlashingStatus.Requested, + SlashingRequestIsCancelled() + ); + + request.status = IVetoableSlasherTypes.SlashingStatus.Completed; + + _fulfillSlashingRequest(requestId, request.params); + } + + /// @notice Internal function to verify if an account is the veto committee + /// @param account The address to check + /// @dev Reverts if the account is not the veto committee function _checkVetoCommittee( address account ) internal view virtual { diff --git a/src/slashers/base/SlasherBase.sol b/src/slashers/base/SlasherBase.sol index a6917d39..c0f74b93 100644 --- a/src/slashers/base/SlasherBase.sol +++ b/src/slashers/base/SlasherBase.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.27; -import {Initializable} from "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; import {SlasherStorage, ISlashingRegistryCoordinator} from "./SlasherStorage.sol"; import { IAllocationManagerTypes, @@ -9,33 +8,35 @@ import { } from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; -abstract contract SlasherBase is Initializable, SlasherStorage { +/// @title SlasherBase +/// @notice Base contract for implementing slashing functionality in EigenLayer middleware +/// @dev Provides core slashing functionality and interfaces with EigenLayer's AllocationManager +abstract contract SlasherBase is SlasherStorage { + /// @notice Ensures only the authorized slasher can call certain functions modifier onlySlasher() { _checkSlasher(msg.sender); _; } + /// @notice Constructs the base slasher contract + /// @param _allocationManager The EigenLayer allocation manager contract + /// @param _registryCoordinator The registry coordinator for this middleware + /// @param _slasher The address of the slasher constructor( IAllocationManager _allocationManager, - ISlashingRegistryCoordinator _registryCoordinator - ) SlasherStorage(_allocationManager, _registryCoordinator) { - _disableInitializers(); - } - - function __SlasherBase_init( + ISlashingRegistryCoordinator _registryCoordinator, address _slasher - ) internal onlyInitializing { - slasher = _slasher; - } + ) SlasherStorage(_allocationManager, _registryCoordinator, _slasher) {} + /// @notice Internal function to execute a slashing request + /// @param _requestId The ID of the slashing request to fulfill + /// @param _params Parameters defining the slashing request including operator, strategies, and amounts + /// @dev Calls AllocationManager.slashOperator to perform the actual slashing function _fulfillSlashingRequest( uint256 _requestId, IAllocationManager.SlashingParams memory _params ) internal virtual { - allocationManager.slashOperator({ - avs: slashingRegistryCoordinator.accountIdentifier(), - params: _params - }); + allocationManager.slashOperator({avs: slashingRegistryCoordinator.avs(), params: _params}); emit OperatorSlashed( _requestId, _params.operator, @@ -45,6 +46,9 @@ abstract contract SlasherBase is Initializable, SlasherStorage { ); } + /// @notice Internal function to verify if an account is the authorized slasher + /// @param account The address to check + /// @dev Reverts if the account is not the authorized slasher function _checkSlasher( address account ) internal view virtual { diff --git a/src/slashers/base/SlasherStorage.sol b/src/slashers/base/SlasherStorage.sol index 54627326..993d49fb 100644 --- a/src/slashers/base/SlasherStorage.sol +++ b/src/slashers/base/SlasherStorage.sol @@ -3,10 +3,13 @@ pragma solidity ^0.8.27; import {IAllocationManager} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; -import {ISlasher} from "../../interfaces/ISlasher.sol"; import {ISlashingRegistryCoordinator} from "../../interfaces/ISlashingRegistryCoordinator.sol"; +import {ISlasher} from "../../interfaces/ISlasher.sol"; -contract SlasherStorage is ISlasher { +/// @title SlasherStorage +/// @notice Base storage contract for slashing functionality +/// @dev Provides storage variables and events for slashing operations +abstract contract SlasherStorage is ISlasher { /** * * CONSTANTS AND IMMUTABLES @@ -17,22 +20,20 @@ contract SlasherStorage is ISlasher { IAllocationManager public immutable allocationManager; /// @notice the SlashingRegistryCoordinator for this AVS ISlashingRegistryCoordinator public immutable slashingRegistryCoordinator; - /** - * - * STATE - * - */ - address public slasher; + /// @notice the address of the slasher + address public immutable slasher; uint256 public nextRequestId; constructor( IAllocationManager _allocationManager, - ISlashingRegistryCoordinator _slashingRegistryCoordinator + ISlashingRegistryCoordinator _slashingRegistryCoordinator, + address _slasher ) { allocationManager = _allocationManager; slashingRegistryCoordinator = _slashingRegistryCoordinator; + slasher = _slasher; } - uint256[48] private __gap; + uint256[49] private __gap; } diff --git a/test/ffi/UpdateOperators.t.sol b/test/ffi/UpdateOperators.t.sol index 349bb8d1..8a22b49a 100644 --- a/test/ffi/UpdateOperators.t.sol +++ b/test/ffi/UpdateOperators.t.sol @@ -1077,7 +1077,8 @@ contract Integration_AVS_Sync_GasCosts_FFI is IntegrationChecks { numQuorums: ONE, numStrategies: TWENTYFIVE, minimumStake: NO_MINIMUM, - fillTypes: FULL + fillTypes: FULL, + quorumType: DELEGATED_STAKE }) }); _updateOperators_SingleQuorum(); @@ -1093,7 +1094,8 @@ contract Integration_AVS_Sync_GasCosts_FFI is IntegrationChecks { numQuorums: ONE, numStrategies: TWENTY, minimumStake: NO_MINIMUM, - fillTypes: FULL + fillTypes: FULL, + quorumType: DELEGATED_STAKE }) }); @@ -1109,7 +1111,8 @@ contract Integration_AVS_Sync_GasCosts_FFI is IntegrationChecks { numQuorums: ONE, numStrategies: FIFTEEN, minimumStake: NO_MINIMUM, - fillTypes: FULL + fillTypes: FULL, + quorumType: DELEGATED_STAKE }) }); _updateOperators_SingleQuorum(); diff --git a/test/harnesses/RegistryCoordinatorHarness.t.sol b/test/harnesses/RegistryCoordinatorHarness.t.sol index 050fae43..7ff67bd8 100644 --- a/test/harnesses/RegistryCoordinatorHarness.t.sol +++ b/test/harnesses/RegistryCoordinatorHarness.t.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.27; import "../../src/RegistryCoordinator.sol"; - import {ISocketRegistry} from "../../src/interfaces/ISocketRegistry.sol"; +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; import "forge-std/Test.sol"; @@ -49,7 +49,13 @@ contract RegistryCoordinatorHarness is RegistryCoordinator, Test { string memory socket, SignatureWithSaltAndExpiry memory operatorSignature ) external returns (RegisterResults memory results) { - return _registerOperator(operator, operatorId, quorumNumbers, socket); + return _registerOperator({ + operator: operator, + operatorId: operatorId, + quorumNumbers: quorumNumbers, + socket: socket, + checkMaxOperatorCount: true + }); } // @notice exposes the internal `_deregisterOperator` function, overriding all access controls @@ -57,15 +63,6 @@ contract RegistryCoordinatorHarness is RegistryCoordinator, Test { _deregisterOperator(operator, quorumNumbers); } - // @notice exposes the internal `_updateOperator` function, overriding all access controls - function _updateOperatorExternal( - address operator, - OperatorInfo memory operatorInfo, - bytes memory quorumsToUpdate - ) external { - _updateOperator(operator, operatorInfo, quorumsToUpdate); - } - // @notice exposes the internal `_updateOperatorBitmap` function, overriding all access controls function _updateOperatorBitmapExternal(bytes32 operatorId, uint192 quorumBitmap) external { _updateOperatorBitmap(operatorId, quorumBitmap); @@ -77,9 +74,21 @@ contract RegistryCoordinatorHarness is RegistryCoordinator, Test { operatorSetsEnabled = enabled; } - function setM2QuorumsDisabled( + function setM2QuorumRegistrationDisabled( bool disabled ) external { - m2QuorumsDisabled = disabled; + isM2QuorumRegistrationDisabled = disabled; + } + + function setM2QuorumBitmap( + uint256 bitmap + ) external { + _m2QuorumBitmap = bitmap; + } + + function supportsAVS( + address avs + ) public view override(IAVSRegistrar, SlashingRegistryCoordinator) returns (bool) { + return avs == address(serviceManager); } } diff --git a/test/integration/CoreRegistration.t.sol b/test/integration/CoreRegistration.t.sol index 42359be0..80719427 100644 --- a/test/integration/CoreRegistration.t.sol +++ b/test/integration/CoreRegistration.t.sol @@ -188,14 +188,20 @@ contract Test_CoreRegistration is MockAVSDeployer { emit log_named_bytes("quorumNumbers", quorumNumbers); _registerOperator(quorumNumbers); + IAVSDirectoryTypes.OperatorAVSRegistrationStatus operatorStatus = + avsDirectory.avsOperatorStatus(address(serviceManager), operator); + assertEq( + uint8(operatorStatus), + uint8(IAVSDirectoryTypes.OperatorAVSRegistrationStatus.REGISTERED) + ); + // Deregister Operator with single quorum quorumNumbers = new bytes(1); cheats.prank(operator); registryCoordinator.deregisterOperator(quorumNumbers); // Check operator is still registered - IAVSDirectoryTypes.OperatorAVSRegistrationStatus operatorStatus = - avsDirectory.avsOperatorStatus(address(serviceManager), operator); + operatorStatus = avsDirectory.avsOperatorStatus(address(serviceManager), operator); assertEq( uint8(operatorStatus), uint8(IAVSDirectoryTypes.OperatorAVSRegistrationStatus.REGISTERED) diff --git a/test/integration/IntegrationBase.t.sol b/test/integration/IntegrationBase.t.sol index 180edd11..47c06f78 100644 --- a/test/integration/IntegrationBase.t.sol +++ b/test/integration/IntegrationBase.t.sol @@ -22,7 +22,7 @@ abstract contract IntegrationBase is IntegrationConfig { function assert_HasOperatorInfoWithId(User user, string memory err) internal { bytes32 expectedId = user.operatorId(); - bytes32 actualId = registryCoordinator.getOperatorId(address(user)); + bytes32 actualId = slashingRegistryCoordinator.getOperatorId(address(user)); assertEq(expectedId, actualId, err); } @@ -39,20 +39,20 @@ abstract contract IntegrationBase is IntegrationConfig { function assert_HasRegisteredStatus(User user, string memory err) internal { ISlashingRegistryCoordinatorTypes.OperatorStatus status = - registryCoordinator.getOperatorStatus(address(user)); + slashingRegistryCoordinator.getOperatorStatus(address(user)); assertTrue(status == ISlashingRegistryCoordinatorTypes.OperatorStatus.REGISTERED, err); } function assert_HasDeregisteredStatus(User user, string memory err) internal { ISlashingRegistryCoordinatorTypes.OperatorStatus status = - registryCoordinator.getOperatorStatus(address(user)); + slashingRegistryCoordinator.getOperatorStatus(address(user)); assertTrue(status == ISlashingRegistryCoordinatorTypes.OperatorStatus.DEREGISTERED, err); } function assert_EmptyQuorumBitmap(User user, string memory err) internal { - uint192 bitmap = registryCoordinator.getCurrentQuorumBitmap(user.operatorId()); + uint192 bitmap = slashingRegistryCoordinator.getCurrentQuorumBitmap(user.operatorId()); assertTrue(bitmap == 0, err); } @@ -62,7 +62,7 @@ abstract contract IntegrationBase is IntegrationConfig { bytes memory quorums, string memory err ) internal { - uint192 bitmap = registryCoordinator.getCurrentQuorumBitmap(user.operatorId()); + uint192 bitmap = slashingRegistryCoordinator.getCurrentQuorumBitmap(user.operatorId()); for (uint256 i = 0; i < quorums.length; i++) { uint8 quorum = uint8(quorums[i]); @@ -77,15 +77,30 @@ abstract contract IntegrationBase is IntegrationConfig { bytes memory quorums, string memory err ) internal { - uint192 currentBitmap = registryCoordinator.getCurrentQuorumBitmap(user.operatorId()); + uint192 currentBitmap = + slashingRegistryCoordinator.getCurrentQuorumBitmap(user.operatorId()); uint192 subsetBitmap = uint192(quorums.orderedBytesArrayToBitmap()); assertTrue(subsetBitmap.isSubsetOf(currentBitmap), err); } + /// @dev Checks that the user is registered for each of the operator sets in the AllocationManager + function assert_RegisteredForOperatorSets( + User user, + bytes memory operatorSetIds, + string memory err + ) internal { + for (uint256 i = 0; i < operatorSetIds.length; i++) { + uint32 operatorSetId = uint32(uint8(operatorSetIds[i])); + OperatorSet memory operatorSet = + OperatorSet({avs: avsAccountIdentifier, id: operatorSetId}); + assertTrue(allocationManager.isMemberOfOperatorSet(address(user), operatorSet), err); + } + } + /// @dev Checks whether each of the quorums has been initialized in the RegistryCoordinator function assert_QuorumsExist(bytes memory quorums, string memory err) internal { - uint8 count = registryCoordinator.quorumCount(); + uint8 count = slashingRegistryCoordinator.quorumCount(); for (uint256 i = 0; i < quorums.length; i++) { uint8 quorum = uint8(quorums[i]); @@ -176,7 +191,7 @@ abstract contract IntegrationBase is IntegrationConfig { uint8 quorum = uint8(quorums[i]); uint32 maxOperatorCount = - registryCoordinator.getOperatorSetParams(quorum).maxOperatorCount; + slashingRegistryCoordinator.getOperatorSetParams(quorum).maxOperatorCount; uint32 curOperatorCount = indexRegistry.totalOperatorsForQuorum(quorum); assertTrue(curOperatorCount < maxOperatorCount, err); @@ -244,6 +259,20 @@ abstract contract IntegrationBase is IntegrationConfig { assertTrue(quorumsRemoved.noBitsInCommon(curBitmap), err); } + /// @dev Checks that user has deregistered from all operator sets in the AllocationManager + function assert_Snap_Deregistered_FromOperatorSets( + User user, + bytes memory quorums, + string memory err + ) internal { + for (uint256 i = 0; i < quorums.length; i++) { + uint32 operatorSetId = uint32(uint8(quorums[i])); + OperatorSet memory operatorSet = + OperatorSet({avs: avsAccountIdentifier, id: operatorSetId}); + assertFalse(allocationManager.isMemberOfOperatorSet(address(user), operatorSet), err); + } + } + function assert_Snap_Unchanged_OperatorInfo(User user, string memory err) internal { ISlashingRegistryCoordinatorTypes.OperatorInfo memory curInfo = _getOperatorInfo(user); ISlashingRegistryCoordinatorTypes.OperatorInfo memory prevInfo = _getPrevOperatorInfo(user); @@ -869,7 +898,7 @@ abstract contract IntegrationBase is IntegrationConfig { function _getOperatorInfo( User user ) internal view returns (ISlashingRegistryCoordinatorTypes.OperatorInfo memory) { - return registryCoordinator.getOperator(address(user)); + return slashingRegistryCoordinator.getOperator(address(user)); } function _getPrevOperatorInfo( @@ -881,7 +910,7 @@ abstract contract IntegrationBase is IntegrationConfig { function _getQuorumBitmap( bytes32 operatorId ) internal view returns (uint192) { - return registryCoordinator.getCurrentQuorumBitmap(operatorId); + return slashingRegistryCoordinator.getCurrentQuorumBitmap(operatorId); } function _getPrevQuorumBitmap( diff --git a/test/integration/IntegrationChecks.t.sol b/test/integration/IntegrationChecks.t.sol index ba9c3d23..f32093a1 100644 --- a/test/integration/IntegrationChecks.t.sol +++ b/test/integration/IntegrationChecks.t.sol @@ -39,6 +39,11 @@ contract IntegrationChecks is IntegrationBase { function check_Register_State(User operator, bytes memory quorums) internal { _log("check_Register_State", operator); + // AllocationManager + assert_RegisteredForOperatorSets( + operator, quorums, "operator did not register for all operator sets" + ); + // RegistryCoordinator assert_HasOperatorInfoWithId(operator, "operatorInfo should have operatorId"); assert_HasRegisteredStatus(operator, "operatorInfo status should be REGISTERED"); @@ -74,7 +79,7 @@ contract IntegrationChecks is IntegrationBase { ); // AVSDirectory - assert_IsRegisteredToAVS(operator, "operator should be registered to AVS"); + // assert_IsRegisteredToAVS(operator, "operator should be registered to AVS"); } /// @dev Combines many checks of check_Register_State and check_Deregister_State @@ -153,7 +158,7 @@ contract IntegrationChecks is IntegrationBase { ); // AVSDirectory - assert_IsRegisteredToAVS(incomingOperator, "operator should be registered to AVS"); + // assert_IsRegisteredToAVS(incomingOperator, "operator should be registered to AVS"); // Check that churnedOperators are deregistered from churnedQuorums for (uint256 i = 0; i < churnedOperators.length; i++) { @@ -177,6 +182,13 @@ contract IntegrationChecks is IntegrationBase { "churned operator did not deregister from churned quorum" ); + // AllocationManager + assert_Snap_Deregistered_FromOperatorSets( + churnedOperator, + churnedQuorum, + "churned operator did not deregister from operatorSets" + ); + // BLSApkRegistry assert_HasRegisteredPubkey( churnedOperator, "churned operator should still have a registered pubkey" @@ -276,7 +288,7 @@ contract IntegrationChecks is IntegrationBase { ); // AVSDirectory - assert_IsRegisteredToAVS(operator, "operator should be registered to AVS"); + // assert_IsRegisteredToAVS(operator, "operator should be registered to AVS"); } /// @dev Validate state directly after the operator exits from Eigenlayer core (by queuing withdrawals) @@ -320,6 +332,11 @@ contract IntegrationChecks is IntegrationBase { function check_WithdrawUpdate_State(User operator, bytes memory quorums) internal { _log("check_WithdrawUpdate_State", operator); + // AllocationManager + assert_Snap_Deregistered_FromOperatorSets( + operator, quorums, "operator did not deregister from all operator sets" + ); + // RegistryCoordinator assert_HasOperatorInfoWithId(operator, "operatorInfo should still have operatorId"); assert_EmptyQuorumBitmap(operator, "operator should not have any bits in bitmap"); @@ -393,6 +410,11 @@ contract IntegrationChecks is IntegrationBase { function check_Deregister_State(User operator, bytes memory quorums) internal { _log("check_Deregister_State", operator); + // AllocationManager + assert_Snap_Deregistered_FromOperatorSets( + operator, quorums, "operator did not deregister from all operator sets" + ); + // RegistryCoordinator assert_HasOperatorInfoWithId(operator, "operatorInfo should still have operatorId"); assert_NotRegisteredForQuorums( diff --git a/test/integration/IntegrationConfig.t.sol b/test/integration/IntegrationConfig.t.sol index 92c394bb..c201534a 100644 --- a/test/integration/IntegrationConfig.t.sol +++ b/test/integration/IntegrationConfig.t.sol @@ -47,13 +47,17 @@ contract IntegrationConfig is IntegrationDeployer, G2Operations, Constants { bytes numStrategyFlags; bytes minStakeFlags; bytes fillTypeFlags; - + bytes quorumTypeFlags; uint256 constant FLAG = 1; /// @dev Flags for userTypes uint256 constant DEFAULT = (FLAG << 0); uint256 constant ALT_METHODS = (FLAG << 1); + /// @dev Flags for using SlashingRegistryCoordinator or RegistryCoordinator (M2) + uint256 constant SLASHING = (FLAG << 0); + uint256 constant M2 = (FLAG << 1); + /// @dev Flags for numQuorums and numStrategies uint256 constant ONE = (FLAG << 0); uint256 constant TWO = (FLAG << 1); @@ -71,6 +75,11 @@ contract IntegrationConfig is IntegrationDeployer, G2Operations, Constants { uint256 constant SOME_FILL = (FLAG << 1); uint256 constant FULL = (FLAG << 2); + /// @dev Flags for quorumType + uint256 constant DELEGATED_STAKE = (FLAG << 0); + uint256 constant SLASHABLE_STAKE = (FLAG << 1); + uint256 constant BOTH = (FLAG << 2); + /// @dev Tracking variables for pregenerated BLS keypairs: /// (See _fetchKeypair) uint256 fetchIdx = 0; @@ -121,6 +130,8 @@ contract IntegrationConfig is IntegrationDeployer, G2Operations, Constants { /// @dev Whether each quorum created is pre-populated with operators /// NOTE: Default uint256 fillTypes; // EMPTY | SOME_FILL | FULL + /// @dev Whether quorums created are delegated stake quorum, slashable stake quorums, or both + uint256 quorumType; // DELEGATED_STAKE | SLASHABLE_STAKE | BOTH } /** @@ -143,7 +154,7 @@ contract IntegrationConfig is IntegrationDeployer, G2Operations, Constants { numStrategyFlags = _bitmapToBytes(_quorumConfig.numStrategies); minStakeFlags = _bitmapToBytes(_quorumConfig.minimumStake); fillTypeFlags = _bitmapToBytes(_quorumConfig.fillTypes); - + quorumTypeFlags = _bitmapToBytes(_quorumConfig.quorumType); // Sanity check config assertTrue(userFlags.length != 0, "_configRand: invalid _userTypes, no flags passed"); assertTrue(numQuorumFlags.length != 0, "_configRand: invalid numQuorums, no flags passed"); @@ -152,7 +163,7 @@ contract IntegrationConfig is IntegrationDeployer, G2Operations, Constants { ); assertTrue(minStakeFlags.length != 0, "_configRand: invalid minimumStake, no flags passed"); assertTrue(fillTypeFlags.length != 0, "_configRand: invalid fillTypes, no flags passed"); - + assertTrue(quorumTypeFlags.length != 0, "_configRand: invalid quorumType, no flags passed"); // Decide how many quorums to initialize quorumCount = _randQuorumCount(); quorumBitmap = uint192((1 << quorumCount) - 1); @@ -172,19 +183,68 @@ contract IntegrationConfig is IntegrationDeployer, G2Operations, Constants { IStakeRegistryTypes.StrategyParams[] memory strategyParams = _randStrategyParams(); uint96 minimumStake = _randMinStake(); + uint256 quorumType = _randValue(quorumTypeFlags); + emit log_named_uint("_configRand: creating quorum", i); emit log_named_uint("- Max operator count", operatorSet.maxOperatorCount); emit log_named_uint("- Num strategies considered", strategyParams.length); emit log_named_uint("- Minimum stake", minimumStake); + emit log_named_uint("- Quorum type", quorumType); + + if (quorumType == DELEGATED_STAKE) { + cheats.prank(registryCoordinatorOwner); + slashingRegistryCoordinator.createTotalDelegatedStakeQuorum({ + operatorSetParams: operatorSet, + minimumStake: minimumStake, + strategyParams: strategyParams + }); + } else if (quorumType == SLASHABLE_STAKE) { + cheats.prank(registryCoordinatorOwner); + slashingRegistryCoordinator.createSlashableStakeQuorum({ + operatorSetParams: operatorSet, + minimumStake: minimumStake, + strategyParams: strategyParams, + lookAheadPeriod: 0 + }); + } else if (quorumType == BOTH) { + // randomly choose one of the two + uint256 randomChoice = _randValue(quorumTypeFlags); + if (randomChoice == DELEGATED_STAKE) { + cheats.prank(registryCoordinatorOwner); + slashingRegistryCoordinator.createTotalDelegatedStakeQuorum({ + operatorSetParams: operatorSet, + minimumStake: minimumStake, + strategyParams: strategyParams + }); + } else if (randomChoice == SLASHABLE_STAKE) { + cheats.prank(registryCoordinatorOwner); + slashingRegistryCoordinator.createSlashableStakeQuorum({ + operatorSetParams: operatorSet, + minimumStake: minimumStake, + strategyParams: strategyParams, + lookAheadPeriod: 0 + }); + } + } + + cheats.prank(address(serviceManager)); + allocationManager.updateAVSMetadataURI(address(serviceManager), "test-avs-metadata"); cheats.prank(registryCoordinatorOwner); - registryCoordinator.createTotalDelegatedStakeQuorum({ + slashingRegistryCoordinator.createTotalDelegatedStakeQuorum({ operatorSetParams: operatorSet, minimumStake: minimumStake, strategyParams: strategyParams }); } + /// Setup the RegistryCoordinator as M2 RegistryCoordinator with M2 quorums + /// TODO: refactor Integration framework to test both M2 upgrade path and new + /// registration/deregistration flow of operatorSets + _setOperatorSetsEnabled(false); + _setM2QuorumsDisabled(false); + _setM2QuorumBitmap(0); + // Decide how many operators to register for each quorum initially uint256 initialOperators = _randInitialOperators(operatorSet); emit log( @@ -243,10 +303,10 @@ contract IntegrationConfig is IntegrationDeployer, G2Operations, Constants { uint256 userType = _randValue(userFlags); if (userType == DEFAULT) { - user = new User(name, privKey, pubkey); + user = new OperatorSetUser(name, privKey, pubkey); } else if (userType == ALT_METHODS) { name = string.concat(name, "_Alt"); - user = new User_AltMethods(name, privKey, pubkey); + user = new OperatorSetUser_AltMethods(name, privKey, pubkey); } emit log_named_string("_randUser: Created user", user.NAME()); @@ -322,7 +382,7 @@ contract IntegrationConfig is IntegrationDeployer, G2Operations, Constants { uint8 quorum = uint8(churnQuorums[i]); ISlashingRegistryCoordinatorTypes.OperatorSetParam memory params = - registryCoordinator.getOperatorSetParams(quorum); + slashingRegistryCoordinator.getOperatorSetParams(quorum); // Sanity check - make sure we're at the operator cap uint32 curNumOperators = indexRegistry.totalOperatorsForQuorum(quorum); @@ -395,7 +455,7 @@ contract IntegrationConfig is IntegrationDeployer, G2Operations, Constants { for (uint256 i = 0; i < quorums.length; i++) { uint8 quorum = uint8(quorums[i]); uint32 maxOperatorCount = - registryCoordinator.getOperatorSetParams(quorum).maxOperatorCount; + slashingRegistryCoordinator.getOperatorSetParams(quorum).maxOperatorCount; // Continue deregistering until we're under the cap // This uses while in case we tested a config change that lowered the max count diff --git a/test/integration/IntegrationDeployer.t.sol b/test/integration/IntegrationDeployer.t.sol index 3e38d697..625a9e9b 100644 --- a/test/integration/IntegrationDeployer.t.sol +++ b/test/integration/IntegrationDeployer.t.sol @@ -40,6 +40,7 @@ import "src/libraries/BitmapUtils.sol"; import "eigenlayer-contracts/src/test/mocks/EmptyContract.sol"; // import "src/test/integration/mocks/ServiceManagerMock.t.sol"; import "test/integration/User.t.sol"; +import "test/integration/OperatorSetUser.t.sol"; abstract contract IntegrationDeployer is Test, IUserDeployer { using Strings for *; @@ -56,15 +57,16 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { IBeacon eigenPodBeacon; EigenPod pod; ETHPOSDepositMock ethPOSDeposit; - AllocationManager allocationManager; + AllocationManager public allocationManager; PermissionController permissionController; // Base strategy implementation in case we want to create more strategies later StrategyBase baseStrategyImplementation; // Middleware contracts to deploy + SlashingRegistryCoordinator public slashingRegistryCoordinator; RegistryCoordinator public registryCoordinator; - ServiceManagerMock serviceManager; + ServiceManagerMock public serviceManager; BLSApkRegistry blsApkRegistry; StakeRegistry stakeRegistry; IndexRegistry indexRegistry; @@ -90,6 +92,12 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { address ejector = address(uint160(uint256(keccak256("ejector")))); address rewardsUpdater = address(uint160(uint256(keccak256("rewardsUpdater")))); + /// @dev Account identifier for the AVS. This is the unique identifier address for the AVS in the AllocationManager. + /// This address is by default a UAM PermissionController admin and can transfer admin permissions to other addresses. + /// For existing AVSs using ServiceManagers, this should be the same address as the ServiceManager and there exist + /// interfaces on the ServiceManager to interact with UAM. + address avsAccountIdentifier = address(uint160(uint256(keccak256("avsAccountIdentifier")))); + // Constants/Defaults uint64 constant GENESIS_TIME_LOCAL = 1 hours * 12; uint256 constant MIN_BALANCE = 1e6; @@ -314,6 +322,11 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { new TransparentUpgradeableProxy(address(emptyContract), address(proxyAdmin), "") ) ); + slashingRegistryCoordinator = SlashingRegistryCoordinator( + address( + new TransparentUpgradeableProxy(address(emptyContract), address(proxyAdmin), "") + ) + ); stakeRegistry = StakeRegistry( address( @@ -347,25 +360,25 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { cheats.stopPrank(); StakeRegistry stakeRegistryImplementation = new StakeRegistry( - ISlashingRegistryCoordinator(registryCoordinator), + ISlashingRegistryCoordinator(slashingRegistryCoordinator), IDelegationManager(delegationManager), IAVSDirectory(avsDirectory), allocationManager ); BLSApkRegistry blsApkRegistryImplementation = - new BLSApkRegistry(ISlashingRegistryCoordinator(registryCoordinator)); + new BLSApkRegistry(ISlashingRegistryCoordinator(slashingRegistryCoordinator)); IndexRegistry indexRegistryImplementation = - new IndexRegistry(ISlashingRegistryCoordinator(registryCoordinator)); + new IndexRegistry(ISlashingRegistryCoordinator(slashingRegistryCoordinator)); ServiceManagerMock serviceManagerImplementation = new ServiceManagerMock( IAVSDirectory(avsDirectory), rewardsCoordinator, - ISlashingRegistryCoordinator(registryCoordinator), + ISlashingRegistryCoordinator(slashingRegistryCoordinator), stakeRegistry, permissionController, allocationManager ); SocketRegistry socketRegistryImplementation = - new SocketRegistry(IRegistryCoordinator(registryCoordinator)); + new SocketRegistry(ISlashingRegistryCoordinator(slashingRegistryCoordinator)); proxyAdmin.upgrade( TransparentUpgradeableProxy(payable(address(stakeRegistry))), @@ -419,11 +432,33 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { churnApprover, ejector, 0, /*initialPausedStatus*/ - new IRegistryCoordinator.OperatorSetParam[](0), - new uint96[](0), - new IStakeRegistryTypes.StrategyParams[][](0), - quorumStakeTypes, - slashableStakeQuorumLookAheadPeriods + address(serviceManager) /* accountIdentifier */ + ) + ); + + SlashingRegistryCoordinator slashingRegistryCoordinatorImplementation = new SlashingRegistryCoordinator( + stakeRegistry, + blsApkRegistry, + indexRegistry, + socketRegistry, + allocationManager, + pauserRegistry + ); + cheats.prank(avsAccountIdentifier); + allocationManager.updateAVSMetadataURI( + address(avsAccountIdentifier), "ipfs://mock-metadata-uri" + ); + + proxyAdmin.upgradeAndCall( + TransparentUpgradeableProxy(payable(address(slashingRegistryCoordinator))), + address(slashingRegistryCoordinatorImplementation), + abi.encodeWithSelector( + SlashingRegistryCoordinator.initialize.selector, + registryCoordinatorOwner, + churnApprover, + ejector, + 0, /*initialPausedStatus*/ + avsAccountIdentifier /* accountIdentifier */ ) ); @@ -437,34 +472,80 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { target: address(allocationManager), selector: IAllocationManager.setAVSRegistrar.selector }); - // 2. create operator sets + + // 2. set AVS metadata + serviceManager.setAppointee({ + appointee: serviceManager.owner(), + target: address(allocationManager), + selector: IAllocationManager.updateAVSMetadataURI.selector + }); + // 3. create operator sets serviceManager.setAppointee({ appointee: address(registryCoordinator), target: address(allocationManager), selector: IAllocationManager.createOperatorSets.selector }); - // 3. deregister operator from operator sets + // 4. deregister operator from operator sets serviceManager.setAppointee({ appointee: address(registryCoordinator), target: address(allocationManager), selector: IAllocationManager.deregisterFromOperatorSets.selector }); - // 4. add strategies to operator sets + // 5. add strategies to operator sets serviceManager.setAppointee({ appointee: address(registryCoordinator), target: address(stakeRegistry), selector: IAllocationManager.addStrategiesToOperatorSet.selector }); - // 5. remove strategies from operator sets + // 6. remove strategies from operator sets serviceManager.setAppointee({ appointee: address(registryCoordinator), target: address(stakeRegistry), selector: IAllocationManager.removeStrategiesFromOperatorSet.selector }); cheats.stopPrank(); - _setOperatorSetsEnabled(false); _setM2QuorumsDisabled(false); + _setM2QuorumBitmap(0); + + /// Setup UAM Permissions for SlashingRegistryCoordinator + cheats.startPrank(avsAccountIdentifier); + permissionController.setAppointee({ + account: avsAccountIdentifier, + appointee: address(avsAccountIdentifier), + target: address(allocationManager), + selector: IAllocationManager.setAVSRegistrar.selector + }); + permissionController.setAppointee({ + account: avsAccountIdentifier, + appointee: address(slashingRegistryCoordinator), + target: address(allocationManager), + selector: IAllocationManager.createOperatorSets.selector + }); + permissionController.setAppointee({ + account: avsAccountIdentifier, + appointee: address(slashingRegistryCoordinator), + target: address(allocationManager), + selector: IAllocationManager.deregisterFromOperatorSets.selector + }); + permissionController.setAppointee({ + account: avsAccountIdentifier, + appointee: address(stakeRegistry), + target: address(allocationManager), + selector: IAllocationManager.addStrategiesToOperatorSet.selector + }); + permissionController.setAppointee({ + account: avsAccountIdentifier, + appointee: address(stakeRegistry), + target: address(allocationManager), + selector: IAllocationManager.removeStrategiesFromOperatorSet.selector + }); + // set AVS Registrar to slashingRegistryCoordinator + allocationManager.setAVSRegistrar( + avsAccountIdentifier, IAVSRegistrar(address(slashingRegistryCoordinator)) + ); + + cheats.stopPrank(); } /// @notice Overwrite RegistryCoordinator.operatorSetsEnabled to the specified value. @@ -474,14 +555,14 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { ) internal { // 1. First read the current value of the entire slot // which holds operatorSetsEnabled, m2QuorumsDisabled, and accountIdentifier - bytes32 currentSlot = cheats.load(address(registryCoordinator), bytes32(uint256(161))); + bytes32 currentSlot = cheats.load(address(registryCoordinator), bytes32(uint256(200))); // 2. Clear only the first byte (operatorSetsEnabled) while keeping the rest bytes32 newSlot = (currentSlot & ~bytes32(uint256(0xff))) | bytes32(uint256(operatorSetsEnabled ? 0x01 : 0x00)); // 3. Store the modified slot - cheats.store(address(registryCoordinator), bytes32(uint256(161)), newSlot); + cheats.store(address(registryCoordinator), bytes32(uint256(200)), newSlot); } /// @notice Overwrite RegistryCoordinator.m2QuorumsDisabled to the specified value. @@ -490,14 +571,23 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { ) internal { // 1. First read the current value of the entire slot // which holds operatorSetsEnabled, m2QuorumsDisabled, and accountIdentifier - bytes32 currentSlot = cheats.load(address(registryCoordinator), bytes32(uint256(161))); + bytes32 currentSlot = cheats.load(address(registryCoordinator), bytes32(uint256(200))); // 2. Clear only the second byte (m2QuorumsDisabled) while keeping the rest bytes32 newSlot = (currentSlot & ~bytes32(uint256(0xff) << 8)) | bytes32(uint256(m2QuorumsDisabled ? 0x01 : 0x00) << 8); // 3. Store the modified slot - cheats.store(address(registryCoordinator), bytes32(uint256(161)), newSlot); + cheats.store(address(registryCoordinator), bytes32(uint256(200)), newSlot); + } + + /// @notice Overwrite RegistryCoordinator._m2QuorumBitmap to the specified value + function _setM2QuorumBitmap( + uint256 m2QuorumBitmap + ) internal { + bytes32 currentSlot = cheats.load(address(registryCoordinator), bytes32(uint256(200))); + + cheats.store(address(registryCoordinator), bytes32(uint256(200)), bytes32(m2QuorumBitmap)); } /// @dev Deploy a strategy and its underlying token, push to global lists of tokens/strategies, and whitelist diff --git a/test/integration/OperatorSetUser.t.sol b/test/integration/OperatorSetUser.t.sol new file mode 100644 index 00000000..371c5770 --- /dev/null +++ b/test/integration/OperatorSetUser.t.sol @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "test/integration/User.t.sol"; +import "forge-std/console.sol"; + +contract OperatorSetUser is User { + using BN254 for *; + using Strings for *; + using BitmapStrings for *; + using BitmapUtils for *; + + SlashingRegistryCoordinator slashingRegistryCoordinator; + AllocationManager allocationManager; + address avs; + + constructor( + string memory name, + uint256 _privKey, + IBLSApkRegistryTypes.PubkeyRegistrationParams memory _pubkeyParams + ) User(name, _privKey, _pubkeyParams) { + IUserDeployer deployer = IUserDeployer(msg.sender); + + slashingRegistryCoordinator = deployer.slashingRegistryCoordinator(); + allocationManager = AllocationManager(deployer.allocationManager()); + + avs = slashingRegistryCoordinator.avs(); + + // Generate BN254 keypair and registration signature + privKey = _privKey; + pubkeyParams = _pubkeyParams; + + BN254.G1Point memory registrationMessageHash = + slashingRegistryCoordinator.pubkeyRegistrationMessageHash(address(this)); + pubkeyParams.pubkeyRegistrationSignature = registrationMessageHash.scalar_mul(privKey); + + operatorId = pubkeyParams.pubkeyG1.hashG1Point(); + } + + function registerOperator( + bytes calldata quorums + ) public virtual override createSnapshot returns (bytes32) { + _log("registerOperator", quorums); + + vm.warp(block.timestamp + 1); + // If operator has previously registered and is still slashable, roll blocks until they are + // no longer slashable + vm.roll(block.number + uint256(allocationManager.DEALLOCATION_DELAY()) + 1); + + // encode bytes data field passed into SlashingRegistryCoordinator.registerOperator + // Register params for AllocationManager + bytes memory data = abi.encode( + ISlashingRegistryCoordinatorTypes.RegistrationType.NORMAL, NAME, pubkeyParams + ); + IAllocationManagerTypes.RegisterParams memory registerParams = IAllocationManagerTypes + .RegisterParams({avs: avs, operatorSetIds: _getOperatorSetIds(quorums), data: data}); + + allocationManager.registerForOperatorSets({operator: address(this), params: registerParams}); + + return pubkeyParams.pubkeyG1.hashG1Point(); + } + + function registerOperatorWithChurn( + bytes calldata churnQuorums, + User[] calldata churnTargets, + bytes calldata standardQuorums + ) public virtual override createSnapshot { + _logChurn("registerOperatorWithChurn", churnQuorums, churnTargets, standardQuorums); + + vm.warp(block.timestamp + 1); + // If operator has previously registered and is still slashable, roll blocks until they are + // no longer slashable + vm.roll(block.number + uint256(allocationManager.DEALLOCATION_DELAY()) + 1); + + // Sanity check input: + // - churnQuorums and churnTargets should have equal length + // - churnQuorums and standardQuorums should not have any bits in common + uint192 churnBitmap = uint192(churnQuorums.orderedBytesArrayToBitmap()); + uint192 standardBitmap = uint192(standardQuorums.orderedBytesArrayToBitmap()); + assertEq( + churnQuorums.length, + churnTargets.length, + "User.registerOperatorWithChurn: input length mismatch" + ); + assertTrue( + churnBitmap.noBitsInCommon(standardBitmap), + "User.registerOperatorWithChurn: input quorums have common bits" + ); + bytes memory allQuorums = churnBitmap.plus(standardBitmap).bitmapToBytesArray(); + + ( + ISlashingRegistryCoordinatorTypes.OperatorKickParam[] memory kickParams, + ISignatureUtils.SignatureWithSaltAndExpiry memory churnApproverSignature + ) = _generateOperatorKickParams(allQuorums, churnQuorums, churnTargets, standardQuorums); + + // Encode with RegistrationType.CHURN + bytes memory data = abi.encode( + ISlashingRegistryCoordinatorTypes.RegistrationType.CHURN, + NAME, + pubkeyParams, + kickParams, + churnApproverSignature + ); + + IAllocationManagerTypes.RegisterParams memory registerParams = IAllocationManagerTypes + .RegisterParams({avs: avs, operatorSetIds: _getOperatorSetIds(allQuorums), data: data}); + allocationManager.registerForOperatorSets({operator: address(this), params: registerParams}); + } + + function deregisterOperator( + bytes calldata quorums + ) public virtual override createSnapshot { + _log("deregisterOperator", quorums); + uint32[] memory operatorSetIds = _getOperatorSetIds(quorums); + IAllocationManagerTypes.DeregisterParams memory deregisterParams = IAllocationManagerTypes + .DeregisterParams({avs: avs, operator: address(this), operatorSetIds: operatorSetIds}); + allocationManager.deregisterFromOperatorSets({params: deregisterParams}); + } + + /// @dev Uses updateOperators to update this user's stake + function updateStakes() public virtual override createSnapshot { + _log("updateStakes (updateOperators)"); + + // get all quorums this operator is registered for + uint192 currentBitmap = slashingRegistryCoordinator.getCurrentQuorumBitmap(operatorId); + bytes memory quorumNumbers = currentBitmap.bitmapToBytesArray(); + + // get all operators in those quorums + address[][] memory operatorsPerQuorum = new address[][](quorumNumbers.length); + for (uint256 i = 0; i < quorumNumbers.length; i++) { + bytes32[] memory operatorIds = indexRegistry.getOperatorListAtBlockNumber( + uint8(quorumNumbers[i]), uint32(block.number) + ); + operatorsPerQuorum[i] = new address[](operatorIds.length); + for (uint256 j = 0; j < operatorIds.length; j++) { + operatorsPerQuorum[i][j] = blsApkRegistry.pubkeyHashToOperator(operatorIds[j]); + } + + operatorsPerQuorum[i] = Sort.sortAddresses(operatorsPerQuorum[i]); + } + slashingRegistryCoordinator.updateOperatorsForQuorum(operatorsPerQuorum, quorumNumbers); + } + + function _getOperatorSetIds( + bytes memory quorums + ) internal pure returns (uint32[] memory) { + uint32[] memory operatorSetIds = new uint32[](quorums.length); + for (uint256 i = 0; i < quorums.length; i++) { + operatorSetIds[i] = uint32(uint8(quorums[i])); + } + return operatorSetIds; + } + + function _generateOperatorKickParams( + bytes memory allQuorums, + bytes calldata churnQuorums, + User[] calldata churnTargets, + bytes calldata standardQuorums + ) + internal + virtual + override + returns ( + ISlashingRegistryCoordinatorTypes.OperatorKickParam[] memory, + ISignatureUtils.SignatureWithSaltAndExpiry memory + ) + { + ISlashingRegistryCoordinator.OperatorKickParam[] memory kickParams = + new ISlashingRegistryCoordinator.OperatorKickParam[](allQuorums.length); + + // this constructs OperatorKickParam[] in ascending quorum order + // (yikes) + uint256 churnIdx; + uint256 stdIdx; + while (churnIdx + stdIdx < allQuorums.length) { + if (churnIdx == churnQuorums.length) { + kickParams[churnIdx + stdIdx] = ISlashingRegistryCoordinatorTypes.OperatorKickParam({ + quorumNumber: 0, + operator: address(0) + }); + stdIdx++; + } else if ( + stdIdx == standardQuorums.length || churnQuorums[churnIdx] < standardQuorums[stdIdx] + ) { + kickParams[churnIdx + stdIdx] = ISlashingRegistryCoordinatorTypes.OperatorKickParam({ + quorumNumber: uint8(churnQuorums[churnIdx]), + operator: address(churnTargets[churnIdx]) + }); + churnIdx++; + } else if (standardQuorums[stdIdx] < churnQuorums[churnIdx]) { + kickParams[churnIdx + stdIdx] = ISlashingRegistryCoordinatorTypes.OperatorKickParam({ + quorumNumber: 0, + operator: address(0) + }); + stdIdx++; + } else { + revert("User.registerOperatorWithChurn: malformed input"); + } + } + + // Generate churn approver signature + bytes32 _salt = keccak256(abi.encodePacked(++salt, address(this))); + uint256 expiry = type(uint256).max; + bytes32 digest = slashingRegistryCoordinator.calculateOperatorChurnApprovalDigestHash({ + registeringOperator: address(this), + registeringOperatorId: operatorId, + operatorKickParams: kickParams, + salt: _salt, + expiry: expiry + }); + + // Sign digest + (uint8 v, bytes32 r, bytes32 s) = cheats.sign(churnApproverPrivateKey, digest); + bytes memory signature = new bytes(65); + assembly { + mstore(add(signature, 0x20), r) + mstore(add(signature, 0x40), s) + } + signature[signature.length - 1] = bytes1(v); + ISignatureUtils.SignatureWithSaltAndExpiry memory churnApproverSignature = ISignatureUtils + .SignatureWithSaltAndExpiry({signature: signature, salt: _salt, expiry: expiry}); + + return (kickParams, churnApproverSignature); + } +} + +contract OperatorSetUser_AltMethods is OperatorSetUser { + using BitmapUtils for *; + + modifier createSnapshot() virtual override { + cheats.roll(block.number + 1); + timeMachine.createSnapshot(); + _; + } + + constructor( + string memory name, + uint256 _privKey, + IBLSApkRegistryTypes.PubkeyRegistrationParams memory _pubkeyParams + ) OperatorSetUser(name, _privKey, _pubkeyParams) {} + + /// @dev Rather than calling deregisterOperator, this pranks the ejector and calls + /// ejectOperator + function deregisterOperator( + bytes calldata quorums + ) public virtual override createSnapshot { + _log("deregisterOperator (eject)", quorums); + + address ejector = slashingRegistryCoordinator.ejector(); + + cheats.prank(ejector); + slashingRegistryCoordinator.ejectOperator(address(this), quorums); + } + + /// @dev Uses updateOperatorsForQuorum to update stakes of all operators in all quorums + function updateStakes() public virtual override createSnapshot { + _log("updateStakes (updateOperatorsForQuorum)"); + + bytes memory allQuorums = + ((1 << slashingRegistryCoordinator.quorumCount()) - 1).bitmapToBytesArray(); + address[][] memory operatorsPerQuorum = new address[][](allQuorums.length); + + for (uint256 i = 0; i < allQuorums.length; i++) { + uint8 quorum = uint8(allQuorums[i]); + bytes32[] memory operatorIds = + indexRegistry.getOperatorListAtBlockNumber(quorum, uint32(block.number)); + + operatorsPerQuorum[i] = new address[](operatorIds.length); + + for (uint256 j = 0; j < operatorIds.length; j++) { + operatorsPerQuorum[i][j] = blsApkRegistry.getOperatorFromPubkeyHash(operatorIds[j]); + } + + operatorsPerQuorum[i] = Sort.sortAddresses(operatorsPerQuorum[i]); + } + + slashingRegistryCoordinator.updateOperatorsForQuorum(operatorsPerQuorum, allQuorums); + } +} diff --git a/test/integration/User.t.sol b/test/integration/User.t.sol index 8a1a9b61..3824c421 100644 --- a/test/integration/User.t.sol +++ b/test/integration/User.t.sol @@ -14,6 +14,7 @@ import "eigenlayer-contracts/src/contracts/core/DelegationManager.sol"; import "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; import "eigenlayer-contracts/src/contracts/core/StrategyManager.sol"; import "eigenlayer-contracts/src/contracts/core/AVSDirectory.sol"; +import "eigenlayer-contracts/src/contracts/core/AllocationManager.sol"; // Middleware import "src/interfaces/IRegistryCoordinator.sol"; @@ -28,10 +29,14 @@ import "src/libraries/BitmapUtils.sol"; import "test/integration/TimeMachine.t.sol"; import "test/integration/utils/Sort.t.sol"; import "test/integration/utils/BitmapStrings.t.sol"; +import "test/mocks/ServiceManagerMock.sol"; interface IUserDeployer { + function slashingRegistryCoordinator() external view returns (SlashingRegistryCoordinator); function registryCoordinator() external view returns (RegistryCoordinator); function avsDirectory() external view returns (AVSDirectory); + function allocationManager() external view returns (AllocationManager); + function serviceManager() external view returns (ServiceManagerMock); function timeMachine() external view returns (TimeMachine); function churnApproverPrivateKey() external view returns (uint256); function churnApprover() external view returns (address); @@ -82,7 +87,7 @@ contract User is Test { registryCoordinator = deployer.registryCoordinator(); avsDirectory = deployer.avsDirectory(); - serviceManager = ServiceManagerBase(address(registryCoordinator.serviceManager())); + serviceManager = ServiceManagerBase(address(deployer.serviceManager())); blsApkRegistry = BLSApkRegistry(address(registryCoordinator.blsApkRegistry())); stakeRegistry = StakeRegistry(address(registryCoordinator.stakeRegistry())); @@ -124,6 +129,7 @@ contract User is Test { _log("registerOperator", quorums); vm.warp(block.timestamp + 1); + registryCoordinator.registerOperator({ quorumNumbers: quorums, socket: NAME, @@ -161,61 +167,10 @@ contract User is Test { bytes memory allQuorums = churnBitmap.plus(standardBitmap).bitmapToBytesArray(); - ISlashingRegistryCoordinator.OperatorKickParam[] memory kickParams = - new ISlashingRegistryCoordinator.OperatorKickParam[](allQuorums.length); - - // this constructs OperatorKickParam[] in ascending quorum order - // (yikes) - uint256 churnIdx; - uint256 stdIdx; - while (churnIdx + stdIdx < allQuorums.length) { - if (churnIdx == churnQuorums.length) { - kickParams[churnIdx + stdIdx] = ISlashingRegistryCoordinatorTypes.OperatorKickParam({ - quorumNumber: 0, - operator: address(0) - }); - stdIdx++; - } else if ( - stdIdx == standardQuorums.length || churnQuorums[churnIdx] < standardQuorums[stdIdx] - ) { - kickParams[churnIdx + stdIdx] = ISlashingRegistryCoordinatorTypes.OperatorKickParam({ - quorumNumber: uint8(churnQuorums[churnIdx]), - operator: address(churnTargets[churnIdx]) - }); - churnIdx++; - } else if (standardQuorums[stdIdx] < churnQuorums[churnIdx]) { - kickParams[churnIdx + stdIdx] = ISlashingRegistryCoordinatorTypes.OperatorKickParam({ - quorumNumber: 0, - operator: address(0) - }); - stdIdx++; - } else { - revert("User.registerOperatorWithChurn: malformed input"); - } - } - - // Generate churn approver signature - bytes32 _salt = keccak256(abi.encodePacked(++salt, address(this))); - uint256 expiry = type(uint256).max; - bytes32 digest = registryCoordinator.calculateOperatorChurnApprovalDigestHash({ - registeringOperator: address(this), - registeringOperatorId: operatorId, - operatorKickParams: kickParams, - salt: _salt, - expiry: expiry - }); - - // Sign digest - (uint8 v, bytes32 r, bytes32 s) = cheats.sign(churnApproverPrivateKey, digest); - bytes memory signature = new bytes(65); - assembly { - mstore(add(signature, 0x20), r) - mstore(add(signature, 0x40), s) - } - signature[signature.length - 1] = bytes1(v); - - ISignatureUtils.SignatureWithSaltAndExpiry memory churnApproverSignature = ISignatureUtils - .SignatureWithSaltAndExpiry({signature: signature, salt: _salt, expiry: expiry}); + ( + ISlashingRegistryCoordinatorTypes.OperatorKickParam[] memory kickParams, + ISignatureUtils.SignatureWithSaltAndExpiry memory churnApproverSignature + ) = _generateOperatorKickParams(allQuorums, churnQuorums, churnTargets, standardQuorums); vm.warp(block.timestamp + 1); registryCoordinator.registerOperatorWithChurn({ @@ -240,10 +195,24 @@ contract User is Test { function updateStakes() public virtual createSnapshot { _log("updateStakes (updateOperators)"); - address[] memory addrs = new address[](1); - addrs[0] = address(this); + // get all quorums this operator is registered for + uint192 currentBitmap = registryCoordinator.getCurrentQuorumBitmap(operatorId); + bytes memory quorumNumbers = currentBitmap.bitmapToBytesArray(); - registryCoordinator.updateOperators(addrs); + // get all operators in those quorums + address[][] memory operatorsPerQuorum = new address[][](quorumNumbers.length); + for (uint256 i = 0; i < quorumNumbers.length; i++) { + bytes32[] memory operatorIds = indexRegistry.getOperatorListAtBlockNumber( + uint8(quorumNumbers[i]), uint32(block.number) + ); + operatorsPerQuorum[i] = new address[](operatorIds.length); + for (uint256 j = 0; j < operatorIds.length; j++) { + operatorsPerQuorum[i][j] = blsApkRegistry.pubkeyHashToOperator(operatorIds[j]); + } + + operatorsPerQuorum[i] = Sort.sortAddresses(operatorsPerQuorum[i]); + } + registryCoordinator.updateOperatorsForQuorum(operatorsPerQuorum, quorumNumbers); } /** @@ -375,6 +344,77 @@ contract User is Test { emit log_named_string("- churnTargets", targetString); } + + function _generateOperatorKickParams( + bytes memory allQuorums, + bytes calldata churnQuorums, + User[] calldata churnTargets, + bytes calldata standardQuorums + ) + internal + virtual + returns ( + ISlashingRegistryCoordinatorTypes.OperatorKickParam[] memory, + ISignatureUtils.SignatureWithSaltAndExpiry memory + ) + { + ISlashingRegistryCoordinator.OperatorKickParam[] memory kickParams = + new ISlashingRegistryCoordinator.OperatorKickParam[](allQuorums.length); + + // this constructs OperatorKickParam[] in ascending quorum order + // (yikes) + uint256 churnIdx; + uint256 stdIdx; + while (churnIdx + stdIdx < allQuorums.length) { + if (churnIdx == churnQuorums.length) { + kickParams[churnIdx + stdIdx] = ISlashingRegistryCoordinatorTypes.OperatorKickParam({ + quorumNumber: 0, + operator: address(0) + }); + stdIdx++; + } else if ( + stdIdx == standardQuorums.length || churnQuorums[churnIdx] < standardQuorums[stdIdx] + ) { + kickParams[churnIdx + stdIdx] = ISlashingRegistryCoordinatorTypes.OperatorKickParam({ + quorumNumber: uint8(churnQuorums[churnIdx]), + operator: address(churnTargets[churnIdx]) + }); + churnIdx++; + } else if (standardQuorums[stdIdx] < churnQuorums[churnIdx]) { + kickParams[churnIdx + stdIdx] = ISlashingRegistryCoordinatorTypes.OperatorKickParam({ + quorumNumber: 0, + operator: address(0) + }); + stdIdx++; + } else { + revert("User.registerOperatorWithChurn: malformed input"); + } + } + + // Generate churn approver signature + bytes32 _salt = keccak256(abi.encodePacked(++salt, address(this))); + uint256 expiry = type(uint256).max; + bytes32 digest = registryCoordinator.calculateOperatorChurnApprovalDigestHash({ + registeringOperator: address(this), + registeringOperatorId: operatorId, + operatorKickParams: kickParams, + salt: _salt, + expiry: expiry + }); + + // Sign digest + (uint8 v, bytes32 r, bytes32 s) = cheats.sign(churnApproverPrivateKey, digest); + bytes memory signature = new bytes(65); + assembly { + mstore(add(signature, 0x20), r) + mstore(add(signature, 0x40), s) + } + signature[signature.length - 1] = bytes1(v); + ISignatureUtils.SignatureWithSaltAndExpiry memory churnApproverSignature = ISignatureUtils + .SignatureWithSaltAndExpiry({signature: signature, salt: _salt, expiry: expiry}); + + return (kickParams, churnApproverSignature); + } } contract User_AltMethods is User { diff --git a/test/integration/tests/Full_Register_Deregister.t.sol b/test/integration/tests/Full_Register_Deregister.t.sol index be7f672b..875df9cd 100644 --- a/test/integration/tests/Full_Register_Deregister.t.sol +++ b/test/integration/tests/Full_Register_Deregister.t.sol @@ -21,7 +21,8 @@ contract Integration_Full_Register_Deregister is IntegrationChecks { numQuorums: ONE | TWO | MANY, numStrategies: ONE | TWO | MANY, minimumStake: NO_MINIMUM | HAS_MINIMUM, - fillTypes: FULL + fillTypes: FULL, + quorumType: DELEGATED_STAKE }) }); @@ -71,7 +72,8 @@ contract Integration_Full_Register_Deregister is IntegrationChecks { numQuorums: ONE | TWO | MANY, numStrategies: ONE | TWO | MANY, minimumStake: NO_MINIMUM | HAS_MINIMUM, - fillTypes: FULL + fillTypes: FULL, + quorumType: DELEGATED_STAKE }) }); @@ -136,7 +138,8 @@ contract Integration_Full_Register_Deregister is IntegrationChecks { numQuorums: ONE | TWO | MANY, numStrategies: ONE | TWO | MANY, minimumStake: NO_MINIMUM | HAS_MINIMUM, - fillTypes: FULL + fillTypes: FULL, + quorumType: DELEGATED_STAKE }) }); diff --git a/test/integration/tests/NonFull_Register_CoreBalanceChange_Update.t.sol b/test/integration/tests/NonFull_Register_CoreBalanceChange_Update.t.sol index 679ea1b5..43851e12 100644 --- a/test/integration/tests/NonFull_Register_CoreBalanceChange_Update.t.sol +++ b/test/integration/tests/NonFull_Register_CoreBalanceChange_Update.t.sol @@ -20,7 +20,8 @@ contract Integration_NonFull_Register_CoreBalanceChange_Update is IntegrationChe numQuorums: ONE | TWO | MANY, numStrategies: ONE | TWO | MANY, minimumStake: HAS_MINIMUM, - fillTypes: EMPTY | SOME_FILL + fillTypes: EMPTY | SOME_FILL, + quorumType: DELEGATED_STAKE }) }); @@ -64,7 +65,8 @@ contract Integration_NonFull_Register_CoreBalanceChange_Update is IntegrationChe numQuorums: ONE | TWO | MANY, numStrategies: ONE | TWO | MANY, minimumStake: HAS_MINIMUM, - fillTypes: EMPTY | SOME_FILL + fillTypes: EMPTY | SOME_FILL, + quorumType: DELEGATED_STAKE }) }); @@ -103,7 +105,8 @@ contract Integration_NonFull_Register_CoreBalanceChange_Update is IntegrationChe numQuorums: ONE | TWO | MANY, numStrategies: ONE | TWO | MANY, minimumStake: HAS_MINIMUM, - fillTypes: EMPTY | SOME_FILL + fillTypes: EMPTY | SOME_FILL, + quorumType: DELEGATED_STAKE }) }); @@ -138,7 +141,8 @@ contract Integration_NonFull_Register_CoreBalanceChange_Update is IntegrationChe numQuorums: ONE | TWO | MANY, numStrategies: ONE | TWO | MANY, minimumStake: HAS_MINIMUM, - fillTypes: EMPTY | SOME_FILL + fillTypes: EMPTY | SOME_FILL, + quorumType: DELEGATED_STAKE }) }); @@ -174,7 +178,8 @@ contract Integration_NonFull_Register_CoreBalanceChange_Update is IntegrationChe numQuorums: ONE | TWO | MANY, numStrategies: ONE | TWO | MANY, minimumStake: HAS_MINIMUM, - fillTypes: EMPTY | SOME_FILL + fillTypes: EMPTY | SOME_FILL, + quorumType: DELEGATED_STAKE }) }); diff --git a/test/integration/tests/NonFull_Register_Deregister.t.sol b/test/integration/tests/NonFull_Register_Deregister.t.sol index 5eea66e4..e0acf31f 100644 --- a/test/integration/tests/NonFull_Register_Deregister.t.sol +++ b/test/integration/tests/NonFull_Register_Deregister.t.sol @@ -20,7 +20,8 @@ contract Integration_NonFull_Register_Deregister is IntegrationChecks { numQuorums: ONE | TWO | MANY, numStrategies: ONE | TWO | MANY, minimumStake: NO_MINIMUM | HAS_MINIMUM, - fillTypes: EMPTY | SOME_FILL + fillTypes: EMPTY | SOME_FILL, + quorumType: DELEGATED_STAKE }) }); @@ -52,7 +53,8 @@ contract Integration_NonFull_Register_Deregister is IntegrationChecks { numQuorums: ONE | TWO | MANY, numStrategies: ONE | TWO | MANY, minimumStake: NO_MINIMUM | HAS_MINIMUM, - fillTypes: EMPTY | SOME_FILL + fillTypes: EMPTY | SOME_FILL, + quorumType: DELEGATED_STAKE }) }); @@ -93,7 +95,8 @@ contract Integration_NonFull_Register_Deregister is IntegrationChecks { numQuorums: ONE | TWO | MANY, numStrategies: ONE | TWO | MANY, minimumStake: NO_MINIMUM | HAS_MINIMUM, - fillTypes: EMPTY | SOME_FILL + fillTypes: EMPTY | SOME_FILL, + quorumType: DELEGATED_STAKE }) }); diff --git a/test/mocks/AVSRegistrarMock.sol b/test/mocks/AVSRegistrarMock.sol index 9a61b26e..ae7d2077 100644 --- a/test/mocks/AVSRegistrarMock.sol +++ b/test/mocks/AVSRegistrarMock.sol @@ -6,12 +6,20 @@ import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSR contract AVSRegistrarMock is IAVSRegistrar { function registerOperator( address operator, + address avs, uint32[] calldata operatorSetIds, bytes calldata data ) external override {} function deregisterOperator( address operator, + address avs, uint32[] calldata operatorSetIds ) external override {} + + function supportsAVS( + address + ) external pure override returns (bool) { + return true; + } } diff --git a/test/mocks/DelegationMock.sol b/test/mocks/DelegationMock.sol index 11bce967..9aadf9e2 100644 --- a/test/mocks/DelegationMock.sol +++ b/test/mocks/DelegationMock.sol @@ -232,7 +232,7 @@ contract DelegationIntermediate is IDelegationManager { IStrategy strategy, uint64 prevMaxMagnitude, uint64 newMaxMagnitude - ) external override {} + ) external {} function getQueuedWithdrawal( bytes32 withdrawalRoot @@ -282,6 +282,20 @@ contract DelegationMock is DelegationIntermediate { return shares; } + function getOperatorsShares( + address[] memory operators, + IStrategy[] memory strategies + ) external view override returns (uint256[][] memory) { + uint256[][] memory operatorSharesArray = new uint256[][](operators.length); + for (uint256 i = 0; i < operators.length; i++) { + operatorSharesArray[i] = new uint256[](strategies.length); + for (uint256 j = 0; j < strategies.length; j++) { + operatorSharesArray[i][j] = _weightOf[operators[i]][strategies[j]]; + } + } + return operatorSharesArray; + } + function minWithdrawalDelayBlocks() external view override returns (uint32) { return 10000; } diff --git a/test/mocks/RewardsCoordinatorMock.sol b/test/mocks/RewardsCoordinatorMock.sol index 5a76bd46..d376db78 100644 --- a/test/mocks/RewardsCoordinatorMock.sol +++ b/test/mocks/RewardsCoordinatorMock.sol @@ -32,18 +32,18 @@ contract RewardsCoordinatorMock is IRewardsCoordinator { function createOperatorDirectedOperatorSetRewardsSubmission( OperatorSet calldata operatorSet, OperatorDirectedRewardsSubmission[] calldata operatorDirectedRewardsSubmissions - ) external override {} + ) external {} function getOperatorSetSplit( address operator, OperatorSet calldata operatorSet - ) external view override returns (uint16) {} + ) external view returns (uint16) {} function setOperatorSetSplit( address operator, OperatorSet calldata operatorSet, uint16 split - ) external override {} + ) external {} function createOperatorDirectedAVSRewardsSubmission( address avs, diff --git a/test/mocks/StakeRegistryMock.sol b/test/mocks/StakeRegistryMock.sol index f428928e..9ddfa002 100644 --- a/test/mocks/StakeRegistryMock.sol +++ b/test/mocks/StakeRegistryMock.sol @@ -264,12 +264,12 @@ contract StakeRegistryMock is IStakeRegistry { * If the operator no longer has the minimum stake required for a quorum, they are * added to the */ - function updateOperatorStake( - address, /*operator*/ - bytes32, /*operatorId*/ - bytes calldata /*quorumNumbers*/ - ) external returns (uint192) { - return updateOperatorStakeReturnBitmap; + function updateOperatorsStake( + address[] memory operators, + bytes32[] memory, + uint8 + ) external pure returns (bool[] memory) { + return new bool[](operators.length); } function getMockOperatorId( diff --git a/test/unit/AVSRegistrar.t.sol b/test/unit/AVSRegistrar.t.sol index 1dd90226..93658852 100644 --- a/test/unit/AVSRegistrar.t.sol +++ b/test/unit/AVSRegistrar.t.sol @@ -21,6 +21,8 @@ contract AVSRegistrarTest is MockAVSDeployer { function setUp() public virtual { _deployMockEigenLayerAndAVS(); + vm.prank(address(serviceManager)); + allocationManager.updateAVSMetadataURI(address(serviceManager), "test-avs-metadata"); avsRegistrarMock = new AVSRegistrarMock(); } diff --git a/test/unit/BLSApkRegistryUnit.t.sol b/test/unit/BLSApkRegistryUnit.t.sol index 337190f0..76f06716 100644 --- a/test/unit/BLSApkRegistryUnit.t.sol +++ b/test/unit/BLSApkRegistryUnit.t.sol @@ -927,4 +927,33 @@ contract BLSApkRegistryUnitTests_quorumApkUpdates is BLSApkRegistryUnitTests { assertEq(quorumApk.Y, 0, "quorum apk not set to zero"); } } + + /** + * @dev test that attempting to get APK indices for a block number before the first update reverts + */ + function testFuzz_quorumApkUpdates_BlockNumberBeforeFirstUpdate( + uint32 blockNumber, + uint8 quorumNumber + ) external { + // Initialize quorum if not already initialized + if (!initializedQuorums[quorumNumber]) { + _initializeFuzzedQuorum(quorumNumber); + } + + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(quorumNumber); + + // Register an operator to create first update + address operator = _selectNewOperator(); + _registerDefaultBLSPubkey(operator); + _registerOperator(operator, quorumNumbers); + uint32 firstUpdateBlock = uint32(block.number); + + // Ensure blockNumber is before first update + cheats.assume(blockNumber < firstUpdateBlock); + + // Expect revert when querying block before first update + cheats.expectRevert(IBLSApkRegistryErrors.BlockNumberBeforeFirstUpdate.selector); + blsApkRegistry.getApkIndicesAtBlockNumber(quorumNumbers, blockNumber); + } } diff --git a/test/unit/BLSSignatureCheckerUnit.t.sol b/test/unit/BLSSignatureCheckerUnit.t.sol index ef13c575..9f3ea9c0 100644 --- a/test/unit/BLSSignatureCheckerUnit.t.sol +++ b/test/unit/BLSSignatureCheckerUnit.t.sol @@ -582,4 +582,75 @@ contract BLSSignatureCheckerUnitTests is BLSMockAVSDeployer { msgHash, quorumNumbers, referenceBlockNumber, nonSignerStakesAndSignature ); } + + function test_trySignatureAndApkVerification_success() public { + uint256 numNonSigners = 0; + uint256 quorumBitmap = 1; + ( + uint32 referenceBlockNumber, + BLSSignatureChecker.NonSignerStakesAndSignature memory nonSignerStakesAndSignature + ) = _registerSignatoriesAndGetNonSignerStakeAndSignatureRandom( + 1, numNonSigners, quorumBitmap + ); + + (bool pairingSuccessful, bool signatureIsValid) = blsSignatureChecker + .trySignatureAndApkVerification( + msgHash, + nonSignerStakesAndSignature.quorumApks[0], + nonSignerStakesAndSignature.apkG2, + nonSignerStakesAndSignature.sigma + ); + + assertTrue(pairingSuccessful, "Pairing should be successful"); + assertTrue(signatureIsValid, "Signature should be valid"); + } + + function test_trySignatureAndApkVerification_invalidSignature() public { + uint256 numNonSigners = 0; + uint256 quorumBitmap = 1; + ( + uint32 referenceBlockNumber, + BLSSignatureChecker.NonSignerStakesAndSignature memory nonSignerStakesAndSignature + ) = _registerSignatoriesAndGetNonSignerStakeAndSignatureRandom( + 1, numNonSigners, quorumBitmap + ); + + // Modify sigma to make it invalid + nonSignerStakesAndSignature.sigma.X++; + + cheats.expectRevert(); + blsSignatureChecker.trySignatureAndApkVerification( + msgHash, + nonSignerStakesAndSignature.quorumApks[0], + nonSignerStakesAndSignature.apkG2, + nonSignerStakesAndSignature.sigma + ); + } + + function test_trySignatureAndApkVerification_invalidPairing() public { + uint256 numNonSigners = 0; + uint256 quorumBitmap = 1; + ( + uint32 referenceBlockNumber, + BLSSignatureChecker.NonSignerStakesAndSignature memory nonSignerStakesAndSignature + ) = _registerSignatoriesAndGetNonSignerStakeAndSignatureRandom( + 1, numNonSigners, quorumBitmap + ); + + // Create invalid G2 point + BN254.G2Point memory invalidG2Point = BN254.G2Point( + [type(uint256).max, type(uint256).max], [type(uint256).max, type(uint256).max] + ); + + (bool pairingSuccessful, bool signatureIsValid) = blsSignatureChecker + .trySignatureAndApkVerification( + msgHash, + nonSignerStakesAndSignature.quorumApks[0], + invalidG2Point, + nonSignerStakesAndSignature.sigma + ); + + assertFalse(pairingSuccessful, "Pairing should fail"); + assertFalse(signatureIsValid, "Signature should be invalid"); + } } diff --git a/test/unit/ECDSAServiceManager.t.sol b/test/unit/ECDSAServiceManager.t.sol index aac2bd38..5afc891e 100644 --- a/test/unit/ECDSAServiceManager.t.sol +++ b/test/unit/ECDSAServiceManager.t.sol @@ -1,192 +1,217 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.27; - -// import {Test, console} from "forge-std/Test.sol"; - -// import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; -// import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; -// import {IRewardsCoordinator} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol"; -// import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; - -// import {ECDSAServiceManagerMock} from "../mocks/ECDSAServiceManagerMock.sol"; -// import {ECDSAStakeRegistryMock} from "../mocks/ECDSAStakeRegistryMock.sol"; -// import {IECDSAStakeRegistryTypes.Quorum, StrategyParams} from "../../src/interfaces/IECDSAStakeRegistry.sol"; - -// contract MockDelegationManager { -// function operatorShares(address, address) external pure returns (uint256) { -// return 1000; // Return a dummy value for simplicity -// } - -// function getOperatorShares( -// address, -// IStrategy[] memory strategies -// ) external pure returns (uint256[] memory) { -// uint256[] memory response = new uint256[](strategies.length); -// for (uint256 i; i < strategies.length; i++) { -// response[i] = 1000; -// } -// return response; // Return a dummy value for simplicity -// } -// } - -// contract MockAVSDirectory { -// function registerOperatorToAVS( -// address, -// ISignatureUtils.SignatureWithSaltAndExpiry memory -// ) external pure {} - -// function deregisterOperatorFromAVS(address) external pure {} - -// function updateAVSMetadataURI(string memory) external pure {} -// } - -// contract MockAllocationManager {} - -// contract MockRewardsCoordinator { -// function createAVSRewardsSubmission( -// address avs, -// IRewardsCoordinator.RewardsSubmission[] calldata -// ) external pure {} -// } - -// contract ECDSAServiceManagerSetup is Test { -// MockDelegationManager public mockDelegationManager; -// MockAVSDirectory public mockAVSDirectory; -// MockAllocationManager public mockAllocationManager; -// ECDSAStakeRegistryMock public mockStakeRegistry; -// MockRewardsCoordinator public mockRewardsCoordinator; -// ECDSAServiceManagerMock public serviceManager; -// address internal operator1; -// address internal operator2; -// uint256 internal operator1Pk; -// uint256 internal operator2Pk; - -// function setUp() public { -// mockDelegationManager = new MockDelegationManager(); -// mockAVSDirectory = new MockAVSDirectory(); -// mockAllocationManager = new MockAllocationManager(); -// mockStakeRegistry = new ECDSAStakeRegistryMock( -// IDelegationManager(address(mockDelegationManager)) -// ); -// mockRewardsCoordinator = new MockRewardsCoordinator(); - -// serviceManager = new ECDSAServiceManagerMock( -// address(mockAVSDirectory), -// address(mockStakeRegistry), -// address(mockRewardsCoordinator), -// address(mockDelegationManager), -// address(mockAllocationManager) -// ); - -// operator1Pk = 1; -// operator2Pk = 2; -// operator1 = vm.addr(operator1Pk); -// operator2 = vm.addr(operator2Pk); - -// // Create a quorum -// IECDSAStakeRegistryTypes.Quorum memory quorum = IECDSAStakeRegistryTypes.Quorum({strategies: new StrategyParams[](2)}); -// quorum.strategies[0] = StrategyParams({ -// strategy: IStrategy(address(420)), -// multiplier: 5000 -// }); -// quorum.strategies[1] = StrategyParams({ -// strategy: IStrategy(address(421)), -// multiplier: 5000 -// }); -// address[] memory operators = new address[](0); - -// vm.prank(mockStakeRegistry.owner()); -// mockStakeRegistry.initialize( -// address(serviceManager), -// 10_000, // Assuming a threshold weight of 10000 basis points -// quorum -// ); -// ISignatureUtils.SignatureWithSaltAndExpiry memory dummySignature; - -// vm.prank(operator1); -// mockStakeRegistry.registerOperatorWithSignature( -// dummySignature, -// operator1 -// ); - -// vm.prank(operator2); -// mockStakeRegistry.registerOperatorWithSignature( -// dummySignature, -// operator2 -// ); -// } - -// function testRegisterOperatorToAVS() public { -// address operator = operator1; -// ISignatureUtils.SignatureWithSaltAndExpiry memory signature; - -// vm.prank(address(mockStakeRegistry)); -// serviceManager.registerOperatorToAVS(operator, signature); -// } - -// function testDeregisterOperatorFromAVS() public { -// address operator = operator1; - -// vm.prank(address(mockStakeRegistry)); -// serviceManager.deregisterOperatorFromAVS(operator); -// } - -// function testGetRestakeableStrategies() public { -// address[] memory strategies = serviceManager.getRestakeableStrategies(); -// } - -// function testGetOperatorRestakedStrategies() public { -// address operator = operator1; -// address[] memory strategies = serviceManager -// .getOperatorRestakedStrategies(operator); -// } - -// function test_Regression_GetOperatorRestakedStrategies_NoShares() public { -// address operator = operator1; -// IStrategy[] memory strategies = new IStrategy[](2); -// strategies[0] = IStrategy(address(420)); -// strategies[1] = IStrategy(address(421)); - -// uint96[] memory shares = new uint96[](2); -// shares[0] = 0; -// shares[1] = 1; - -// vm.mockCall( -// address(mockDelegationManager), -// abi.encodeCall( -// IDelegationManager.getOperatorShares, -// (operator, strategies) -// ), -// abi.encode(shares) -// ); - -// address[] memory restakedStrategies = serviceManager -// .getOperatorRestakedStrategies(operator); -// assertEq( -// restakedStrategies.length, -// 1, -// "Expected no restaked strategies" -// ); -// } - -// function testUpdateAVSMetadataURI() public { -// string memory newURI = "https://new-metadata-uri.com"; - -// vm.prank(mockStakeRegistry.owner()); -// serviceManager.updateAVSMetadataURI(newURI); -// } - -// function testCreateAVSRewardsSubmission() public { -// IRewardsCoordinator.RewardsSubmission[] memory submissions; - -// vm.prank(serviceManager.rewardsInitiator()); -// serviceManager.createAVSRewardsSubmission(submissions); -// } - -// function testSetRewardsInitiator() public { -// address newInitiator = address(0x123); - -// vm.prank(mockStakeRegistry.owner()); -// serviceManager.setRewardsInitiator(newInitiator); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {Test, console} from "forge-std/Test.sol"; + +import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {IDelegationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IRewardsCoordinator} from + "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; + +import {ECDSAServiceManagerMock} from "../mocks/ECDSAServiceManagerMock.sol"; +import {ECDSAStakeRegistryMock} from "../mocks/ECDSAStakeRegistryMock.sol"; +import {IECDSAStakeRegistryTypes} from "../../src/interfaces/IECDSAStakeRegistry.sol"; + +contract MockDelegationManager { + function operatorShares(address, address) external pure returns (uint256) { + return 1000; // Return a dummy value for simplicity + } + + function getOperatorShares( + address, + IStrategy[] memory strategies + ) external pure returns (uint256[] memory) { + uint256[] memory response = new uint256[](strategies.length); + for (uint256 i; i < strategies.length; i++) { + response[i] = 1000; + } + return response; // Return a dummy value for simplicity + } +} + +contract MockAVSDirectory { + function registerOperatorToAVS( + address, + ISignatureUtils.SignatureWithSaltAndExpiry memory + ) external pure {} + + function deregisterOperatorFromAVS( + address + ) external pure {} + + function updateAVSMetadataURI( + string memory + ) external pure {} +} + +contract MockAllocationManager { + function setAVSRegistrar(address avs, address registrar) external {} +} + +contract MockRewardsCoordinator { + function createAVSRewardsSubmission( + address avs, + IRewardsCoordinator.RewardsSubmission[] calldata + ) external pure {} + + function createOperatorDirectedAVSRewardsSubmission( + address avs, + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] calldata + ) external pure {} + + function setClaimerFor( + address claimer + ) external pure {} +} + +contract ECDSAServiceManagerSetup is Test { + MockDelegationManager public mockDelegationManager; + MockAVSDirectory public mockAVSDirectory; + MockAllocationManager public mockAllocationManager; + ECDSAStakeRegistryMock public mockStakeRegistry; + MockRewardsCoordinator public mockRewardsCoordinator; + ECDSAServiceManagerMock public serviceManager; + address internal operator1; + address internal operator2; + uint256 internal operator1Pk; + uint256 internal operator2Pk; + + function setUp() public { + mockDelegationManager = new MockDelegationManager(); + mockAVSDirectory = new MockAVSDirectory(); + mockAllocationManager = new MockAllocationManager(); + mockStakeRegistry = + new ECDSAStakeRegistryMock(IDelegationManager(address(mockDelegationManager))); + mockRewardsCoordinator = new MockRewardsCoordinator(); + + serviceManager = new ECDSAServiceManagerMock( + address(mockAVSDirectory), + address(mockStakeRegistry), + address(mockRewardsCoordinator), + address(mockDelegationManager), + address(mockAllocationManager) + ); + + operator1Pk = 1; + operator2Pk = 2; + operator1 = vm.addr(operator1Pk); + operator2 = vm.addr(operator2Pk); + + // Create a quorum + IECDSAStakeRegistryTypes.Quorum memory quorum = IECDSAStakeRegistryTypes.Quorum({ + strategies: new IECDSAStakeRegistryTypes.StrategyParams[](2) + }); + quorum.strategies[0] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(420)), + multiplier: 5000 + }); + quorum.strategies[1] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(421)), + multiplier: 5000 + }); + address[] memory operators = new address[](0); + + vm.prank(mockStakeRegistry.owner()); + mockStakeRegistry.initialize( + address(serviceManager), + 10000, // Assuming a threshold weight of 10000 basis points + quorum + ); + ISignatureUtils.SignatureWithSaltAndExpiry memory dummySignature; + + vm.prank(operator1); + mockStakeRegistry.registerOperatorWithSignature(dummySignature, operator1); + + vm.prank(operator2); + mockStakeRegistry.registerOperatorWithSignature(dummySignature, operator2); + } + + function testRegisterOperatorToAVS() public { + address operator = operator1; + ISignatureUtils.SignatureWithSaltAndExpiry memory signature; + + vm.prank(address(mockStakeRegistry)); + serviceManager.registerOperatorToAVS(operator, signature); + } + + function testDeregisterOperatorFromAVS() public { + address operator = operator1; + + vm.prank(address(mockStakeRegistry)); + serviceManager.deregisterOperatorFromAVS(operator); + } + + function testGetRestakeableStrategies() public { + address[] memory strategies = serviceManager.getRestakeableStrategies(); + } + + function testGetOperatorRestakedStrategies() public { + address operator = operator1; + address[] memory strategies = serviceManager.getOperatorRestakedStrategies(operator); + } + + function test_Regression_GetOperatorRestakedStrategies_NoShares() public { + address operator = operator1; + IStrategy[] memory strategies = new IStrategy[](2); + strategies[0] = IStrategy(address(420)); + strategies[1] = IStrategy(address(421)); + + uint96[] memory shares = new uint96[](2); + shares[0] = 0; + shares[1] = 1; + + vm.mockCall( + address(mockDelegationManager), + abi.encodeCall(IDelegationManager.getOperatorShares, (operator, strategies)), + abi.encode(shares) + ); + + address[] memory restakedStrategies = serviceManager.getOperatorRestakedStrategies(operator); + assertEq(restakedStrategies.length, 1, "Expected no restaked strategies"); + } + + function testUpdateAVSMetadataURI() public { + string memory newURI = "https://new-metadata-uri.com"; + + vm.prank(mockStakeRegistry.owner()); + serviceManager.updateAVSMetadataURI(newURI); + } + + // function testCreateAVSRewardsSubmission() public { + // IRewardsCoordinator.RewardsSubmission[] memory submissions; + + // vm.prank(serviceManager.rewardsInitiator()); + // serviceManager.createAVSRewardsSubmission(submissions); + // } + + function testSetRewardsInitiator() public { + address newInitiator = address(0x123); + + vm.prank(mockStakeRegistry.owner()); + serviceManager.setRewardsInitiator(newInitiator); + } + + function testCreateOperatorDirectedAVSRewardsSubmission() public { + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] memory submissions; + + vm.prank(serviceManager.rewardsInitiator()); + serviceManager.createOperatorDirectedAVSRewardsSubmission(submissions); + } + + function testSetClaimerFor() public { + address claimer = address(0x123); + + vm.prank(mockStakeRegistry.owner()); + serviceManager.setClaimerFor(claimer); + } + + function testSetAVSRegistrar() public { + address registrar = address(0x123); + + vm.prank(mockStakeRegistry.owner()); + serviceManager.setAVSRegistrar(IAVSRegistrar(registrar)); + } +} diff --git a/test/unit/InstantSlasher.t.sol b/test/unit/InstantSlasher.t.sol new file mode 100644 index 00000000..2e919395 --- /dev/null +++ b/test/unit/InstantSlasher.t.sol @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {Test, console2 as console} from "forge-std/Test.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {InstantSlasher} from "../../src/slashers/InstantSlasher.sol"; +import { + IAllocationManager, + IAllocationManagerTypes +} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; +import {IRegistryCoordinator} from "../../src/interfaces/IRegistryCoordinator.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {ISlasher, ISlasherTypes, ISlasherErrors} from "../../src/interfaces/ISlasher.sol"; +import {ISlashingRegistryCoordinator} from "../../src/interfaces/ISlashingRegistryCoordinator.sol"; +import {IStakeRegistry, IStakeRegistryTypes} from "../../src/interfaces/IStakeRegistry.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {EmptyContract} from "eigenlayer-contracts/src/test/mocks/EmptyContract.sol"; +import {AllocationManager} from "eigenlayer-contracts/src/contracts/core/AllocationManager.sol"; +import {PermissionController} from + "eigenlayer-contracts/src/contracts/permissions/PermissionController.sol"; +import {PauserRegistry} from "eigenlayer-contracts/src/contracts/permissions/PauserRegistry.sol"; +import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol"; +import {IDelegationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IStrategyManager} from "eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; +import {DelegationMock} from "../mocks/DelegationMock.sol"; +import {SlashingRegistryCoordinator} from "../../src/SlashingRegistryCoordinator.sol"; +import {ISlashingRegistryCoordinatorTypes} from + "../../src/interfaces/ISlashingRegistryCoordinator.sol"; +import {IBLSApkRegistry, IBLSApkRegistryTypes} from "../../src/interfaces/IBLSApkRegistry.sol"; +import {IIndexRegistry} from "../../src/interfaces/IIndexRegistry.sol"; +import {ISocketRegistry} from "../../src/interfaces/ISocketRegistry.sol"; +import {CoreDeploymentLib} from "../utils/CoreDeployLib.sol"; +import { + OperatorWalletLib, + Operator, + Wallet, + BLSWallet, + SigningKeyOperationsLib +} from "../utils/OperatorWalletLib.sol"; +import {OperatorSet} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; +import {StrategyFactory} from "eigenlayer-contracts/src/contracts/strategies/StrategyFactory.sol"; +import {StakeRegistry} from "../../src/StakeRegistry.sol"; +import {BLSApkRegistry} from "../../src/BLSApkRegistry.sol"; +import {IndexRegistry} from "../../src/IndexRegistry.sol"; +import {SocketRegistry} from "../../src/SocketRegistry.sol"; +import {MiddlewareDeployLib} from "../utils/MiddlewareDeployLib.sol"; + +contract InstantSlasherTest is Test { + InstantSlasher public instantSlasher; + ProxyAdmin public proxyAdmin; + EmptyContract public emptyContract; + SlashingRegistryCoordinator public slashingRegistryCoordinator; + CoreDeploymentLib.DeploymentData public coreDeployment; + PauserRegistry public pauserRegistry; + ERC20Mock public mockToken; + StrategyFactory public strategyFactory; + StakeRegistry public stakeRegistry; + BLSApkRegistry public blsApkRegistry; + IndexRegistry public indexRegistry; + SocketRegistry public socketRegistry; + + address public slasher; + address public serviceManager; + Operator public operatorWallet; + IStrategy public mockStrategy; + address public proxyAdminOwner = address(uint160(uint256(keccak256("proxyAdminOwner")))); + address public pauser = address(uint160(uint256(keccak256("pauser")))); + address public unpauser = address(uint160(uint256(keccak256("unpauser")))); + address public churnApprover = address(uint160(uint256(keccak256("churnApprover")))); + address public ejector = address(uint160(uint256(keccak256("ejector")))); + + uint32 constant DEALLOCATION_DELAY = 7 days; + uint32 constant ALLOCATION_CONFIGURATION_DELAY = 1 days; + + function setUp() public { + serviceManager = address(0x2); + slasher = address(0x3); + operatorWallet = OperatorWalletLib.createOperator("operator"); + + mockToken = new ERC20Mock("Mock Token", "MOCK", address(this), 0); + + vm.startPrank(proxyAdminOwner); + proxyAdmin = new ProxyAdmin(); + emptyContract = new EmptyContract(); + + address[] memory pausers = new address[](1); + pausers[0] = pauser; + pauserRegistry = new PauserRegistry(pausers, unpauser); + + CoreDeploymentLib.DeploymentConfigData memory configData; + configData.strategyManager.initialOwner = proxyAdminOwner; + configData.strategyManager.initialStrategyWhitelister = proxyAdminOwner; + configData.strategyManager.initPausedStatus = 0; + + configData.delegationManager.initialOwner = proxyAdminOwner; + configData.delegationManager.minWithdrawalDelayBlocks = 50400; + configData.delegationManager.initPausedStatus = 0; + + configData.eigenPodManager.initialOwner = proxyAdminOwner; + configData.eigenPodManager.initPausedStatus = 0; + + configData.allocationManager.initialOwner = proxyAdminOwner; + configData.allocationManager.deallocationDelay = DEALLOCATION_DELAY; + configData.allocationManager.allocationConfigurationDelay = ALLOCATION_CONFIGURATION_DELAY; + configData.allocationManager.initPausedStatus = 0; + + configData.strategyFactory.initialOwner = proxyAdminOwner; + configData.strategyFactory.initPausedStatus = 0; + + configData.avsDirectory.initialOwner = proxyAdminOwner; + configData.avsDirectory.initPausedStatus = 0; + + configData.rewardsCoordinator.initialOwner = proxyAdminOwner; + configData.rewardsCoordinator.rewardsUpdater = + address(0x14dC79964da2C08b23698B3D3cc7Ca32193d9955); + configData.rewardsCoordinator.initPausedStatus = 0; + configData.rewardsCoordinator.activationDelay = 0; + configData.rewardsCoordinator.defaultSplitBips = 1000; + configData.rewardsCoordinator.calculationIntervalSeconds = 86400; + configData.rewardsCoordinator.maxRewardsDuration = 864000; + configData.rewardsCoordinator.maxRetroactiveLength = 86400; + configData.rewardsCoordinator.maxFutureLength = 86400; + configData.rewardsCoordinator.genesisRewardsTimestamp = 1672531200; + + configData.ethPOSDeposit.ethPOSDepositAddress = address(0x123); + + coreDeployment = CoreDeploymentLib.deployContracts(address(proxyAdmin), configData); + + address strategyManagerOwner = Ownable(coreDeployment.strategyManager).owner(); + vm.stopPrank(); + + vm.startPrank(strategyManagerOwner); + IStrategyManager(coreDeployment.strategyManager).setStrategyWhitelister( + coreDeployment.strategyFactory + ); + vm.stopPrank(); + + vm.startPrank(proxyAdminOwner); + mockStrategy = IStrategy( + StrategyFactory(coreDeployment.strategyFactory).deployNewStrategy( + IERC20(address(mockToken)) + ) + ); + vm.stopPrank(); + + MiddlewareDeployLib.MiddlewareDeployConfig memory middlewareConfig; + middlewareConfig.instantSlasher.initialOwner = proxyAdminOwner; + middlewareConfig.instantSlasher.slasher = slasher; + middlewareConfig.slashingRegistryCoordinator.initialOwner = proxyAdminOwner; + middlewareConfig.slashingRegistryCoordinator.churnApprover = churnApprover; + middlewareConfig.slashingRegistryCoordinator.ejector = ejector; + middlewareConfig.slashingRegistryCoordinator.initPausedStatus = 0; + middlewareConfig.slashingRegistryCoordinator.serviceManager = serviceManager; + middlewareConfig.socketRegistry.initialOwner = proxyAdminOwner; + middlewareConfig.indexRegistry.initialOwner = proxyAdminOwner; + middlewareConfig.stakeRegistry.initialOwner = proxyAdminOwner; + middlewareConfig.stakeRegistry.minimumStake = 1 ether; + middlewareConfig.stakeRegistry.strategyParams = 0; + middlewareConfig.stakeRegistry.delegationManager = coreDeployment.delegationManager; + middlewareConfig.stakeRegistry.avsDirectory = coreDeployment.avsDirectory; + middlewareConfig.instantSlasher.slasher = slasher; + { + IStakeRegistryTypes.StrategyParams[] memory stratParams = + new IStakeRegistryTypes.StrategyParams[](1); + stratParams[0] = + IStakeRegistryTypes.StrategyParams({strategy: mockStrategy, multiplier: 1 ether}); + middlewareConfig.stakeRegistry.strategyParamsArray = stratParams; + } + middlewareConfig.stakeRegistry.lookAheadPeriod = 0; + middlewareConfig.stakeRegistry.stakeType = IStakeRegistryTypes.StakeType(1); + middlewareConfig.blsApkRegistry.initialOwner = proxyAdminOwner; + + vm.startPrank(proxyAdminOwner); + MiddlewareDeployLib.MiddlewareDeployData memory middlewareDeployments = MiddlewareDeployLib + .deployMiddleware( + address(proxyAdmin), + coreDeployment.allocationManager, + address(pauserRegistry), + middlewareConfig + ); + vm.stopPrank(); + + vm.startPrank(serviceManager); + PermissionController(coreDeployment.permissionController).setAppointee( + address(serviceManager), + address(instantSlasher), + coreDeployment.allocationManager, + AllocationManager.slashOperator.selector + ); + + slashingRegistryCoordinator = + SlashingRegistryCoordinator(middlewareDeployments.slashingRegistryCoordinator); + instantSlasher = InstantSlasher(middlewareDeployments.instantSlasher); + + PermissionController(coreDeployment.permissionController).setAppointee( + address(serviceManager), + address(slashingRegistryCoordinator), + coreDeployment.allocationManager, + AllocationManager.createOperatorSets.selector + ); + + PermissionController(coreDeployment.permissionController).setAppointee( + address(serviceManager), + address(instantSlasher), + coreDeployment.allocationManager, + AllocationManager.slashOperator.selector + ); + + PermissionController(coreDeployment.permissionController).setAppointee( + address(serviceManager), + proxyAdminOwner, + coreDeployment.allocationManager, + AllocationManager.updateAVSMetadataURI.selector + ); + + vm.stopPrank(); + + uint8 quorumNumber = 0; + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = mockStrategy; + + uint96[] memory minimumStakes = new uint96[](1); + minimumStakes[0] = 1 ether; + + IStakeRegistryTypes.StrategyParams[] memory strategyParams = + new IStakeRegistryTypes.StrategyParams[](1); + strategyParams[0] = + IStakeRegistryTypes.StrategyParams({strategy: mockStrategy, multiplier: 1 ether}); + + ISlashingRegistryCoordinatorTypes.OperatorSetParam memory operatorSetParams = + ISlashingRegistryCoordinatorTypes.OperatorSetParam({ + maxOperatorCount: 10, + kickBIPsOfOperatorStake: 0, + kickBIPsOfTotalStake: 0 + }); + + vm.startPrank(proxyAdminOwner); + IAllocationManager(coreDeployment.allocationManager).updateAVSMetadataURI( + serviceManager, "fake-avs-metadata" + ); + slashingRegistryCoordinator.createSlashableStakeQuorum( + operatorSetParams, 1 ether, strategyParams, 0 + ); + vm.stopPrank(); + + vm.label(address(instantSlasher), "InstantSlasher Proxy"); + vm.label(address(slashingRegistryCoordinator), "SlashingRegistryCoordinator Proxy"); + vm.label(address(proxyAdmin), "ProxyAdmin"); + vm.label(coreDeployment.allocationManager, "AllocationManager Proxy"); + } + + function test_initialization() public { + assertEq(instantSlasher.slasher(), slasher); + } + + function _createMockSlashingParams() + internal + view + returns (IAllocationManagerTypes.SlashingParams memory) + { + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = mockStrategy; + + uint256[] memory wadsToSlash = new uint256[](1); + wadsToSlash[0] = 0.5e18; // 50% slash + + return IAllocationManagerTypes.SlashingParams({ + operator: operatorWallet.key.addr, + operatorSetId: 1, + strategies: strategies, + wadsToSlash: wadsToSlash, + description: "Test slashing" + }); + } + + function test_fulfillSlashingRequest_revert_notSlasher() public { + IAllocationManagerTypes.SlashingParams memory params = _createMockSlashingParams(); + vm.expectRevert(ISlasherErrors.OnlySlasher.selector); + instantSlasher.fulfillSlashingRequest(params); + } + + function test_fulfillSlashingRequest() public { + vm.skip(false); + vm.startPrank(operatorWallet.key.addr); + IDelegationManager(coreDeployment.delegationManager).registerAsOperator( + address(0), 1, "metadata" + ); + + // Mint tokens and deposit into strategy + uint256 depositAmount = 1 ether; + mockToken.mint(operatorWallet.key.addr, depositAmount); + mockToken.approve(address(coreDeployment.strategyManager), depositAmount); + IStrategyManager(coreDeployment.strategyManager).depositIntoStrategy( + mockStrategy, mockToken, depositAmount + ); + + // Set allocation delay + uint32 minDelay = 1; + IAllocationManager(coreDeployment.allocationManager).setAllocationDelay( + operatorWallet.key.addr, minDelay + ); + vm.stopPrank(); + + // Assert operator has allocation delay set + (bool isSet,) = IAllocationManager(coreDeployment.allocationManager).getAllocationDelay( + operatorWallet.key.addr + ); + assertFalse(isSet, "Operator allocation delay not set"); + + vm.roll(block.number + ALLOCATION_CONFIGURATION_DELAY + 1); + + // Set up allocation parameters + IStrategy[] memory allocStrategies = new IStrategy[](1); + allocStrategies[0] = mockStrategy; + + uint64[] memory magnitudes = new uint64[](1); + magnitudes[0] = uint64(1 ether); // Allocate full magnitude + + OperatorSet memory operatorSet = OperatorSet({avs: address(serviceManager), id: 0}); + + vm.startPrank(serviceManager); + IAllocationManagerTypes.CreateSetParams[] memory createParams = + new IAllocationManagerTypes.CreateSetParams[](1); + createParams[0] = + IAllocationManagerTypes.CreateSetParams({operatorSetId: 0, strategies: allocStrategies}); + IAllocationManager(coreDeployment.allocationManager).setAVSRegistrar( + address(serviceManager), IAVSRegistrar(address(slashingRegistryCoordinator)) + ); + vm.stopPrank(); + + vm.startPrank(operatorWallet.key.addr); + + IAllocationManagerTypes.AllocateParams[] memory allocParams = + new IAllocationManagerTypes.AllocateParams[](1); + allocParams[0] = IAllocationManagerTypes.AllocateParams({ + operatorSet: operatorSet, + strategies: allocStrategies, + newMagnitudes: magnitudes + }); + + IAllocationManager(coreDeployment.allocationManager).modifyAllocations( + operatorWallet.key.addr, allocParams + ); + vm.roll(block.number + 100); + + uint32[] memory operatorSetIds = new uint32[](1); + operatorSetIds[0] = 0; + // Create BLS signing key params + bytes32 messageHash = slashingRegistryCoordinator.calculatePubkeyRegistrationMessageHash( + operatorWallet.key.addr + ); + IBLSApkRegistryTypes.PubkeyRegistrationParams memory pubkeyParams = IBLSApkRegistryTypes + .PubkeyRegistrationParams({ + pubkeyRegistrationSignature: SigningKeyOperationsLib.sign( + operatorWallet.signingKey, messageHash + ), + pubkeyG1: operatorWallet.signingKey.publicKeyG1, + pubkeyG2: operatorWallet.signingKey.publicKeyG2 + }); + + // Encode registration data with socket and pubkey params + bytes memory registrationData = abi.encode( + ISlashingRegistryCoordinatorTypes.RegistrationType.NORMAL, "socket", pubkeyParams + ); + + IAllocationManagerTypes.RegisterParams memory registerParams = IAllocationManagerTypes + .RegisterParams({ + avs: address(serviceManager), + operatorSetIds: operatorSetIds, + data: registrationData + }); + IAllocationManager(coreDeployment.allocationManager).registerForOperatorSets( + operatorWallet.key.addr, registerParams + ); + vm.stopPrank(); + + vm.roll(block.number + 100); + + // Create slashing params + IAllocationManagerTypes.SlashingParams memory params = IAllocationManagerTypes + .SlashingParams({ + operator: operatorWallet.key.addr, + operatorSetId: 0, + strategies: allocStrategies, + wadsToSlash: new uint256[](allocStrategies.length), + description: "Test slashing" + }); + + // Set each wad to slash to 1e18 (100% slash) + for (uint256 i = 0; i < params.wadsToSlash.length; i++) { + params.wadsToSlash[i] = 1e18; + } + + // Execute slashing + vm.prank(slasher); + instantSlasher.fulfillSlashingRequest(params); + } +} diff --git a/test/unit/OperatorStateRetrieverUnit.t.sol b/test/unit/OperatorStateRetrieverUnit.t.sol index 79a93988..4020604f 100644 --- a/test/unit/OperatorStateRetrieverUnit.t.sol +++ b/test/unit/OperatorStateRetrieverUnit.t.sol @@ -649,4 +649,82 @@ contract OperatorStateRetrieverUnitTests is MockAVSDeployer { } } } + + function test_getBatchOperatorId_emptyArray() public { + address[] memory operators = new address[](0); + bytes32[] memory operatorIds = + operatorStateRetriever.getBatchOperatorId(registryCoordinator, operators); + assertEq(operatorIds.length, 0, "Should return empty array for empty input"); + } + + function test_getBatchOperatorId_unregisteredOperators() public { + address[] memory operators = new address[](2); + operators[0] = address(1); + operators[1] = address(2); + + bytes32[] memory operatorIds = + operatorStateRetriever.getBatchOperatorId(registryCoordinator, operators); + + assertEq(operatorIds.length, 2, "Should return array of same length as input"); + assertEq(operatorIds[0], bytes32(0), "Unregistered operator should return 0"); + assertEq(operatorIds[1], bytes32(0), "Unregistered operator should return 0"); + } + + function test_getBatchOperatorId_mixedRegistration() public { + // Register one operator + cheats.roll(registrationBlockNumber); + _registerOperatorWithCoordinator(defaultOperator, 1, defaultPubKey); + + // Create test array with one registered and one unregistered operator + address[] memory operators = new address[](2); + operators[0] = defaultOperator; + operators[1] = address(2); // unregistered + + bytes32[] memory operatorIds = + operatorStateRetriever.getBatchOperatorId(registryCoordinator, operators); + + assertEq(operatorIds.length, 2, "Should return array of same length as input"); + assertEq( + operatorIds[0], defaultOperatorId, "Should return correct ID for registered operator" + ); + assertEq(operatorIds[1], bytes32(0), "Should return 0 for unregistered operator"); + } + + function test_getBatchOperatorFromId_emptyArray() public { + bytes32[] memory operatorIds = new bytes32[](0); + address[] memory operators = + operatorStateRetriever.getBatchOperatorFromId(registryCoordinator, operatorIds); + assertEq(operators.length, 0, "Should return empty array for empty input"); + } + + function test_getBatchOperatorFromId_unregisteredIds() public { + bytes32[] memory operatorIds = new bytes32[](2); + operatorIds[0] = bytes32(uint256(1)); + operatorIds[1] = bytes32(uint256(2)); + + address[] memory operators = + operatorStateRetriever.getBatchOperatorFromId(registryCoordinator, operatorIds); + + assertEq(operators.length, 2, "Should return array of same length as input"); + assertEq(operators[0], address(0), "Unregistered ID should return address(0)"); + assertEq(operators[1], address(0), "Unregistered ID should return address(0)"); + } + + function test_getBatchOperatorFromId_mixedRegistration() public { + // Register one operator + cheats.roll(registrationBlockNumber); + _registerOperatorWithCoordinator(defaultOperator, 1, defaultPubKey); + + // Create test array with one registered and one unregistered operator ID + bytes32[] memory operatorIds = new bytes32[](2); + operatorIds[0] = defaultOperatorId; + operatorIds[1] = bytes32(uint256(2)); // unregistered + + address[] memory operators = + operatorStateRetriever.getBatchOperatorFromId(registryCoordinator, operatorIds); + + assertEq(operators.length, 2, "Should return array of same length as input"); + assertEq(operators[0], defaultOperator, "Should return correct address for registered ID"); + assertEq(operators[1], address(0), "Should return address(0) for unregistered ID"); + } } diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index a734171a..9711f7ee 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -8,10 +8,22 @@ import { ISlashingRegistryCoordinatorErrors } from "../../src/interfaces/ISlashingRegistryCoordinator.sol"; +import { + IRegistryCoordinator, + IRegistryCoordinatorTypes, + IRegistryCoordinatorErrors +} from "../../src/interfaces/IRegistryCoordinator.sol"; + import {IBLSApkRegistryTypes} from "../../src/interfaces/IBLSApkRegistry.sol"; import {QuorumBitmapHistoryLib} from "../../src/libraries/QuorumBitmapHistoryLib.sol"; import {BitmapUtils} from "../../src/libraries/BitmapUtils.sol"; import {console} from "forge-std/console.sol"; +import { + OperatorWalletLib, + SigningKeyOperationsLib, + OperatorKeyOperationsLib, + Operator +} from "../utils/OperatorWalletLib.sol"; contract RegistryCoordinatorUnitTests is MockAVSDeployer { using BN254 for BN254.G1Point; @@ -296,7 +308,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni quorumNumbersTooLarge[0] = 0xC0; - cheats.expectRevert(BitmapUtils.BitmapValueTooLarge.selector); + cheats.expectRevert(IRegistryCoordinatorErrors.OnlyM2QuorumsAllowed.selector); cheats.prank(defaultOperator); registryCoordinator.registerOperator( quorumNumbersTooLarge, defaultSocket, pubkeyRegistrationParams, emptySig @@ -311,7 +323,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni quorumNumbersNotCreated[0] = 0x0B; cheats.prank(defaultOperator); - cheats.expectRevert(BitmapUtils.BitmapValueTooLarge.selector); + cheats.expectRevert(IRegistryCoordinatorErrors.OnlyM2QuorumsAllowed.selector); registryCoordinator.registerOperator( quorumNumbersNotCreated, defaultSocket, pubkeyRegistrationParams, emptySig ); @@ -389,6 +401,8 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSocketUpdate(defaultOperatorId, defaultSocket); + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit OperatorRegistered(defaultOperator, defaultOperatorId); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); emit OperatorAddedToQuorums(defaultOperator, defaultOperatorId, quorumNumbers); for (uint256 i = 0; i < quorumNumbers.length; i++) { @@ -399,8 +413,6 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni cheats.expectEmit(true, true, true, true, address(indexRegistry)); emit QuorumIndexUpdate(defaultOperatorId, uint8(quorumNumbers[i]), 0); } - cheats.expectEmit(true, true, true, true, address(registryCoordinator)); - emit OperatorRegistered(defaultOperator, defaultOperatorId); uint256 gasBefore = gasleft(); cheats.prank(defaultOperator); @@ -630,6 +642,8 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSocketUpdate(defaultOperatorId, defaultSocket); + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit OperatorRegistered(defaultOperator, defaultOperatorId); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); emit OperatorAddedToQuorums(defaultOperator, defaultOperatorId, quorumNumbers); cheats.expectEmit(true, true, true, true, address(stakeRegistry)); @@ -682,10 +696,6 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is } function test_deregisterOperator_revert_notRegistered() public { - // enable operatorSets - cheats.prank(registryCoordinator.owner()); - registryCoordinator.enableOperatorSets(); - bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); @@ -695,10 +705,6 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is } function test_deregisterOperator_revert_incorrectQuorums() public { - // enable operatorSets - cheats.prank(registryCoordinator.owner()); - registryCoordinator.enableOperatorSets(); - console.log("quorumCount", registryCoordinator.quorumCount()); assertTrue( @@ -1708,6 +1714,9 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSocketUpdate(operatorToRegisterId, defaultSocket); + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit OperatorRegistered(operatorToRegister, operatorToRegisterId); + cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); emit OperatorAddedToQuorums(operatorToRegister, operatorToRegisterId, quorumNumbers); cheats.expectEmit(true, true, true, false, address(stakeRegistry)); @@ -1726,8 +1735,6 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord emit OperatorStakeUpdate(operatorToKickId, defaultQuorumNumber, 0); cheats.expectEmit(true, true, true, true, address(indexRegistry)); emit QuorumIndexUpdate(operatorToRegisterId, defaultQuorumNumber, numOperators - 1); - cheats.expectEmit(true, true, true, true, address(registryCoordinator)); - emit OperatorRegistered(operatorToRegister, operatorToRegisterId); { ISignatureUtils.SignatureWithSaltAndExpiry memory emptyAVSRegSig; @@ -2328,8 +2335,12 @@ contract RegistryCoordinatorUnitTests_BeforeMigration is RegistryCoordinatorUnit function test_registerALMHook_Reverts() public { cheats.prank(address(registryCoordinator.allocationManager())); cheats.expectRevert(); + /// TODO: registryCoordinator.registerOperator( - defaultOperator, new uint32[](0), abi.encode(defaultSocket, pubkeyRegistrationParams) + defaultOperator, + address(0), + new uint32[](0), + abi.encode(defaultSocket, pubkeyRegistrationParams) ); } @@ -2338,7 +2349,8 @@ contract RegistryCoordinatorUnitTests_BeforeMigration is RegistryCoordinatorUnit operatorSetIds[0] = 0; cheats.prank(address(registryCoordinator.allocationManager())); cheats.expectRevert(); - registryCoordinator.deregisterOperator(defaultOperator, operatorSetIds); + /// TODO: + registryCoordinator.deregisterOperator(defaultOperator, address(0), operatorSetIds); } function test_CreateTotalDelegatedStakeQuorum() public { @@ -2395,83 +2407,81 @@ contract RegistryCoordinatorUnitTests_BeforeMigration is RegistryCoordinatorUnit }); uint32 lookAheadPeriod = 100; + assertEq(registryCoordinator.quorumCount(), 0, "No quorums should exist before"); + // Attempt to create quorum with slashable stake type before enabling operator sets cheats.prank(registryCoordinatorOwner); - cheats.expectRevert(); registryCoordinator.createSlashableStakeQuorum( operatorSetParams, minimumStake, strategyParams, lookAheadPeriod ); - } - - function test_MigrateToOperatorSets() public { - _deployMockEigenLayerAndAVS(0); - cheats.prank(registryCoordinatorOwner); - registryCoordinator.enableOperatorSets(); - assertTrue(registryCoordinator.operatorSetsEnabled()); + assertEq(registryCoordinator.quorumCount(), 1, "New quorum 0 should be created"); + assertFalse(registryCoordinator.isM2Quorum(0), "Quorum created should not be an M2 quorum"); } } contract RegistryCoordinatorUnitTests_AfterMigration is RegistryCoordinatorUnitTests { - function test_MigrateToOperatorSets() public { - cheats.prank(registryCoordinatorOwner); - registryCoordinator.enableOperatorSets(); - assertTrue(registryCoordinator.operatorSetsEnabled()); - } - function test_M2_Deregister() public { - vm.skip(true); - /// Create 2 M2 quorums _deployMockEigenLayerAndAVS(2); - address operatorToRegister = address(420); + Operator memory operatorToRegister = OperatorWalletLib.createOperator("4"); - ISignatureUtils.SignatureWithSaltAndExpiry memory emptySignature = ISignatureUtils - .SignatureWithSaltAndExpiry({signature: new bytes(0), salt: bytes32(0), expiry: 0}); + /// NOTE: resolves stack too deep + { + // Set operator shares for quorum 0 + (IStrategy strategy,) = stakeRegistry.strategyParams(0, 0); + delegationMock.setOperatorShares(operatorToRegister.key.addr, strategy, 1 ether); + } + bytes32 salt = bytes32(uint256(1)); + uint256 expiry = block.timestamp + 1 days; + bytes32 digestHash = avsDirectory.calculateOperatorAVSRegistrationDigestHash( + operatorToRegister.key.addr, address(registryCoordinator), salt, expiry + ); + bytes memory signature = OperatorKeyOperationsLib.sign(operatorToRegister.key, digestHash); + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature = ISignatureUtils + .SignatureWithSaltAndExpiry({signature: signature, salt: salt, expiry: expiry}); + bytes32 messageHash = + registryCoordinator.calculatePubkeyRegistrationMessageHash(operatorToRegister.key.addr); IBLSApkRegistryTypes.PubkeyRegistrationParams memory operatorRegisterApkParams = IBLSApkRegistryTypes.PubkeyRegistrationParams({ - pubkeyRegistrationSignature: BN254.G1Point({X: 0, Y: 0}), - pubkeyG1: BN254.G1Point({X: 0, Y: 0}), - pubkeyG2: BN254.G2Point({X: [uint256(0), uint256(0)], Y: [uint256(0), uint256(0)]}) + pubkeyRegistrationSignature: SigningKeyOperationsLib.sign( + operatorToRegister.signingKey, messageHash + ), + pubkeyG1: operatorToRegister.signingKey.publicKeyG1, + pubkeyG2: operatorToRegister.signingKey.publicKeyG2 }); string memory socket = "socket"; // register for quorum 0 - vm.prank(operatorToRegister); + vm.prank(operatorToRegister.key.addr); registryCoordinator.registerOperator( new bytes(1), // Convert 0 to bytes1 first socket, operatorRegisterApkParams, - emptySignature + operatorSignature ); - /// migrate to operator sets - registryCoordinator.enableOperatorSets(); - /// Deregistration for m2 should for the first two operator sets - vm.prank(defaultOperator); + vm.prank(operatorToRegister.key.addr); registryCoordinator.deregisterOperator(new bytes(1)); // Verify operator was deregistered by checking their bitmap is empty - bytes32 operatorId = registryCoordinator.getOperatorId(operatorToRegister); + bytes32 operatorId = registryCoordinator.getOperatorId(operatorToRegister.key.addr); uint192 bitmap = registryCoordinator.getCurrentQuorumBitmap(operatorId); assertEq(bitmap, 0, "Operator bitmap should be empty after deregistration"); // Verify operator status is NEVER_REGISTERED ISlashingRegistryCoordinatorTypes.OperatorStatus status = - registryCoordinator.getOperatorStatus(operatorToRegister); + registryCoordinator.getOperatorStatus(operatorToRegister.key.addr); assertEq( uint8(status), - uint8(ISlashingRegistryCoordinatorTypes.OperatorStatus.NEVER_REGISTERED), + uint8(ISlashingRegistryCoordinatorTypes.OperatorStatus.DEREGISTERED), "Operator status should be NEVER_REGISTERED" ); } function test_M2_Register_Reverts() public { - cheats.prank(registryCoordinatorOwner); - registryCoordinator.enableOperatorSets(); - bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(uint8(0)); IBLSApkRegistryTypes.PubkeyRegistrationParams memory params; @@ -2487,10 +2497,6 @@ contract RegistryCoordinatorUnitTests_AfterMigration is RegistryCoordinatorUnitT // Deploy with 0 quorums _deployMockEigenLayerAndAVS(0); - // Enable operator sets first - cheats.prank(registryCoordinatorOwner); - registryCoordinator.enableOperatorSets(); - // Create quorum params ISlashingRegistryCoordinatorTypes.OperatorSetParam memory operatorSetParams = ISlashingRegistryCoordinatorTypes.OperatorSetParam({ @@ -2516,10 +2522,6 @@ contract RegistryCoordinatorUnitTests_AfterMigration is RegistryCoordinatorUnitT // Deploy with 0 quorums _deployMockEigenLayerAndAVS(0); - // Enable operator sets first - cheats.prank(registryCoordinatorOwner); - registryCoordinator.enableOperatorSets(); - // Create quorum params ISlashingRegistryCoordinatorTypes.OperatorSetParam memory operatorSetParams = ISlashingRegistryCoordinatorTypes.OperatorSetParam({ @@ -2545,10 +2547,6 @@ contract RegistryCoordinatorUnitTests_AfterMigration is RegistryCoordinatorUnitT _deployMockEigenLayerAndAVS(0); - // Enable operator sets first - cheats.prank(registryCoordinatorOwner); - registryCoordinator.enableOperatorSets(); - // Create quorum params ISlashingRegistryCoordinatorTypes.OperatorSetParam memory operatorSetParams = ISlashingRegistryCoordinatorTypes.OperatorSetParam({ @@ -2584,15 +2582,15 @@ contract RegistryCoordinatorUnitTests_AfterMigration is RegistryCoordinatorUnitT abi.encode(ISlashingRegistryCoordinatorTypes.RegistrationType.NORMAL, socket, params); cheats.prank(address(registryCoordinator.allocationManager())); - registryCoordinator.registerOperator(defaultOperator, operatorSetIds, data); + /// TODO: + registryCoordinator.registerOperator( + defaultOperator, address(serviceManager), operatorSetIds, data + ); } function test_registerHook_WithChurn() public { vm.skip(true); _deployMockEigenLayerAndAVS(0); - // Enable operator sets first - cheats.prank(registryCoordinatorOwner); - registryCoordinator.enableOperatorSets(); // Create quorum params ISlashingRegistryCoordinatorTypes.OperatorSetParam memory operatorSetParams = @@ -2644,7 +2642,9 @@ contract RegistryCoordinatorUnitTests_AfterMigration is RegistryCoordinatorUnitT // Prank as allocation manager and call register hook cheats.prank(address(registryCoordinator.allocationManager())); - registryCoordinator.registerOperator(defaultOperator, operatorSetIds, data); + registryCoordinator.registerOperator( + defaultOperator, address(serviceManager), operatorSetIds, data + ); } function test_updateStakesForQuorum() public { @@ -2676,9 +2676,6 @@ contract RegistryCoordinatorUnitTests_AfterMigration is RegistryCoordinatorUnitT function test_deregisterHook() public { _deployMockEigenLayerAndAVS(0); - // Enable operator sets first - cheats.prank(registryCoordinatorOwner); - registryCoordinator.enableOperatorSets(); // Create quorum params ISlashingRegistryCoordinatorTypes.OperatorSetParam memory operatorSetParams = @@ -2716,9 +2713,9 @@ contract RegistryCoordinatorUnitTests_AfterMigration is RegistryCoordinatorUnitT abi.encode(ISlashingRegistryCoordinatorTypes.RegistrationType.NORMAL, socket, params); cheats.startPrank(address(registryCoordinator.allocationManager())); - registryCoordinator.registerOperator(defaultOperator, operatorSetIds, data); - - // registryCoordinator.deregisterOperator(defaultOperator, operatorSetIds); + registryCoordinator.registerOperator( + defaultOperator, address(serviceManager), operatorSetIds, data + ); cheats.stopPrank(); } @@ -2726,10 +2723,6 @@ contract RegistryCoordinatorUnitTests_AfterMigration is RegistryCoordinatorUnitT function test_registerHook_Reverts_WhenNotALM() public { _deployMockEigenLayerAndAVS(0); - // Enable operator sets first - cheats.prank(registryCoordinatorOwner); - registryCoordinator.enableOperatorSets(); - // Create quorum params ISlashingRegistryCoordinatorTypes.OperatorSetParam memory operatorSetParams = ISlashingRegistryCoordinatorTypes.OperatorSetParam({ @@ -2765,18 +2758,14 @@ contract RegistryCoordinatorUnitTests_AfterMigration is RegistryCoordinatorUnitT abi.encode(ISlashingRegistryCoordinatorTypes.RegistrationType.NORMAL, socket, params); vm.expectRevert(); - registryCoordinator.registerOperator(defaultOperator, operatorSetIds, data); + registryCoordinator.registerOperator( + defaultOperator, address(serviceManager), operatorSetIds, data + ); } function test_deregisterHook_Reverts_WhenNotALM() public { _deployMockEigenLayerAndAVS(0); - // Enable operator sets first - cheats.prank(registryCoordinatorOwner); - registryCoordinator.enableOperatorSets(); - - assertTrue(registryCoordinator.operatorSetsEnabled(), "operatorSetsEnabled should be true"); - // Create quorum params ISlashingRegistryCoordinatorTypes.OperatorSetParam memory operatorSetParams = ISlashingRegistryCoordinatorTypes.OperatorSetParam({ @@ -2795,6 +2784,9 @@ contract RegistryCoordinatorUnitTests_AfterMigration is RegistryCoordinatorUnitT cheats.prank(registryCoordinatorOwner); registryCoordinator.createTotalDelegatedStakeQuorum(operatorSetParams, 0, strategyParams); + // operator sets should be enabled after creating a new quorum + assertTrue(registryCoordinator.operatorSetsEnabled(), "operatorSetsEnabled should be true"); + // Prank as allocation manager and call register hook uint32[] memory operatorSetIds = new uint32[](1); operatorSetIds[0] = 0; @@ -2812,10 +2804,14 @@ contract RegistryCoordinatorUnitTests_AfterMigration is RegistryCoordinatorUnitT abi.encode(ISlashingRegistryCoordinatorTypes.RegistrationType.NORMAL, socket, params); cheats.prank(address(registryCoordinator.allocationManager())); - registryCoordinator.registerOperator(defaultOperator, operatorSetIds, data); + registryCoordinator.registerOperator( + defaultOperator, address(serviceManager), operatorSetIds, data + ); cheats.expectRevert(); - registryCoordinator.deregisterOperator(defaultOperator, operatorSetIds); + registryCoordinator.deregisterOperator( + defaultOperator, address(serviceManager), operatorSetIds + ); } function test_DeregisterHook_Reverts_WhenM2Quorum() public { diff --git a/test/unit/ServiceManagerBase.t.sol b/test/unit/ServiceManagerBase.t.sol index b2494e64..72cb562e 100644 --- a/test/unit/ServiceManagerBase.t.sol +++ b/test/unit/ServiceManagerBase.t.sol @@ -15,6 +15,11 @@ import {IStrategyManager} from "eigenlayer-contracts/src/contracts/interfaces/IS import {IServiceManagerBaseEvents} from "../events/IServiceManagerBaseEvents.sol"; import {IServiceManagerErrors} from "../../src/interfaces/IServiceManager.sol"; +import { + IAllocationManagerTypes, + IAllocationManager +} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; + import "../utils/MockAVSDeployer.sol"; contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEvents { @@ -544,4 +549,383 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve cheats.prank(caller); serviceManager.setRewardsInitiator(newRewardsInitiator); } + + function testFuzz_addPendingAdmin( + address admin + ) public filterFuzzedAddressInputs(admin) { + // Mock the expected call to permissionController + cheats.expectCall( + address(permissionControllerMock), + abi.encodeCall(PermissionController.addPendingAdmin, (address(serviceManager), admin)) + ); + + // Call should only work from owner + cheats.prank(serviceManagerOwner); + serviceManager.addPendingAdmin(admin); + } + + function testFuzz_addPendingAdmin_revert_notOwner( + address admin, + address caller + ) public filterFuzzedAddressInputs(admin) filterFuzzedAddressInputs(caller) { + cheats.assume(caller != serviceManagerOwner); + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(caller); + serviceManager.addPendingAdmin(admin); + } + + function testFuzz_removePendingAdmin( + address pendingAdmin + ) public filterFuzzedAddressInputs(pendingAdmin) { + // Mock the expected call to permissionController + cheats.expectCall( + address(permissionControllerMock), + abi.encodeCall( + PermissionController.removePendingAdmin, (address(serviceManager), pendingAdmin) + ) + ); + + // Call should only work from owner + cheats.prank(serviceManagerOwner); + serviceManager.removePendingAdmin(pendingAdmin); + } + + function testFuzz_removePendingAdmin_revert_notOwner( + address pendingAdmin, + address caller + ) public filterFuzzedAddressInputs(pendingAdmin) filterFuzzedAddressInputs(caller) { + cheats.assume(caller != serviceManagerOwner); + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(caller); + serviceManager.removePendingAdmin(pendingAdmin); + } + + function testFuzz_removeAdmin( + address admin + ) public filterFuzzedAddressInputs(admin) { + // Mock the expected call to permissionController + cheats.expectCall( + address(permissionControllerMock), + abi.encodeCall(PermissionController.removeAdmin, (address(serviceManager), admin)) + ); + + // Call should only work from owner + cheats.prank(serviceManagerOwner); + serviceManager.removeAdmin(admin); + } + + function testFuzz_removeAdmin_revert_notOwner( + address admin, + address caller + ) public filterFuzzedAddressInputs(admin) filterFuzzedAddressInputs(caller) { + cheats.assume(caller != serviceManagerOwner); + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(caller); + serviceManager.removeAdmin(admin); + } + + function testFuzz_removeAppointee( + address appointee, + address target, + bytes4 selector + ) public filterFuzzedAddressInputs(appointee) filterFuzzedAddressInputs(target) { + // Mock the expected call to permissionController + cheats.expectCall( + address(permissionControllerMock), + abi.encodeCall( + PermissionController.removeAppointee, + (address(serviceManager), appointee, target, selector) + ) + ); + + // Call should only work from owner + cheats.prank(serviceManagerOwner); + serviceManager.removeAppointee(appointee, target, selector); + } + + function testFuzz_removeAppointee_revert_notOwner( + address appointee, + address target, + bytes4 selector, + address caller + ) + public + filterFuzzedAddressInputs(appointee) + filterFuzzedAddressInputs(target) + filterFuzzedAddressInputs(caller) + { + cheats.assume(caller != serviceManagerOwner); + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(caller); + serviceManager.removeAppointee(appointee, target, selector); + } + + function testFuzz_createOperatorDirectedAVSRewardsSubmission_Revert_WhenNotOwner( + address caller + ) public filterFuzzedAddressInputs(caller) { + cheats.assume(caller != rewardsInitiator); + IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory rewardsSubmissions; + + cheats.prank(caller); + cheats.expectRevert(IServiceManagerErrors.OnlyRewardsInitiator.selector); + serviceManager.createOperatorDirectedAVSRewardsSubmission(rewardsSubmissions); + } + + function test_createOperatorDirectedAVSRewardsSubmission_Revert_WhenERC20NotApproved() public { + IERC20 token = new ERC20PresetFixedSupply( + "dog wif hat", "MOCK1", mockTokenInitialSupply, rewardsInitiator + ); + + IRewardsCoordinatorTypes.OperatorReward[] memory operatorRewards = + new IRewardsCoordinatorTypes.OperatorReward[](1); + operatorRewards[0] = + IRewardsCoordinatorTypes.OperatorReward({operator: address(0x1), amount: 100}); + + IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory rewardsSubmissions = + new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1); + rewardsSubmissions[0] = IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: token, + operatorRewards: operatorRewards, + startTimestamp: uint32(block.timestamp), + duration: uint32(1 weeks), + description: "Test Rewards" + }); + + cheats.prank(rewardsInitiator); + cheats.expectRevert("ERC20: insufficient allowance"); + serviceManager.createOperatorDirectedAVSRewardsSubmission(rewardsSubmissions); + } + + function testFuzz_createOperatorDirectedAVSRewardsSubmission_SingleSubmission( + uint256 startTimestamp, + uint256 duration, + uint256 amount + ) public { + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply( + "dog wif hat", "MOCK1", mockTokenInitialSupply, rewardsInitiator + ); + amount = bound(amount, 1, MAX_REWARDS_AMOUNT); + duration = bound(duration, 0, MAX_REWARDS_DURATION); + duration = duration - (duration % CALCULATION_INTERVAL_SECONDS); + startTimestamp = bound( + startTimestamp, + uint256( + _maxTimestamp( + GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + ) + ) + CALCULATION_INTERVAL_SECONDS - 1, + block.timestamp + uint256(MAX_FUTURE_LENGTH) + ); + startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS); + + vm.warp(startTimestamp + duration + 1); + + // 2. Create reward submission input param + // Create operator rewards array + IRewardsCoordinatorTypes.OperatorReward[] memory operatorRewards = + new IRewardsCoordinatorTypes.OperatorReward[](1); + operatorRewards[0] = + IRewardsCoordinatorTypes.OperatorReward({operator: address(0x1), amount: amount}); + + // Create rewards submission + IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory rewardsSubmissions = + new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1); + rewardsSubmissions[0] = IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: operatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "Test Rewards" + }); + + // 3. Approve serviceManager for ERC20 + cheats.startPrank(rewardsInitiator); + rewardToken.approve(address(serviceManager), amount); + + // 4. call createAVSRewardsSubmission() with expected event emitted + uint256 rewardsInitiatorBalanceBefore = rewardToken.balanceOf(address(rewardsInitiator)); + uint256 rewardsCoordinatorBalanceBefore = rewardToken.balanceOf(address(rewardsCoordinator)); + + rewardToken.approve(address(rewardsCoordinator), amount); + uint256 currSubmissionNonce = rewardsCoordinator.submissionNonce(address(serviceManager)); + bytes32 avsSubmissionHash = keccak256( + abi.encode(address(serviceManager), currSubmissionNonce, rewardsSubmissions[0]) + ); + + // cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); + // emit AVSRewardsSubmissionCreated( + // address(serviceManager), currSubmissionNonce, avsSubmissionHash, rewardsSubmissions[0] + // ); + serviceManager.createOperatorDirectedAVSRewardsSubmission(rewardsSubmissions); + cheats.stopPrank(); + + assertTrue( + rewardsCoordinator.isOperatorDirectedAVSRewardsSubmissionHash( + address(serviceManager), avsSubmissionHash + ), + "reward submission hash not submitted" + ); + assertEq( + currSubmissionNonce + 1, + rewardsCoordinator.submissionNonce(address(serviceManager)), + "submission nonce not incremented" + ); + assertEq( + rewardsInitiatorBalanceBefore - amount, + rewardToken.balanceOf(rewardsInitiator), + "rewardsInitiator balance not decremented by amount of reward submission" + ); + assertEq( + rewardsCoordinatorBalanceBefore + amount, + rewardToken.balanceOf(address(rewardsCoordinator)), + "RewardsCoordinator balance not incremented by amount of reward submission" + ); + } + + function testFuzz_createOperatorDirectedAVSRewardsSubmission_MultipleSubmissions( + uint256 startTimestamp, + uint256 duration, + uint256 amount, + uint256 numSubmissions + ) public { + numSubmissions = bound(numSubmissions, 2, 10); + cheats.prank(rewardsCoordinator.owner()); + + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] memory rewardsSubmissions = + new IRewardsCoordinator.OperatorDirectedRewardsSubmission[](numSubmissions); + bytes32[] memory avsSubmissionHashes = new bytes32[](numSubmissions); + uint256 startSubmissionNonce = rewardsCoordinator.submissionNonce(address(serviceManager)); + _deployMockRewardTokens(rewardsInitiator, numSubmissions); + + uint256[] memory avsBalancesBefore = _getBalanceForTokens(rewardTokens, rewardsInitiator); + uint256[] memory rewardsCoordinatorBalancesBefore = + _getBalanceForTokens(rewardTokens, address(rewardsCoordinator)); + // uint256[] memory amounts = new uint256[](numSubmissions); + + uint256 latestStartTimestamp = 0; + uint256 longestDuration = 0; + + // Create multiple rewards submissions and their expected event + for (uint256 i = 0; i < numSubmissions; ++i) { + // 1. Bound fuzz inputs to valid ranges and amounts using randSeed for each + amount = bound(amount + i, 1, MAX_REWARDS_AMOUNT); + // amounts[i] = amount; + duration = bound(duration + i, 0, MAX_REWARDS_DURATION); + duration = duration - (duration % CALCULATION_INTERVAL_SECONDS); + startTimestamp = bound( + startTimestamp + i, + uint256( + _maxTimestamp( + GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + ) + ) + CALCULATION_INTERVAL_SECONDS - 1, + block.timestamp - 1 // Must be in past for operator directed rewards + ); + startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS); + + // loop and find the latest startTimestamp and the longest duration, then warp start + duration + 1 + + if (startTimestamp > latestStartTimestamp) { + latestStartTimestamp = startTimestamp; + } + if (duration > longestDuration) { + longestDuration = duration; + } + + // 2. Create reward submission input param + IRewardsCoordinatorTypes.OperatorReward[] memory operatorRewards = + new IRewardsCoordinatorTypes.OperatorReward[](1); + operatorRewards[0] = + IRewardsCoordinatorTypes.OperatorReward({operator: address(0x1), amount: amount}); + + IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory rewardsSubmission = + IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardTokens[i], + operatorRewards: operatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "Test Rewards" + }); + rewardsSubmissions[i] = rewardsSubmission; + + // 3. expected event emitted for this rewardsSubmission + avsSubmissionHashes[i] = keccak256( + abi.encode(address(serviceManager), startSubmissionNonce + i, rewardsSubmissions[i]) + ); + } + + vm.warp(latestStartTimestamp + longestDuration + 1); + + // 4. call createOperatorDirectedAVSRewardsSubmission() + cheats.prank(rewardsInitiator); + serviceManager.createOperatorDirectedAVSRewardsSubmission(rewardsSubmissions); + + // 5. Check for submissionNonce() and avsSubmissionHashes being set + assertEq( + startSubmissionNonce + numSubmissions, + rewardsCoordinator.submissionNonce(address(serviceManager)), + "avs submission nonce not incremented properly" + ); + + for (uint256 i = 0; i < numSubmissions; ++i) { + assertTrue( + rewardsCoordinator.isOperatorDirectedAVSRewardsSubmissionHash( + address(serviceManager), avsSubmissionHashes[i] + ), + "rewards submission hash not submitted" + ); + // assertEq( + // avsBalancesBefore[i] - amounts[i], + // rewardTokens[i].balanceOf(rewardsInitiator), + // "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" + // ); + } + } + + function testFuzz_deregisterOperatorFromOperatorSets( + address operator, + uint32[] memory operatorSetIds + ) public { + // Mock the expected call to allocationManager + IAllocationManagerTypes.DeregisterParams memory expectedParams = IAllocationManagerTypes + .DeregisterParams({ + operator: operator, + avs: address(serviceManager), + operatorSetIds: operatorSetIds + }); + + cheats.expectCall( + address(allocationManagerMock), + abi.encodeCall(IAllocationManager.deregisterFromOperatorSets, (expectedParams)) + ); + + // Call should only work from registryCoordinator + cheats.prank(address(registryCoordinatorImplementation)); + serviceManager.deregisterOperatorFromOperatorSets(operator, operatorSetIds); + } + + function testFuzz_deregisterOperatorFromOperatorSets_revert_notRegistryCoordinator( + address operator, + uint32[] memory operatorSetIds, + address caller + ) public filterFuzzedAddressInputs(caller) { + cheats.assume(caller != address(registryCoordinatorImplementation)); + + cheats.prank(caller); + cheats.expectRevert(IServiceManagerErrors.OnlyRegistryCoordinator.selector); + serviceManager.deregisterOperatorFromOperatorSets(operator, operatorSetIds); + } } diff --git a/test/unit/SocketRegistryUnit.t.sol b/test/unit/SocketRegistryUnit.t.sol index 834b3977..986519d9 100644 --- a/test/unit/SocketRegistryUnit.t.sol +++ b/test/unit/SocketRegistryUnit.t.sol @@ -2,10 +2,15 @@ pragma solidity ^0.8.12; -import {SocketRegistry} from "../../src/SocketRegistry.sol"; +import {SocketRegistry, ISlashingRegistryCoordinator} from "../../src/SocketRegistry.sol"; +import {ISocketRegistry, ISocketRegistryErrors} from "../../src/interfaces/ISocketRegistry.sol"; import {IRegistryCoordinator} from "../../src/interfaces/IRegistryCoordinator.sol"; import "../utils/MockAVSDeployer.sol"; +interface IOwnable { + function owner() external view returns (address); +} + contract SocketRegistryUnitTests is MockAVSDeployer { function setUp() public virtual { _deployMockEigenLayerAndAVS(); @@ -17,11 +22,9 @@ contract SocketRegistryUnitTests is MockAVSDeployer { assertEq(socketRegistry.getOperatorSocket(defaultOperatorId), "testSocket"); } - function test_setOperatorSocket_revert_notRegistryCoordinator() public { + function test_setOperatorSocket_revert_notSlashingRegistryCoordinator() public { vm.startPrank(address(0)); - vm.expectRevert( - "SocketRegistry.onlyRegistryCoordinator: caller is not the RegistryCoordinator" - ); + vm.expectRevert(ISocketRegistryErrors.OnlySlashingRegistryCoordinator.selector); socketRegistry.setOperatorSocket(defaultOperatorId, "testSocket"); } } diff --git a/test/unit/StakeRegistryUnit.t.sol b/test/unit/StakeRegistryUnit.t.sol index 94e1d9fe..ba98c4d7 100644 --- a/test/unit/StakeRegistryUnit.t.sol +++ b/test/unit/StakeRegistryUnit.t.sol @@ -1782,12 +1782,30 @@ contract StakeRegistryUnitTests_Deregister is StakeRegistryUnitTests { contract StakeRegistryUnitTests_StakeUpdates is StakeRegistryUnitTests { using BitmapUtils for *; + function _wrap( + address operator + ) internal pure returns (address[] memory) { + address[] memory operators = new address[](1); + operators[0] = operator; + return operators; + } + + function _wrap( + bytes32 operatorId + ) internal pure returns (bytes32[] memory) { + bytes32[] memory operatorIds = new bytes32[](1); + operatorIds[0] = operatorId; + return operatorIds; + } + function test_updateOperatorStake_Revert_WhenNotRegistryCoordinator() public { UpdateSetup memory setup = _fuzz_setupUpdateOperatorStake({registeredFor: initializedQuorumBitmap, fuzzy_Delta: 0}); cheats.expectRevert(IStakeRegistryErrors.OnlySlashingRegistryCoordinator.selector); - stakeRegistry.updateOperatorStake(setup.operator, setup.operatorId, setup.quorumNumbers); + stakeRegistry.updateOperatorsStake( + _wrap(setup.operator), _wrap(setup.operatorId), uint8(setup.quorumNumbers[0]) + ); } function testFuzz_updateOperatorStake_Revert_WhenQuorumDoesNotExist( @@ -1799,10 +1817,13 @@ contract StakeRegistryUnitTests_StakeUpdates is StakeRegistryUnitTests { // Get a list of valid quorums ending in an invalid quorum number bytes memory invalidQuorums = _fuzz_getInvalidQuorums(rand); + uint256 length = invalidQuorums.length; cheats.expectRevert(IStakeRegistryErrors.QuorumDoesNotExist.selector); cheats.prank(address(registryCoordinator)); - stakeRegistry.updateOperatorStake(setup.operator, setup.operatorId, invalidQuorums); + stakeRegistry.updateOperatorsStake( + _wrap(setup.operator), _wrap(setup.operatorId), uint8(invalidQuorums[length - 1]) + ); } /** @@ -1828,9 +1849,14 @@ contract StakeRegistryUnitTests_StakeUpdates is StakeRegistryUnitTests { _getLatestTotalStakeUpdates(setup.quorumNumbers); // updateOperatorStake - cheats.prank(address(registryCoordinator)); - uint192 quorumsToRemove = - stakeRegistry.updateOperatorStake(setup.operator, setup.operatorId, setup.quorumNumbers); + bool[] memory shouldBeDeregistered = new bool[](setup.quorumNumbers.length); + for (uint256 i = 0; i < setup.quorumNumbers.length; i++) { + cheats.prank(address(registryCoordinator)); + bool[] memory shouldBeDeregisteredForQuorum = stakeRegistry.updateOperatorsStake( + _wrap(setup.operator), _wrap(setup.operatorId), uint8(setup.quorumNumbers[i]) + ); + shouldBeDeregistered[i] = shouldBeDeregisteredForQuorum[0]; + } // Get ending state IStakeRegistry.StakeUpdate[] memory newOperatorStakes = @@ -1873,7 +1899,8 @@ contract StakeRegistryUnitTests_StakeUpdates is StakeRegistryUnitTests { ); // Return value should be empty since we're still above the minimum assertTrue( - quorumsToRemove.isEmpty(), "positive stake delta should not remove any quorums" + !shouldBeDeregistered[i], + "positive stake delta should not lead to deregistration" ); } else if (endingWeight < minimumStake) { // Check updating an operator who is now below the minimum: @@ -1892,9 +1919,7 @@ contract StakeRegistryUnitTests_StakeUpdates is StakeRegistryUnitTests { ); assertEq(newOperatorStake.stake, 0, "operator stake should now be zero"); // IECDSAStakeRegistryTypes.Quorum should be added to return bitmap - assertTrue( - quorumsToRemove.isSet(quorumNumber), "quorum should be in removal bitmap" - ); + assertTrue(shouldBeDeregistered[i], "operator should be deregistered"); } else { // Check that no update occurs if weight remains the same assertTrue( @@ -1907,7 +1932,8 @@ contract StakeRegistryUnitTests_StakeUpdates is StakeRegistryUnitTests { ); // Check that return value is empty - we're still at the minimum, so no quorums should be removed assertTrue( - quorumsToRemove.isEmpty(), "neutral stake delta should not remove any quorums" + !shouldBeDeregistered[i], + "neutral stake delta should not lead to deregistration" ); } } @@ -1946,8 +1972,12 @@ contract StakeRegistryUnitTests_StakeUpdates is StakeRegistryUnitTests { UpdateSetup memory setup = setups[i]; // updateOperatorStake - cheats.prank(address(registryCoordinator)); - stakeRegistry.updateOperatorStake(setup.operator, setup.operatorId, setup.quorumNumbers); + for (uint256 j = 0; j < setup.quorumNumbers.length; j++) { + cheats.prank(address(registryCoordinator)); + stakeRegistry.updateOperatorsStake( + _wrap(setup.operator), _wrap(setup.operatorId), uint8(setup.quorumNumbers[j]) + ); + } } // Check final results for each quorum @@ -2036,11 +2066,15 @@ contract StakeRegistryUnitTests_StakeUpdates is StakeRegistryUnitTests { uint256 currBlock = startBlock + j; cheats.roll(currBlock); - // updateOperatorStake - cheats.prank(address(registryCoordinator)); - uint192 quorumsToRemove = stakeRegistry.updateOperatorStake( - setup.operator, setup.operatorId, setup.quorumNumbers - ); + // updateOperatorsStake + bool[] memory shouldBeDeregistered = new bool[](setup.quorumNumbers.length); + for (uint256 i = 0; i < setup.quorumNumbers.length; i++) { + cheats.prank(address(registryCoordinator)); + bool[] memory shouldBeDeregisteredForQuorum = stakeRegistry.updateOperatorsStake( + _wrap(setup.operator), _wrap(setup.operatorId), uint8(setup.quorumNumbers[i]) + ); + shouldBeDeregistered[i] = shouldBeDeregisteredForQuorum[0]; + } // Get ending state IStakeRegistry.StakeUpdate[] memory newOperatorStakes = @@ -2084,8 +2118,8 @@ contract StakeRegistryUnitTests_StakeUpdates is StakeRegistryUnitTests { ); // Return value should be empty since we're still above the minimum assertTrue( - quorumsToRemove.isEmpty(), - "positive stake delta should not remove any quorums" + !shouldBeDeregistered[i], + "positive stake delta should not lead to deregistration" ); assertEq( prevOperatorHistoryLengths[i] + 1, @@ -2105,9 +2139,7 @@ contract StakeRegistryUnitTests_StakeUpdates is StakeRegistryUnitTests { // assertEq(prevTotalStake.stake - stakeRemoved, newTotalStake.stake, "failed to remove delta from total stake"); assertEq(newOperatorStake.stake, 0, "operator stake should now be zero"); // IECDSAStakeRegistryTypes.Quorum should be added to return bitmap - assertTrue( - quorumsToRemove.isSet(quorumNumber), "quorum should be in removal bitmap" - ); + assertTrue(shouldBeDeregistered[i], "operator should be deregistered"); if (prevOperatorStake.stake >= minimumStake) { // Total stakes and operator history should be updated assertEq( @@ -2145,7 +2177,7 @@ contract StakeRegistryUnitTests_StakeUpdates is StakeRegistryUnitTests { ); // Check that return value is empty - we're still at the minimum, so no quorums should be removed assertTrue( - quorumsToRemove.isEmpty(), + !shouldBeDeregistered[i], "neutral stake delta should not remove any quorums" ); assertEq( diff --git a/test/unit/VetoableSlasher.t.sol b/test/unit/VetoableSlasher.t.sol new file mode 100644 index 00000000..9dde1a98 --- /dev/null +++ b/test/unit/VetoableSlasher.t.sol @@ -0,0 +1,505 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {Test} from "forge-std/Test.sol"; +import {VetoableSlasher} from "../../src/slashers/VetoableSlasher.sol"; +import { + IAllocationManager, + IAllocationManagerTypes +} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; +import {IRegistryCoordinator} from "../../src/interfaces/IRegistryCoordinator.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {ISlasher, ISlasherTypes, ISlasherErrors} from "../../src/interfaces/ISlasher.sol"; +import {ISlashingRegistryCoordinator} from "../../src/interfaces/ISlashingRegistryCoordinator.sol"; +import {IStakeRegistry, IStakeRegistryTypes} from "../../src/interfaces/IStakeRegistry.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {EmptyContract} from "eigenlayer-contracts/src/test/mocks/EmptyContract.sol"; +import {AllocationManager} from "eigenlayer-contracts/src/contracts/core/AllocationManager.sol"; +import {PermissionController} from + "eigenlayer-contracts/src/contracts/permissions/PermissionController.sol"; +import {PauserRegistry} from "eigenlayer-contracts/src/contracts/permissions/PauserRegistry.sol"; +import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol"; +import {IDelegationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IStrategyManager} from "eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; +import {DelegationMock} from "../mocks/DelegationMock.sol"; +import {SlashingRegistryCoordinator} from "../../src/SlashingRegistryCoordinator.sol"; +import {ISlashingRegistryCoordinatorTypes} from + "../../src/interfaces/ISlashingRegistryCoordinator.sol"; +import {IBLSApkRegistry, IBLSApkRegistryTypes} from "../../src/interfaces/IBLSApkRegistry.sol"; +import {IIndexRegistry} from "../../src/interfaces/IIndexRegistry.sol"; +import {ISocketRegistry} from "../../src/interfaces/ISocketRegistry.sol"; +import {CoreDeploymentLib} from "../utils/CoreDeployLib.sol"; +import { + OperatorWalletLib, + Operator, + Wallet, + BLSWallet, + SigningKeyOperationsLib +} from "../utils/OperatorWalletLib.sol"; +import {OperatorSet} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; +import {StrategyFactory} from "eigenlayer-contracts/src/contracts/strategies/StrategyFactory.sol"; +import {StakeRegistry} from "../../src/StakeRegistry.sol"; +import {BLSApkRegistry} from "../../src/BLSApkRegistry.sol"; +import {IndexRegistry} from "../../src/IndexRegistry.sol"; +import {IVetoableSlasherTypes} from "../../src/interfaces/IVetoableSlasher.sol"; +import {SocketRegistry} from "../../src/SocketRegistry.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IVetoableSlasherErrors} from "../../src/interfaces/IVetoableSlasher.sol"; +import {MiddlewareDeployLib} from "../utils/MiddlewareDeployLib.sol"; + +contract VetoableSlasherTest is Test { + VetoableSlasher public vetoableSlasher; + VetoableSlasher public vetoableSlasherImplementation; + ProxyAdmin public proxyAdmin; + EmptyContract public emptyContract; + CoreDeploymentLib.DeploymentData public coreDeployment; + PauserRegistry public pauserRegistry; + ERC20Mock public mockToken; + StrategyFactory public strategyFactory; + StakeRegistry public stakeRegistry; + BLSApkRegistry public blsApkRegistry; + IndexRegistry public indexRegistry; + SocketRegistry public socketRegistry; + SlashingRegistryCoordinator public slashingRegistryCoordinator; + SlashingRegistryCoordinator public slashingRegistryCoordinatorImplementation; + + address public vetoCommittee; + address public slasher; + address public serviceManager; + Operator public operatorWallet; + IStrategy public mockStrategy; + address public proxyAdminOwner = address(uint160(uint256(keccak256("proxyAdminOwner")))); + address public pauser = address(uint160(uint256(keccak256("pauser")))); + address public unpauser = address(uint160(uint256(keccak256("unpauser")))); + address public churnApprover = address(uint160(uint256(keccak256("churnApprover")))); + address public ejector = address(uint160(uint256(keccak256("ejector")))); + + uint32 constant vetoWindowBlocks = 3 days / 12 seconds; + uint32 constant DEALLOCATION_DELAY = 7 days; + uint32 constant ALLOCATION_CONFIGURATION_DELAY = 1 days; + + function setUp() public { + serviceManager = address(0x2); + vetoCommittee = address(0x3); + slasher = address(0x4); + operatorWallet = OperatorWalletLib.createOperator("operator"); + + mockToken = new ERC20Mock("Mock Token", "MOCK", address(this), 0); + + vm.startPrank(proxyAdminOwner); + proxyAdmin = new ProxyAdmin(); + emptyContract = new EmptyContract(); + + address[] memory pausers = new address[](1); + pausers[0] = pauser; + pauserRegistry = new PauserRegistry(pausers, unpauser); + + CoreDeploymentLib.DeploymentConfigData memory configData; + configData.strategyManager.initialOwner = proxyAdminOwner; + configData.strategyManager.initialStrategyWhitelister = proxyAdminOwner; + configData.strategyManager.initPausedStatus = 0; + + configData.delegationManager.initialOwner = proxyAdminOwner; + configData.delegationManager.minWithdrawalDelayBlocks = 50400; + configData.delegationManager.initPausedStatus = 0; + + configData.eigenPodManager.initialOwner = proxyAdminOwner; + configData.eigenPodManager.initPausedStatus = 0; + + configData.allocationManager.initialOwner = proxyAdminOwner; + configData.allocationManager.deallocationDelay = DEALLOCATION_DELAY; + configData.allocationManager.allocationConfigurationDelay = ALLOCATION_CONFIGURATION_DELAY; + configData.allocationManager.initPausedStatus = 0; + + configData.strategyFactory.initialOwner = proxyAdminOwner; + configData.strategyFactory.initPausedStatus = 0; + + configData.avsDirectory.initialOwner = proxyAdminOwner; + configData.avsDirectory.initPausedStatus = 0; + + configData.rewardsCoordinator.initialOwner = proxyAdminOwner; + configData.rewardsCoordinator.rewardsUpdater = + address(0x14dC79964da2C08b23698B3D3cc7Ca32193d9955); + configData.rewardsCoordinator.initPausedStatus = 0; + configData.rewardsCoordinator.activationDelay = 0; + configData.rewardsCoordinator.defaultSplitBips = 1000; + configData.rewardsCoordinator.calculationIntervalSeconds = 86400; + configData.rewardsCoordinator.maxRewardsDuration = 864000; + configData.rewardsCoordinator.maxRetroactiveLength = 86400; + configData.rewardsCoordinator.maxFutureLength = 86400; + configData.rewardsCoordinator.genesisRewardsTimestamp = 1672531200; + + configData.ethPOSDeposit.ethPOSDepositAddress = address(0x123); + + coreDeployment = CoreDeploymentLib.deployContracts(address(proxyAdmin), configData); + + address strategyManagerOwner = Ownable(coreDeployment.strategyManager).owner(); + vm.stopPrank(); + + vm.startPrank(strategyManagerOwner); + IStrategyManager(coreDeployment.strategyManager).setStrategyWhitelister( + coreDeployment.strategyFactory + ); + vm.stopPrank(); + + vm.startPrank(proxyAdminOwner); + mockStrategy = IStrategy( + StrategyFactory(coreDeployment.strategyFactory).deployNewStrategy( + IERC20(address(mockToken)) + ) + ); + + MiddlewareDeployLib.MiddlewareDeployConfig memory middlewareConfig; + middlewareConfig.instantSlasher.initialOwner = proxyAdminOwner; + middlewareConfig.instantSlasher.slasher = slasher; + middlewareConfig.slashingRegistryCoordinator.initialOwner = proxyAdminOwner; + middlewareConfig.slashingRegistryCoordinator.churnApprover = churnApprover; + middlewareConfig.slashingRegistryCoordinator.ejector = ejector; + middlewareConfig.slashingRegistryCoordinator.initPausedStatus = 0; + middlewareConfig.slashingRegistryCoordinator.serviceManager = serviceManager; + middlewareConfig.socketRegistry.initialOwner = proxyAdminOwner; + middlewareConfig.indexRegistry.initialOwner = proxyAdminOwner; + middlewareConfig.stakeRegistry.initialOwner = proxyAdminOwner; + middlewareConfig.stakeRegistry.minimumStake = 1 ether; + middlewareConfig.stakeRegistry.strategyParams = 0; + middlewareConfig.stakeRegistry.delegationManager = coreDeployment.delegationManager; + middlewareConfig.stakeRegistry.avsDirectory = coreDeployment.avsDirectory; + { + IStakeRegistryTypes.StrategyParams[] memory stratParams = + new IStakeRegistryTypes.StrategyParams[](1); + stratParams[0] = + IStakeRegistryTypes.StrategyParams({strategy: mockStrategy, multiplier: 1 ether}); + middlewareConfig.stakeRegistry.strategyParamsArray = stratParams; + } + middlewareConfig.stakeRegistry.lookAheadPeriod = 0; + middlewareConfig.stakeRegistry.stakeType = IStakeRegistryTypes.StakeType(1); + middlewareConfig.blsApkRegistry.initialOwner = proxyAdminOwner; + + MiddlewareDeployLib.MiddlewareDeployData memory middlewareDeployments = MiddlewareDeployLib + .deployMiddleware( + address(proxyAdmin), + coreDeployment.allocationManager, + address(pauserRegistry), + middlewareConfig + ); + vm.stopPrank(); + + vetoableSlasher = VetoableSlasher(middlewareDeployments.instantSlasher); + slashingRegistryCoordinator = + SlashingRegistryCoordinator(middlewareDeployments.slashingRegistryCoordinator); + stakeRegistry = StakeRegistry(middlewareDeployments.stakeRegistry); + blsApkRegistry = BLSApkRegistry(middlewareDeployments.blsApkRegistry); + indexRegistry = IndexRegistry(middlewareDeployments.indexRegistry); + socketRegistry = SocketRegistry(middlewareDeployments.socketRegistry); + + vetoableSlasherImplementation = new VetoableSlasher( + IAllocationManager(coreDeployment.allocationManager), + ISlashingRegistryCoordinator(slashingRegistryCoordinator), + slasher, + vetoCommittee, + vetoWindowBlocks + ); + + vm.startPrank(proxyAdminOwner); + vetoableSlasher = VetoableSlasher( + address( + new TransparentUpgradeableProxy(address(emptyContract), address(proxyAdmin), "") + ) + ); + + proxyAdmin.upgrade( + TransparentUpgradeableProxy(payable(address(vetoableSlasher))), + address(vetoableSlasherImplementation) + ); + vm.stopPrank(); + + vm.startPrank(serviceManager); + PermissionController(coreDeployment.permissionController).setAppointee( + address(serviceManager), + address(vetoableSlasher), + coreDeployment.allocationManager, + AllocationManager.slashOperator.selector + ); + + PermissionController(coreDeployment.permissionController).setAppointee( + address(serviceManager), + address(slashingRegistryCoordinator), + coreDeployment.allocationManager, + AllocationManager.createOperatorSets.selector + ); + + PermissionController(coreDeployment.permissionController).setAppointee( + address(serviceManager), + proxyAdminOwner, + coreDeployment.allocationManager, + AllocationManager.updateAVSMetadataURI.selector + ); + + vm.stopPrank(); + + uint8 quorumNumber = 0; + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = mockStrategy; + + uint96[] memory minimumStakes = new uint96[](1); + minimumStakes[0] = 1 ether; + + IStakeRegistryTypes.StrategyParams[] memory strategyParams = + new IStakeRegistryTypes.StrategyParams[](1); + strategyParams[0] = + IStakeRegistryTypes.StrategyParams({strategy: mockStrategy, multiplier: 1 ether}); + + ISlashingRegistryCoordinatorTypes.OperatorSetParam memory operatorSetParams = + ISlashingRegistryCoordinatorTypes.OperatorSetParam({ + maxOperatorCount: 10, + kickBIPsOfOperatorStake: 0, + kickBIPsOfTotalStake: 0 + }); + + vm.startPrank(proxyAdminOwner); + IAllocationManager(coreDeployment.allocationManager).updateAVSMetadataURI( + serviceManager, "fake-avs-metadata" + ); + slashingRegistryCoordinator.createSlashableStakeQuorum( + operatorSetParams, 1 ether, strategyParams, 0 + ); + vm.stopPrank(); + + vm.label(address(vetoableSlasher), "VetoableSlasher Proxy"); + vm.label(address(vetoableSlasherImplementation), "VetoableSlasher Implementation"); + vm.label(address(slashingRegistryCoordinator), "SlashingRegistryCoordinator Proxy"); + vm.label( + address(slashingRegistryCoordinatorImplementation), + "SlashingRegistryCoordinator Implementation" + ); + vm.label(address(proxyAdmin), "ProxyAdmin"); + vm.label(coreDeployment.allocationManager, "AllocationManager Proxy"); + } + + function test_initialization() public { + assertEq(vetoableSlasher.vetoCommittee(), vetoCommittee); + assertEq(vetoableSlasher.vetoWindowBlocks(), vetoWindowBlocks); + } + + function _createMockSlashingParams() + internal + view + returns (IAllocationManagerTypes.SlashingParams memory) + { + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = mockStrategy; + + uint256[] memory wadsToSlash = new uint256[](1); + wadsToSlash[0] = 0.5e18; // 50% slash + + return IAllocationManagerTypes.SlashingParams({ + operator: operatorWallet.key.addr, + operatorSetId: 1, + strategies: strategies, + wadsToSlash: wadsToSlash, + description: "Test slashing" + }); + } + + function test_queueSlashingRequest_revert_notSlasher() public { + IAllocationManagerTypes.SlashingParams memory params = _createMockSlashingParams(); + vm.expectRevert(ISlasherErrors.OnlySlasher.selector); + vetoableSlasher.queueSlashingRequest(params); + } + + function test_queueSlashingRequest() public { + IAllocationManagerTypes.SlashingParams memory params = _createMockSlashingParams(); + + vm.prank(slasher); + vetoableSlasher.queueSlashingRequest(params); + + ( + IAllocationManagerTypes.SlashingParams memory resultParams, + uint256 requestTimestamp, + IVetoableSlasherTypes.SlashingStatus status + ) = vetoableSlasher.slashingRequests(0); + IVetoableSlasherTypes.VetoableSlashingRequest memory request = + IVetoableSlasherTypes.VetoableSlashingRequest(params, requestTimestamp, status); + assertEq(resultParams.operator, operatorWallet.key.addr); + assertEq(resultParams.operatorSetId, 1); + assertEq(resultParams.wadsToSlash[0], 0.5e18); + assertEq(resultParams.description, "Test slashing"); + assertEq(uint8(status), uint8(IVetoableSlasherTypes.SlashingStatus.Requested)); + assertEq(requestTimestamp, block.timestamp); + } + + function test_cancelSlashingRequest_revert_notVetoCommittee() public { + IAllocationManagerTypes.SlashingParams memory params = _createMockSlashingParams(); + + vm.prank(slasher); + vetoableSlasher.queueSlashingRequest(params); + + vm.expectRevert(IVetoableSlasherErrors.OnlyVetoCommittee.selector); + vetoableSlasher.cancelSlashingRequest(0); + } + + function test_cancelSlashingRequest_revert_afterVetoPeriod() public { + IAllocationManagerTypes.SlashingParams memory params = _createMockSlashingParams(); + + vm.prank(slasher); + vetoableSlasher.queueSlashingRequest(params); + + vm.roll(block.number + vetoWindowBlocks + 1); + + vm.prank(vetoCommittee); + vm.expectRevert(IVetoableSlasherErrors.VetoPeriodPassed.selector); + vetoableSlasher.cancelSlashingRequest(0); + } + + function test_cancelSlashingRequest() public { + IAllocationManagerTypes.SlashingParams memory params = _createMockSlashingParams(); + + vm.prank(slasher); + vetoableSlasher.queueSlashingRequest(params); + + vm.prank(vetoCommittee); + vetoableSlasher.cancelSlashingRequest(0); + + ( + IAllocationManagerTypes.SlashingParams memory resultParams, + uint256 requestTimestamp, + IVetoableSlasherTypes.SlashingStatus status + ) = vetoableSlasher.slashingRequests(0); + assertEq(uint8(status), uint8(IVetoableSlasherTypes.SlashingStatus.Cancelled)); + } + + function test_fulfillSlashingRequest_revert_beforeVetoPeriod() public { + IAllocationManagerTypes.SlashingParams memory params = _createMockSlashingParams(); + + vm.prank(slasher); + vetoableSlasher.queueSlashingRequest(params); + + vm.prank(slasher); + vm.expectRevert(IVetoableSlasherErrors.VetoPeriodNotPassed.selector); + vetoableSlasher.fulfillSlashingRequest(0); + } + + function test_fulfillSlashingRequest() public { + vm.startPrank(operatorWallet.key.addr); + IDelegationManager(coreDeployment.delegationManager).registerAsOperator( + address(0), 1, "metadata" + ); + + uint256 depositAmount = 1 ether; + mockToken.mint(operatorWallet.key.addr, depositAmount); + mockToken.approve(address(coreDeployment.strategyManager), depositAmount); + IStrategyManager(coreDeployment.strategyManager).depositIntoStrategy( + mockStrategy, mockToken, depositAmount + ); + + uint32 minDelay = 1; + IAllocationManager(coreDeployment.allocationManager).setAllocationDelay( + operatorWallet.key.addr, minDelay + ); + vm.stopPrank(); + + vm.roll(block.number + ALLOCATION_CONFIGURATION_DELAY + 1); + + IStrategy[] memory allocStrategies = new IStrategy[](1); + allocStrategies[0] = mockStrategy; + + uint64[] memory magnitudes = new uint64[](1); + magnitudes[0] = uint64(1 ether); // Allocate full magnitude + + OperatorSet memory operatorSet = OperatorSet({avs: address(serviceManager), id: 0}); + + vm.startPrank(serviceManager); + IAllocationManagerTypes.CreateSetParams[] memory createParams = + new IAllocationManagerTypes.CreateSetParams[](1); + createParams[0] = + IAllocationManagerTypes.CreateSetParams({operatorSetId: 0, strategies: allocStrategies}); + IAllocationManager(coreDeployment.allocationManager).setAVSRegistrar( + address(serviceManager), IAVSRegistrar(address(slashingRegistryCoordinator)) + ); + vm.stopPrank(); + + vm.startPrank(operatorWallet.key.addr); + + IAllocationManagerTypes.AllocateParams[] memory allocParams = + new IAllocationManagerTypes.AllocateParams[](1); + allocParams[0] = IAllocationManagerTypes.AllocateParams({ + operatorSet: operatorSet, + strategies: allocStrategies, + newMagnitudes: magnitudes + }); + + IAllocationManager(coreDeployment.allocationManager).modifyAllocations( + operatorWallet.key.addr, allocParams + ); + vm.roll(block.number + 100); + + uint32[] memory operatorSetIds = new uint32[](1); + operatorSetIds[0] = 0; + bytes32 messageHash = slashingRegistryCoordinator.calculatePubkeyRegistrationMessageHash( + operatorWallet.key.addr + ); + IBLSApkRegistryTypes.PubkeyRegistrationParams memory pubkeyParams = IBLSApkRegistryTypes + .PubkeyRegistrationParams({ + pubkeyRegistrationSignature: SigningKeyOperationsLib.sign( + operatorWallet.signingKey, messageHash + ), + pubkeyG1: operatorWallet.signingKey.publicKeyG1, + pubkeyG2: operatorWallet.signingKey.publicKeyG2 + }); + + bytes memory registrationData = abi.encode( + ISlashingRegistryCoordinatorTypes.RegistrationType.NORMAL, "socket", pubkeyParams + ); + + IAllocationManagerTypes.RegisterParams memory registerParams = IAllocationManagerTypes + .RegisterParams({ + avs: address(serviceManager), + operatorSetIds: operatorSetIds, + data: registrationData + }); + IAllocationManager(coreDeployment.allocationManager).registerForOperatorSets( + operatorWallet.key.addr, registerParams + ); + vm.stopPrank(); + + vm.roll(block.number + 100); + + // Create slashing params + IAllocationManagerTypes.SlashingParams memory params = IAllocationManagerTypes + .SlashingParams({ + operator: operatorWallet.key.addr, + operatorSetId: 0, + strategies: allocStrategies, + wadsToSlash: new uint256[](allocStrategies.length), + description: "Test slashing" + }); + + // Set each wad to slash to 1e18 (100% slash) + for (uint256 i = 0; i < params.wadsToSlash.length; i++) { + params.wadsToSlash[i] = 1e18; + } + + vm.prank(slasher); + vetoableSlasher.queueSlashingRequest(params); + + // Wait for veto period to pass + vm.roll(block.number + vetoWindowBlocks + 1); + + vm.prank(slasher); + vetoableSlasher.fulfillSlashingRequest(0); + + ( + IAllocationManagerTypes.SlashingParams memory resultParams, + uint256 requestTimestamp, + IVetoableSlasherTypes.SlashingStatus status + ) = vetoableSlasher.slashingRequests(0); + assertEq(uint8(status), uint8(IVetoableSlasherTypes.SlashingStatus.Completed)); + } +} diff --git a/test/utils/BN256G2.sol b/test/utils/BN256G2.sol new file mode 100644 index 00000000..9d407b41 --- /dev/null +++ b/test/utils/BN256G2.sol @@ -0,0 +1,337 @@ +pragma solidity ^0.8.0; + +/** + * @title Elliptic curve operations on twist points for alt_bn128 + * @author Mustafa Al-Bassam (mus@musalbas.com) + * @dev Homepage: https://github.com/musalbas/solidity-BN256G2 + */ +library BN256G2 { + uint256 internal constant FIELD_MODULUS = + 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47; + uint256 internal constant TWISTBX = + 0x2b149d40ceb8aaae81be18991be06ac3b5b4c5e559dbefa33267e6dc24a138e5; + uint256 internal constant TWISTBY = + 0x9713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d2; + uint256 internal constant PTXX = 0; + uint256 internal constant PTXY = 1; + uint256 internal constant PTYX = 2; + uint256 internal constant PTYY = 3; + uint256 internal constant PTZX = 4; + uint256 internal constant PTZY = 5; + + /** + * @notice Add two twist points + * @param pt1xx Coefficient 1 of x on point 1 + * @param pt1xy Coefficient 2 of x on point 1 + * @param pt1yx Coefficient 1 of y on point 1 + * @param pt1yy Coefficient 2 of y on point 1 + * @param pt2xx Coefficient 1 of x on point 2 + * @param pt2xy Coefficient 2 of x on point 2 + * @param pt2yx Coefficient 1 of y on point 2 + * @param pt2yy Coefficient 2 of y on point 2 + * @return (pt3xx, pt3xy, pt3yx, pt3yy) + */ + function ECTwistAdd( + uint256 pt1xx, + uint256 pt1xy, + uint256 pt1yx, + uint256 pt1yy, + uint256 pt2xx, + uint256 pt2xy, + uint256 pt2yx, + uint256 pt2yy + ) public view returns (uint256, uint256, uint256, uint256) { + if (pt1xx == 0 && pt1xy == 0 && pt1yx == 0 && pt1yy == 0) { + if (!(pt2xx == 0 && pt2xy == 0 && pt2yx == 0 && pt2yy == 0)) { + assert(_isOnCurve(pt2xx, pt2xy, pt2yx, pt2yy)); + } + return (pt2xx, pt2xy, pt2yx, pt2yy); + } else if (pt2xx == 0 && pt2xy == 0 && pt2yx == 0 && pt2yy == 0) { + assert(_isOnCurve(pt1xx, pt1xy, pt1yx, pt1yy)); + return (pt1xx, pt1xy, pt1yx, pt1yy); + } + + assert(_isOnCurve(pt1xx, pt1xy, pt1yx, pt1yy)); + assert(_isOnCurve(pt2xx, pt2xy, pt2yx, pt2yy)); + + uint256[6] memory pt3 = + _ECTwistAddJacobian(pt1xx, pt1xy, pt1yx, pt1yy, 1, 0, pt2xx, pt2xy, pt2yx, pt2yy, 1, 0); + + return _fromJacobian(pt3[PTXX], pt3[PTXY], pt3[PTYX], pt3[PTYY], pt3[PTZX], pt3[PTZY]); + } + + /** + * @notice Multiply a twist point by a scalar + * @param s Scalar to multiply by + * @param pt1xx Coefficient 1 of x + * @param pt1xy Coefficient 2 of x + * @param pt1yx Coefficient 1 of y + * @param pt1yy Coefficient 2 of y + * @return (pt2xx, pt2xy, pt2yx, pt2yy) + */ + function ECTwistMul( + uint256 s, + uint256 pt1xx, + uint256 pt1xy, + uint256 pt1yx, + uint256 pt1yy + ) public view returns (uint256, uint256, uint256, uint256) { + uint256 pt1zx = 1; + if (pt1xx == 0 && pt1xy == 0 && pt1yx == 0 && pt1yy == 0) { + pt1xx = 1; + pt1yx = 1; + pt1zx = 0; + } else { + assert(_isOnCurve(pt1xx, pt1xy, pt1yx, pt1yy)); + } + + uint256[6] memory pt2 = _ECTwistMulJacobian(s, pt1xx, pt1xy, pt1yx, pt1yy, pt1zx, 0); + + return _fromJacobian(pt2[PTXX], pt2[PTXY], pt2[PTYX], pt2[PTYY], pt2[PTZX], pt2[PTZY]); + } + + /** + * @notice Get the field modulus + * @return The field modulus + */ + function GetFieldModulus() public pure returns (uint256) { + return FIELD_MODULUS; + } + + function submod(uint256 a, uint256 b, uint256 n) internal pure returns (uint256) { + return addmod(a, n - b, n); + } + + function _FQ2Mul( + uint256 xx, + uint256 xy, + uint256 yx, + uint256 yy + ) internal pure returns (uint256, uint256) { + return ( + submod(mulmod(xx, yx, FIELD_MODULUS), mulmod(xy, yy, FIELD_MODULUS), FIELD_MODULUS), + addmod(mulmod(xx, yy, FIELD_MODULUS), mulmod(xy, yx, FIELD_MODULUS), FIELD_MODULUS) + ); + } + + function _FQ2Muc(uint256 xx, uint256 xy, uint256 c) internal pure returns (uint256, uint256) { + return (mulmod(xx, c, FIELD_MODULUS), mulmod(xy, c, FIELD_MODULUS)); + } + + function _FQ2Add( + uint256 xx, + uint256 xy, + uint256 yx, + uint256 yy + ) internal pure returns (uint256, uint256) { + return (addmod(xx, yx, FIELD_MODULUS), addmod(xy, yy, FIELD_MODULUS)); + } + + function _FQ2Sub( + uint256 xx, + uint256 xy, + uint256 yx, + uint256 yy + ) internal pure returns (uint256 rx, uint256 ry) { + return (submod(xx, yx, FIELD_MODULUS), submod(xy, yy, FIELD_MODULUS)); + } + + function _FQ2Div( + uint256 xx, + uint256 xy, + uint256 yx, + uint256 yy + ) internal view returns (uint256, uint256) { + (yx, yy) = _FQ2Inv(yx, yy); + return _FQ2Mul(xx, xy, yx, yy); + } + + function _FQ2Inv(uint256 x, uint256 y) internal view returns (uint256, uint256) { + uint256 inv = _modInv( + addmod(mulmod(y, y, FIELD_MODULUS), mulmod(x, x, FIELD_MODULUS), FIELD_MODULUS), + FIELD_MODULUS + ); + return (mulmod(x, inv, FIELD_MODULUS), FIELD_MODULUS - mulmod(y, inv, FIELD_MODULUS)); + } + + function _isOnCurve( + uint256 xx, + uint256 xy, + uint256 yx, + uint256 yy + ) internal pure returns (bool) { + uint256 yyx; + uint256 yyy; + uint256 xxxx; + uint256 xxxy; + (yyx, yyy) = _FQ2Mul(yx, yy, yx, yy); + (xxxx, xxxy) = _FQ2Mul(xx, xy, xx, xy); + (xxxx, xxxy) = _FQ2Mul(xxxx, xxxy, xx, xy); + (yyx, yyy) = _FQ2Sub(yyx, yyy, xxxx, xxxy); + (yyx, yyy) = _FQ2Sub(yyx, yyy, TWISTBX, TWISTBY); + return yyx == 0 && yyy == 0; + } + + function _modInv(uint256 a, uint256 n) internal view returns (uint256 result) { + bool success; + assembly ("memory-safe") { + let freemem := mload(0x40) + mstore(freemem, 0x20) + mstore(add(freemem, 0x20), 0x20) + mstore(add(freemem, 0x40), 0x20) + mstore(add(freemem, 0x60), a) + mstore(add(freemem, 0x80), sub(n, 2)) + mstore(add(freemem, 0xA0), n) + success := staticcall(sub(gas(), 2000), 5, freemem, 0xC0, freemem, 0x20) + result := mload(freemem) + } + require(success); + } + + function _fromJacobian( + uint256 pt1xx, + uint256 pt1xy, + uint256 pt1yx, + uint256 pt1yy, + uint256 pt1zx, + uint256 pt1zy + ) internal view returns (uint256 pt2xx, uint256 pt2xy, uint256 pt2yx, uint256 pt2yy) { + uint256 invzx; + uint256 invzy; + (invzx, invzy) = _FQ2Inv(pt1zx, pt1zy); + (pt2xx, pt2xy) = _FQ2Mul(pt1xx, pt1xy, invzx, invzy); + (pt2yx, pt2yy) = _FQ2Mul(pt1yx, pt1yy, invzx, invzy); + } + + function _ECTwistAddJacobian( + uint256 pt1xx, + uint256 pt1xy, + uint256 pt1yx, + uint256 pt1yy, + uint256 pt1zx, + uint256 pt1zy, + uint256 pt2xx, + uint256 pt2xy, + uint256 pt2yx, + uint256 pt2yy, + uint256 pt2zx, + uint256 pt2zy + ) internal pure returns (uint256[6] memory pt3) { + if (pt1zx == 0 && pt1zy == 0) { + (pt3[PTXX], pt3[PTXY], pt3[PTYX], pt3[PTYY], pt3[PTZX], pt3[PTZY]) = + (pt2xx, pt2xy, pt2yx, pt2yy, pt2zx, pt2zy); + return pt3; + } else if (pt2zx == 0 && pt2zy == 0) { + (pt3[PTXX], pt3[PTXY], pt3[PTYX], pt3[PTYY], pt3[PTZX], pt3[PTZY]) = + (pt1xx, pt1xy, pt1yx, pt1yy, pt1zx, pt1zy); + return pt3; + } + + (pt2yx, pt2yy) = _FQ2Mul(pt2yx, pt2yy, pt1zx, pt1zy); // U1 = y2 * z1 + (pt3[PTYX], pt3[PTYY]) = _FQ2Mul(pt1yx, pt1yy, pt2zx, pt2zy); // U2 = y1 * z2 + (pt2xx, pt2xy) = _FQ2Mul(pt2xx, pt2xy, pt1zx, pt1zy); // V1 = x2 * z1 + (pt3[PTZX], pt3[PTZY]) = _FQ2Mul(pt1xx, pt1xy, pt2zx, pt2zy); // V2 = x1 * z2 + + if (pt2xx == pt3[PTZX] && pt2xy == pt3[PTZY]) { + if (pt2yx == pt3[PTYX] && pt2yy == pt3[PTYY]) { + (pt3[PTXX], pt3[PTXY], pt3[PTYX], pt3[PTYY], pt3[PTZX], pt3[PTZY]) = + _ECTwistDoubleJacobian(pt1xx, pt1xy, pt1yx, pt1yy, pt1zx, pt1zy); + return pt3; + } + (pt3[PTXX], pt3[PTXY], pt3[PTYX], pt3[PTYY], pt3[PTZX], pt3[PTZY]) = (1, 0, 1, 0, 0, 0); + return pt3; + } + + (pt2zx, pt2zy) = _FQ2Mul(pt1zx, pt1zy, pt2zx, pt2zy); // W = z1 * z2 + (pt1xx, pt1xy) = _FQ2Sub(pt2yx, pt2yy, pt3[PTYX], pt3[PTYY]); // U = U1 - U2 + (pt1yx, pt1yy) = _FQ2Sub(pt2xx, pt2xy, pt3[PTZX], pt3[PTZY]); // V = V1 - V2 + (pt1zx, pt1zy) = _FQ2Mul(pt1yx, pt1yy, pt1yx, pt1yy); // V_squared = V * V + (pt2yx, pt2yy) = _FQ2Mul(pt1zx, pt1zy, pt3[PTZX], pt3[PTZY]); // V_squared_times_V2 = V_squared * V2 + (pt1zx, pt1zy) = _FQ2Mul(pt1zx, pt1zy, pt1yx, pt1yy); // V_cubed = V * V_squared + (pt3[PTZX], pt3[PTZY]) = _FQ2Mul(pt1zx, pt1zy, pt2zx, pt2zy); // newz = V_cubed * W + (pt2xx, pt2xy) = _FQ2Mul(pt1xx, pt1xy, pt1xx, pt1xy); // U * U + (pt2xx, pt2xy) = _FQ2Mul(pt2xx, pt2xy, pt2zx, pt2zy); // U * U * W + (pt2xx, pt2xy) = _FQ2Sub(pt2xx, pt2xy, pt1zx, pt1zy); // U * U * W - V_cubed + (pt2zx, pt2zy) = _FQ2Muc(pt2yx, pt2yy, 2); // 2 * V_squared_times_V2 + (pt2xx, pt2xy) = _FQ2Sub(pt2xx, pt2xy, pt2zx, pt2zy); // A = U * U * W - V_cubed - 2 * V_squared_times_V2 + (pt3[PTXX], pt3[PTXY]) = _FQ2Mul(pt1yx, pt1yy, pt2xx, pt2xy); // newx = V * A + (pt1yx, pt1yy) = _FQ2Sub(pt2yx, pt2yy, pt2xx, pt2xy); // V_squared_times_V2 - A + (pt1yx, pt1yy) = _FQ2Mul(pt1xx, pt1xy, pt1yx, pt1yy); // U * (V_squared_times_V2 - A) + (pt1xx, pt1xy) = _FQ2Mul(pt1zx, pt1zy, pt3[PTYX], pt3[PTYY]); // V_cubed * U2 + (pt3[PTYX], pt3[PTYY]) = _FQ2Sub(pt1yx, pt1yy, pt1xx, pt1xy); // newy = U * (V_squared_times_V2 - A) - V_cubed * U2 + } + + function _ECTwistDoubleJacobian( + uint256 pt1xx, + uint256 pt1xy, + uint256 pt1yx, + uint256 pt1yy, + uint256 pt1zx, + uint256 pt1zy + ) + internal + pure + returns ( + uint256 pt2xx, + uint256 pt2xy, + uint256 pt2yx, + uint256 pt2yy, + uint256 pt2zx, + uint256 pt2zy + ) + { + (pt2xx, pt2xy) = _FQ2Muc(pt1xx, pt1xy, 3); // 3 * x + (pt2xx, pt2xy) = _FQ2Mul(pt2xx, pt2xy, pt1xx, pt1xy); // W = 3 * x * x + (pt1zx, pt1zy) = _FQ2Mul(pt1yx, pt1yy, pt1zx, pt1zy); // S = y * z + (pt2yx, pt2yy) = _FQ2Mul(pt1xx, pt1xy, pt1yx, pt1yy); // x * y + (pt2yx, pt2yy) = _FQ2Mul(pt2yx, pt2yy, pt1zx, pt1zy); // B = x * y * S + (pt1xx, pt1xy) = _FQ2Mul(pt2xx, pt2xy, pt2xx, pt2xy); // W * W + (pt2zx, pt2zy) = _FQ2Muc(pt2yx, pt2yy, 8); // 8 * B + (pt1xx, pt1xy) = _FQ2Sub(pt1xx, pt1xy, pt2zx, pt2zy); // H = W * W - 8 * B + (pt2zx, pt2zy) = _FQ2Mul(pt1zx, pt1zy, pt1zx, pt1zy); // S_squared = S * S + (pt2yx, pt2yy) = _FQ2Muc(pt2yx, pt2yy, 4); // 4 * B + (pt2yx, pt2yy) = _FQ2Sub(pt2yx, pt2yy, pt1xx, pt1xy); // 4 * B - H + (pt2yx, pt2yy) = _FQ2Mul(pt2yx, pt2yy, pt2xx, pt2xy); // W * (4 * B - H) + (pt2xx, pt2xy) = _FQ2Muc(pt1yx, pt1yy, 8); // 8 * y + (pt2xx, pt2xy) = _FQ2Mul(pt2xx, pt2xy, pt1yx, pt1yy); // 8 * y * y + (pt2xx, pt2xy) = _FQ2Mul(pt2xx, pt2xy, pt2zx, pt2zy); // 8 * y * y * S_squared + (pt2yx, pt2yy) = _FQ2Sub(pt2yx, pt2yy, pt2xx, pt2xy); // newy = W * (4 * B - H) - 8 * y * y * S_squared + (pt2xx, pt2xy) = _FQ2Muc(pt1xx, pt1xy, 2); // 2 * H + (pt2xx, pt2xy) = _FQ2Mul(pt2xx, pt2xy, pt1zx, pt1zy); // newx = 2 * H * S + (pt2zx, pt2zy) = _FQ2Mul(pt1zx, pt1zy, pt2zx, pt2zy); // S * S_squared + (pt2zx, pt2zy) = _FQ2Muc(pt2zx, pt2zy, 8); // newz = 8 * S * S_squared + } + + function _ECTwistMulJacobian( + uint256 d, + uint256 pt1xx, + uint256 pt1xy, + uint256 pt1yx, + uint256 pt1yy, + uint256 pt1zx, + uint256 pt1zy + ) internal pure returns (uint256[6] memory pt2) { + while (d != 0) { + if ((d & 1) != 0) { + pt2 = _ECTwistAddJacobian( + pt2[PTXX], + pt2[PTXY], + pt2[PTYX], + pt2[PTYY], + pt2[PTZX], + pt2[PTZY], + pt1xx, + pt1xy, + pt1yx, + pt1yy, + pt1zx, + pt1zy + ); + } + (pt1xx, pt1xy, pt1yx, pt1yy, pt1zx, pt1zy) = + _ECTwistDoubleJacobian(pt1xx, pt1xy, pt1yx, pt1yy, pt1zx, pt1zy); + + d = d / 2; + } + } +} diff --git a/test/utils/CoreDeployLib.sol b/test/utils/CoreDeployLib.sol index d25dd95a..c0f474e7 100644 --- a/test/utils/CoreDeployLib.sol +++ b/test/utils/CoreDeployLib.sol @@ -1,271 +1,345 @@ -// // SPDX-License-Identifier: UNLICENSED -// pragma solidity ^0.8.0; - -// import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -// import {TransparentUpgradeableProxy} from -// "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -// import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; -// import {DelegationManager} from "eigenlayer-contracts/src/contracts/core/DelegationManager.sol"; -// import {StrategyManager} from "eigenlayer-contracts/src/contracts/core/StrategyManager.sol"; -// import {AVSDirectory} from "eigenlayer-contracts/src/contracts/core/AVSDirectory.sol"; -// import {EigenPodManager} from "eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol"; -// import {RewardsCoordinator} from "eigenlayer-contracts/src/contracts/core/RewardsCoordinator.sol"; -// import {StrategyBase} from "eigenlayer-contracts/src/contracts/strategies/StrategyBase.sol"; -// import {EigenPod} from "eigenlayer-contracts/src/contracts/pods/EigenPod.sol"; -// import {IETHPOSDeposit} from "eigenlayer-contracts/src/contracts/interfaces/IETHPOSDeposit.sol"; -// import {StrategyBaseTVLLimits} from "eigenlayer-contracts/src/contracts/strategies/StrategyBaseTVLLimits.sol"; -// import {PauserRegistry} from "eigenlayer-contracts/src/contracts/permissions/PauserRegistry.sol"; -// import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; -// import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -// import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; -// import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; -// import {IBeacon} from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; -// import {IStrategyManager} from "eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; -// import {IEigenPodManager} from "eigenlayer-contracts/src/contracts/interfaces/IEigenPodManager.sol"; -// import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; -// import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol"; -// import {StrategyFactory} from "eigenlayer-contracts/src/contracts/strategies/StrategyFactory.sol"; - -// import {UpgradeableProxyLib} from "../unit/UpgradeableProxyLib.sol"; - -// library CoreDeploymentLib { -// using UpgradeableProxyLib for address; - -// struct StrategyManagerConfig { -// uint256 initPausedStatus; -// uint256 initWithdrawalDelayBlocks; -// } - -// struct DelegationManagerConfig { -// uint256 initPausedStatus; -// IStrategy[] strategies; -// uint256 minWithdrawalDelayBlocks; -// uint256[] withdrawalDelayBlocks; - -// } - -// struct EigenPodManagerConfig { -// uint256 initPausedStatus; -// } - -// struct RewardsCoordinatorConfig { -// uint256 initPausedStatus; -// uint256 maxRewardsDuration; -// uint256 maxRetroactiveLength; -// uint256 maxFutureLength; -// uint256 genesisRewardsTimestamp; -// address updater; -// uint256 activationDelay; -// uint256 calculationIntervalSeconds; -// uint256 globalOperatorCommissionBips; -// } - -// struct StrategyFactoryConfig { -// uint256 initPausedStatus; -// } - -// struct DeploymentConfigData { -// StrategyManagerConfig strategyManager; -// DelegationManagerConfig delegationManager; -// EigenPodManagerConfig eigenPodManager; -// RewardsCoordinatorConfig rewardsCoordinator; -// StrategyFactoryConfig strategyFactory; -// } - -// struct DeploymentData { -// address delegationManager; -// address avsDirectory; -// address strategyManager; -// address eigenPodManager; -// address rewardsCoordinator; -// address eigenPodBeacon; -// address pauserRegistry; -// address strategyFactory; -// address strategyBeacon; -// } - -// function deployContracts( -// address proxyAdmin, -// DeploymentConfigData memory configData -// ) internal returns (DeploymentData memory) { -// DeploymentData memory result; - -// result.delegationManager = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); -// result.avsDirectory = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); -// result.strategyManager = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); -// result.eigenPodManager = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); -// result.rewardsCoordinator = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); -// result.eigenPodBeacon = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); -// result.pauserRegistry = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); -// result.strategyFactory = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); - -// // Deploy the implementation contracts, using the proxy contracts as inputs -// address delegationManagerImpl = address( -// new DelegationManager( -// IStrategyManager(result.strategyManager), -// IEigenPodManager(result.eigenPodManager) -// ) -// ); -// address avsDirectoryImpl = -// address(new AVSDirectory(IDelegationManager(result.delegationManager))); - -// address strategyManagerImpl = address( -// new StrategyManager( -// IDelegationManager(result.delegationManager), -// IEigenPodManager(result.eigenPodManager) -// ) -// ); - -// address strategyFactoryImpl = -// address(new StrategyFactory(IStrategyManager(result.strategyManager))); - -// address ethPOSDeposit; -// if (block.chainid == 1) { -// ethPOSDeposit = 0x00000000219ab540356cBB839Cbe05303d7705Fa; -// } else { -// // For non-mainnet chains, you might want to deploy a mock or read from a config -// // This assumes you have a similar config setup as in M2_Deploy_From_Scratch.s.sol -// /// TODO: Handle Eth pos -// } - -// address eigenPodManagerImpl = address( -// new EigenPodManager( -// IETHPOSDeposit(ethPOSDeposit), -// IBeacon(result.eigenPodBeacon), -// IStrategyManager(result.strategyManager), -// IDelegationManager(result.delegationManager) -// ) -// ); - -// /// TODO: Get actual values -// uint32 CALCULATION_INTERVAL_SECONDS = 1 days; -// uint32 MAX_REWARDS_DURATION = 1 days; -// uint32 MAX_RETROACTIVE_LENGTH = 1; -// uint32 MAX_FUTURE_LENGTH = 1; -// uint32 GENESIS_REWARDS_TIMESTAMP = 10 days; -// address rewardsCoordinatorImpl = address( -// new RewardsCoordinator( -// IDelegationManager(result.delegationManager), -// IStrategyManager(result.strategyManager), -// CALCULATION_INTERVAL_SECONDS, -// MAX_REWARDS_DURATION, -// MAX_RETROACTIVE_LENGTH, -// MAX_FUTURE_LENGTH, -// GENESIS_REWARDS_TIMESTAMP -// ) -// ); - -// /// TODO: Get actual genesis time -// uint64 GENESIS_TIME = 1_564_000; - -// address eigenPodImpl = address( -// new EigenPod( -// IETHPOSDeposit(ethPOSDeposit), -// IEigenPodManager(result.eigenPodManager), -// GENESIS_TIME -// ) -// ); -// address eigenPodBeaconImpl = address(new UpgradeableBeacon(eigenPodImpl)); -// address baseStrategyImpl = -// address(new StrategyBase(IStrategyManager(result.strategyManager))); -// /// TODO: PauserRegistry isn't upgradeable -// address pauserRegistryImpl = address( -// new PauserRegistry( -// new address[](0), // Empty array for pausers -// proxyAdmin // ProxyAdmin as the unpauser -// ) -// ); - -// // Deploy and configure the strategy beacon -// result.strategyBeacon = address(new UpgradeableBeacon(baseStrategyImpl)); - -// // Upgrade contracts -// /// TODO: Get from config -// bytes memory upgradeCall = abi.encodeWithSelector( /// TODO: Fix abi.encodeCall was failing Cannot implicitly convert component at position 4 from "IStrategy[]" to "IStrategy[]" -// DelegationManager.initialize.selector, -// proxyAdmin, // initialOwner -// IPauserRegistry(result.pauserRegistry), // _pauserRegistry -// configData.delegationManager.initPausedStatus, // initialPausedStatus -// configData.delegationManager.minWithdrawalDelayBlocks, // _minWithdrawalDelayBlocks -// configData.delegationManager.strategies, // _strategies -// configData.delegationManager.withdrawalDelayBlocks // _withdrawalDelayBlocks -// ); -// UpgradeableProxyLib.upgradeAndCall( -// result.delegationManager, delegationManagerImpl, upgradeCall -// ); - -// // Upgrade StrategyManager contract -// upgradeCall = abi.encodeCall( -// StrategyManager.initialize, -// ( -// proxyAdmin, // initialOwner -// result.strategyFactory, // initialStrategyWhitelister -// IPauserRegistry(result.pauserRegistry), // _pauserRegistry -// configData.strategyManager.initPausedStatus // initialPausedStatus -// ) -// ); -// UpgradeableProxyLib.upgradeAndCall(result.strategyManager, strategyManagerImpl, upgradeCall); - -// // Upgrade StrategyFactory contract -// upgradeCall = abi.encodeCall( -// StrategyFactory.initialize, -// ( -// proxyAdmin, // initialOwner -// IPauserRegistry(result.pauserRegistry), // _pauserRegistry -// configData.strategyFactory.initPausedStatus, // initialPausedStatus -// IBeacon(result.strategyBeacon) -// ) -// ); -// UpgradeableProxyLib.upgradeAndCall(result.strategyFactory, strategyFactoryImpl, upgradeCall); - -// // Upgrade EigenPodManager contract -// upgradeCall = abi.encodeCall( -// EigenPodManager.initialize, -// ( -// proxyAdmin, // initialOwner -// IPauserRegistry(result.pauserRegistry), // _pauserRegistry -// configData.eigenPodManager.initPausedStatus // initialPausedStatus -// ) -// ); -// UpgradeableProxyLib.upgradeAndCall(result.eigenPodManager, eigenPodManagerImpl, upgradeCall); - -// // Upgrade AVSDirectory contract -// upgradeCall = abi.encodeCall( -// AVSDirectory.initialize, -// ( -// proxyAdmin, // initialOwner -// IPauserRegistry(result.pauserRegistry), // _pauserRegistry -// 0 // TODO: AVS Missing configinitialPausedStatus -// ) -// ); -// UpgradeableProxyLib.upgradeAndCall(result.avsDirectory, avsDirectoryImpl, upgradeCall); - -// // Upgrade RewardsCoordinator contract -// upgradeCall = abi.encodeCall( -// RewardsCoordinator.initialize, -// ( -// proxyAdmin, // initialOwner -// IPauserRegistry(result.pauserRegistry), // _pauserRegistry -// configData.rewardsCoordinator.initPausedStatus, // initialPausedStatus -// /// TODO: is there a setter and is this expected? -// address(0), // rewards updater -// uint32(configData.rewardsCoordinator.activationDelay), // _activationDelay -// uint16(configData.rewardsCoordinator.globalOperatorCommissionBips) // _globalCommissionBips -// ) -// ); -// UpgradeableProxyLib.upgradeAndCall( -// result.rewardsCoordinator, rewardsCoordinatorImpl, upgradeCall -// ); - -// // Upgrade EigenPod contract -// upgradeCall = abi.encodeCall( -// EigenPod.initialize, -// // TODO: Double check this -// (address(result.eigenPodManager)) // _podOwner -// ); -// UpgradeableProxyLib.upgradeAndCall(result.eigenPodBeacon, eigenPodImpl, upgradeCall); - -// return result; -// } - -// } +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import {DelegationManager} from "eigenlayer-contracts/src/contracts/core/DelegationManager.sol"; +import {StrategyManager} from "eigenlayer-contracts/src/contracts/core/StrategyManager.sol"; +import {AVSDirectory} from "eigenlayer-contracts/src/contracts/core/AVSDirectory.sol"; +import {EigenPodManager} from "eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol"; +import {RewardsCoordinator} from "eigenlayer-contracts/src/contracts/core/RewardsCoordinator.sol"; +import {StrategyBase} from "eigenlayer-contracts/src/contracts/strategies/StrategyBase.sol"; +import {EigenPod} from "eigenlayer-contracts/src/contracts/pods/EigenPod.sol"; +import {IETHPOSDeposit} from "eigenlayer-contracts/src/contracts/interfaces/IETHPOSDeposit.sol"; +import {StrategyBaseTVLLimits} from + "eigenlayer-contracts/src/contracts/strategies/StrategyBaseTVLLimits.sol"; +import {PauserRegistry} from "eigenlayer-contracts/src/contracts/permissions/PauserRegistry.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {IDelegationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IBeacon} from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; +import {IStrategyManager} from "eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; +import {IEigenPodManager} from "eigenlayer-contracts/src/contracts/interfaces/IEigenPodManager.sol"; +import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; +import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol"; +import {StrategyFactory} from "eigenlayer-contracts/src/contracts/strategies/StrategyFactory.sol"; +import {IPermissionController} from + "eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {AllocationManager} from "eigenlayer-contracts/src/contracts/core/AllocationManager.sol"; +import {PermissionController} from + "eigenlayer-contracts/src/contracts/permissions/PermissionController.sol"; + +import {UpgradeableProxyLib} from "../unit/UpgradeableProxyLib.sol"; + +library CoreDeploymentLib { + using UpgradeableProxyLib for address; + + struct StrategyManagerConfig { + uint256 initPausedStatus; + address initialOwner; + address initialStrategyWhitelister; + } + + struct DelegationManagerConfig { + uint256 initPausedStatus; + address initialOwner; + uint32 minWithdrawalDelayBlocks; + } + + struct EigenPodManagerConfig { + uint256 initPausedStatus; + address initialOwner; + } + + struct AllocationManagerConfig { + uint256 initPausedStatus; + address initialOwner; + uint32 deallocationDelay; + uint32 allocationConfigurationDelay; + } + + struct StrategyFactoryConfig { + uint256 initPausedStatus; + address initialOwner; + } + + struct AVSDirectoryConfig { + uint256 initPausedStatus; + address initialOwner; + } + + struct RewardsCoordinatorConfig { + uint256 initPausedStatus; + address initialOwner; + address rewardsUpdater; + uint32 activationDelay; + uint16 defaultSplitBips; + uint32 calculationIntervalSeconds; + uint32 maxRewardsDuration; + uint32 maxRetroactiveLength; + uint32 maxFutureLength; + uint32 genesisRewardsTimestamp; + } + + struct ETHPOSDepositConfig { + address ethPOSDepositAddress; + } + + struct EigenPodConfig { + uint64 genesisTimestamp; + } + + struct DeploymentConfigData { + StrategyManagerConfig strategyManager; + DelegationManagerConfig delegationManager; + EigenPodManagerConfig eigenPodManager; + AllocationManagerConfig allocationManager; + StrategyFactoryConfig strategyFactory; + RewardsCoordinatorConfig rewardsCoordinator; + AVSDirectoryConfig avsDirectory; + ETHPOSDepositConfig ethPOSDeposit; + EigenPodConfig eigenPod; + } + + struct DeploymentData { + address delegationManager; + address avsDirectory; + address strategyManager; + address eigenPodManager; + address allocationManager; + address eigenPodBeacon; + address pauserRegistry; + address strategyFactory; + address strategyBeacon; + address rewardsCoordinator; + address permissionController; + } + + function deployContracts( + address proxyAdmin, + DeploymentConfigData memory configData + ) internal returns (DeploymentData memory result) { + result = deployEmptyProxies(proxyAdmin); + + deployAndConfigureCore(result, configData); + deployAndConfigurePods(result, configData); + deployAndConfigureStrategies(result, configData); + deployAndConfigureRewards(result, configData); + + return result; + } + + function deployEmptyProxies( + address proxyAdmin + ) internal returns (DeploymentData memory proxies) { + proxies.delegationManager = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.avsDirectory = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.strategyManager = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.eigenPodManager = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.allocationManager = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.eigenPodBeacon = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.pauserRegistry = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.strategyFactory = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.rewardsCoordinator = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.permissionController = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + return proxies; + } + + function deployAndConfigureCore( + DeploymentData memory deployments, + DeploymentConfigData memory config + ) internal { + // Deploy core implementations + address permissionControllerImpl = address(new PermissionController()); + + address strategyManagerImpl = address( + new StrategyManager( + IDelegationManager(deployments.delegationManager), + IPauserRegistry(deployments.pauserRegistry) + ) + ); + + address allocationManagerImpl = address( + new AllocationManager( + IDelegationManager(deployments.delegationManager), + IPauserRegistry(deployments.pauserRegistry), + IPermissionController(deployments.permissionController), + config.allocationManager.deallocationDelay, + config.allocationManager.allocationConfigurationDelay + ) + ); + + address delegationManagerImpl = address( + new DelegationManager( + IStrategyManager(deployments.strategyManager), + IEigenPodManager(deployments.eigenPodManager), + IAllocationManager(deployments.allocationManager), + IPauserRegistry(deployments.pauserRegistry), + IPermissionController(deployments.permissionController), + config.delegationManager.minWithdrawalDelayBlocks + ) + ); + + address avsDirectoryImpl = address( + new AVSDirectory( + IDelegationManager(deployments.delegationManager), + IPauserRegistry(deployments.pauserRegistry) + ) + ); + + // Initialize core contracts + UpgradeableProxyLib.upgrade(deployments.permissionController, permissionControllerImpl); + + bytes memory upgradeCall = abi.encodeCall( + StrategyManager.initialize, + ( + config.strategyManager.initialOwner, + config.strategyManager.initialStrategyWhitelister, + config.strategyManager.initPausedStatus + ) + ); + UpgradeableProxyLib.upgradeAndCall( + deployments.strategyManager, strategyManagerImpl, upgradeCall + ); + + upgradeCall = abi.encodeCall( + DelegationManager.initialize, + (config.delegationManager.initialOwner, config.delegationManager.initPausedStatus) + ); + UpgradeableProxyLib.upgradeAndCall( + deployments.delegationManager, delegationManagerImpl, upgradeCall + ); + + upgradeCall = abi.encodeCall( + AllocationManager.initialize, + (config.allocationManager.initialOwner, config.allocationManager.initPausedStatus) + ); + UpgradeableProxyLib.upgradeAndCall( + deployments.allocationManager, allocationManagerImpl, upgradeCall + ); + + upgradeCall = abi.encodeCall( + AVSDirectory.initialize, + (config.avsDirectory.initialOwner, config.avsDirectory.initPausedStatus) + ); + UpgradeableProxyLib.upgradeAndCall(deployments.avsDirectory, avsDirectoryImpl, upgradeCall); + } + + function deployAndConfigurePods( + DeploymentData memory deployments, + DeploymentConfigData memory config + ) internal { + address ethPOSDeposit = config.ethPOSDeposit.ethPOSDepositAddress; + if (ethPOSDeposit == address(0)) { + if (block.chainid == 1) { + ethPOSDeposit = 0x00000000219ab540356cBB839Cbe05303d7705Fa; + } else { + revert("DEPLOY_MOCK_ETHPOS_CONTRACT"); + } + } + + address eigenPodImpl = address( + new EigenPod( + IETHPOSDeposit(ethPOSDeposit), + IEigenPodManager(deployments.eigenPodManager), + config.eigenPod.genesisTimestamp == 0 + ? uint64(block.timestamp) + : config.eigenPod.genesisTimestamp + ) + ); + + address eigenPodBeaconImpl = address(new UpgradeableBeacon(eigenPodImpl)); + UpgradeableProxyLib.upgrade(deployments.eigenPodBeacon, eigenPodBeaconImpl); + + address eigenPodManagerImpl = address( + new EigenPodManager( + IETHPOSDeposit(ethPOSDeposit), + IBeacon(deployments.eigenPodBeacon), + IDelegationManager(deployments.delegationManager), + IPauserRegistry(deployments.pauserRegistry) + ) + ); + + bytes memory upgradeCall = abi.encodeCall( + EigenPodManager.initialize, + (config.eigenPodManager.initialOwner, config.eigenPodManager.initPausedStatus) + ); + UpgradeableProxyLib.upgradeAndCall( + deployments.eigenPodManager, eigenPodManagerImpl, upgradeCall + ); + } + + function deployAndConfigureStrategies( + DeploymentData memory deployments, + DeploymentConfigData memory config + ) internal { + address baseStrategyImpl = address( + new StrategyBase( + IStrategyManager(deployments.strategyManager), + IPauserRegistry(deployments.pauserRegistry) + ) + ); + + deployments.strategyBeacon = address(new UpgradeableBeacon(baseStrategyImpl)); + + address strategyFactoryImpl = address( + new StrategyFactory( + IStrategyManager(deployments.strategyManager), + IPauserRegistry(deployments.pauserRegistry) + ) + ); + + bytes memory upgradeCall = abi.encodeCall( + StrategyFactory.initialize, + ( + config.strategyFactory.initialOwner, + config.strategyFactory.initPausedStatus, + IBeacon(deployments.strategyBeacon) + ) + ); + UpgradeableProxyLib.upgradeAndCall( + deployments.strategyFactory, strategyFactoryImpl, upgradeCall + ); + } + + function deployAndConfigureRewards( + DeploymentData memory deployments, + DeploymentConfigData memory config + ) internal { + address rewardsCoordinatorImpl = address( + new RewardsCoordinator( + IDelegationManager(deployments.delegationManager), + IStrategyManager(deployments.strategyManager), + IAllocationManager(deployments.allocationManager), + IPauserRegistry(deployments.pauserRegistry), + IPermissionController(deployments.permissionController), + config.rewardsCoordinator.calculationIntervalSeconds, + config.rewardsCoordinator.maxRewardsDuration, + config.rewardsCoordinator.maxRetroactiveLength, + config.rewardsCoordinator.maxFutureLength, + config.rewardsCoordinator.genesisRewardsTimestamp + ) + ); + + bytes memory upgradeCall = abi.encodeCall( + RewardsCoordinator.initialize, + ( + config.rewardsCoordinator.initialOwner, + config.rewardsCoordinator.initPausedStatus, + config.rewardsCoordinator.rewardsUpdater, + config.rewardsCoordinator.activationDelay, + config.rewardsCoordinator.defaultSplitBips + ) + ); + + UpgradeableProxyLib.upgradeAndCall( + deployments.rewardsCoordinator, rewardsCoordinatorImpl, upgradeCall + ); + } +} diff --git a/test/utils/MiddlewareDeployLib.sol b/test/utils/MiddlewareDeployLib.sol new file mode 100644 index 00000000..643701ef --- /dev/null +++ b/test/utils/MiddlewareDeployLib.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol"; +import {IDelegationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; + +import {InstantSlasher} from "../../src/slashers/InstantSlasher.sol"; +import {SlashingRegistryCoordinator} from "../../src/SlashingRegistryCoordinator.sol"; +import {SocketRegistry} from "../../src/SocketRegistry.sol"; +import {IndexRegistry} from "../../src/IndexRegistry.sol"; +import {StakeRegistry} from "../../src/StakeRegistry.sol"; +import {BLSApkRegistry} from "../../src/BLSApkRegistry.sol"; +import {IStakeRegistry, IStakeRegistryTypes} from "../../src/interfaces/IStakeRegistry.sol"; +import {IBLSApkRegistry} from "../../src/interfaces/IBLSApkRegistry.sol"; +import {IIndexRegistry} from "../../src/interfaces/IIndexRegistry.sol"; +import {ISocketRegistry} from "../../src/interfaces/ISocketRegistry.sol"; +import {ISlashingRegistryCoordinator} from "../../src/interfaces/ISlashingRegistryCoordinator.sol"; + +import {UpgradeableProxyLib} from "../unit/UpgradeableProxyLib.sol"; + +library MiddlewareDeployLib { + using UpgradeableProxyLib for address; + + struct InstantSlasherConfig { + address initialOwner; + address slasher; + } + + struct SlashingRegistryCoordinatorConfig { + address initialOwner; + address churnApprover; + address ejector; + uint256 initPausedStatus; + address serviceManager; + } + + struct SocketRegistryConfig { + address initialOwner; + } + + struct IndexRegistryConfig { + address initialOwner; + } + + struct StakeRegistryConfig { + address initialOwner; + uint256 minimumStake; + uint32 strategyParams; + address delegationManager; + address avsDirectory; + IStakeRegistryTypes.StrategyParams[] strategyParamsArray; + uint32 lookAheadPeriod; + IStakeRegistryTypes.StakeType stakeType; + } + + struct BLSApkRegistryConfig { + address initialOwner; + } + + struct MiddlewareDeployConfig { + InstantSlasherConfig instantSlasher; + SlashingRegistryCoordinatorConfig slashingRegistryCoordinator; + SocketRegistryConfig socketRegistry; + IndexRegistryConfig indexRegistry; + StakeRegistryConfig stakeRegistry; + BLSApkRegistryConfig blsApkRegistry; + } + + struct MiddlewareDeployData { + address instantSlasher; + address slashingRegistryCoordinator; + address socketRegistry; + address indexRegistry; + address stakeRegistry; + address blsApkRegistry; + } + + function deployMiddleware( + address proxyAdmin, + address allocationManager, + address pauserRegistry, + MiddlewareDeployConfig memory config + ) internal returns (MiddlewareDeployData memory result) { + result = deployEmptyProxies(proxyAdmin); + + upgradeRegistries(result, allocationManager, pauserRegistry, config); + upgradeCoordinator( + result, allocationManager, pauserRegistry, config.slashingRegistryCoordinator + ); + upgradeInstantSlasher(result, allocationManager, config.instantSlasher); + + return result; + } + + function deployEmptyProxies( + address proxyAdmin + ) internal returns (MiddlewareDeployData memory proxies) { + proxies.instantSlasher = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.slashingRegistryCoordinator = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.socketRegistry = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.indexRegistry = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.stakeRegistry = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + proxies.blsApkRegistry = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); + return proxies; + } + + function upgradeRegistries( + MiddlewareDeployData memory deployments, + address allocationManager, + address pauserRegistry, + MiddlewareDeployConfig memory config + ) internal { + address blsApkRegistryImpl = address( + new BLSApkRegistry( + ISlashingRegistryCoordinator(deployments.slashingRegistryCoordinator) + ) + ); + UpgradeableProxyLib.upgrade(deployments.blsApkRegistry, blsApkRegistryImpl); + + address indexRegistryImpl = address( + new IndexRegistry(ISlashingRegistryCoordinator(deployments.slashingRegistryCoordinator)) + ); + UpgradeableProxyLib.upgrade(deployments.indexRegistry, indexRegistryImpl); + + address socketRegistryImpl = address( + new SocketRegistry( + ISlashingRegistryCoordinator(deployments.slashingRegistryCoordinator) + ) + ); + UpgradeableProxyLib.upgrade(deployments.socketRegistry, socketRegistryImpl); + + // StakeRegistry upgrade + address stakeRegistryImpl = address( + new StakeRegistry( + ISlashingRegistryCoordinator(deployments.slashingRegistryCoordinator), + IDelegationManager(config.stakeRegistry.delegationManager), + IAVSDirectory(config.stakeRegistry.avsDirectory), + IAllocationManager(allocationManager) + ) + ); + UpgradeableProxyLib.upgrade(deployments.stakeRegistry, stakeRegistryImpl); + } + + function upgradeCoordinator( + MiddlewareDeployData memory deployments, + address allocationManager, + address pauserRegistry, + SlashingRegistryCoordinatorConfig memory coordinatorConfig + ) internal { + address coordinatorImpl = address( + new SlashingRegistryCoordinator( + IStakeRegistry(deployments.stakeRegistry), + IBLSApkRegistry(deployments.blsApkRegistry), + IIndexRegistry(deployments.indexRegistry), + ISocketRegistry(deployments.socketRegistry), + IAllocationManager(allocationManager), + IPauserRegistry(pauserRegistry) + ) + ); + bytes memory upgradeCall = abi.encodeCall( + SlashingRegistryCoordinator.initialize, + ( + coordinatorConfig.initialOwner, + coordinatorConfig.churnApprover, + coordinatorConfig.ejector, + coordinatorConfig.initPausedStatus, + coordinatorConfig.serviceManager + ) + ); + UpgradeableProxyLib.upgradeAndCall( + deployments.slashingRegistryCoordinator, coordinatorImpl, upgradeCall + ); + } + + // Upgrade and initialize InstantSlasher with its config data + function upgradeInstantSlasher( + MiddlewareDeployData memory deployments, + address allocationManager, + InstantSlasherConfig memory slasherConfig + ) internal { + address instantSlasherImpl = address( + new InstantSlasher( + IAllocationManager(allocationManager), + ISlashingRegistryCoordinator(deployments.slashingRegistryCoordinator), + slasherConfig.slasher + ) + ); + UpgradeableProxyLib.upgrade(deployments.instantSlasher, instantSlasherImpl); + } +} diff --git a/test/utils/MockAVSDeployer.sol b/test/utils/MockAVSDeployer.sol index e527e0f8..0b049bf3 100644 --- a/test/utils/MockAVSDeployer.sol +++ b/test/utils/MockAVSDeployer.sol @@ -53,6 +53,7 @@ import {BLSApkRegistryHarness} from "../harnesses/BLSApkRegistryHarness.sol"; import {EmptyContract} from "eigenlayer-contracts/src/test/mocks/EmptyContract.sol"; import {StakeRegistryHarness} from "../harnesses/StakeRegistryHarness.sol"; +import {OperatorWalletLib, Operator} from "../utils/OperatorWalletLib.sol"; import "forge-std/Test.sol"; @@ -356,8 +357,10 @@ contract MockAVSDeployer is Test { operatorStateRetriever = new OperatorStateRetriever(); + // Set RegistryCoordinator as M2 state with existing quorums + registryCoordinator.setM2QuorumBitmap(0); registryCoordinator.setOperatorSetsEnabled(false); - registryCoordinator.setM2QuorumsDisabled(false); + registryCoordinator.setM2QuorumRegistrationDisabled(false); } function _labelContracts() internal { @@ -547,4 +550,17 @@ contract MockAVSDeployer is Test { salt: salt }); } + + function _createOperators( + uint256 numOperators, + uint256 startIndex + ) internal returns (Operator[] memory) { + Operator[] memory operators = new Operator[](numOperators); + for (uint256 i = 0; i < numOperators; i++) { + operators[i] = OperatorWalletLib.createOperator( + string(abi.encodePacked("operator-", i + startIndex)) + ); + } + return operators; + } } diff --git a/test/utils/OperatorWalletLib.sol b/test/utils/OperatorWalletLib.sol new file mode 100644 index 00000000..d46127fc --- /dev/null +++ b/test/utils/OperatorWalletLib.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Vm} from "forge-std/Vm.sol"; +import {BN254} from "src/libraries/BN254.sol"; +import {BN256G2} from "./BN256G2.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +struct Wallet { + uint256 privateKey; + address addr; +} + +struct BLSWallet { + uint256 privateKey; + BN254.G2Point publicKeyG2; + BN254.G1Point publicKeyG1; +} + +struct Operator { + Wallet key; + BLSWallet signingKey; +} + +library OperatorKeyOperationsLib { + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + function sign(Wallet memory wallet, bytes32 digest) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, digest); + return abi.encodePacked(r, s, v); + } +} + +library SigningKeyOperationsLib { + using BN254 for BN254.G1Point; + + function sign( + BLSWallet memory blsWallet, + bytes32 messageHash + ) internal view returns (BN254.G1Point memory) { + // Hash the message to a point on G1 + BN254.G1Point memory messagePoint = BN254.hashToG1(messageHash); + + // Sign by multiplying the hashed message point with the private key + return messagePoint.scalar_mul(blsWallet.privateKey); + } + + function aggregate( + BN254.G2Point memory pk1, + BN254.G2Point memory pk2 + ) internal view returns (BN254.G2Point memory apk) { + (apk.X[0], apk.X[1], apk.Y[0], apk.Y[1]) = BN256G2.ECTwistAdd( + pk1.X[0], pk1.X[1], pk1.Y[0], pk1.Y[1], pk2.X[0], pk2.X[1], pk2.Y[0], pk2.Y[1] + ); + } +} + +library OperatorWalletLib { + using BN254 for *; + using Strings for uint256; + + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + function createBLSWallet( + uint256 salt + ) internal returns (BLSWallet memory) { + uint256 privateKey = uint256(keccak256(abi.encodePacked(salt))); + BN254.G1Point memory publicKeyG1 = BN254.generatorG1().scalar_mul(privateKey); + BN254.G2Point memory publicKeyG2 = mul(privateKey); + + return + BLSWallet({privateKey: privateKey, publicKeyG2: publicKeyG2, publicKeyG1: publicKeyG1}); + } + + function createWallet( + uint256 salt + ) internal pure returns (Wallet memory) { + uint256 privateKey = uint256(keccak256(abi.encodePacked(salt))); + address addr = vm.addr(privateKey); + + return Wallet({privateKey: privateKey, addr: addr}); + } + + function createOperator( + string memory name + ) internal returns (Operator memory) { + uint256 salt = uint256(keccak256(abi.encodePacked(name))); + Wallet memory vmWallet = createWallet(salt); + BLSWallet memory blsWallet = createBLSWallet(salt); + + return Operator({key: vmWallet, signingKey: blsWallet}); + } + + function mul( + uint256 x + ) internal returns (BN254.G2Point memory g2Point) { + string[] memory inputs = new string[](5); + inputs[0] = "go"; + inputs[1] = "run"; + inputs[2] = "test/ffi/go/g2mul.go"; + inputs[3] = x.toString(); + + inputs[4] = "1"; + bytes memory res = vm.ffi(inputs); + g2Point.X[1] = abi.decode(res, (uint256)); + + inputs[4] = "2"; + res = vm.ffi(inputs); + g2Point.X[0] = abi.decode(res, (uint256)); + + inputs[4] = "3"; + res = vm.ffi(inputs); + g2Point.Y[1] = abi.decode(res, (uint256)); + + inputs[4] = "4"; + res = vm.ffi(inputs); + g2Point.Y[0] = abi.decode(res, (uint256)); + } +}