diff --git a/.gitmodules b/.gitmodules index 43ea590cc..6aefc3176 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,7 +4,6 @@ [submodule "lib/eigenlayer-contracts"] path = lib/eigenlayer-contracts url = https://github.com/Layr-labs/eigenlayer-contracts - branch = dev [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/Openzeppelin/openzeppelin-contracts diff --git a/CHANGELOG/CHANGELOG-1.4.0.md b/CHANGELOG/CHANGELOG-1.4.0.md new file mode 100644 index 000000000..7245c0ce6 --- /dev/null +++ b/CHANGELOG/CHANGELOG-1.4.0.md @@ -0,0 +1,44 @@ +# v1.4.0 MultiChain/MiddlewareV2 + +The multichain/middlewareV2 release enables AVSs to launch their services and make verified Operator outputs available on any EVM chain, meeting their customers where they are. AVSs can specify custom operator weights to be transported to any destination chain. The release has 3 components: + +1. Core Contracts +2. AVS Contracts +3. Offchain Infrastructure + +The below release notes cover AVS Contracts. For more information on the end to end protocol, see our [docs](../docs/middlewareV2/README.md), [core contract docs](https://github.com/Layr-Labs/eigenlayer-contracts/tree/main/docs/multichain), and [ELIP-008](https://github.com/eigenfoundation/ELIPs/blob/elip-008v1/ELIPs/ELIP-008.md). + +## Release Manager + +@ypatil12 @eigenmikem + +## Highlights + +This multichain release only introduces new standards and contracts. As a result, there are **no breaking changes or deprecations**. All new contracts are in the [middlewareV2 folder](../src/middlewareV2/). + +🚀 New Features – Highlight major new functionality + +- `AVSRegistrar`: The primary interface for managing operator registration and deregistration within an AVS. It integrates with core EigenLayer contracts to ensure operators have valid keys and are properly registered in operator sets +- `OperatorTableCalculator`: Responsible for calculating stake weights of operator. These stake weights are aggregated and transported using the [Eigenlayer Multichain Protocol](https://github.com/eigenfoundation/ELIPs/blob/elip-008v1/ELIPs/ELIP-008.md). In order to utilize the multichain protocol, an AVS *MUST* deploy an `OperatorTableCalculator` and register it in the `CrossChainRegistry` in EigenLayer core. See our [core documentation](https://github.com/Layr-Labs/eigenlayer-contracts/tree/main/docs/multichain#common-user-flows) for this process. + +🔧 Improvements – Enhancements to existing features. + +- The multichain protocol has protocol-ized several AVS-deployed contracts, enabling an simpler AVS developer experience. These include: + - `KeyRegistrar`: Manages BLS and ECDSA signing keys. AVSs no longer have to deploy a `BLSAPKRegistry` + - `CertificateVerifier`: Handles signature verification for BLS and ECDSA keys. AVSs no longer have to deploy a `BLSSignatureChecker` + - Offchain Multichain Transport: AVSs no longer have to maintain [avs-sync](https://github.com/Layr-Labs/avs-sync) to keep operator stakes fresh + +## Changelog + +- fix: avs registrar as identifier [PR #494](https://github.com/layr-labs/eigenlayer-middleware/pull/494) +- fix: table calc interface [PR #493](https://github.com/layr-labs/eigenlayer-middleware/pull/493) +- docs: middlewareV2/multichain [PR #489](https://github.com/layr-labs/eigenlayer-middleware/pull/489) +- chore: add avs registrar interfaces [PR #491](https://github.com/layr-labs/eigenlayer-middleware/pull/491) +- chore: remove unused imports [PR #490](https://github.com/layr-labs/eigenlayer-middleware/pull/490) +- feat: add table calculators [PR #488](https://github.com/layr-labs/eigenlayer-middleware/pull/488) +- chore: remove interfaces [PR #485](https://github.com/layr-labs/eigenlayer-middleware/pull/485) +- chore: bump up ecdsa dependency [PR #487](https://github.com/layr-labs/eigenlayer-middleware/pull/487) +- chore: bump up `eigenlayer-contracts` dependency [PR #486](https://github.com/layr-labs/eigenlayer-middleware/pull/486) +- feat: avs registrar [PR #484](https://github.com/layr-labs/eigenlayer-middleware/pull/484) +- refactor: singleton cv combining ECDSA and BN254 [PR #479](https://github.com/layr-labs/eigenlayer-middleware/pull/479) +- feat: multichain interfaces [PR #477](https://github.com/layr-labs/eigenlayer-middleware/pull/477) \ No newline at end of file diff --git a/CHANGELOG/CHANGELOG-template.md b/CHANGELOG/CHANGELOG-template.md new file mode 100644 index 000000000..88d178db5 --- /dev/null +++ b/CHANGELOG/CHANGELOG-template.md @@ -0,0 +1,84 @@ +# + +**Use this template to draft changelog and submit PR to review by the team** + +## Release Manager + +github handle of release manager + +## Highlights + +🚀 New Features – Highlight major new functionality +- ... +- ... + +⛔ Breaking Changes – Call out backward-incompatible changes. +- ... +- ... + +📌 Deprecations – Mention features that are being phased out. +- ... +- ... + +🛠️ Security Fixes – Specify patched vulnerabilities. +- ... +- ... + +🔧 Improvements – Enhancements to existing features. +- ... +- ... + +🐛 Bug Fixes – List resolved issues. +- ... +- ... + + +## Changelog + +To generate a changelog of commits added since the last release using Git on +the command line, follow these steps: + +1. Identify the last release tag + +First, list your tags (assuming you use Git tags for releases): + +``` +git tag --sort=-creatordate +``` + +This shows your most recent tags at the top. Let's say the last release tag is `v1.4.2` + + +2. Generate the changelog + +Now, use the following command to list the commits since that tag, and auto generate github PR link if there's any + +``` +git log v1.3.1..HEAD --pretty=format:"%s" --no-merges | \ +sed -E 's/^(.*)\(#([0-9]+)\)$/- \1[PR #\2](https:\/\/github.com\/layr-labs\/eigenlayer-middleware\/pull\/\2)/' | \ +sed -E '/\[PR #[0-9]+\]/! s/^(.*)$/- \1/' +``` + +This will show: + +- Only commits since v1.3.1 up to the current HEAD +- One-line commit messages (%s) with the author name (%an) + + +An example output is: + +``` +- ci: add explicit permissions to workflows to mitigate security concerns [PR #1392](https://github.com/layr-labs/eigenlayer-contracts/pull/1392) +- ci: remove branch constraint for foundry coverage job +- docs: add release managers to changelogs +- docs: add templates for changelog and release notes [PR #1382](https://github.com/layr-labs/eigenlayer-contracts/pull/1382) +- docs: add doc for steps to write deploy scripts [PR #1380](https://github.com/layr-labs/eigenlayer-contracts/pull/1380) +- ci: add testnet envs sepolia and hoodi to validate-deployment-scripts [PR #1378](https://github.com/layr-labs/eigenlayer-contracts/pull/1378) +- docs: update MAINTENANCE to include practices of merging multiple release-dev branches +- docs: updating readme for dead links, readability, new language, and more [PR #1377](https://github.com/layr-labs/eigenlayer-contracts/pull/1377) +... +``` + +3. Commit the Changelog + +Copy the output and add here with a commit, then proceed to cut the release from the commit. diff --git a/bin/storage-report.sh b/bin/storage-report.sh index 4c34d5ac1..2164f1889 100644 --- a/bin/storage-report.sh +++ b/bin/storage-report.sh @@ -36,10 +36,14 @@ for file in $(find src/ -name "*.sol" ! -path "*/interfaces/*" ! -path "*/librar log "Processing contract: $contract_name" - # Run forge inspect and capture errors - if ! forge inspect "$contract_name" storage > "$OUTPUT_DIR/$contract_name.md"; then - error "Failed to generate storage report for contract: $contract_name" + # Create a unique output filename based on the full path to handle duplicate contract names + # Replace slashes with underscores and remove the src/ prefix + unique_name=$(echo "$file" | sed 's|^src/||' | sed 's|/|_|g' | sed 's|\.sol$||') + + # Run forge inspect using the full path to handle duplicate contract names + if ! forge inspect "$file:$contract_name" storage > "$OUTPUT_DIR/$unique_name.md"; then + error "Failed to generate storage report for contract: $contract_name at $file" else - log "Storage report generated for contract: $contract_name" + log "Storage report generated for contract: $contract_name at $file" fi done \ No newline at end of file diff --git a/docs/middlewareV2/AVSRegistrar.md b/docs/middlewareV2/AVSRegistrar.md new file mode 100644 index 000000000..cac782d0e --- /dev/null +++ b/docs/middlewareV2/AVSRegistrar.md @@ -0,0 +1,421 @@ +## AVSRegistrar + +| File | Type | Notes | +| -------- | -------- | -------- | +| [`AVSRegistrar.sol`](../../src/middlewareV2/registrar/AVSRegistrar.sol) | Base Contract | Core registrar with hooks for extensibility | +| [`AVSRegistrarWithSocket.sol`](../../src/middlewareV2/registrar/presets/AVSRegistrarWithSocket.sol) | Preset | Adds socket URL management | +| [`AVSRegistrarWithAllowlist.sol`](../../src/middlewareV2/registrar/presets/AVSRegistrarWithAllowlist.sol) | Preset | Restricts registration to allowlisted operators | +| [`AVSRegistrarAsIdentifier.sol`](../../src/middlewareV2/registrar/presets/AVSRegistrarAsIdentifier.sol) | Preset | Serves as the AVS identifier | + +Interfaces: + +| File | Notes | +| -------- | -------- | +| [`IAVSRegistrar.sol`](../../lib/eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol) | Main interface (in core repo) | +| [`IAVSRegistrarInternal.sol`](../../src/interfaces/IAVSRegistrarInternal.sol) | Errors and events | +| [`ISocketRegistry.sol`](../../src/interfaces/ISocketRegistryV2.sol) | Socket management interface | +| [`IAllowlist.sol`](../../src/interfaces/IAllowlist.sol) | Allowlist management interface | + +--- + +## Overview + +The AVSRegistrar is the interface between AVSs and the EigenLayer core protocol for managing operator registration. It enforces that operators have valid keys registered in the `KeyRegistrar` for a given `operatorSet` before allowing them to register. The `AVSRegistrar` manages multiple operatorSets for a single AVS. + +### Key Features + +- **Access Control**: All registration/deregistration calls must originate from the `AllocationManager` +- **Key Validation**: Ensures operators have registered appropriate keys (ECDSA or BN254) for their operator sets with the [Core `KeyRegistrar`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/main/docs/permissions/KeyRegistrar.md) +- **Extensibility**: Hooks: `_before/afterRegisterOperator` and `_before/AfterDeregisterOperator` enable custom logic to gate operator registration + +### System Diagrams +The below system diagrams assume the *basic* interaction with the AVSRegistrar. Note that AVSs can enact more complex actions such as: + +1. Ejecting operators from an operatorSet on the AllocationManager using the [Permission Controller]() +2. Propagating registration and deregistrations to external, AVS-controlled contracts +3. Gating operator registration based on custom stake-weighted parameters + +#### Initialization +```mermaid +sequenceDiagram + participant AVSAdmin as AVS Admin + participant AllocationManager + participant AVSRegistrar + + AVSAdmin->>AllocationManager: Tx1: set metadataURI + AVSAdmin->>AVSRegistrar: Tx2: deploy AVSRegistrar + AVSAdmin->>AllocationManager: Tx3: set AVSRegistrar + AllocationManager->>AVSRegistrar: Tx3: check supportsAVS() +``` + +The `AVSAdmin` is the entity that conducts on-chain operations on behalf of the AVS. It can be a multisig, eoa, or governance contract. In Tx1, when the `metadataURI` is set, the identifier for the AVS in the core protocol is the address of the `AVSAdmin`. For ergonomic purposes, it is possible to have the identifier be the [`AVSRegistrar`](#avsregistrarasidentifier). See the [Core `PermissionController`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/main/docs/permissions/PermissionController.md) for more information on how the admin can be changed. + +#### Registration +```mermaid +sequenceDiagram + participant Operator + participant AllocationManager + participant AVSRegistrar + + Operator->>AllocationManager: Tx1: Register for opSet + AllocationManager->>AVSRegistrar: Tx1: Send opSets, Data +``` + +#### Deregistration +```mermaid +sequenceDiagram + participant Operator + participant AllocationManager + participant AVSRegistrar + + Operator->>AllocationManager: Tx1: Deregister + AllocationManager->>AVSRegistrar: Tx1: Send Deregistration +``` + +--- + +## AVSRegistrar (Base Contract) + +The base `AVSRegistrar` contract validates operator key registration. It should be inherited from to build additional logic gating registration, such as [`AVSRegistrarWithSocket`](#avsregistrarwithsocket) and [`AVSRegistrarWithAllowlist](#avsregistrarwithallowlist). + +### Core Functions + +#### `registerOperator` + +```solidity +/** + * @notice Called by the AllocationManager when an operator wants to register + * for one or more operator sets + * @param operator The registering operator + * @param avs The AVS the operator is registering for (must match this.avs()) + * @param operatorSetIds The list of operator set ids being registered for + * @param data Arbitrary data the operator can provide as part of registration + * @dev This method reverts if registration is unsuccessful + */ +function registerOperator( + address operator, + address avs, + uint32[] calldata operatorSetIds, + bytes calldata data +) external virtual onlyAllocationManager; +``` + +Registers an operator to one or more operator sets after validating their keys. + +*Effects:* +- Emits `OperatorRegistered` event + +*Requirements:* +- Caller MUST be the `AllocationManager` +- Operator MUST have registered a key for the operatorSet in the `KeyRegistrar` + +##### `_validateOperatorKeys` + +```solidity +/** + * @notice Validates that the operator has registered a key for the given operator sets + * @param operator The operator to validate + * @param operatorSetIds The operator sets to validate + * @dev This function assumes the operator has already registered a key in the Key Registrar + */ +function _validateOperatorKeys( + address operator, + uint32[] calldata operatorSetIds +) internal view; +``` + +Ensures the operator has registered appropriate keys for all specified operator sets. + +#### `deregisterOperator` + +```solidity +/** + * @notice Called by the AllocationManager when an operator is deregistered from + * one or more operator sets. If this method reverts, it is ignored. + * @param operator the deregistering operator + * @param avs the AVS the operator is deregistering from. This should be the same as IAVSRegistrar.avs() + * @param operatorSetIds the list of operator set ids being deregistered from + */ +function deregisterOperator( + address operator, + address avs, + uint32[] calldata operatorSetIds +) external virtual onlyAllocationManager; +``` + +Deregisters an operator from one or more operator sets. This function can be called by an operator OR by the AVSs ejector if it has configured permissions in the [Core `Permission Controller`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/main/docs/permissions/PermissionController.md). + +*Effects:* +- Emits `OperatorDeregistered` event + +*Requirements:* +- Caller MUST be the `AllocationManager` + +#### `supportsAVS` + +```solidity +/** + * @notice Returns true if the AVS is supported by the registrar + * @param _avs The AVS to check + * @return true if the AVS is supported, false otherwise + */ +function supportsAVS( + address _avs +) public view virtual returns (bool); +``` + +This function is called by the `AllocationManager` to ensure that a malicious entity cannot set the AVSRegistrar that is not theirs. See [`AllocationManager.setAVSRegistrar`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/main/docs/core/AllocationManager.md#setavsregistrar) for more information. + +*Returns*: +* `true` if `_avs` matches the configured AVS address +* `false` otherwise + +### Hooks + +The AVSRegistrar implements a hooks to add further logic to gate registration or deregistration. + +#### Hook Functions + +```solidity +/** + * @notice Hook called before the operator is registered + * @param operator The operator to register + * @param operatorSetIds The operator sets to register + * @param data The data to register + */ +function _beforeRegisterOperator( + address operator, + uint32[] calldata operatorSetIds, + bytes calldata data +) internal virtual {} + +/** + * @notice Hook called after the operator is registered + * @param operator The operator to register + * @param operatorSetIds The operator sets to register + * @param data The data to register + */ +function _afterRegisterOperator( + address operator, + uint32[] calldata operatorSetIds, + bytes calldata data +) internal virtual {} + +/** + * @notice Hook called before the operator is deregistered + * @param operator The operator to deregister + * @param operatorSetIds The operator sets to deregister + */ +function _beforeDeregisterOperator( + address operator, + uint32[] calldata operatorSetIds +) internal virtual {} + +/** + * @notice Hook called after the operator is deregistered + * @param operator The operator to deregister + * @param operatorSetIds The operator sets to deregister + */ +function _afterDeregisterOperator( + address operator, + uint32[] calldata operatorSetIds +) internal virtual {} +``` + +#### Hook Usage Patterns + +**Before Hooks** are ideal for: +- Access control checks (e.g., allowlist verification) +- Validation of registration data +- Checking operator eligibility +- Enforcing custom requirements + +**After Hooks** are ideal for: +- Storing operator metadata (e.g., socket URLs) +- Updating internal state +- Triggering external notifications +- Recording additional information + + +--- + +## AVSRegistrarWithSocket + +Extends the base registrar to store a socket URL for each operator. + +The `AVSRegistrarWithSocket`: +- Inherits from `SocketRegistry` module +- Stores socket URLs in the `_afterRegisterOperator` hook +- Allows operators to update their socket URLs post-registration + +**Note: Sockets are global for the AVS and cannot be set on a per-operatorSet basis.** The socket is updated on every registration call, regardless if the operator has already registered to a different operatorSet. Sockets are *not* cleared upon deregistration. + +### Key Methods (from SocketRegistry) + +#### `getOperatorSocket` + +```solidity +/** + * @notice Get the socket URL for an operator + * @param operator The operator address + * @return The operator's socket URL + */ +function getOperatorSocket( + address operator +) external view returns (string memory); +``` + +*Returns:* +- The operator's socket URL (empty string if not set) + +#### `updateSocket` + +```solidity +/** + * @notice Update the socket URL for the calling operator + * @param operator The operator address (must be msg.sender) + * @param socket The new socket URL + */ +function updateSocket(address operator, string memory socket) external; +``` + +Allows an operator to update their socket URL after registration. The operator does NOT need to be registered to update socket + + +*Effects:* +- Updates the stored socket URL for the operator +- Emits `OperatorSocketSet` event + +*Requirements:* +- `msg.sender` MUST be the `operator` + +--- + +## AVSRegistrarWithAllowlist + +Gates registration by maintaining per-operatorSet allowlists. + +The `AVSRegistrarWithAllowlist`: +- Inherits from `Allowlist` module +- Checks allowlist in the `_beforeRegisterOperator` hook +- Gates registration on allowlist membership. Deregistration can be completed regardless of allowlist membership + +### Initialization + +```solidity +/** + * @notice Initialize the allowlist with an admin + * @param admin The address that can manage the allowlist + */ +function initialize(address admin) public override initializer; +``` + +The admin set in this function is the owner of the contract, which gates the below `Allowlist` methods. + +### Key Methods (from Allowlist) + +#### `addOperatorToAllowlist` + +```solidity +/** + * @notice Add an operator to the allowlist for a specific operator set + * @param operatorSet The operator set to update + * @param operator The operator to add + * @dev Only callable by owner + */ +function addOperatorToAllowlist( + OperatorSet memory operatorSet, + address operator +) external onlyOwner; +``` + +Adds an operator to the allowlist for a specific operator set, enabling them to register. + +*Effects:* +- Adds operator to the allowlist for the specified operator set +- Emits `OperatorAddedToAllowlist` event + +*Requirements:* +- Caller MUST be the contract owner +- Operator MUST NOT already be in the allowlist for this operator set + +#### `removeOperatorFromAllowlist` + +```solidity +/** + * @notice Remove an operator from the allowlist + * @param operatorSet The operator set to update + * @param operator The operator to remove + * @dev Only callable by owner + */ +function removeOperatorFromAllowlist( + OperatorSet memory operatorSet, + address operator +) external onlyOwner; +``` + +Removes an operator from the allowlist for a specific operator set. + +*Effects:* +- Removes operator from the allowlist for the specified operator set +- Emits `OperatorRemovedFromAllowlist` event + +*Requirements:* +- Caller MUST be the contract owner +- Operator MUST be in the allowlist for this operator set + +#### `isOperatorAllowed` + +```solidity +/** + * @notice Check if an operator is allowed for a specific operator set + * @param operatorSet The operator set to check + * @param operator The operator to check + * @return true if allowed, false otherwise + */ +function isOperatorAllowed( + OperatorSet memory operatorSet, + address operator +) public view returns (bool); +``` + +Checks whether an operator is on the allowlist for a specific operator set. + +*Returns:* +- `true` if the operator is allowed for the operator set +- `false` otherwise + +#### `getAllowedOperators` + +```solidity +/** + * @notice Get all operators on the allowlist for a specific operator set + * @param operatorSet The operator set to query + * @return Array of allowed operator addresses + */ +function getAllowedOperators( + OperatorSet memory operatorSet +) external view returns (address[] memory); +``` + +Returns all operators currently on the allowlist for a specific operator set. + +--- + +## AVSRegistrarAsIdentifier + +A specialized registrar that makes the identifier of the AVS in the core protocol the AVSRegistrar. + +```solidity +/** + * @notice Initialize the AVS with metadata and admin + * @param admin The address that will control the AVS + * @param metadataURI The metadata URI for the AVS + */ +function initialize(address admin, string memory metadataURI) public initializer; +``` + +*Initialization Process*: +1. Updates AVS metadata URI in the AllocationManager +2. Sets itself as the AVS registrar +3. Initiates admin transfer via PermissionController diff --git a/docs/middlewareV2/OperatorTableCalculator.md b/docs/middlewareV2/OperatorTableCalculator.md new file mode 100644 index 000000000..4075023d0 --- /dev/null +++ b/docs/middlewareV2/OperatorTableCalculator.md @@ -0,0 +1,255 @@ +## OperatorTableCalculator + +| File | Type | Notes | +| -------- | -------- | +| [`ECDSATableCalculatorBase.sol`](../../src/middlewareV2/tableCalculator/ECDSATableCalculatorBase.sol) | Abstract | Base functionality for ECDSA operator tables | +| [`BN254TableCalculatorBase.sol`](../../src/middlewareV2/tableCalculator/BN254TableCalculatorBase.sol) | Abstract | Base functionality for BN254 operator tables | + +Interfaces: + +| File | Notes | +| -------- | -------- | +| [`IOperatorTableCalculator.sol`](../../lib/eigenlayer-contracts/src/contracts/interfaces/IOperatorTableCalculator.sol) | Base interface for all calculators (in core repo) | +| [`IECDSATableCalculator.sol`](../../src/interfaces/IECDSATableCalculator.sol) | ECDSA-specific interface | +| [`IBN254TableCalculator.sol`](../../src/interfaces/IBN254TableCalculator.sol) | BN254-specific interface | + +--- + +## Overview + +The OperatorTableCalculator contracts are responsible for calculating stake weights of operator. These stake weights are aggregated and transported using the [Eigenlayer Multichain Protocol](https://github.com/eigenfoundation/ELIPs/blob/elip-008v1/ELIPs/ELIP-008.md). In order to utilize the multichain protocol, an AVS *MUST* deploy an `OperatorTableCalculator` and register it in the `CrossChainRegistry` in EigenLayer core. See our [core documentation](https://github.com/Layr-Labs/eigenlayer-contracts/tree/main/docs/multichain#common-user-flows) for this process. + +The base contracts (`ECDSATableCalculatorBase` and `BN254TableCalculatorBase`) provide the core logic for table calculation to be consumed by EigenLayer core, while leaving weight calculation as an unimplemented method to be implemented by derived contracts. + +### Stake Weighting + +It is up to the AVS to define each operator's weights array in an operatorSet. Some examples include: + +- A single array evaluated purely on slashable stake `[slashable_stake]` +- An array of 2 values can be used for evaluating on slashable and delegated stake `[slashable_stake, delegated_stake]` +- An array of several values can be used for evaluating stake on multiple strategies `[slashable_stake_stETH, slashable_stake_USDC, slashable_stake_EIGEN]` + +In addition, an AVS can build custom calculation methodologies that include: +- Capping the stake of an operator +- Using oracles to price stake + +The [`ECDSATableCalculator`](../../src/middlewareV2/tableCalculator/ECDSATableCalculator.sol) and [`BN254TableCalculator`](../../src/middlewareV2/tableCalculator/BN254TableCalculator.sol) value slashable stake equally across all strategies. For example, if an operator allocates 100 stETH, 100 wETH, and 100 USDC the calculator would return 300 for the stake weight of the operator. + + +--- + +## ECDSATableCalculatorBase + +The `ECDSATableCalculatorBase` provides base functionality for calculating ECDSA operator tables. It handles operator key retrieval and table construction. + +### Core Functions + +#### `calculateOperatorTable` + +```solidity +/** + * @notice A struct that contains information about a single operator + * @param pubkey The address of the signing ECDSA key of the operator and not the operator address itself. + * This is read from the KeyRegistrar contract. + * @param weights The weights of the operator for a single operatorSet + * @dev The `weights` array can be defined as a list of arbitrary groupings. For example, + * it can be [slashable_stake, delegated_stake, strategy_i_stake, ...] + * @dev The `weights` array should be the same length for each operator in the operatorSet. + */ +struct ECDSAOperatorInfo { + address pubkey; + uint256[] weights; +} + +/** + * @notice calculates the operatorInfos for a given operatorSet + * @param operatorSet the operatorSet to calculate the operator table for + * @return operatorInfos the list of operatorInfos for the given operatorSet + * @dev The output of this function is converted to bytes via the `calculateOperatorTableBytes` function + */ +function calculateOperatorTable( + OperatorSet calldata operatorSet +) external view returns (ECDSAOperatorInfo[] memory operatorInfos); +``` + +Calculates and returns an array of `ECDSAOperatorInfo` structs containing public keys and weights for all operators in the operatorSet who have registered ECDSA keys. + +*Effects*: +* None (view function) + +*Process*: +* Calls `_getOperatorWeights` to retrieve operator addresses and their weights +* For each operator with a registered ECDSA key: + * Retrieves the ECDSA address (public key) from the `KeyRegistrar` + * Creates an `ECDSAOperatorInfo` struct with the public key and weights +* Returns only operators with registered keys + +#### `calculateOperatorTableBytes` + +```solidity +/** + * @notice Calculates the operator table bytes for a given operatorSet + * @param operatorSet The operatorSet to calculate the operator table for + * @return operatorTableBytes The encoded operator table bytes + */ +function calculateOperatorTableBytes( + OperatorSet calldata operatorSet +) external view returns (bytes memory operatorTableBytes); +``` + +Returns the ABI-encoded bytes representation of the operator table, which is used by the `CrossChainRegistry` to calculate the operatorTable. + +*Returns*: +* ABI-encoded array of `ECDSAOperatorInfo` structs + +### Abstract Methods + +#### `_getOperatorWeights` + +```solidity +/** + * @notice Abstract function to get the operator weights for a given operatorSet + * @param operatorSet The operatorSet to get the weights for + * @return operators The addresses of the operators in the operatorSet + * @return weights The weights for each operator in the operatorSet + */ +function _getOperatorWeights( + OperatorSet calldata operatorSet +) internal view virtual returns (address[] memory operators, uint256[][] memory weights); +``` + +Must be implemented by derived contracts to define the weight calculation strategy. See [stakeWeighting](#stake-weighting) for more information. + +An example integration is in [`ECDSATableCalculator`](../../src/middlewareV2/tableCalculator/ECDSATableCalculator.sol) + +--- + +## BN254TableCalculatorBase + +The `BN254TableCalculatorBase` provides base functionality for calculating BN254 operator tables. + +### Core Functions + +#### `calculateOperatorTable` + +```solidity +/** + * @notice A struct that contains information about a single operator + * @param pubkey The G1 public key of the operator. + * @param weights The weights of the operator for a single operatorSet. + * @dev The `weights` array can be defined as a list of arbitrary groupings. For example, + * it can be [slashable_stake, delegated_stake, strategy_i_stake, ...] + */ +struct BN254OperatorInfo { + BN254.G1Point pubkey; + uint256[] weights; +} + +/** + * @notice A struct that contains information about all operators for a given operatorSet + * @param operatorInfoTreeRoot The root of the operatorInfo tree. Each leaf is a `BN254OperatorInfo` struct + * @param numOperators The number of operators in the operatorSet. + * @param aggregatePubkey The aggregate G1 public key of the operators in the operatorSet. + * @param totalWeights The total weights of the operators in the operatorSet. + * + * @dev The operatorInfoTreeRoot is the root of a merkle tree that contains the operatorInfos for each operator in the operatorSet. + * It is calculated in this function and used by the `IBN254CertificateVerifier` to verify stakes against the non-signing operators + * + * @dev Retrieval of the `aggregatePubKey` depends on maintaining a key registry contract, see `BN254TableCalculatorBase` for an example implementation. + * + * @dev The `totalWeights` array should be the same length as each individual `weights` array in `operatorInfos`. + */ +struct BN254OperatorSetInfo { + bytes32 operatorInfoTreeRoot; + uint256 numOperators; + BN254.G1Point aggregatePubkey; + uint256[] totalWeights; +} + +/** + * @notice calculates the operatorInfos for a given operatorSet + * @param operatorSet the operatorSet to calculate the operator table for + * @return operatorSetInfo the operatorSetInfo for the given operatorSet + * @dev The output of this function is converted to bytes via the `calculateOperatorTableBytes` function + */ +function calculateOperatorTable( + OperatorSet calldata operatorSet +) external view returns (BN254OperatorSetInfo memory operatorSetInfo); +``` + +Calculates and returns a `BN254OperatorSetInfo` struct containing: +- A merkle tree root of operator information +- The total number of operators +- An aggregate BN254 public key +- Total weights across all operators + +*Effects*: +* None (view function) + +*Process*: +* Calls `_getOperatorWeights` to retrieve operator addresses and their weights +* For each operator with a registered BN254 key: + * Retrieves the BN254 G1 point from the `KeyRegistrar` + * Adds the operator's weights to the total weights + * Creates a merkle leaf from the operator info + * Adds the G1 point to the aggregate public key +* Constructs a merkle tree from all operator info leaves +* Returns the complete operator set information + +BN254 tables take advantage of signature aggregation. As such, we add operator's weights to the total weights. We generate a merkle root that contains individual operator stakes (`BN254OperatorInfo`) to lower transport costs. See the core [`BN254CertificateVerifier`](https://github.com/Layr-Labs/eigenlayer-contracts/tree/main/docs/multichain/destination/CertificateVerifier.md) for more information on the caching and verification scheme. + +#### `calculateOperatorTableBytes` + +```solidity +/** + * @notice Calculates the operator table bytes for a given operatorSet + * @param operatorSet The operatorSet to calculate the operator table for + * @return operatorTableBytes The encoded operator table bytes + */ +function calculateOperatorTableBytes( + OperatorSet calldata operatorSet +) external view returns (bytes memory operatorTableBytes); +``` + +Returns the ABI-encoded bytes representation of the operator table, which is used by the `CrossChainRegistry` to calculate the operatorTable. + +*Returns*: +* ABI-encoded `BN254OperatorSetInfo` struct + +#### `getOperatorInfos` + +```solidity +/** + * @notice Get the operatorInfos for a given operatorSet + * @param operatorSet the operatorSet to get the operatorInfos for + * @return operatorInfos the operatorInfos for the given operatorSet + */ +function getOperatorInfos( + OperatorSet calldata operatorSet +) external view returns (BN254OperatorInfo[] memory operatorInfos); +``` + +Returns an array of `BN254OperatorInfo` structs for all operators in the operatorSet who have registered BN254 keys. + +*Effects*: +* None (view function) + +### Abstract Methods + +#### `_getOperatorWeights` + +```solidity +/** + * @notice Abstract function to get the operator weights for a given operatorSet + * @param operatorSet The operatorSet to get the weights for + * @return operators The addresses of the operators in the operatorSet + * @return weights The weights for each operator in the operatorSet + */ +function _getOperatorWeights( + OperatorSet calldata operatorSet +) internal view virtual returns (address[] memory operators, uint256[][] memory weights); +``` + +Must be implemented by derived contracts to define the weight calculation strategy. Similar to ECDSA, weights are a 2D array supporting multiple weight types per operator. + +An example integration is defined in [`BN254TableCalculator`](../../src/middlewareV2/tableCalculator/BN254TableCalculator.sol). diff --git a/docs/middlewareV2/README.md b/docs/middlewareV2/README.md new file mode 100644 index 000000000..f2045d060 --- /dev/null +++ b/docs/middlewareV2/README.md @@ -0,0 +1,192 @@ +[elip-008]: https://github.com/eigenfoundation/ELIPs/blob/elip-008v1/ELIPs/ELIP-008.md +[core-multichain-docs]: https://github.com/Layr-Labs/eigenlayer-contracts/tree/main/docs/multichain + +## MiddlewareV2 + +The middlewareV2 architecture simplifies AVS development by: +1. Utilizing core protocol contracts for operator key storage (`KeyRegistrar`) and task verification (`BN254CertificateVerifier` and `ECDSACertificateVerifier`) +2. Utilizing core contracts for OperatorSet (ie. quorum) membership and strategy composition in the `AllocationManager` +3. Utilizing the EigenLabs-run offchain services to update stakes instead of [`avs-sync`](https://github.com/Layr-Labs/avs-sync) + +--- + +### Contents + +* [System Diagram](#system-diagram) +* [AVS Registrar](#avs-registrar) +* [Operator Table Calculator](#operator-table-calculator) + * [`ECSDATableCalculator`](#ecdsatablecalculator) + * [`BN254TableCalculator`](#bn254tablecalculator) +* [Core Contract Integrations](#core-contract-integrations) + * [`KeyRegistrar`](#key-registrar) + * [`AllocationManager`](#allocation-manager) + * [`CertificateVerifier`](#certificate-verifier) +* [Roles and Actors](#roles-and-actors) +* [Migration](#migration) + +--- + +### System Diagram + +```mermaid +classDiagram +direction TD +namespace Middleware-on-Ethereum{ + class OperatorTableCalculator { + StakeCapping + StakeWeighting (Multiplier, Oracle) + ProtocolVotingPowerCalc + } + class AVSAdmin { + metadataURI + Permissions/multisigs/governance + verificationDelay + transportPayments + } + class AVSRegistrar { + registerOperator + deregisterOperator + } + class SlasherEjector { + submitEvidence + slashOperator () + ejectOperator () + } + class RegistrationHooks{ + RegistrationLogic + OperatorCaps + Churn + Sockets + } +} +namespace Ethereum-EigenLayer-Core{ + class AllocationManager { + registerForOperatorSets + deregisterFromOperatorSets + allocateStake + deallocateStake + slashOperator() + } + class KeyRegistrar{ + registerKey + deregisterKey + getKey (operator addr) + isRegistered (operator addr) + } + class CrossChainRegistry{ + setOperatorTableCalculator + getOperatorTableCalculator + makeGenerationReservation + addTransportDestination + calculateOperatorTableBytes() + } +} +namespace TargetChain{ + class OperatorTableUpdater{ + confirmGlobalTableRoot + updateOperatorTable() + } + class CertificateVerifier{ + n Operator Tables + updateOperatorTable() + verifyCert (bool) + } + class AVSConsumer{ + requests Operator task + receives cert () + } +} + +namespace Offchain{ + class Operator { + consumer input + return certificate() + } + class Transport{ + getOperatorTables + n calculateOperatorTableBytes + calculateGlobalStakeTable() + } +} +AllocationManager --> AVSRegistrar +AVSAdmin --> CrossChainRegistry +CrossChainRegistry --> OperatorTableCalculator : Calculates Operator Tables +AVSRegistrar --> RegistrationHooks +RegistrationHooks --> KeyRegistrar +SlasherEjector --> AllocationManager : Slash or eject Operator +CrossChainRegistry --> Transport : Transports Operator tables +Transport --> OperatorTableUpdater: Update global stake root +OperatorTableUpdater --> CertificateVerifier: Update Operator Table +Operator --> AVSConsumer : Produces certificate +Operator <-- AVSConsumer : Requests task +AVS Consumer --> CertificateVerifier : Verifies Certificate +``` + +AVS developers only have to manage deployments of the the following contracts on-chain: +- [`AVSRegistrar`](#avs-registrar) +- [`OperatorTableCalculator`](#operator-table-calculator) +- [`Slasher`](../slashing/SlasherBase.md) +- Admin Functionality + - Rewards Submission + - Ejection *Note: A module for programmatic ejection will be available in a future release*. + +MiddlewareV2 architecture defines standards for the `AVSRegistrar` and `OperatorTableCalculator`. + +See the [`multichain-ELIP`](elip-008) and [`core contracts documentation`](core-multichain-docs) for more information. + +--- + +### AVS Registrar + +The AVS Registrar is the primary interface for managing operator registration and deregistration within an AVS. It integrates with core EigenLayer contracts to ensure operators have valid keys and are properly registered in operator sets. + +| File | Type | Description | +| -------- | -------- | -------- | +| [`AVSRegistrar.sol`](../../src/middlewareV2/registrar/AVSRegistrar.sol) | Proxy | Core registrar contract that handles operator registration/deregistration | +| [`AVSRegistrarWithSocket.sol`](../../src/middlewareV2/registrar/presets/AVSRegistrarWithSocket.sol) | Proxy | Adds socket URL registration for operator communication | +| [`AVSRegistrarWithAllowlist.sol`](../../src/middlewareV2/registrar/presets/AVSRegistrarWithAllowlist.sol) | Proxy | Restricts registration to allowlisted operators | +| [`AVSRegistrarAsIdentifier.sol`](../../src/middlewareV2/registrar/presets/AVSRegistrarAsIdentifier.sol) | Proxy | Serves as the AVS identifier and manages permissions | + +#### Base AVSRegistrar + +The `AVSRegistrar` provides base functionality for AVSs to register and deregister operators to their operatorSet. A single `AVSRegistrar` supports multiple operatorSets. ***This contract expects operator registrations and deregistrations to originate from the `AllocationManager`***. + +See full documentation in [`./AVSRegistrar.md`](./AVSRegistrar.md). + +--- + +### Operator Table Calculator + +| File | Type | +| -------- | -------- | +| [`BN254TableCalculatorBase.sol`](../../src/middlewareV2/tableCalculator/BN254TableCalculatorBase.sol) | Abstract Contract | +| [`BN254TableCalculator.sol`](../../src/middlewareV2/tableCalculator/BN254TableCalculator.sol) | Basic table calculator that sums slashable stake across all strategies | +| [`ECDSATableCalculatorBase.sol`](../../src/middlewareV2/tableCalculator/ECDSATableCalculator.sol) | Abstract Contract | +| [`ECDSATableCalculator.sol`](../../src/middlewareV2/tableCalculator/ECDSATableCalculator.sol) | Basic table calculator that sums slashable stake across all strategies | + +These contracts define custom stake weights of operators in an operatorSet. They are segmented by key-type. + +See full documentation in [`/operatorTableCalculator.md`](./OperatorTableCalculator.md). + +--- + +### Core Contract Integrations + +#### Key Registrar +The `KeyRegistrar` manages cryptographic keys for operators across different operator sets. It supports both ECDSA and BN254 key types and ensures global uniqueness of keys across all operator sets. + +When an operator registers to an operatorSet, the [`AVSRegistrar`](./AVSRegistrar.md) checks membership of the key in the operatorSet. + +#### Allocation Manager +The [`AllocationManager`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/main/docs/core/AllocationManager.md) is the entrypoint for all operator<>avs interactions. It: + +- Manages operator registration and deregistration +- Enables an AVS to configure its metadataURI and `AVSRegistrar` +- Enables an AVS to configure strategy composition in an operatorSet +- Manages allocation and deallocation of slashable stake +- Enables an AVS to slash an operator + +See the [`AVSRegistrar`](./AVSRegistrar.md#system-diagrams) for how an AVS is initialized to the core protocol. + +#### Certificate Verifier +The [`CertificateVerifier`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/main/docs/multichain/destination/CertificateVerifier.md) is responsible for verifying certificates from an offchain task, on-chain. The stakes in the certificate verifier are defined by the AVS-deployed `OperatorTableCalculator` and transported via an off-chain process run by Eigen labs. The `CertificateVerifier` contracts support two signature schemes: ECDSA for individual signatures and BN254 for aggregated signatures. See [multichain docs](core-multichain-docs) for more information. diff --git a/lib/eigenlayer-contracts b/lib/eigenlayer-contracts index 9a9707b28..8e5550aea 160000 --- a/lib/eigenlayer-contracts +++ b/lib/eigenlayer-contracts @@ -1 +1 @@ -Subproject commit 9a9707b28fa6acbd512d8983ad7a71d7ecb222ad +Subproject commit 8e5550aeac4d0e20573cc9f6bc517f0254ae7fa8 diff --git a/lib/forge-std b/lib/forge-std index 1eea5bae1..77041d2ce 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 +Subproject commit 77041d2ce690e692d6e03cc812b57d1ddaa4d505 diff --git a/src/interfaces/IAVSRegistrarInternal.sol b/src/interfaces/IAVSRegistrarInternal.sol new file mode 100644 index 000000000..d5c6740b0 --- /dev/null +++ b/src/interfaces/IAVSRegistrarInternal.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +interface IAVSRegistrarErrors { + /// @notice Thrown when a key is not registered + error KeyNotRegistered(); + /// @notice Thrown when the caller is not the allocation manager + error NotAllocationManager(); +} + +interface IAVSRegistrarEvents { + /// @notice Emitted when a new operator is registered + event OperatorRegistered(address indexed operator, uint32[] operatorSetIds); + + /// @notice Emitted when an operator is deregistered + event OperatorDeregistered(address indexed operator, uint32[] operatorSetIds); +} + +/// @notice Since we have already defined a public interface, we add the events and errors here +interface IAVSRegistrarInternal is IAVSRegistrarErrors, IAVSRegistrarEvents { + /// @notice Returns the address of the AVS in EigenLayer core + function getAVS() external view returns (address); +} diff --git a/src/interfaces/IAVSRegistrarWithAllowlist.sol b/src/interfaces/IAVSRegistrarWithAllowlist.sol new file mode 100644 index 000000000..9610159c7 --- /dev/null +++ b/src/interfaces/IAVSRegistrarWithAllowlist.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IAllowlist} from "./IAllowlist.sol"; + +interface IAVSRegistrarWithAllowlist is IAVSRegistrar, IAllowlist {} diff --git a/src/interfaces/IAVSRegistrarWithSocket.sol b/src/interfaces/IAVSRegistrarWithSocket.sol new file mode 100644 index 000000000..7829a6397 --- /dev/null +++ b/src/interfaces/IAVSRegistrarWithSocket.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {ISocketRegistry} from "./ISocketRegistryV2.sol"; + +interface IAVSRegistrarWithSocket is IAVSRegistrar, ISocketRegistry {} diff --git a/src/interfaces/IAllowlist.sol b/src/interfaces/IAllowlist.sol new file mode 100644 index 000000000..558c47344 --- /dev/null +++ b/src/interfaces/IAllowlist.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; + +interface IAllowlistErrors { + /// @notice Thrown when the operator is already in the allowlist + error OperatorAlreadyInAllowlist(); + /// @notice Thrown when the operator is not in the allowlist + error OperatorNotInAllowlist(); +} + +interface IAllowlistEvents { + /// @notice Emitted when an operator is added to the allowlist + event OperatorAddedToAllowlist(OperatorSet indexed operatorSet, address indexed operator); + /// @notice Emitted when an operator is removed from the allowlist + event OperatorRemovedFromAllowlist(OperatorSet indexed operatorSet, address indexed operator); +} + +interface IAllowlist is IAllowlistErrors, IAllowlistEvents { + /** + * @notice Adds an operator to the allowlist + * @param operatorSet The operator set to add the operator to + * @param operator The operator to add to the allowlist + * @dev Only callable by the owner + */ + function addOperatorToAllowlist(OperatorSet memory operatorSet, address operator) external; + + /** + * @notice Removes an operator from the allowlist + * @param operatorSet The operator set to remove the operator from + * @param operator The operator to remove from the allowlist + * @dev If an operator is removed from the allowlist and is already registered, the avs + * must then handle state changes appropriately (ie. eject the operator) + * @dev Only callable by the owner + */ + function removeOperatorFromAllowlist( + OperatorSet memory operatorSet, + address operator + ) external; + + /** + * @notice Checks if an operator is in the allowlist + * @param operatorSet The operator set to check the operator in + * @param operator The operator to check + * @return True if the operator is in the allowlist, false otherwise + */ + function isOperatorAllowed( + OperatorSet memory operatorSet, + address operator + ) external view returns (bool); + + /** + * @notice Returns all operators in the allowlist + * @param operatorSet The operator set to get the allowed operators from + * @return An array of all operators in the allowlist + * @dev This function should be used with caution, as it can be expensive to call on-chain + */ + function getAllowedOperators( + OperatorSet memory operatorSet + ) external view returns (address[] memory); +} diff --git a/src/interfaces/IBN254TableCalculator.sol b/src/interfaces/IBN254TableCalculator.sol new file mode 100644 index 000000000..2ee9ec252 --- /dev/null +++ b/src/interfaces/IBN254TableCalculator.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import { + IOperatorTableCalculator, + IOperatorTableCalculatorTypes +} from "eigenlayer-contracts/src/contracts/interfaces/IOperatorTableCalculator.sol"; + +interface IBN254TableCalculator is IOperatorTableCalculator, IOperatorTableCalculatorTypes { + /** + * @notice calculates the operatorInfos for a given operatorSet + * @param operatorSet the operatorSet to calculate the operator table for + * @return operatorSetInfo the operatorSetInfo for the given operatorSet + * @dev The output of this function is converted to bytes via the `calculateOperatorTableBytes` function + */ + function calculateOperatorTable( + OperatorSet calldata operatorSet + ) external view returns (BN254OperatorSetInfo memory operatorSetInfo); + + /** + * @notice Get the operatorInfos for a given operatorSet + * @param operatorSet the operatorSet to get the operatorInfos for + * @return operatorInfos the operatorInfos for the given operatorSet + */ + function getOperatorInfos( + OperatorSet calldata operatorSet + ) external view returns (BN254OperatorInfo[] memory operatorInfos); +} diff --git a/src/interfaces/IECDSATableCalculator.sol b/src/interfaces/IECDSATableCalculator.sol new file mode 100644 index 000000000..158d02805 --- /dev/null +++ b/src/interfaces/IECDSATableCalculator.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import { + IOperatorTableCalculator, + IOperatorTableCalculatorTypes +} from "eigenlayer-contracts/src/contracts/interfaces/IOperatorTableCalculator.sol"; + +interface IECDSATableCalculator is IOperatorTableCalculator, IOperatorTableCalculatorTypes { + /** + * @notice calculates the operatorInfos for a given operatorSet + * @param operatorSet the operatorSet to calculate the operator table for + * @return operatorInfos the list of operatorInfos for the given operatorSet + * @dev The output of this function is converted to bytes via the `calculateOperatorTableBytes` function + */ + function calculateOperatorTable( + OperatorSet calldata operatorSet + ) external view returns (ECDSAOperatorInfo[] memory operatorInfos); +} diff --git a/src/interfaces/IKeyRegistrar.sol b/src/interfaces/IKeyRegistrar.sol new file mode 100644 index 000000000..cb496e678 --- /dev/null +++ b/src/interfaces/IKeyRegistrar.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; + +/// @notice A dummy interface for the KeyRegistrar +interface IKeyRegistrar { + enum CurveType { + ECDSA, + BN254 + } + + function checkAndUpdateKey( + OperatorSet calldata operatorSet, + address operator + ) external returns (bool); + + function removeKey(OperatorSet calldata operatorSet, address operator) external; + + function isRegistered( + OperatorSet calldata operatorSet, + address operator + ) external view returns (bool); +} diff --git a/src/interfaces/ISocketRegistryV2.sol b/src/interfaces/ISocketRegistryV2.sol new file mode 100644 index 000000000..73b9256d7 --- /dev/null +++ b/src/interfaces/ISocketRegistryV2.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +interface ISocketRegistryErrors { + /// @notice Thrown when the caller is not the operator + error CallerNotOperator(); + + /// @notice Thrown when the data length mismatch + error DataLengthMismatch(); +} + +interface ISocketRegistryEvents { + /// @notice Emitted when an operator socket is set + event OperatorSocketSet(address indexed operator, string socket); +} + +interface ISocketRegistry is ISocketRegistryErrors, ISocketRegistryEvents { + /** + * @notice Gets the socket for an operator. + * @param operator The operator to get the socket for. + * @return The socket for the operator. + */ + function getOperatorSocket( + address operator + ) external view returns (string memory); + + /** + * @notice Updates the socket for an operator. + * @param operator The operator to set the socket for. + * @param socket The socket to set for the operator. + * @dev This function can only be called by the operator themselves. + */ + function updateSocket(address operator, string memory socket) external; +} diff --git a/src/middlewareV2/registrar/AVSRegistrar.sol b/src/middlewareV2/registrar/AVSRegistrar.sol new file mode 100644 index 000000000..4611ecf77 --- /dev/null +++ b/src/middlewareV2/registrar/AVSRegistrar.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IAVSRegistrarInternal} from "../../interfaces/IAVSRegistrarInternal.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import { + OperatorSetLib, + OperatorSet +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import { + IKeyRegistrarTypes, + IKeyRegistrar +} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; + +import {AVSRegistrarStorage} from "./AVSRegistrarStorage.sol"; + +import {Initializable} from "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; + +/// @notice A minimal AVSRegistrar contract that is used to register/deregister operators for an AVS +contract AVSRegistrar is Initializable, AVSRegistrarStorage { + using OperatorSetLib for OperatorSet; + + modifier onlyAllocationManager() { + require(msg.sender == address(allocationManager), NotAllocationManager()); + _; + } + + constructor( + address _avs, + IAllocationManager _allocationManager, + IKeyRegistrar _keyRegistrar + ) AVSRegistrarStorage(_avs, _allocationManager, _keyRegistrar) { + _disableInitializers(); + } + + /// @inheritdoc IAVSRegistrar + function registerOperator( + address operator, + address, /* avs */ + uint32[] calldata operatorSetIds, + bytes calldata data + ) external virtual onlyAllocationManager { + _beforeRegisterOperator(operator, operatorSetIds, data); + + // Check that the operator has a valid key and update key if needed + _validateOperatorKeys(operator, operatorSetIds); + + _afterRegisterOperator(operator, operatorSetIds, data); + + emit OperatorRegistered(operator, operatorSetIds); + } + + /// @inheritdoc IAVSRegistrar + function deregisterOperator( + address operator, + address, /* avs */ + uint32[] calldata operatorSetIds + ) external virtual onlyAllocationManager { + _beforeDeregisterOperator(operator, operatorSetIds); + + _afterDeregisterOperator(operator, operatorSetIds); + + emit OperatorDeregistered(operator, operatorSetIds); + } + + /// @inheritdoc IAVSRegistrar + function supportsAVS( + address _avs + ) public view virtual returns (bool) { + return _avs == avs; + } + + /// @inheritdoc IAVSRegistrarInternal + function getAVS() external view virtual returns (address) { + return avs; + } + + /* + * + * INTERNAL FUNCTIONS + * + */ + + /** + * @notice Validates that the operator has registered a key for the given operator sets + * @param operator The operator to validate + * @param operatorSetIds The operator sets to validate + * @dev This function assumes the operator has already registered a key in the Key Registrar + */ + function _validateOperatorKeys( + address operator, + uint32[] calldata operatorSetIds + ) internal view { + for (uint32 i = 0; i < operatorSetIds.length; i++) { + OperatorSet memory operatorSet = OperatorSet({avs: avs, id: operatorSetIds[i]}); + require(keyRegistrar.checkKey(operatorSet, operator), KeyNotRegistered()); + } + } + + /** + * @notice Hook called before the operator is registered + * @param operator The operator to register + * @param operatorSetIds The operator sets to register + * @param data The data to register + */ + function _beforeRegisterOperator( + address operator, + uint32[] calldata operatorSetIds, + bytes calldata data + ) internal virtual {} + + /** + * @notice Hook called after the operator is registered + * @param operator The operator to register + * @param operatorSetIds The operator sets to register + * @param data The data to register + */ + function _afterRegisterOperator( + address operator, + uint32[] calldata operatorSetIds, + bytes calldata data + ) internal virtual {} + + /** + * @notice Hook called before the operator is deregistered + * @param operator The operator to deregister + * @param operatorSetIds The operator sets to deregister + */ + function _beforeDeregisterOperator( + address operator, + uint32[] calldata operatorSetIds + ) internal virtual {} + + /** + * @notice Hook called after the operator is deregistered + * @param operator The operator to deregister + * @param operatorSetIds The operator sets to deregister + */ + function _afterDeregisterOperator( + address operator, + uint32[] calldata operatorSetIds + ) internal virtual {} +} diff --git a/src/middlewareV2/registrar/AVSRegistrarStorage.sol b/src/middlewareV2/registrar/AVSRegistrarStorage.sol new file mode 100644 index 000000000..185121615 --- /dev/null +++ b/src/middlewareV2/registrar/AVSRegistrarStorage.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import {IAVSRegistrarInternal} from "../../interfaces/IAVSRegistrarInternal.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; + +/// @notice A minimal storage contract for the AVSRegistrar +abstract contract AVSRegistrarStorage is IAVSRegistrar, IAVSRegistrarInternal { + /** + * + * CONSTANTS AND IMMUTABLES + * + */ + + /// @notice The AVS that this registrar is for + /// @dev In practice, the AVS address in EigenLayer core is address that initialized the Metadata URI. + address internal immutable avs; + + /// @notice The allocation manager in EigenLayer core + IAllocationManager public immutable allocationManager; + + /// @notice Pointer to the EigenLayer core Key Registrar + IKeyRegistrar public immutable keyRegistrar; + + constructor(address _avs, IAllocationManager _allocationManager, IKeyRegistrar _keyRegistrar) { + avs = _avs; + allocationManager = _allocationManager; + keyRegistrar = _keyRegistrar; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __GAP; +} diff --git a/src/middlewareV2/registrar/modules/Allowlist.sol b/src/middlewareV2/registrar/modules/Allowlist.sol new file mode 100644 index 000000000..6dd6f2eff --- /dev/null +++ b/src/middlewareV2/registrar/modules/Allowlist.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IAllowlist} from "../../../interfaces/IAllowlist.sol"; +import {AllowlistStorage} from "./AllowlistStorage.sol"; + +import {OwnableUpgradeable} from + "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import {EnumerableSetUpgradeable} from + "openzeppelin-contracts-upgradeable/contracts/utils/structs/EnumerableSetUpgradeable.sol"; + +import {Initializable} from "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import { + OperatorSet, + OperatorSetLib +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; + +abstract contract Allowlist is OwnableUpgradeable, AllowlistStorage { + using OperatorSetLib for OperatorSet; + using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet; + + function initialize( + address _owner + ) public virtual initializer { + _initializeAllowlist(_owner); + } + + function _initializeAllowlist( + address _owner + ) internal onlyInitializing { + __Ownable_init(); + _transferOwnership(_owner); + } + + /// @inheritdoc IAllowlist + function addOperatorToAllowlist( + OperatorSet memory operatorSet, + address operator + ) external onlyOwner { + require(_allowedOperators[operatorSet.key()].add(operator), OperatorAlreadyInAllowlist()); + emit OperatorAddedToAllowlist(operatorSet, operator); + } + + /// @inheritdoc IAllowlist + function removeOperatorFromAllowlist( + OperatorSet memory operatorSet, + address operator + ) external onlyOwner { + require(_allowedOperators[operatorSet.key()].remove(operator), OperatorNotInAllowlist()); + emit OperatorRemovedFromAllowlist(operatorSet, operator); + } + + /// @inheritdoc IAllowlist + function isOperatorAllowed( + OperatorSet memory operatorSet, + address operator + ) public view returns (bool) { + return _allowedOperators[operatorSet.key()].contains(operator); + } + + /// @inheritdoc IAllowlist + function getAllowedOperators( + OperatorSet memory operatorSet + ) external view returns (address[] memory) { + return _allowedOperators[operatorSet.key()].values(); + } +} diff --git a/src/middlewareV2/registrar/modules/AllowlistStorage.sol b/src/middlewareV2/registrar/modules/AllowlistStorage.sol new file mode 100644 index 000000000..3c419f8c8 --- /dev/null +++ b/src/middlewareV2/registrar/modules/AllowlistStorage.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IAllowlist} from "../../../interfaces/IAllowlist.sol"; + +import {EnumerableSetUpgradeable} from + "openzeppelin-contracts-upgradeable/contracts/utils/structs/EnumerableSetUpgradeable.sol"; + +abstract contract AllowlistStorage is IAllowlist { + using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet; + + /// @dev Mapping from operatorSet to the allowed operators for that operatorSet + mapping(bytes32 operatorSetKey => EnumerableSetUpgradeable.AddressSet allowedOperators) internal + _allowedOperators; + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __GAP; +} diff --git a/src/middlewareV2/registrar/modules/SocketRegistry.sol b/src/middlewareV2/registrar/modules/SocketRegistry.sol new file mode 100644 index 000000000..f255dbe96 --- /dev/null +++ b/src/middlewareV2/registrar/modules/SocketRegistry.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {ISocketRegistry} from "../../../interfaces/ISocketRegistryV2.sol"; +import {SocketRegistryStorage} from "./SocketRegistryStorage.sol"; +import { + OperatorSetLib, + OperatorSet +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; + +/// @notice A module that allows for the setting and removal of operator sockets +/// @dev This contract assumes a single socket per operator +abstract contract SocketRegistry is SocketRegistryStorage { + using OperatorSetLib for OperatorSet; + + /// @inheritdoc ISocketRegistry + function getOperatorSocket( + address operator + ) external view returns (string memory) { + return _operatorToSocket[operator]; + } + + /// @inheritdoc ISocketRegistry + function updateSocket(address operator, string memory socket) external { + require(msg.sender == operator, CallerNotOperator()); + _setOperatorSocket(operator, socket); + } + + /** + * @notice Sets the socket for an operator. + * @param operator The address of the operator to set the socket for. + * @param socket The socket (any arbitrary string as deemed useful by an AVS) to set. + * @dev This function sets a single socket per operator, regardless of operatorSet. + */ + function _setOperatorSocket(address operator, string memory socket) internal { + _operatorToSocket[operator] = socket; + emit OperatorSocketSet(operator, socket); + } +} diff --git a/src/middlewareV2/registrar/modules/SocketRegistryStorage.sol b/src/middlewareV2/registrar/modules/SocketRegistryStorage.sol new file mode 100644 index 000000000..5363fb6a3 --- /dev/null +++ b/src/middlewareV2/registrar/modules/SocketRegistryStorage.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {ISocketRegistry} from "../../../interfaces/ISocketRegistryV2.sol"; + +/** + * @title Storage variables for the `SocketRegistry` contract. + * @author Layr Labs, Inc. + */ +abstract contract SocketRegistryStorage is ISocketRegistry { + /** + * + * STATE + * + */ + + /// @notice A mapping from operator address to socket + mapping(address operator => string operatorSocket) internal _operatorToSocket; + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __GAP; +} diff --git a/src/middlewareV2/registrar/presets/AVSRegistrarAsIdentifier.sol b/src/middlewareV2/registrar/presets/AVSRegistrarAsIdentifier.sol new file mode 100644 index 000000000..f4aa17b28 --- /dev/null +++ b/src/middlewareV2/registrar/presets/AVSRegistrarAsIdentifier.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IAVSRegistrarInternal} from "../../../interfaces/IAVSRegistrarInternal.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IPermissionController} from + "eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; + +import {AVSRegistrar} from "../AVSRegistrar.sol"; + +import {Initializable} from "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; + +/// @notice An AVSRegistrar that is the identifier for the AVS in EigenLayer core. +/// @dev Once deployed, the `admin` will control other parameters of the AVS, such as creating operatorSets, slashing, etc. +contract AVSRegistrarAsIdentifier is AVSRegistrar { + /// @notice The permission controller for the AVS + IPermissionController public immutable permissionController; + + /// @dev The immutable avs address `AVSRegistrar` is NOT the address of the AVS in EigenLayer core. + /// @dev The address of the AVS in EigenLayer core is the proxy contract, and it is set via the `initialize` function below. + constructor( + address _avs, + IAllocationManager _allocationManager, + IPermissionController _permissionController, + IKeyRegistrar _keyRegistrar + ) AVSRegistrar(_avs, _allocationManager, _keyRegistrar) { + // Set the permission controller for future interactions + permissionController = _permissionController; + } + + /** + * @notice Initializes the AVSRegistrarAsIdentifier + * @param admin The admin for the AVS + * @param metadataURI The metadataURI for the AVS + * @dev This function enables the address of the AVS in the core protocol to be the proxy AVSRegistrarAsIdentifier contract + */ + function initialize(address admin, string memory metadataURI) public initializer { + // Set the metadataURI and the registrar for the AVS to this registrar contract + allocationManager.updateAVSMetadataURI(address(this), metadataURI); + allocationManager.setAVSRegistrar(address(this), this); + + // Set the admin for the AVS + permissionController.addPendingAdmin(address(this), admin); + } + + /// @inheritdoc IAVSRegistrar + function supportsAVS( + address _avs + ) public view override returns (bool) { + return _avs == address(this); + } + + /// @inheritdoc IAVSRegistrarInternal + function getAVS() external view override returns (address) { + return address(this); + } +} diff --git a/src/middlewareV2/registrar/presets/AVSRegistrarWithAllowlist.sol b/src/middlewareV2/registrar/presets/AVSRegistrarWithAllowlist.sol new file mode 100644 index 000000000..470b7b340 --- /dev/null +++ b/src/middlewareV2/registrar/presets/AVSRegistrarWithAllowlist.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IAVSRegistrarWithAllowlist} from "../../../interfaces/IAVSRegistrarWithAllowlist.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; + +import {AVSRegistrar} from "../AVSRegistrar.sol"; +import {Allowlist} from "../modules/Allowlist.sol"; +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; + +contract AVSRegistrarWithAllowlist is AVSRegistrar, Allowlist, IAVSRegistrarWithAllowlist { + constructor( + address _avs, + IAllocationManager _allocationManager, + IKeyRegistrar _keyRegistrar + ) AVSRegistrar(_avs, _allocationManager, _keyRegistrar) {} + + function initialize( + address admin + ) public override initializer { + _initializeAllowlist(admin); + } + + /// @notice Before registering operator, check if the operator is in the allowlist + function _beforeRegisterOperator( + address operator, + uint32[] calldata operatorSetIds, + bytes calldata data + ) internal override { + super._beforeRegisterOperator(operator, operatorSetIds, data); + + for (uint32 i; i < operatorSetIds.length; ++i) { + require( + isOperatorAllowed(OperatorSet({avs: avs, id: operatorSetIds[i]}), operator), + OperatorNotInAllowlist() + ); + } + } +} diff --git a/src/middlewareV2/registrar/presets/AVSRegistrarWithSocket.sol b/src/middlewareV2/registrar/presets/AVSRegistrarWithSocket.sol new file mode 100644 index 000000000..53f077f79 --- /dev/null +++ b/src/middlewareV2/registrar/presets/AVSRegistrarWithSocket.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; + +import {IAVSRegistrarWithSocket} from "../../../interfaces/IAVSRegistrarWithSocket.sol"; +import {AVSRegistrar} from "../AVSRegistrar.sol"; +import {SocketRegistry} from "../modules/SocketRegistry.sol"; +import { + OperatorSetLib, + OperatorSet +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; + +contract AVSRegistrarWithSocket is AVSRegistrar, SocketRegistry, IAVSRegistrarWithSocket { + constructor( + address _avs, + IAllocationManager _allocationManager, + IKeyRegistrar _keyRegistrar + ) AVSRegistrar(_avs, _allocationManager, _keyRegistrar) {} + + /// @notice Set the socket for the operator + /// @dev This function sets the socket even if the operator is already registered + /// @dev Operators should make sure to always provide the socket when registering + function _afterRegisterOperator( + address operator, + uint32[] calldata operatorSetIds, + bytes calldata data + ) internal override { + super._afterRegisterOperator(operator, operatorSetIds, data); + + // Set operator socket + string memory socket = abi.decode(data, (string)); + _setOperatorSocket(operator, socket); + } +} diff --git a/src/middlewareV2/tableCalculator/BN254TableCalculator.sol b/src/middlewareV2/tableCalculator/BN254TableCalculator.sol new file mode 100644 index 000000000..19e7ce4bf --- /dev/null +++ b/src/middlewareV2/tableCalculator/BN254TableCalculator.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; + +import "./BN254TableCalculatorBase.sol"; + +/** + * @title BN254TableCalculator + * @notice Implementation that calculates BN254 operator tables using the sum of the minimum slashable stake weights + * @dev This contract assumes that slashable stake is valued the **same** across all strategies. + */ +contract BN254TableCalculator is BN254TableCalculatorBase { + // Immutables + /// @notice AllocationManager contract for managing operator allocations + IAllocationManager public immutable allocationManager; + /// @notice The default lookahead blocks for the slashable stake lookup + uint256 public immutable LOOKAHEAD_BLOCKS; + + constructor( + IKeyRegistrar _keyRegistrar, + IAllocationManager _allocationManager, + uint256 _LOOKAHEAD_BLOCKS + ) BN254TableCalculatorBase(_keyRegistrar) { + allocationManager = _allocationManager; + LOOKAHEAD_BLOCKS = _LOOKAHEAD_BLOCKS; + } + + /** + * @notice Get the operator weights for a given operatorSet based on the slashable stake. + * @param operatorSet The operatorSet to get the weights for + * @return operators The addresses of the operators in the operatorSet + * @return weights The weights for each operator in the operatorSet, this is a 2D array where the first index is the operator + * and the second index is the type of weight. In this case its of length 1 and returns the slashable stake for the operatorSet. + */ + function _getOperatorWeights( + OperatorSet calldata operatorSet + ) internal view override returns (address[] memory operators, uint256[][] memory weights) { + // Get all operators & strategies in the operatorSet + address[] memory registeredOperators = allocationManager.getMembers(operatorSet); + IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet); + + // Get the minimum slashable stake for each operator + uint256[][] memory minSlashableStake = allocationManager.getMinimumSlashableStake({ + operatorSet: operatorSet, + operators: registeredOperators, + strategies: strategies, + futureBlock: uint32(block.number + LOOKAHEAD_BLOCKS) + }); + + operators = new address[](registeredOperators.length); + weights = new uint256[][](registeredOperators.length); + uint256 operatorCount = 0; + for (uint256 i = 0; i < registeredOperators.length; ++i) { + // For the given operator, loop through the strategies and sum together to calculate the operator's weight for the operatorSet + uint256 totalWeight; + for (uint256 stratIndex = 0; stratIndex < strategies.length; ++stratIndex) { + totalWeight += minSlashableStake[i][stratIndex]; + } + + // If the operator has nonzero slashable stake, add them to the operators array + if (totalWeight > 0) { + // Initialize operator weights array of length 1 just for slashable stake + weights[operatorCount] = new uint256[](1); + weights[operatorCount][0] = totalWeight; + + // Add the operator to the operators array + operators[operatorCount] = registeredOperators[i]; + operatorCount++; + } + } + + // Resize arrays to be the size of the number of operators with nonzero slashable stake + assembly { + mstore(operators, operatorCount) + mstore(weights, operatorCount) + } + + return (operators, weights); + } +} diff --git a/src/middlewareV2/tableCalculator/BN254TableCalculatorBase.sol b/src/middlewareV2/tableCalculator/BN254TableCalculatorBase.sol new file mode 100644 index 000000000..fecb81360 --- /dev/null +++ b/src/middlewareV2/tableCalculator/BN254TableCalculatorBase.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {IOperatorTableCalculator} from + "eigenlayer-contracts/src/contracts/interfaces/IOperatorTableCalculator.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import {Merkle} from "eigenlayer-contracts/src/contracts/libraries/Merkle.sol"; +import {BN254} from "eigenlayer-contracts/src/contracts/libraries/BN254.sol"; +import {IBN254TableCalculator} from "../../interfaces/IBN254TableCalculator.sol"; + +/** + * @title BN254TableCalculatorBase + * @notice Abstract contract that provides base functionality for calculating BN254 operator tables + * @dev This contract contains all the core logic for operator table calculations, + * with weight calculation left to be implemented by derived contracts + */ +abstract contract BN254TableCalculatorBase is IBN254TableCalculator { + using Merkle for bytes32[]; + using BN254 for BN254.G1Point; + + // Immutables + /// @notice KeyRegistrar contract for managing operator keys + IKeyRegistrar public immutable keyRegistrar; + + constructor( + IKeyRegistrar _keyRegistrar + ) { + keyRegistrar = _keyRegistrar; + } + + /// @inheritdoc IBN254TableCalculator + function calculateOperatorTable( + OperatorSet calldata operatorSet + ) external view virtual returns (BN254OperatorSetInfo memory operatorSetInfo) { + return _calculateOperatorTable(operatorSet); + } + + /// @inheritdoc IOperatorTableCalculator + function calculateOperatorTableBytes( + OperatorSet calldata operatorSet + ) external view virtual returns (bytes memory operatorTableBytes) { + return abi.encode(_calculateOperatorTable(operatorSet)); + } + + /// @inheritdoc IOperatorTableCalculator + function getOperatorSetWeights( + OperatorSet calldata operatorSet + ) external view virtual returns (address[] memory operators, uint256[][] memory weights) { + return _getOperatorWeights(operatorSet); + } + + /// @inheritdoc IOperatorTableCalculator + function getOperatorWeights( + OperatorSet calldata operatorSet, + address operator + ) external view virtual returns (uint256[] memory) { + (address[] memory operators, uint256[][] memory weights) = _getOperatorWeights(operatorSet); + + // Find the index of the operator in the operators array + for (uint256 i = 0; i < operators.length; i++) { + if (operators[i] == operator) { + return weights[i]; + } + } + + return new uint256[](0); + } + + /// @inheritdoc IBN254TableCalculator + function getOperatorInfos( + OperatorSet calldata operatorSet + ) external view virtual returns (BN254OperatorInfo[] memory) { + // Get the weights for all operators + (address[] memory operators, uint256[][] memory weights) = _getOperatorWeights(operatorSet); + + BN254OperatorInfo[] memory operatorInfos = new BN254OperatorInfo[](operators.length); + + for (uint256 i = 0; i < operators.length; i++) { + // Skip if the operator has not registered their key + if (!keyRegistrar.isRegistered(operatorSet, operators[i])) { + continue; + } + + (BN254.G1Point memory g1Point,) = keyRegistrar.getBN254Key(operatorSet, operators[i]); + operatorInfos[i] = BN254OperatorInfo({pubkey: g1Point, weights: weights[i]}); + } + + return operatorInfos; + } + + /** + * @notice Abstract function to get the operator weights for a given operatorSet + * @param operatorSet The operatorSet to get the weights for + * @return operators The addresses of the operators in the operatorSet + * @return weights The weights for each operator in the operatorSet, this is a 2D array where the first index is the operator + * and the second index is the type of weight + * @dev Must be implemented by derived contracts to define specific weight calculation logic + */ + function _getOperatorWeights( + OperatorSet calldata operatorSet + ) internal view virtual returns (address[] memory operators, uint256[][] memory weights); + + /** + * @notice Calculates the operator table for a given operatorSet, also calculates the aggregate pubkey for the operatorSet + * @param operatorSet The operatorSet to calculate the operator table for + * @return operatorSetInfo The operator table for the given operatorSet + * @dev This function: + * 1. Gets operator weights from the weight calculator + * 2. Collates weights into total weights + * 3. Creates a merkle tree of operator info + * - assumes that the operator has a registered BN254 key + * 4. Calculates the aggregate public key + */ + function _calculateOperatorTable( + OperatorSet calldata operatorSet + ) internal view returns (BN254OperatorSetInfo memory operatorSetInfo) { + // Get the weights for all operators in the operatorSet + (address[] memory operators, uint256[][] memory weights) = _getOperatorWeights(operatorSet); + + // If there are no weights, return an empty operator set info + if (weights.length == 0) { + return BN254OperatorSetInfo({ + operatorInfoTreeRoot: bytes32(0), + numOperators: 0, + aggregatePubkey: BN254.G1Point(0, 0), + totalWeights: new uint256[](0) + }); + } + + // Initialize arrays + uint256 subArrayLength = weights[0].length; + uint256[] memory totalWeights = new uint256[](subArrayLength); + bytes32[] memory operatorInfoLeaves = new bytes32[](operators.length); + BN254.G1Point memory aggregatePubkey; + uint256 operatorCount = 0; + + for (uint256 i = 0; i < operators.length; i++) { + // Skip if the operator has not registered their key + if (!keyRegistrar.isRegistered(operatorSet, operators[i])) { + continue; + } + + // Read the weights for the operator and encode them into the operatorInfoLeaves + // for all weights, add them to the total weights. The ith index returns the weights array for the ith operator + for (uint256 j = 0; j < subArrayLength; j++) { + totalWeights[j] += weights[i][j]; + } + (BN254.G1Point memory g1Point,) = keyRegistrar.getBN254Key(operatorSet, operators[i]); + operatorInfoLeaves[i] = + keccak256(abi.encode(BN254OperatorInfo({pubkey: g1Point, weights: weights[i]}))); + + // Add the operator's G1 point to the aggregate pubkey + aggregatePubkey = aggregatePubkey.plus(g1Point); + + // Increment the operator count + operatorCount++; + } + + // If there are no operators, return an empty operator set info + if (operatorCount == 0) { + return BN254OperatorSetInfo({ + operatorInfoTreeRoot: bytes32(0), + numOperators: 0, + aggregatePubkey: BN254.G1Point(0, 0), + totalWeights: new uint256[](0) + }); + } + + // Resize the operatorInfoLeaves array to the number of operators and merkleize + assembly { + mstore(operatorInfoLeaves, operatorCount) + } + + bytes32 operatorInfoTreeRoot = operatorInfoLeaves.merkleizeKeccak(); + + return BN254OperatorSetInfo({ + operatorInfoTreeRoot: operatorInfoTreeRoot, + numOperators: operatorCount, + aggregatePubkey: aggregatePubkey, + totalWeights: totalWeights + }); + } +} diff --git a/src/middlewareV2/tableCalculator/ECDSATableCalculator.sol b/src/middlewareV2/tableCalculator/ECDSATableCalculator.sol new file mode 100644 index 000000000..6d0ae22cf --- /dev/null +++ b/src/middlewareV2/tableCalculator/ECDSATableCalculator.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; + +import "./ECDSATableCalculatorBase.sol"; + +/** + * @title ECDSATableCalculator + * @notice Implementation that calculates ECDSA operator tables using the sum of the minimum slashable stake weights + * @dev This contract assumes that slashable stake is valued the **same** across all strategies. + */ +contract ECDSATableCalculator is ECDSATableCalculatorBase { + // Immutables + /// @notice AllocationManager contract for managing operator allocations + IAllocationManager public immutable allocationManager; + /// @notice The default lookahead blocks for the slashable stake lookup + uint256 public immutable LOOKAHEAD_BLOCKS; + + constructor( + IKeyRegistrar _keyRegistrar, + IAllocationManager _allocationManager, + uint256 _LOOKAHEAD_BLOCKS + ) ECDSATableCalculatorBase(_keyRegistrar) { + allocationManager = _allocationManager; + LOOKAHEAD_BLOCKS = _LOOKAHEAD_BLOCKS; + } + + /** + * @notice Get the operator weights for a given operatorSet based on the slashable stake. + * @param operatorSet The operatorSet to get the weights for + * @return operators The addresses of the operators in the operatorSet + * @return weights The weights for each operator in the operatorSet, this is a 2D array where the first index is the operator + * and the second index is the type of weight. In this case its of length 1 and returns the slashable stake for the operatorSet. + */ + function _getOperatorWeights( + OperatorSet calldata operatorSet + ) internal view override returns (address[] memory operators, uint256[][] memory weights) { + // Get all operators & strategies in the operatorSet + address[] memory registeredOperators = allocationManager.getMembers(operatorSet); + IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet); + + // Get the minimum slashable stake for each operator + uint256[][] memory minSlashableStake = allocationManager.getMinimumSlashableStake({ + operatorSet: operatorSet, + operators: registeredOperators, + strategies: strategies, + futureBlock: uint32(block.number + LOOKAHEAD_BLOCKS) + }); + + operators = new address[](registeredOperators.length); + weights = new uint256[][](registeredOperators.length); + uint256 operatorCount = 0; + for (uint256 i = 0; i < registeredOperators.length; ++i) { + // For the given operator, loop through the strategies and sum together to calculate the operator's weight for the operatorSet + uint256 totalWeight; + for (uint256 stratIndex = 0; stratIndex < strategies.length; ++stratIndex) { + totalWeight += minSlashableStake[i][stratIndex]; + } + + // If the operator has nonzero slashable stake, add them to the operators array + if (totalWeight > 0) { + // Initialize operator weights array of length 1 just for slashable stake + weights[operatorCount] = new uint256[](1); + weights[operatorCount][0] = totalWeight; + + // Add the operator to the operators array + operators[operatorCount] = registeredOperators[i]; + operatorCount++; + } + } + + // Resize arrays to be the size of the number of operators with nonzero slashable stake + assembly { + mstore(operators, operatorCount) + mstore(weights, operatorCount) + } + + return (operators, weights); + } +} diff --git a/src/middlewareV2/tableCalculator/ECDSATableCalculatorBase.sol b/src/middlewareV2/tableCalculator/ECDSATableCalculatorBase.sol new file mode 100644 index 000000000..422415597 --- /dev/null +++ b/src/middlewareV2/tableCalculator/ECDSATableCalculatorBase.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {IOperatorTableCalculator} from + "eigenlayer-contracts/src/contracts/interfaces/IOperatorTableCalculator.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import {Merkle} from "eigenlayer-contracts/src/contracts/libraries/Merkle.sol"; +import {IECDSATableCalculator} from "../../interfaces/IECDSATableCalculator.sol"; + +/** + * @title ECDSATableCalculatorBase + * @notice Abstract contract that provides base functionality for calculating ECDSA operator tables + * @dev This contract contains all the core logic for operator table calculations, + * with weight calculation left to be implemented by derived contracts + */ +abstract contract ECDSATableCalculatorBase is IECDSATableCalculator { + using Merkle for bytes32[]; + + // Immutables + /// @notice KeyRegistrar contract for managing operator keys + IKeyRegistrar public immutable keyRegistrar; + + constructor( + IKeyRegistrar _keyRegistrar + ) { + keyRegistrar = _keyRegistrar; + } + + /// @inheritdoc IECDSATableCalculator + function calculateOperatorTable( + OperatorSet calldata operatorSet + ) external view virtual returns (ECDSAOperatorInfo[] memory operatorInfos) { + return _calculateOperatorTable(operatorSet); + } + + /// @inheritdoc IOperatorTableCalculator + function calculateOperatorTableBytes( + OperatorSet calldata operatorSet + ) external view virtual returns (bytes memory operatorTableBytes) { + return abi.encode(_calculateOperatorTable(operatorSet)); + } + + /// @inheritdoc IOperatorTableCalculator + function getOperatorSetWeights( + OperatorSet calldata operatorSet + ) external view virtual returns (address[] memory operators, uint256[][] memory weights) { + return _getOperatorWeights(operatorSet); + } + + /// @inheritdoc IOperatorTableCalculator + function getOperatorWeights( + OperatorSet calldata operatorSet, + address operator + ) external view virtual returns (uint256[] memory) { + (address[] memory operators, uint256[][] memory weights) = _getOperatorWeights(operatorSet); + + // Find the index of the operator in the operators array + for (uint256 i = 0; i < operators.length; i++) { + if (operators[i] == operator) { + return weights[i]; + } + } + + return new uint256[](0); + } + + /** + * @notice Abstract function to get the operator weights for a given operatorSet + * @param operatorSet The operatorSet to get the weights for + * @return operators The addresses of the operators in the operatorSet + * @return weights The weights for each operator in the operatorSet, this is a 2D array where the first index is the operator + * and the second index is the type of weight + * @dev Must be implemented by derived contracts to define specific weight calculation logic + */ + function _getOperatorWeights( + OperatorSet calldata operatorSet + ) internal view virtual returns (address[] memory operators, uint256[][] memory weights); + + /** + * @notice Calculates the operator table for a given operatorSet + * @param operatorSet The operatorSet to calculate the operator table for + * @return operatorInfos The operator table for the given operatorSet + * @dev This function: + * 1. Gets operator weights from the weight calculator + * 2. Creates ECDSAOperatorInfo structs for each operator with registered ECDSA keys + */ + function _calculateOperatorTable( + OperatorSet calldata operatorSet + ) internal view returns (ECDSAOperatorInfo[] memory operatorInfos) { + // Get the weights for all operators in the operatorSet + (address[] memory operators, uint256[][] memory weights) = _getOperatorWeights(operatorSet); + + // If there are no weights, return an empty array + if (weights.length == 0) { + return new ECDSAOperatorInfo[](0); + } + + // Create the operator infos array with maximum possible size + operatorInfos = new ECDSAOperatorInfo[](operators.length); + uint256 operatorCount = 0; + + for (uint256 i = 0; i < operators.length; i++) { + // Skip if the operator has not registered their ECDSA key + if (!keyRegistrar.isRegistered(operatorSet, operators[i])) { + continue; + } + + // Get the ECDSA address (public key) for the operator + address ecdsaAddress = keyRegistrar.getECDSAAddress(operatorSet, operators[i]); + + // Create the ECDSAOperatorInfo struct + operatorInfos[operatorCount] = + ECDSAOperatorInfo({pubkey: ecdsaAddress, weights: weights[i]}); + + operatorCount++; + } + + // If no operators have registered keys, return empty array + if (operatorCount == 0) { + return new ECDSAOperatorInfo[](0); + } + + // Resize the array to the actual number of operators with registered keys + assembly { + mstore(operatorInfos, operatorCount) + } + + return operatorInfos; + } +} diff --git a/test/harnesses/BLSSigCheckUtilsHarness.sol b/test/harnesses/BLSSigCheckUtilsHarness.sol new file mode 100644 index 000000000..a7d4decc7 --- /dev/null +++ b/test/harnesses/BLSSigCheckUtilsHarness.sol @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {BN254} from "../../src/libraries/BN254.sol"; +import { + BLSSigCheckUtils, + Comparators, + SlotDerivation, + Arrays +} from "../../src/unaudited/BLSSigCheckUtils.sol"; +import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol"; + +/** + * @title BLSSigCheckUtilsHarness + * @notice Test harness to expose internal functions from BLSSigCheckUtils and its libraries for testing + */ +contract BLSSigCheckUtilsHarness { + using BN254 for BN254.G1Point; + using BLSSigCheckUtils for BN254.G1Point; + using SlotDerivation for bytes32; + using SlotDerivation for string; + using Arrays for uint256[]; + using Arrays for address[]; + using Arrays for bytes32[]; + using Arrays for bytes[]; + using Arrays for string[]; + + // Storage arrays for testing storage-related functions + uint256[] public uint256Array; + address[] public addressArray; + bytes32[] public bytes32Array; + bytes[] public bytesArray; + string[] public stringArray; + + /** + * + * BLSSigCheckUtils functions + * + */ + function isOnCurve( + BN254.G1Point memory p + ) public pure returns (bool) { + return p.isOnCurve(); + } + + /** + * + * Comparators library functions + * + */ + function lt(uint256 a, uint256 b) public pure returns (bool) { + return Comparators.lt(a, b); + } + + function gt(uint256 a, uint256 b) public pure returns (bool) { + return Comparators.gt(a, b); + } + + /** + * + * SlotDerivation library functions + * + */ + function erc7201Slot( + string memory namespace + ) public pure returns (bytes32) { + return namespace.erc7201Slot(); + } + + function offset(bytes32 slot, uint256 pos) public pure returns (bytes32) { + return slot.offset(pos); + } + + function deriveArray( + bytes32 slot + ) public pure returns (bytes32) { + return slot.deriveArray(); + } + + function deriveMappingAddress(bytes32 slot, address key) public pure returns (bytes32) { + return slot.deriveMapping(key); + } + + function deriveMappingBool(bytes32 slot, bool key) public pure returns (bytes32) { + return slot.deriveMapping(key); + } + + function deriveMappingBytes32(bytes32 slot, bytes32 key) public pure returns (bytes32) { + return slot.deriveMapping(key); + } + + function deriveMappingUint256(bytes32 slot, uint256 key) public pure returns (bytes32) { + return slot.deriveMapping(key); + } + + function deriveMappingInt256(bytes32 slot, int256 key) public pure returns (bytes32) { + return slot.deriveMapping(key); + } + + function deriveMappingString(bytes32 slot, string memory key) public pure returns (bytes32) { + return slot.deriveMapping(key); + } + + function deriveMappingBytes(bytes32 slot, bytes memory key) public pure returns (bytes32) { + return slot.deriveMapping(key); + } + + /** + * + * Arrays library functions - Sorting + * + */ + function sortUint256( + uint256[] memory array + ) public pure returns (uint256[] memory) { + return array.sort(); + } + + function sortAddress( + address[] memory array + ) public pure returns (address[] memory) { + return array.sort(); + } + + function sortBytes32( + bytes32[] memory array + ) public pure returns (bytes32[] memory) { + return array.sort(); + } + + /** + * + * Arrays library functions - Binary Search + * + */ + function findUpperBound( + uint256 element + ) public view returns (uint256) { + return uint256Array.findUpperBound(element); + } + + function lowerBound( + uint256 element + ) public view returns (uint256) { + return uint256Array.lowerBound(element); + } + + function upperBound( + uint256 element + ) public view returns (uint256) { + return uint256Array.upperBound(element); + } + + function lowerBoundMemory( + uint256[] memory array, + uint256 element + ) public pure returns (uint256) { + return array.lowerBoundMemory(element); + } + + function upperBoundMemory( + uint256[] memory array, + uint256 element + ) public pure returns (uint256) { + return array.upperBoundMemory(element); + } + + /** + * + * Arrays library functions - Unsafe Access + * + */ + function unsafeAccessAddress( + uint256 pos + ) public view returns (address) { + return addressArray.unsafeAccess(pos).value; + } + + function unsafeAccessBytes32( + uint256 pos + ) public view returns (bytes32) { + return bytes32Array.unsafeAccess(pos).value; + } + + function unsafeAccessUint256( + uint256 pos + ) public view returns (uint256) { + return uint256Array.unsafeAccess(pos).value; + } + + function unsafeAccessBytes( + uint256 pos + ) public view returns (bytes memory) { + return bytesArray.unsafeAccess(pos).value; + } + + function unsafeAccessString( + uint256 pos + ) public view returns (string memory) { + return stringArray.unsafeAccess(pos).value; + } + + function unsafeMemoryAccessAddress( + address[] memory arr, + uint256 pos + ) public pure returns (address) { + return arr.unsafeMemoryAccess(pos); + } + + function unsafeMemoryAccessBytes32( + bytes32[] memory arr, + uint256 pos + ) public pure returns (bytes32) { + return arr.unsafeMemoryAccess(pos); + } + + function unsafeMemoryAccessUint256( + uint256[] memory arr, + uint256 pos + ) public pure returns (uint256) { + return arr.unsafeMemoryAccess(pos); + } + + function unsafeMemoryAccessBytes( + bytes[] memory arr, + uint256 pos + ) public pure returns (bytes memory) { + return arr.unsafeMemoryAccess(pos); + } + + function unsafeMemoryAccessString( + string[] memory arr, + uint256 pos + ) public pure returns (string memory) { + return arr.unsafeMemoryAccess(pos); + } + + /** + * + * Arrays library functions - Unsafe Set Length + * + */ + function unsafeSetLengthAddress( + uint256 len + ) public { + addressArray.unsafeSetLength(len); + } + + function unsafeSetLengthBytes32( + uint256 len + ) public { + bytes32Array.unsafeSetLength(len); + } + + function unsafeSetLengthUint256( + uint256 len + ) public { + uint256Array.unsafeSetLength(len); + } + + function unsafeSetLengthBytes( + uint256 len + ) public { + bytesArray.unsafeSetLength(len); + } + + function unsafeSetLengthString( + uint256 len + ) public { + stringArray.unsafeSetLength(len); + } + + /** + * + * Helper functions for testing + * + */ + + // Initialize test arrays + function initializeUint256Array( + uint256[] memory values + ) public { + delete uint256Array; + for (uint256 i = 0; i < values.length; i++) { + uint256Array.push(values[i]); + } + } + + function initializeAddressArray( + address[] memory values + ) public { + delete addressArray; + for (uint256 i = 0; i < values.length; i++) { + addressArray.push(values[i]); + } + } + + function initializeBytes32Array( + bytes32[] memory values + ) public { + delete bytes32Array; + for (uint256 i = 0; i < values.length; i++) { + bytes32Array.push(values[i]); + } + } + + function initializeBytesArray( + bytes[] memory values + ) public { + delete bytesArray; + for (uint256 i = 0; i < values.length; i++) { + bytesArray.push(values[i]); + } + } + + function initializeStringArray( + string[] memory values + ) public { + delete stringArray; + for (uint256 i = 0; i < values.length; i++) { + stringArray.push(values[i]); + } + } + + // Getters for array lengths + function getUint256ArrayLength() public view returns (uint256) { + return uint256Array.length; + } + + function getAddressArrayLength() public view returns (uint256) { + return addressArray.length; + } + + function getBytes32ArrayLength() public view returns (uint256) { + return bytes32Array.length; + } + + function getBytesArrayLength() public view returns (uint256) { + return bytesArray.length; + } + + function getStringArrayLength() public view returns (uint256) { + return stringArray.length; + } + + // Getters for full arrays + function getUint256Array() public view returns (uint256[] memory) { + return uint256Array; + } + + function getAddressArray() public view returns (address[] memory) { + return addressArray; + } + + function getBytes32Array() public view returns (bytes32[] memory) { + return bytes32Array; + } +} diff --git a/test/integration/IntegrationDeployer.t.sol b/test/integration/IntegrationDeployer.t.sol index 1d9dcc9ee..b54e9e4e4 100644 --- a/test/integration/IntegrationDeployer.t.sol +++ b/test/integration/IntegrationDeployer.t.sol @@ -189,7 +189,7 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { ); // Deploy EigenPod Contracts - pod = new EigenPod(ethPOSDeposit, eigenPodManager, GENESIS_TIME_LOCAL, "v0.0.1"); + pod = new EigenPod(ethPOSDeposit, eigenPodManager, "v0.0.1"); eigenPodBeacon = new UpgradeableBeacon(address(pod)); @@ -206,7 +206,7 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { "v0.0.1" ); StrategyManager strategyManagerImplementation = - new StrategyManager(delegationManager, pauserRegistry, "v0.0.1"); + new StrategyManager(allocationManager, delegationManager, pauserRegistry, "v0.0.1"); EigenPodManager eigenPodManagerImplementation = new EigenPodManager( ethPOSDeposit, eigenPodBeacon, delegationManager, pauserRegistry, "v0.0.1" ); @@ -231,6 +231,7 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { AllocationManager allocationManagerImplementation = new AllocationManager( delegationManager, + IStrategy(address(0)), // TODO: update this to the eigenStrategy, pauserRegistry, permissionController, uint32(7 days), // DEALLOCATION_DELAY @@ -247,9 +248,7 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { ITransparentUpgradeableProxy(payable(address(delegationManager))), address(delegationImplementation), abi.encodeWithSelector( - DelegationManager.initialize.selector, - eigenLayerReputedMultisig, // initialOwner - 0 /* initialPausedStatus */ + DelegationManager.initialize.selector, 0 /* initialPausedStatus */ ) ); // StrategyManager @@ -308,7 +307,6 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { address(allocationManagerImplementation), abi.encodeWithSelector( AllocationManager.initialize.selector, - eigenLayerReputedMultisig, // initialOwner 0 // initialPausedStatus ) ); diff --git a/test/mocks/AllocationManagerMock.sol b/test/mocks/AllocationManagerMock.sol index 98bcd3724..f4f6eb6ce 100644 --- a/test/mocks/AllocationManagerMock.sol +++ b/test/mocks/AllocationManagerMock.sol @@ -11,9 +11,14 @@ import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPa import {ISemVerMixin} from "eigenlayer-contracts/src/contracts/interfaces/ISemVerMixin.sol"; contract AllocationManagerIntermediate is IAllocationManager { + mapping(address avs => address avsRegistrar) internal _avsRegistrar; + function initialize(address initialOwner, uint256 initialPausedStatus) external virtual {} - function slashOperator(address avs, SlashingParams calldata params) external virtual {} + function slashOperator( + address avs, + SlashingParams calldata params + ) external virtual returns (uint256 slashId, uint256[] memory shares) {} function modifyAllocations( address operator, @@ -37,7 +42,15 @@ contract AllocationManagerIntermediate is IAllocationManager { function setAllocationDelay(address operator, uint32 delay) external virtual {} - function setAVSRegistrar(address avs, IAVSRegistrar registrar) external virtual {} + function setAVSRegistrar(address avs, IAVSRegistrar avsRegistrar) external { + _avsRegistrar[avs] = address(avsRegistrar); + } + + function getAVSRegistrar( + address avs + ) external view override returns (IAVSRegistrar) { + return IAVSRegistrar(_avsRegistrar[avs]); + } function updateAVSMetadataURI(address avs, string calldata metadataURI) external virtual {} @@ -131,10 +144,6 @@ contract AllocationManagerIntermediate is IAllocationManager { OperatorSet memory operatorSet ) external view virtual returns (uint256) {} - function getAVSRegistrar( - address avs - ) external view virtual returns (IAVSRegistrar) {} - function getStrategiesInOperatorSet( OperatorSet memory operatorSet ) external view virtual returns (IStrategy[] memory strategies) {} @@ -152,7 +161,7 @@ contract AllocationManagerIntermediate is IAllocationManager { ) external view virtual returns (bool) {} function getAllocatedStake( - OperatorSet memory operatorSet, + OperatorSet memory, /* operatorSet */ address[] memory operators, IStrategy[] memory strategies ) external view virtual returns (uint256[][] memory slashableStake) { @@ -167,15 +176,15 @@ contract AllocationManagerIntermediate is IAllocationManager { } function getEncumberedMagnitude( - address operator, - IStrategy strategy + address, /* operator */ + IStrategy /* strategy */ ) external view virtual returns (uint64) { return 0; } function isOperatorSlashable( - address operator, - OperatorSet memory operatorSet + address, /* operator */ + OperatorSet memory /* operatorSet */ ) external view virtual returns (bool) { return false; } @@ -183,15 +192,152 @@ contract AllocationManagerIntermediate is IAllocationManager { function version() external pure virtual returns (string memory) { return "v0.0.1"; } + + function DEALLOCATION_DELAY() external pure virtual returns (uint32) {} + + function createRedistributingOperatorSets( + address avs, + CreateSetParams[] calldata params, + address[] calldata redistributionRecipients + ) external virtual {} + + function getRedistributionRecipient( + OperatorSet memory operatorSet + ) external pure virtual returns (address) {} + + function getSlashCount( + OperatorSet memory operatorSet + ) external pure virtual returns (uint256) {} + + function initialize( + uint256 initialPausedStatus + ) external virtual {} + + function isOperatorRedistributable( + address operator + ) external pure virtual returns (bool) {} + + function isRedistributingOperatorSet( + OperatorSet memory operatorSet + ) external pure virtual returns (bool) {} } contract AllocationManagerMock is AllocationManagerIntermediate { - uint32 public constant DEALLOCATION_DELAY = 86400; + uint32 internal constant _DEALLOCATION_DELAY = 86400; + + mapping(bytes32 operatorSetKey => address[] members) internal _members; + mapping(bytes32 operatorSetKey => IStrategy[] strategies) internal _strategies; + mapping( + bytes32 operatorSetKey + => mapping( + address operator => mapping(IStrategy strategy => uint256 minimumSlashableStake) + ) + ) internal _minimumSlashableStake; + + function DEALLOCATION_DELAY() external pure override returns (uint32) { + return _DEALLOCATION_DELAY; + } + + function createRedistributingOperatorSets( + address avs, + CreateSetParams[] calldata params, + address[] calldata redistributionRecipients + ) external override {} + + function getRedistributionRecipient( + OperatorSet memory /* operatorSet */ + ) external pure override returns (address) { + return address(0); + } + + function getSlashCount( + OperatorSet memory /* operatorSet */ + ) external pure override returns (uint256) { + return 0; + } + + function initialize( + uint256 initialPausedStatus + ) external override {} + + function isOperatorRedistributable( + address /* operator */ + ) external pure override returns (bool) { + return false; + } + + function isRedistributingOperatorSet( + OperatorSet memory /* operatorSet */ + ) external pure override returns (bool) { + return false; + } function getAllocatedStake( - address operator, - IStrategy strategy - ) external view returns (uint256) { + address, /* operator */ + IStrategy /* strategy */ + ) external pure returns (uint256) { return 0; } + + function getMembers( + OperatorSet memory operatorSet + ) external view override returns (address[] memory) { + return _members[operatorSet.key()]; + } + + function setMembersInOperatorSet( + OperatorSet memory operatorSet, + address[] memory members + ) external { + _members[operatorSet.key()] = members; + } + + function setStrategiesInOperatorSet( + OperatorSet memory operatorSet, + IStrategy[] memory strategies + ) external { + _strategies[operatorSet.key()] = strategies; + } + + function getStrategiesInOperatorSet( + OperatorSet memory operatorSet + ) external view override returns (IStrategy[] memory) { + return _strategies[operatorSet.key()]; + } + + function setMinimumSlashableStake( + OperatorSet memory operatorSet, + address[] memory operators, + IStrategy[] memory strategies, + uint256[][] memory minimumSlashableStake + ) external { + for (uint256 i = 0; i < operators.length; ++i) { + for (uint256 j = 0; j < strategies.length; ++j) { + _minimumSlashableStake[operatorSet.key()][operators[i]][strategies[j]] = + minimumSlashableStake[i][j]; + } + } + } + + function getMinimumSlashableStake( + OperatorSet memory operatorSet, + address[] memory operators, + IStrategy[] memory strategies, + uint32 /* futureBlock */ + ) external view override returns (uint256[][] memory) { + uint256[][] memory minimumSlashableStake = new uint256[][](operators.length); + + for (uint256 i = 0; i < operators.length; ++i) { + minimumSlashableStake[i] = new uint256[](strategies.length); + } + + for (uint256 i = 0; i < operators.length; ++i) { + for (uint256 j = 0; j < strategies.length; ++j) { + minimumSlashableStake[i][j] = + _minimumSlashableStake[operatorSet.key()][operators[i]][strategies[j]]; + } + } + + return minimumSlashableStake; + } } diff --git a/test/mocks/DelegationMock.sol b/test/mocks/DelegationMock.sol index 6e0c351ea..dfa3fbab6 100644 --- a/test/mocks/DelegationMock.sol +++ b/test/mocks/DelegationMock.sol @@ -18,10 +18,15 @@ import { } from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtilsMixin.sol"; import {ISemVerMixin} from "eigenlayer-contracts/src/contracts/interfaces/ISemVerMixin.sol"; import {SlashingLib} from "eigenlayer-contracts/src/contracts/libraries/SlashingLib.sol"; +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; contract DelegationIntermediate is IDelegationManager { function initialize(address initialOwner, uint256 initialPausedStatus) external virtual {} + function initialize( + uint256 initialPausedStatus + ) external virtual {} + function registerAsOperator( OperatorDetails calldata registeringOperatorDetails, uint32 allocationDelay, @@ -240,6 +245,15 @@ contract DelegationIntermediate is IDelegationManager { uint64 newMaxMagnitude ) external {} + function slashOperatorShares( + address operator, + OperatorSet calldata operatorSet, + uint256 slashId, + IStrategy strategy, + uint64 prevMaxMagnitude, + uint64 newMaxMagnitude + ) external virtual returns (uint256 totalDepositSharesToSlash) {} + function getQueuedWithdrawal( bytes32 withdrawalRoot ) @@ -309,6 +323,10 @@ contract DelegationMock is DelegationIntermediate { mapping(address => bool) internal _isOperator; mapping(address => mapping(IStrategy => uint256)) internal _weightOf; + function initialize( + uint256 initialPausedStatus + ) external override {} + function setOperatorShares( address operator, IStrategy strategy, diff --git a/test/mocks/EigenPodManagerMock.sol b/test/mocks/EigenPodManagerMock.sol index 121a5435e..577125e6c 100644 --- a/test/mocks/EigenPodManagerMock.sol +++ b/test/mocks/EigenPodManagerMock.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import "eigenlayer-contracts/src/contracts/permissions/Pausable.sol"; import "eigenlayer-contracts/src/contracts/interfaces/IEigenPodManager.sol"; import "eigenlayer-contracts/src/contracts/interfaces/ISemVerMixin.sol"; +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; contract EigenPodManagerMock is Test, Pausable, IEigenPodManager { receive() external payable {} @@ -126,6 +127,13 @@ contract EigenPodManagerMock is Test, Pausable, IEigenPodManager { function increaseBurnableShares(IStrategy strategy, uint256 addedSharesToBurn) external {} + function increaseBurnOrRedistributableShares( + OperatorSet calldata operatorSet, + uint256 slashId, + IStrategy strategy, + uint256 addedSharesToBurn + ) external {} + /** * @notice Returns the version of the contract * @return The version string diff --git a/test/mocks/KeyRegistrarMock.sol b/test/mocks/KeyRegistrarMock.sol new file mode 100644 index 000000000..a4b7c1c8d --- /dev/null +++ b/test/mocks/KeyRegistrarMock.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import { + IKeyRegistrarTypes, + IKeyRegistrar, + BN254 +} from "eigenlayer-contracts/src/contracts/interfaces//IKeyRegistrar.sol"; +import { + OperatorSetLib, + OperatorSet +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {ISemVerMixin} from "eigenlayer-contracts/src/contracts/interfaces/ISemVerMixin.sol"; + +contract KeyRegistrarMock is IKeyRegistrar { + using OperatorSetLib for OperatorSet; + + mapping(bytes32 operatorSetKey => mapping(address => bool)) internal _operatorRegistered; + + function setIsRegistered( + address operator, + OperatorSet calldata operatorSet, + bool _isRegistered + ) external { + bytes32 operatorSetKey = operatorSet.key(); + _operatorRegistered[operatorSetKey][operator] = _isRegistered; + } + + function checkKey( + OperatorSet calldata operatorSet, + address operator + ) external view returns (bool) { + return _operatorRegistered[operatorSet.key()][operator]; + } + + function initialize( + address initialOwner + ) external {} + + function configureOperatorSet(OperatorSet memory operatorSet, CurveType curveType) external {} + + function registerKey( + address operator, + OperatorSet memory operatorSet, + bytes calldata pubkey, + bytes calldata signature + ) external {} + + function deregisterKey(address operator, OperatorSet memory operatorSet) external {} + + function isRegistered( + OperatorSet memory operatorSet, + address operator + ) external pure returns (bool) {} + + function getOperatorSetCurveType( + OperatorSet memory operatorSet + ) external pure returns (CurveType) {} + + function getBN254Key( + OperatorSet memory operatorSet, + address operator + ) external view returns (BN254.G1Point memory g1Point, BN254.G2Point memory g2Point) {} + + /** + * @notice Gets the ECDSA public key for an operator with a specific operator set + * @param operatorSet The operator set to get the key for + * @param operator Address of the operator + * @return pubkey The ECDSA public key + */ + function getECDSAKey( + OperatorSet memory operatorSet, + address operator + ) external pure returns (bytes memory) {} + + /** + * @notice Gets the ECDSA public key for an operator with a specific operator set + * @param operatorSet The operator set to get the key for + * @param operator Address of the operator + * @return pubkey The ECDSA public key + */ + function getECDSAAddress( + OperatorSet memory operatorSet, + address operator + ) external pure returns (address) {} + + function isKeyGloballyRegistered( + bytes32 keyHash + ) external view returns (bool) {} + + function getKeyHash( + OperatorSet memory operatorSet, + address operator + ) external pure returns (bytes32) {} + + function verifyBN254Signature( + bytes32 messageHash, + bytes memory signature, + BN254.G1Point memory pubkeyG1, + BN254.G2Point memory pubkeyG2 + ) external pure {} + + function version() external pure returns (string memory) { + return "v0.0.1"; + } + + function getECDSAKeyRegistrationMessageHash( + address operator, + OperatorSet memory operatorSet, + address keyAddress + ) external pure returns (bytes32) { + return keccak256(abi.encode(operator, operatorSet, keyAddress)); + } + + function getBN254KeyRegistrationMessageHash( + address operator, + OperatorSet memory operatorSet, + bytes calldata keyData + ) external pure returns (bytes32) { + return keccak256(abi.encode(operator, operatorSet, keyData)); + } + + function encodeBN254KeyData( + BN254.G1Point memory g1Point, + BN254.G2Point memory g2Point + ) external pure returns (bytes memory) { + return abi.encode(g1Point, g2Point); + } + + function getOperatorFromSigningKey( + OperatorSet memory, + /** + * operatorSet + */ + bytes calldata + ) + /** + * keyData + */ + external + pure + returns (address, bool) + { + return (address(0), false); + } + + receive() external payable {} + fallback() external payable {} +} diff --git a/test/tree/BN254TableCalculatorBase.tree b/test/tree/BN254TableCalculatorBase.tree new file mode 100644 index 000000000..90b218b5d --- /dev/null +++ b/test/tree/BN254TableCalculatorBase.tree @@ -0,0 +1,31 @@ +. +└── BN254TableCalculatorBase (**** denotes that integration tests are needed to fully validate path) + ├── when calculateOperatorTable is called + │ ├── given that there are no operators + │ │ └── it should return empty operator table with zero aggregate pubkey + │ ├── given that operators have no registered BN254 keys + │ │ └── it should skip those operators and return table without them + │ ├── given that all operators have registered BN254 keys + │ │ └── it should include all operators in the table and calculate aggregate pubkey + │ ├── given that weights array has multiple weight types + │ │ └── it should correctly sum all weight types in totalWeights + │ └── given that operators have mixed registration status + │ └── it should only include registered operators in calculations + ├── when calculateOperatorTableBytes is called + │ └── it should return the encoded bytes of the operator table + ├── when getOperatorWeights is called + │ └── it should return the result from _getOperatorWeights implementation + ├── when getOperatorWeight is called + │ ├── given that the operator exists in the set + │ │ └── it should return the first weight value for that operator + │ ├── given that the operator does not exist in the set + │ │ └── it should return zero + │ └── given that the operator exists but has empty weights + │ └── it should handle gracefully **** + └── when getOperatorInfos is called + ├── given that no operators are registered + │ └── it should return array with empty operator infos + ├── given that some operators are not registered + │ └── it should skip unregistered operators (leaving empty slots) + └── given that all operators are registered + └── it should return complete operator info for each operator \ No newline at end of file diff --git a/test/tree/ECDSATableCalculatorBase.tree b/test/tree/ECDSATableCalculatorBase.tree new file mode 100644 index 000000000..9be537855 --- /dev/null +++ b/test/tree/ECDSATableCalculatorBase.tree @@ -0,0 +1,34 @@ +. +└── ECDSATableCalculatorBase (**** denotes that integration tests are needed to fully validate path) + ├── when calculateOperatorTable is called + │ ├── given that no operators are in the set + │ │ └── it should return empty operator info array + │ ├── given that operators exist but none have registered keys + │ │ └── it should return empty operator info array + │ ├── given that all operators have registered keys + │ │ └── it should return operator infos with correct pubkeys and weights + │ ├── given that operators have multiple weight types + │ │ └── it should return operator infos with all weight types correctly + │ ├── given that some operators have registered keys and some have not + │ │ └── it should only include registered operators in the result + │ └── given that a single operator is registered + │ └── it should return single operator info with correct data + ├── when calculateOperatorTableBytes is called + │ ├── given valid operator set with registered operators + │ │ └── it should encode operator infos correctly as bytes + │ └── given various weight values (fuzz test) + │ └── it should encode and decode weights correctly + ├── when getOperatorWeights is called + │ ├── given operators and weights are set + │ │ └── it should return the implementation result correctly + │ └── given various numbers of operators (fuzz test) + │ └── it should return correct operator and weight arrays + └── when getOperatorWeight is called + ├── given that the operator exists in the set + │ └── it should return the correct weight + ├── given that the operator does not exist in the set + │ └── it should return 0 + ├── given that the operator set is empty + │ └── it should return 0 + └── given various operator and weight combinations (fuzz test) + └── it should return correct weight or 0 \ No newline at end of file diff --git a/test/unit/BLSSigCheckUtilsUnit.t.sol b/test/unit/BLSSigCheckUtilsUnit.t.sol new file mode 100644 index 000000000..7ed2121a8 --- /dev/null +++ b/test/unit/BLSSigCheckUtilsUnit.t.sol @@ -0,0 +1,601 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; +import {BN254} from "../../src/libraries/BN254.sol"; +import {BLSSigCheckUtils} from "../../src/unaudited/BLSSigCheckUtils.sol"; +import {BLSSigCheckUtilsHarness} from "../harnesses/BLSSigCheckUtilsHarness.sol"; + +contract BLSSigCheckUtilsUnitTests is Test { + using BN254 for BN254.G1Point; + using BLSSigCheckUtils for BN254.G1Point; + + uint256 constant FP_MODULUS = + 21888242871839275222246405745257275088696311157297823662689037894645226208583; + + BLSSigCheckUtilsHarness harness; + + struct TestPoint { + uint256 x; + uint256 y; + bool shouldBeOnCurve; + } + + function setUp() public { + harness = new BLSSigCheckUtilsHarness(); + } + + /** + * @notice Test that the generator point is on the curve + */ + function test_isOnCurve_generator() public pure { + BN254.G1Point memory generator = BN254.generatorG1(); + assertTrue(generator.isOnCurve(), "Generator point should be on the curve"); + } + + /** + * @notice Test that the identity element (0, 0) is NOT on the curve + * @dev For BN254, (0, 0) doesn't satisfy y^2 = x^3 + 3 (0 != 3) + */ + function test_isOnCurve_identity() public pure { + BN254.G1Point memory identity = BN254.G1Point(0, 0); + assertFalse(identity.isOnCurve(), "Identity element (0,0) should NOT be on the curve"); + } + + /** + * @notice Test some known valid points on the curve + */ + function test_isOnCurve_validPoints() public pure { + // These are known valid points on the BN254 curve + // We'll use the generator and its scalar multiples which are guaranteed to be on curve + BN254.G1Point memory generator = BN254.generatorG1(); + + assertTrue(generator.isOnCurve(), "Generator should be on the curve"); + + // Test the generator negation + BN254.G1Point memory negatedGen = generator.negate(); + assertTrue(negatedGen.isOnCurve(), "Negated generator should be on the curve"); + } + + /** + * @notice Test invalid points not on the curve + */ + function test_isOnCurve_invalidPoints() public pure { + // These points have valid x coordinates but invalid y coordinates + BN254.G1Point[3] memory invalidPoints = [ + BN254.G1Point(1, 3), // y should be 2 + BN254.G1Point(2, 100), // arbitrary invalid y + BN254.G1Point(3, 1000) // arbitrary invalid y + ]; + + for (uint256 i = 0; i < invalidPoints.length; i++) { + assertFalse( + invalidPoints[i].isOnCurve(), + string( + abi.encodePacked( + "Invalid point ", vm.toString(i), " should not be on the curve" + ) + ) + ); + } + } + + /** + * @notice Test points with coordinates at the field modulus boundary + */ + function test_isOnCurve_boundaryPoints() public pure { + // Test point with x = FP_MODULUS - 1 + uint256 xMax = FP_MODULUS - 1; + uint256 ySquared = mulmod(xMax, xMax, FP_MODULUS); + ySquared = mulmod(ySquared, xMax, FP_MODULUS); + ySquared = addmod(ySquared, 3, FP_MODULUS); + + // This x value doesn't have a valid y on the curve, so any y should return false + BN254.G1Point memory boundaryPoint = BN254.G1Point(xMax, 0); + assertFalse( + boundaryPoint.isOnCurve(), "Point with x at modulus boundary should not be on curve" + ); + + // Test point with coordinates >= FP_MODULUS (should be reduced modulo FP_MODULUS) + BN254.G1Point memory overflowPoint = BN254.G1Point( + FP_MODULUS + 1, // This should be reduced to 1 + FP_MODULUS + 2 // This should be reduced to 2 + ); + assertTrue(overflowPoint.isOnCurve(), "Overflow point should be reduced and be on curve"); + } + + /** + * @notice Fuzz test with random points + */ + function testFuzz_isOnCurve_randomPoints(uint256 x, uint256 y) public pure { + BN254.G1Point memory point = BN254.G1Point(x, y); + + // Calculate expected result + uint256 xMod = x % FP_MODULUS; + uint256 yMod = y % FP_MODULUS; + uint256 y2 = mulmod(yMod, yMod, FP_MODULUS); + uint256 x3 = mulmod(xMod, xMod, FP_MODULUS); + x3 = mulmod(x3, xMod, FP_MODULUS); + uint256 rhs = addmod(x3, 3, FP_MODULUS); + + bool expectedOnCurve = (y2 == rhs); + bool actualOnCurve = point.isOnCurve(); + + assertEq(actualOnCurve, expectedOnCurve, "isOnCurve result mismatch for random point"); + } + + /** + * @notice Test negation of valid points + */ + function test_isOnCurve_negatedPoints() public pure { + BN254.G1Point memory generator = BN254.generatorG1(); + BN254.G1Point memory negatedGenerator = generator.negate(); + + assertTrue(negatedGenerator.isOnCurve(), "Negated generator should be on the curve"); + assertEq(negatedGenerator.X, 1, "Negated generator X should be 1"); + assertEq(negatedGenerator.Y, FP_MODULUS - 2, "Negated generator Y should be p - 2"); + } + + /** + * @notice Test scalar multiplication results are on curve + */ + function test_isOnCurve_scalarMultiplication() public view { + BN254.G1Point memory generator = BN254.generatorG1(); + + // Test small scalar multiplications using scalar_mul_tiny + for (uint16 i = 1; i < 10; i++) { + BN254.G1Point memory multiplied = generator.scalar_mul_tiny(i); + assertTrue( + multiplied.isOnCurve(), + string(abi.encodePacked("Generator * ", vm.toString(i), " should be on the curve")) + ); + } + } + + /** + * @notice Test addition results are on curve + * @dev Skip tests that require precompiles if they're not available + */ + function test_isOnCurve_pointAddition() public view { + BN254.G1Point memory generator = BN254.generatorG1(); + + // Test doubling using scalar multiplication instead of addition + // (since addition might fail without precompiles) + BN254.G1Point memory doubled = generator.scalar_mul_tiny(2); + assertTrue(doubled.isOnCurve(), "Doubled point should be on the curve"); + + // Test that the doubled point is different from the generator + assertTrue( + doubled.X != generator.X || doubled.Y != generator.Y, + "Doubled point should be different" + ); + } + + /** + * @notice Test edge case where y^2 calculation could overflow + */ + function test_isOnCurve_largeCoordinates() public pure { + // Test with very large coordinates (close to modulus) + uint256 largeX = FP_MODULUS - 10; + uint256 largeY = FP_MODULUS - 20; + + BN254.G1Point memory largePoint = BN254.G1Point(largeX, largeY); + + // Calculate expected result + uint256 y2 = mulmod(largeY, largeY, FP_MODULUS); + uint256 x3 = mulmod(largeX, largeX, FP_MODULUS); + x3 = mulmod(x3, largeX, FP_MODULUS); + uint256 rhs = addmod(x3, 3, FP_MODULUS); + + bool expectedOnCurve = (y2 == rhs); + assertEq(largePoint.isOnCurve(), expectedOnCurve, "Large coordinate point check failed"); + } + + /** + * @notice Test batch of known invalid x coordinates + */ + function test_isOnCurve_invalidXCoordinates() public pure { + // Some x values that don't have valid y coordinates on the curve + uint256[5] memory invalidXs = [uint256(4), uint256(5), uint256(7), uint256(8), uint256(10)]; + + for (uint256 i = 0; i < invalidXs.length; i++) { + // Try with y = 0 and y = 1 + BN254.G1Point memory point1 = BN254.G1Point(invalidXs[i], 0); + BN254.G1Point memory point2 = BN254.G1Point(invalidXs[i], 1); + + // These x values don't have valid y coordinates, so both should be false + // (unless by chance one of these y values happens to be valid) + uint256 x3 = mulmod(invalidXs[i], invalidXs[i], FP_MODULUS); + x3 = mulmod(x3, invalidXs[i], FP_MODULUS); + uint256 rhs = addmod(x3, 3, FP_MODULUS); + + if (mulmod(0, 0, FP_MODULUS) != rhs) { + assertFalse( + point1.isOnCurve(), "Point with invalid x and y=0 should not be on curve" + ); + } + if (mulmod(1, 1, FP_MODULUS) != rhs) { + assertFalse( + point2.isOnCurve(), "Point with invalid x and y=1 should not be on curve" + ); + } + } + } + + /** + * @notice Gas usage test for isOnCurve + */ + function test_isOnCurve_gasUsage() public { + BN254.G1Point memory generator = BN254.generatorG1(); + + uint256 gasBefore = gasleft(); + bool result = generator.isOnCurve(); + uint256 gasAfter = gasleft(); + + assertTrue(result, "Generator should be on curve"); + emit log_named_uint("Gas used for isOnCurve", gasBefore - gasAfter); + + // Test with multiple calls to see consistency + gasBefore = gasleft(); + for (uint256 i = 0; i < 10; i++) { + generator.isOnCurve(); + } + gasAfter = gasleft(); + emit log_named_uint("Gas used for 10 isOnCurve calls", gasBefore - gasAfter); + } + + /** + * @notice Test specific known points on the curve + */ + function test_isOnCurve_specificPoints() public pure { + // Test some specific points with known y values + TestPoint[4] memory testPoints; + // Generator + testPoints[0].x = 1; + testPoints[0].y = 2; + testPoints[0].shouldBeOnCurve = true; + + // Point with x=1, wrong y + testPoints[1].x = 1; + testPoints[1].y = 3; + testPoints[1].shouldBeOnCurve = false; + + // Another valid point + testPoints[2].x = + 9727523064272218541460723335320998459488975639302513747055235660443850046724; + testPoints[2].y = + 5031696974169251245229961296941447383441169981934237515842977230762345915487; + testPoints[2].shouldBeOnCurve = true; + + // Invalid point + testPoints[3].x = 2; + testPoints[3].y = 2; + testPoints[3].shouldBeOnCurve = false; + + for (uint256 i = 0; i < testPoints.length; i++) { + BN254.G1Point memory point = BN254.G1Point(testPoints[i].x, testPoints[i].y); + assertEq( + point.isOnCurve(), + testPoints[i].shouldBeOnCurve, + string(abi.encodePacked("Point ", vm.toString(i), " on-curve check failed")) + ); + } + } + + /** + * + * Tests for Comparators library + * + */ + function test_comparators_lt() public { + assertTrue(harness.lt(1, 2), "1 < 2 should be true"); + assertFalse(harness.lt(2, 1), "2 < 1 should be false"); + assertFalse(harness.lt(1, 1), "1 < 1 should be false"); + } + + function test_comparators_gt() public { + assertTrue(harness.gt(2, 1), "2 > 1 should be true"); + assertFalse(harness.gt(1, 2), "1 > 2 should be false"); + assertFalse(harness.gt(1, 1), "1 > 1 should be false"); + } + + function testFuzz_comparators(uint256 a, uint256 b) public { + bool ltResult = harness.lt(a, b); + bool gtResult = harness.gt(a, b); + + if (a < b) { + assertTrue(ltResult, "lt should return true when a < b"); + assertFalse(gtResult, "gt should return false when a < b"); + } else if (a > b) { + assertFalse(ltResult, "lt should return false when a > b"); + assertTrue(gtResult, "gt should return true when a > b"); + } else { + assertFalse(ltResult, "lt should return false when a == b"); + assertFalse(gtResult, "gt should return false when a == b"); + } + } + + /** + * + * Tests for SlotDerivation library + * + */ + function test_erc7201Slot() public { + string memory namespace = "example.namespace"; + bytes32 slot = harness.erc7201Slot(namespace); + + // ERC-7201 formula: keccak256(keccak256(namespace) - 1) & ~bytes32(uint256(0xff)) + bytes32 expectedSlot = keccak256(abi.encode(uint256(keccak256(bytes(namespace))) - 1)) + & ~bytes32(uint256(0xff)); + assertEq(slot, expectedSlot, "ERC-7201 slot calculation mismatch"); + } + + function test_offset() public { + bytes32 baseSlot = bytes32(uint256(100)); + uint256 offset = 5; + bytes32 resultSlot = harness.offset(baseSlot, offset); + + assertEq(uint256(resultSlot), 105, "Offset calculation incorrect"); + } + + function test_deriveArray() public { + bytes32 slot = bytes32(uint256(123)); + bytes32 derivedSlot = harness.deriveArray(slot); + + // Array elements start at keccak256(slot) + bytes32 expectedSlot = keccak256(abi.encode(slot)); + assertEq(derivedSlot, expectedSlot, "Array slot derivation incorrect"); + } + + function test_deriveMapping() public { + bytes32 slot = bytes32(uint256(456)); + + // Test with different key types + address addrKey = address(0x1234); + bytes32 mappingSlotAddr = harness.deriveMappingAddress(slot, addrKey); + assertEq( + mappingSlotAddr, keccak256(abi.encode(addrKey, slot)), "Address mapping slot incorrect" + ); + + uint256 uintKey = 789; + bytes32 mappingSlotUint = harness.deriveMappingUint256(slot, uintKey); + assertEq( + mappingSlotUint, keccak256(abi.encode(uintKey, slot)), "Uint256 mapping slot incorrect" + ); + + bool boolKey = true; + bytes32 mappingSlotBool = harness.deriveMappingBool(slot, boolKey); + assertEq( + mappingSlotBool, keccak256(abi.encode(boolKey, slot)), "Bool mapping slot incorrect" + ); + } + + /** + * + * Tests for Arrays library - Sorting + * + */ + function test_sortUint256() public { + uint256[] memory unsorted = new uint256[](5); + unsorted[0] = 5; + unsorted[1] = 2; + unsorted[2] = 8; + unsorted[3] = 1; + unsorted[4] = 3; + + uint256[] memory sorted = harness.sortUint256(unsorted); + + assertEq(sorted.length, 5, "Sorted array length should be 5"); + assertEq(sorted[0], 1, "First element should be 1"); + assertEq(sorted[1], 2, "Second element should be 2"); + assertEq(sorted[2], 3, "Third element should be 3"); + assertEq(sorted[3], 5, "Fourth element should be 5"); + assertEq(sorted[4], 8, "Fifth element should be 8"); + } + + function test_sortAddress() public { + address[] memory unsorted = new address[](3); + unsorted[0] = address(0x3000); + unsorted[1] = address(0x1000); + unsorted[2] = address(0x2000); + + address[] memory sorted = harness.sortAddress(unsorted); + + assertEq(sorted[0], address(0x1000), "First address should be 0x1000"); + assertEq(sorted[1], address(0x2000), "Second address should be 0x2000"); + assertEq(sorted[2], address(0x3000), "Third address should be 0x3000"); + } + + function test_sortBytes32() public { + bytes32[] memory unsorted = new bytes32[](3); + unsorted[0] = bytes32(uint256(300)); + unsorted[1] = bytes32(uint256(100)); + unsorted[2] = bytes32(uint256(200)); + + bytes32[] memory sorted = harness.sortBytes32(unsorted); + + assertEq(uint256(sorted[0]), 100, "First element should be 100"); + assertEq(uint256(sorted[1]), 200, "Second element should be 200"); + assertEq(uint256(sorted[2]), 300, "Third element should be 300"); + } + + /** + * + * Tests for Arrays library - Binary Search + * + */ + function test_binarySearch() public { + // Initialize a sorted array + uint256[] memory sortedArray = new uint256[](5); + sortedArray[0] = 10; + sortedArray[1] = 20; + sortedArray[2] = 30; + sortedArray[3] = 40; + sortedArray[4] = 50; + + harness.initializeUint256Array(sortedArray); + + // Test findUpperBound + assertEq(harness.findUpperBound(25), 2, "findUpperBound(25) should return 2"); + assertEq(harness.findUpperBound(30), 2, "findUpperBound(30) should return 2"); + assertEq(harness.findUpperBound(5), 0, "findUpperBound(5) should return 0"); + assertEq(harness.findUpperBound(55), 5, "findUpperBound(55) should return 5"); + + // Test lowerBound + assertEq(harness.lowerBound(25), 2, "lowerBound(25) should return 2"); + assertEq(harness.lowerBound(30), 2, "lowerBound(30) should return 2"); + assertEq(harness.lowerBound(5), 0, "lowerBound(5) should return 0"); + + // Test upperBound + assertEq(harness.upperBound(25), 2, "upperBound(25) should return 2"); + assertEq(harness.upperBound(30), 3, "upperBound(30) should return 3"); + assertEq(harness.upperBound(5), 0, "upperBound(5) should return 0"); + } + + function test_binarySearchMemory() public { + uint256[] memory sortedArray = new uint256[](4); + sortedArray[0] = 5; + sortedArray[1] = 15; + sortedArray[2] = 25; + sortedArray[3] = 35; + + assertEq( + harness.lowerBoundMemory(sortedArray, 20), 2, "lowerBoundMemory(20) should return 2" + ); + assertEq( + harness.upperBoundMemory(sortedArray, 20), 2, "upperBoundMemory(20) should return 2" + ); + assertEq( + harness.lowerBoundMemory(sortedArray, 15), 1, "lowerBoundMemory(15) should return 1" + ); + assertEq( + harness.upperBoundMemory(sortedArray, 15), 2, "upperBoundMemory(15) should return 2" + ); + } + + /** + * + * Tests for Arrays library - Unsafe Access + * + */ + function test_unsafeAccess() public { + // Test uint256 array + uint256[] memory uintArray = new uint256[](3); + uintArray[0] = 100; + uintArray[1] = 200; + uintArray[2] = 300; + harness.initializeUint256Array(uintArray); + + assertEq(harness.unsafeAccessUint256(0), 100, "Unsafe access at index 0 should return 100"); + assertEq(harness.unsafeAccessUint256(1), 200, "Unsafe access at index 1 should return 200"); + assertEq(harness.unsafeAccessUint256(2), 300, "Unsafe access at index 2 should return 300"); + + // Test address array + address[] memory addrArray = new address[](2); + addrArray[0] = address(0x1234); + addrArray[1] = address(0x5678); + harness.initializeAddressArray(addrArray); + + assertEq( + harness.unsafeAccessAddress(0), + address(0x1234), + "Unsafe access should return correct address" + ); + + // Test bytes32 array + bytes32[] memory bytes32Array = new bytes32[](2); + bytes32Array[0] = bytes32(uint256(111)); + bytes32Array[1] = bytes32(uint256(222)); + harness.initializeBytes32Array(bytes32Array); + + assertEq( + uint256(harness.unsafeAccessBytes32(0)), + 111, + "Unsafe access should return correct bytes32" + ); + } + + function test_unsafeMemoryAccess() public { + // Test uint256 memory array + uint256[] memory array = new uint256[](3); + array[0] = 10; + array[1] = 20; + array[2] = 30; + + assertEq( + harness.unsafeMemoryAccessUint256(array, 0), 10, "Memory access at 0 should return 10" + ); + assertEq( + harness.unsafeMemoryAccessUint256(array, 1), 20, "Memory access at 1 should return 20" + ); + assertEq( + harness.unsafeMemoryAccessUint256(array, 2), 30, "Memory access at 2 should return 30" + ); + + // Test address memory array + address[] memory addrArray = new address[](2); + addrArray[0] = address(0xABCD); + addrArray[1] = address(0xDEAD); + + assertEq( + harness.unsafeMemoryAccessAddress(addrArray, 0), + address(0xABCD), + "Should return first address" + ); + assertEq( + harness.unsafeMemoryAccessAddress(addrArray, 1), + address(0xDEAD), + "Should return second address" + ); + } + + /** + * + * Tests for Arrays library - Unsafe Set Length + * + */ + function test_unsafeSetLength() public { + // Initialize array with some values + uint256[] memory initialArray = new uint256[](3); + initialArray[0] = 10; + initialArray[1] = 20; + initialArray[2] = 30; + harness.initializeUint256Array(initialArray); + + assertEq(harness.getUint256ArrayLength(), 3, "Initial length should be 3"); + + // Increase length + harness.unsafeSetLengthUint256(5); + assertEq(harness.getUint256ArrayLength(), 5, "Length should be 5 after increase"); + + // Values should still be accessible + assertEq(harness.unsafeAccessUint256(0), 10, "First value should still be 10"); + assertEq(harness.unsafeAccessUint256(1), 20, "Second value should still be 20"); + + // Decrease length + harness.unsafeSetLengthUint256(2); + assertEq(harness.getUint256ArrayLength(), 2, "Length should be 2 after decrease"); + + // Note: The third element is not cleared, just length is changed + // This is the "unsafe" aspect - data may still exist beyond the new length + } + + function test_unsafeSetLength_allTypes() public { + // Test with address array + address[] memory addrArray = new address[](2); + addrArray[0] = address(0x1); + addrArray[1] = address(0x2); + harness.initializeAddressArray(addrArray); + + harness.unsafeSetLengthAddress(4); + assertEq(harness.getAddressArrayLength(), 4, "Address array length should be 4"); + + // Test with bytes32 array + bytes32[] memory b32Array = new bytes32[](1); + b32Array[0] = bytes32(uint256(123)); + harness.initializeBytes32Array(b32Array); + + harness.unsafeSetLengthBytes32(3); + assertEq(harness.getBytes32ArrayLength(), 3, "Bytes32 array length should be 3"); + } +} diff --git a/test/unit/ECDSAServiceManager.t.sol b/test/unit/ECDSAServiceManager.t.sol index 31b2c0a36..7d86cc0c6 100644 --- a/test/unit/ECDSAServiceManager.t.sol +++ b/test/unit/ECDSAServiceManager.t.sol @@ -18,6 +18,8 @@ import {ECDSAServiceManagerMock} from "../mocks/ECDSAServiceManagerMock.sol"; import {ECDSAStakeRegistryMock} from "../mocks/ECDSAStakeRegistryMock.sol"; import {IECDSAStakeRegistryTypes} from "../../src/interfaces/IECDSAStakeRegistry.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + contract MockDelegationManager { function operatorShares(address, address) external pure returns (uint256) { return 1000; // Return a dummy value for simplicity @@ -218,3 +220,253 @@ contract ECDSAServiceManagerSetup is Test { serviceManager.setAVSRegistrar(IAVSRegistrar(registrar)); } } + +contract ECDSAServiceManagerAccessControlTests is ECDSAServiceManagerSetup { + function test_RevertWhen_NotOwner_UpdateAVSMetadataURI() public { + address notOwner = address(0x456); + string memory newURI = "https://new-metadata-uri.com"; + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + serviceManager.updateAVSMetadataURI(newURI); + } + + function test_RevertWhen_NotStakeRegistry_RegisterOperatorToAVS() public { + address notStakeRegistry = address(0x456); + ISignatureUtilsMixinTypes.SignatureWithSaltAndExpiry memory signature; + + vm.prank(notStakeRegistry); + vm.expectRevert(abi.encodeWithSignature("OnlyStakeRegistry()")); + serviceManager.registerOperatorToAVS(operator1, signature); + } + + function test_RevertWhen_NotStakeRegistry_DeregisterOperatorFromAVS() public { + address notStakeRegistry = address(0x456); + + vm.prank(notStakeRegistry); + vm.expectRevert(abi.encodeWithSignature("OnlyStakeRegistry()")); + serviceManager.deregisterOperatorFromAVS(operator1); + } + + function test_RevertWhen_NotRewardsInitiator_CreateAVSRewardsSubmission() public { + address notRewardsInitiator = address(0x456); + IRewardsCoordinator.RewardsSubmission[] memory submissions; + + vm.prank(notRewardsInitiator); + vm.expectRevert(abi.encodeWithSignature("OnlyRewardsInitiator()")); + serviceManager.createAVSRewardsSubmission(submissions); + } + + function test_RevertWhen_NotRewardsInitiator_CreateOperatorDirectedAVSRewardsSubmission() + public + { + address notRewardsInitiator = address(0x456); + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] memory submissions; + + vm.prank(notRewardsInitiator); + vm.expectRevert(abi.encodeWithSignature("OnlyRewardsInitiator()")); + serviceManager.createOperatorDirectedAVSRewardsSubmission(submissions); + } + + function test_RevertWhen_NotOwner_SetClaimerFor() public { + address notOwner = address(0x456); + address claimer = address(0x789); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + serviceManager.setClaimerFor(claimer); + } + + function test_RevertWhen_NotOwner_SetRewardsInitiator() public { + address notOwner = address(0x456); + address newInitiator = address(0x789); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + serviceManager.setRewardsInitiator(newInitiator); + } + + function test_RevertWhen_NotOwner_SetAVSRegistrar() public { + address notOwner = address(0x456); + address registrar = address(0x789); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + serviceManager.setAVSRegistrar(IAVSRegistrar(registrar)); + } +} + +contract ECDSAServiceManagerEventsTests is ECDSAServiceManagerSetup { + event RewardsInitiatorUpdated(address prevRewardsInitiator, address newRewardsInitiator); + + function test_SetRewardsInitiator_EmitsEvent() public { + address currentInitiator = serviceManager.rewardsInitiator(); + address newInitiator = address(0x888); + + vm.expectEmit(true, true, false, true); + emit RewardsInitiatorUpdated(currentInitiator, newInitiator); + + vm.prank(serviceManager.owner()); + serviceManager.setRewardsInitiator(newInitiator); + + assertEq(serviceManager.rewardsInitiator(), newInitiator, "Rewards initiator not updated"); + } +} + +contract ECDSAServiceManagerIntegrationTests is ECDSAServiceManagerSetup { + function test_GetRestakeableStrategies_AfterQuorumUpdate() public { + // Update quorum with new strategies + IStrategy newStrategy1 = IStrategy(address(0x1111)); + IStrategy newStrategy2 = IStrategy(address(0x2222)); + + IECDSAStakeRegistryTypes.Quorum memory newQuorum = IECDSAStakeRegistryTypes.Quorum({ + strategies: new IECDSAStakeRegistryTypes.StrategyParams[](2) + }); + newQuorum.strategies[0] = + IECDSAStakeRegistryTypes.StrategyParams({strategy: newStrategy1, multiplier: 6000}); + newQuorum.strategies[1] = + IECDSAStakeRegistryTypes.StrategyParams({strategy: newStrategy2, multiplier: 4000}); + + address[] memory operators = new address[](0); + vm.prank(mockStakeRegistry.owner()); + mockStakeRegistry.updateQuorumConfig(newQuorum, operators); + + // Check restakeable strategies + address[] memory strategies = serviceManager.getRestakeableStrategies(); + assertEq(strategies.length, 2, "Should have 2 strategies"); + assertEq(strategies[0], address(newStrategy1), "First strategy mismatch"); + assertEq(strategies[1], address(newStrategy2), "Second strategy mismatch"); + } + + function test_GetOperatorRestakedStrategies_AllStrategiesWithShares() public { + // Mock all strategies to have shares + IStrategy[] memory strategies = new IStrategy[](2); + strategies[0] = IStrategy(address(420)); + strategies[1] = IStrategy(address(421)); + + uint256[] memory shares = new uint256[](2); + shares[0] = 1000; + shares[1] = 2000; + + vm.mockCall( + address(mockDelegationManager), + abi.encodeCall(IDelegationManager.getOperatorShares, (operator1, strategies)), + abi.encode(shares) + ); + + address[] memory restakedStrategies = + serviceManager.getOperatorRestakedStrategies(operator1); + assertEq(restakedStrategies.length, 2, "Should have 2 restaked strategies"); + assertEq(restakedStrategies[0], address(strategies[0]), "First strategy mismatch"); + assertEq(restakedStrategies[1], address(strategies[1]), "Second strategy mismatch"); + } + + function test_GetOperatorRestakedStrategies_NoStrategiesWithShares() public { + // Mock all strategies to have zero shares + IStrategy[] memory strategies = new IStrategy[](2); + strategies[0] = IStrategy(address(420)); + strategies[1] = IStrategy(address(421)); + + uint256[] memory shares = new uint256[](2); + shares[0] = 0; + shares[1] = 0; + + vm.mockCall( + address(mockDelegationManager), + abi.encodeCall(IDelegationManager.getOperatorShares, (operator1, strategies)), + abi.encode(shares) + ); + + address[] memory restakedStrategies = + serviceManager.getOperatorRestakedStrategies(operator1); + assertEq(restakedStrategies.length, 0, "Should have no restaked strategies"); + } + + function test_SetAVSRegistrar_CallsAllocationManager() public { + address newRegistrar = address(0xABC); + + // Expect call to allocation manager + vm.expectCall( + address(mockAllocationManager), + abi.encodeCall( + MockAllocationManager.setAVSRegistrar, (address(serviceManager), newRegistrar) + ) + ); + + vm.prank(serviceManager.owner()); + serviceManager.setAVSRegistrar(IAVSRegistrar(newRegistrar)); + } + + function test_UpdateAVSMetadataURI_CallsAVSDirectory() public { + string memory newURI = "https://new-metadata-uri.com"; + + // Expect call to AVS directory + vm.expectCall( + address(mockAVSDirectory), + abi.encodeCall(MockAVSDirectory.updateAVSMetadataURI, (newURI)) + ); + + vm.prank(serviceManager.owner()); + serviceManager.updateAVSMetadataURI(newURI); + } + + function test_RegisterOperatorToAVS_CallsAVSDirectory() public { + ISignatureUtilsMixinTypes.SignatureWithSaltAndExpiry memory signature; + + // Expect call to AVS directory + vm.expectCall( + address(mockAVSDirectory), + abi.encodeCall(MockAVSDirectory.registerOperatorToAVS, (operator1, signature)) + ); + + vm.prank(address(mockStakeRegistry)); + serviceManager.registerOperatorToAVS(operator1, signature); + } + + function test_DeregisterOperatorFromAVS_CallsAVSDirectory() public { + // Expect call to AVS directory + vm.expectCall( + address(mockAVSDirectory), + abi.encodeCall(MockAVSDirectory.deregisterOperatorFromAVS, (operator1)) + ); + + vm.prank(address(mockStakeRegistry)); + serviceManager.deregisterOperatorFromAVS(operator1); + } + + function test_SetClaimerFor_CallsRewardsCoordinator() public { + address claimer = address(0x123); + + // Expect call to rewards coordinator + vm.expectCall( + address(mockRewardsCoordinator), + abi.encodeCall(MockRewardsCoordinator.setClaimerFor, (claimer)) + ); + + vm.prank(serviceManager.owner()); + serviceManager.setClaimerFor(claimer); + } +} + +contract ECDSAServiceManagerFuzzTests is ECDSAServiceManagerSetup { + function testFuzz_SetRewardsInitiator( + address newInitiator + ) public { + vm.assume(newInitiator != address(0)); + + vm.prank(serviceManager.owner()); + serviceManager.setRewardsInitiator(newInitiator); + + assertEq( + serviceManager.rewardsInitiator(), newInitiator, "Rewards initiator not set correctly" + ); + } + + function testFuzz_UpdateAVSMetadataURI( + string memory newURI + ) public { + vm.prank(serviceManager.owner()); + serviceManager.updateAVSMetadataURI(newURI); + // Test passes if no revert + } +} diff --git a/test/unit/ECDSAStakeRegistryUnit.t.sol b/test/unit/ECDSAStakeRegistryUnit.t.sol index 78e7de74a..0d9fbf1c6 100644 --- a/test/unit/ECDSAStakeRegistryUnit.t.sol +++ b/test/unit/ECDSAStakeRegistryUnit.t.sol @@ -1117,3 +1117,293 @@ contract ECDSAStakeRegistryEventsTest is ECDSAStakeRegistrySetup { assertEq(registry.getLastCheckpointTotalWeight(), expectedNewTotal); } } + +contract ECDSAStakeRegistryAdditionalTests is ECDSAStakeRegistrySetup { + function test_GetOperatorWeight_BelowMinimumWeight() public { + // Set minimum weight + uint256 minWeight = 2000; + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + registry.updateMinimumWeight(minWeight, operators); + + // Mock operator to have weight below minimum + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = registry.quorum().strategies[0].strategy; + uint256[] memory shares = new uint256[](1); + shares[0] = 1500; // Below minimum weight + + vm.mockCall( + address(mockDelegationManager), + abi.encodeWithSelector( + MockDelegationManager.getOperatorShares.selector, operator1, strategies + ), + abi.encode(shares) + ); + + uint256 weight = registry.getOperatorWeight(operator1); + assertEq(weight, 0, "Weight should be 0 when below minimum"); + } + + function test_UpdateOperatorsForQuorum_RevertWhenNotAllOperators() public { + address[] memory operators = new address[](1); // Only 1 operator when 2 are registered + operators[0] = operator1; + + address[][] memory operatorsPerQuorum = new address[][](1); + operatorsPerQuorum[0] = operators; + + vm.expectRevert(IECDSAStakeRegistryErrors.MustUpdateAllOperators.selector); + registry.updateOperatorsForQuorum(operatorsPerQuorum, ""); + } + + function test_UpdateOperatorsForQuorum_Success() public { + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + address[][] memory operatorsPerQuorum = new address[][](1); + operatorsPerQuorum[0] = operators; + + registry.updateOperatorsForQuorum(operatorsPerQuorum, ""); + } + + function test_GetOperatorWeightAtBlock_HistoricalLookup() public { + uint32 checkpointBlock = uint32(block.number); + + // Update operator weight + address[] memory operators = new address[](1); + operators[0] = operator1; + registry.updateOperators(operators); + + vm.roll(block.number + 10); + + // Get weight at historical block + uint256 historicalWeight = registry.getOperatorWeightAtBlock(operator1, checkpointBlock); + assertEq(historicalWeight, 1000, "Historical weight should match"); + } + + function test_GetLastCheckpointTotalWeightAtBlock() public { + uint32 checkpointBlock = uint32(block.number); + uint256 totalWeight = registry.getLastCheckpointTotalWeight(); + + vm.roll(block.number + 10); + + // Register new operator + address operator3 = address(0x789); + ISignatureUtilsMixinTypes.SignatureWithSaltAndExpiry memory sig; + vm.prank(operator3); + registry.registerOperatorWithSignature(sig, operator3); + + // Check historical total weight + uint256 historicalTotal = registry.getLastCheckpointTotalWeightAtBlock(checkpointBlock); + assertEq(historicalTotal, totalWeight, "Historical total weight should match"); + + // Current total should be different + uint256 currentTotal = registry.getLastCheckpointTotalWeight(); + assertTrue(currentTotal > historicalTotal, "Current total should be greater"); + } + + function test_GetLastCheckpointThresholdWeightAtBlock() public { + uint256 initialThreshold = registry.getLastCheckpointThresholdWeight(); + uint32 checkpointBlock = uint32(block.number); + + vm.roll(block.number + 5); + + // Update threshold + uint256 newThreshold = 200; + vm.prank(registry.owner()); + registry.updateStakeThreshold(newThreshold); + + vm.roll(block.number + 5); + + // Check historical threshold + uint256 historicalThreshold = + registry.getLastCheckpointThresholdWeightAtBlock(checkpointBlock); + assertEq(historicalThreshold, initialThreshold, "Historical threshold should match initial"); + + // Current threshold should be different + uint256 currentThreshold = registry.getLastCheckpointThresholdWeight(); + assertEq(currentThreshold, newThreshold, "Current threshold should be updated"); + } + + function test_UpdateOperatorSigningKey_SameKey() public { + address currentKey = registry.getLatestOperatorSigningKey(operator1); + + // Update to same key - should not emit event + vm.prank(operator1); + registry.updateOperatorSigningKey(currentKey); + + // Key should remain the same + assertEq( + registry.getLatestOperatorSigningKey(operator1), currentKey, "Key should not change" + ); + } + + function test_UpdateOperatorSigningKey_NotRegistered() public { + address unregisteredOperator = address(0xBEEF); + + vm.prank(unregisteredOperator); + vm.expectRevert(IECDSAStakeRegistryErrors.OperatorNotRegistered.selector); + registry.updateOperatorSigningKey(address(0x123)); + } + + function test_GetOperatorSigningKeyAtBlock_InvalidBlock() public { + // The error is from the Checkpoints library, not InvalidReferenceBlock + vm.expectRevert("Checkpoints: block not yet mined"); + registry.getOperatorSigningKeyAtBlock(operator1, block.number); + } + + function test_UpdateOperators_WithDeregisteredOperator() public { + // Deregister operator1 + vm.prank(operator1); + registry.deregisterOperator(); + + // Update operators including deregistered one + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + registry.updateOperators(operators); + + // Deregistered operator should have 0 weight + assertEq( + registry.getLastCheckpointOperatorWeight(operator1), + 0, + "Deregistered operator should have 0 weight" + ); + } + + function test_MultipleStrategyWeightCalculation() public { + // Setup multiple strategies with different multipliers + IStrategy mockStrategy1 = IStrategy(address(0x1001)); + IStrategy mockStrategy2 = IStrategy(address(0x1002)); + IStrategy mockStrategy3 = IStrategy(address(0x1003)); + + IECDSAStakeRegistryTypes.Quorum memory newQuorum = IECDSAStakeRegistryTypes.Quorum({ + strategies: new IECDSAStakeRegistryTypes.StrategyParams[](3) + }); + newQuorum.strategies[0] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: mockStrategy1, + multiplier: 5000 // 50% + }); + newQuorum.strategies[1] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: mockStrategy2, + multiplier: 3000 // 30% + }); + newQuorum.strategies[2] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: mockStrategy3, + multiplier: 2000 // 20% + }); + + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + registry.updateQuorumConfig(newQuorum, operators); + + // Mock different shares for each strategy + IStrategy[] memory strategies = new IStrategy[](3); + strategies[0] = mockStrategy1; + strategies[1] = mockStrategy2; + strategies[2] = mockStrategy3; + + uint256[] memory shares = new uint256[](3); + shares[0] = 1000; // 1000 shares in strategy 1 + shares[1] = 2000; // 2000 shares in strategy 2 + shares[2] = 3000; // 3000 shares in strategy 3 + + vm.mockCall( + address(mockDelegationManager), + abi.encodeWithSelector( + MockDelegationManager.getOperatorShares.selector, operator1, strategies + ), + abi.encode(shares) + ); + + // Calculate expected weight: (1000 * 5000 + 2000 * 3000 + 3000 * 2000) / 10000 = 1700 + uint256 weight = registry.getOperatorWeight(operator1); + assertEq(weight, 1700, "Weight calculation with multiple strategies incorrect"); + } + + function test_IERC1271_isValidSignature_ReturnsCorrectSelector() public { + msgHash = keccak256("data"); + signers = new address[](1); + signers[0] = operator1; + signatures = new bytes[](1); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(operator1Pk, msgHash); + signatures[0] = abi.encodePacked(r, s, v); + + bytes4 result = + registry.isValidSignature(msgHash, abi.encode(signers, signatures, block.number - 1)); + assertEq(result, bytes4(0x1626ba7e), "Should return EIP-1271 magic value"); + } +} + +contract ECDSAStakeRegistryFuzzTests is ECDSAStakeRegistrySetup { + function testFuzz_UpdateMinimumWeight( + uint256 newMinWeight + ) public { + newMinWeight = bound(newMinWeight, 0, 1e18); + + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + uint256 oldMinWeight = registry.minimumWeight(); + + vm.expectEmit(true, true, true, true); + emit MinimumWeightUpdated(oldMinWeight, newMinWeight); + + registry.updateMinimumWeight(newMinWeight, operators); + assertEq(registry.minimumWeight(), newMinWeight, "Minimum weight not updated correctly"); + } + + function testFuzz_UpdateStakeThreshold( + uint256 threshold + ) public { + threshold = bound(threshold, 1, 1e18); + + vm.expectEmit(true, true, true, true); + emit ThresholdWeightUpdated(threshold); + + vm.prank(registry.owner()); + registry.updateStakeThreshold(threshold); + + assertEq( + registry.getLastCheckpointThresholdWeight(), + threshold, + "Threshold not updated correctly" + ); + } + + function testFuzz_RegisterDeregisterOperator(address operator, uint256 operatorPk) public { + vm.assume(operator != address(0)); + vm.assume(operatorPk != 0); + vm.assume(!registry.operatorRegistered(operator)); + + ISignatureUtilsMixinTypes.SignatureWithSaltAndExpiry memory sig; + + // Register + vm.prank(operator); + registry.registerOperatorWithSignature(sig, operator); + assertTrue(registry.operatorRegistered(operator), "Operator should be registered"); + + // Deregister + vm.prank(operator); + registry.deregisterOperator(); + assertFalse(registry.operatorRegistered(operator), "Operator should be deregistered"); + } + + function testFuzz_UpdateOperatorSigningKey( + address newKey + ) public { + vm.assume(newKey != address(0)); + vm.assume(newKey != registry.getLatestOperatorSigningKey(operator1)); + + vm.prank(operator1); + registry.updateOperatorSigningKey(newKey); + + assertEq(registry.getLatestOperatorSigningKey(operator1), newKey, "Signing key not updated"); + } +} diff --git a/test/unit/middlewareV2/AVSRegistrarAllowlistUnit.t.sol b/test/unit/middlewareV2/AVSRegistrarAllowlistUnit.t.sol new file mode 100644 index 000000000..c8c94998a --- /dev/null +++ b/test/unit/middlewareV2/AVSRegistrarAllowlistUnit.t.sol @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "./AVSRegistrarBase.t.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import {AVSRegistrarWithAllowlist} from + "src/middlewareV2/registrar/presets/AVSRegistrarWithAllowlist.sol"; +import {IAllowlistErrors, IAllowlistEvents} from "src/interfaces/IAllowlist.sol"; + +contract AVSRegistrarWithAllowlistUnitTests is + AVSRegistrarBase, + IAllowlistErrors, + IAllowlistEvents +{ + AVSRegistrarWithAllowlist public avsRegistrarWithAllowlist; + address public allowlistAdmin = address(this); + + function setUp() public override { + super.setUp(); + + avsRegistrarImplementation = new AVSRegistrarWithAllowlist( + AVS, + IAllocationManager(address(allocationManagerMock)), + IKeyRegistrar(address(keyRegistrarMock)) + ); + + avsRegistrarWithAllowlist = AVSRegistrarWithAllowlist( + address( + new TransparentUpgradeableProxy( + address(avsRegistrarImplementation), + address(proxyAdmin), + abi.encodeWithSelector( + AVSRegistrarWithAllowlist.initialize.selector, address(this) + ) + ) + ) + ); + } + + function _addOperatorToAllowlist(address operator, uint32[] memory operatorSetIds) internal { + for (uint32 i; i < operatorSetIds.length; ++i) { + cheats.prank(allowlistAdmin); + avsRegistrarWithAllowlist.addOperatorToAllowlist( + OperatorSet({avs: AVS, id: operatorSetIds[i]}), operator + ); + } + } +} + +contract AVSRegistrarWithAllowlistUnitTests_initialize is AVSRegistrarWithAllowlistUnitTests { + function test_initialization() public view { + // Check the admin is set + assertEq( + avsRegistrarWithAllowlist.owner(), allowlistAdmin, "Initialization: owner incorrect" + ); + } + + function test_revert_alreadyInitialized() public { + cheats.expectRevert("Initializable: contract is already initialized"); + avsRegistrarWithAllowlist.initialize(allowlistAdmin); + } +} + +contract AVSRegistrarWithAllowlistUnitTests_addOperatorToAllowlist is + AVSRegistrarWithAllowlistUnitTests +{ + using ArrayLib for *; + + function testFuzz_revert_notOwner( + address notOwner + ) public { + cheats.assume(notOwner != allowlistAdmin); + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(notOwner); + avsRegistrarWithAllowlist.addOperatorToAllowlist( + OperatorSet({avs: AVS, id: 0}), defaultOperator + ); + } + + function test_revert_operatorAlreadyInAllowlist() public { + _addOperatorToAllowlist(defaultOperator, defaultOperatorSetId.toArrayU32()); + + cheats.expectRevert(OperatorAlreadyInAllowlist.selector); + cheats.prank(allowlistAdmin); + avsRegistrarWithAllowlist.addOperatorToAllowlist( + OperatorSet({avs: AVS, id: 0}), defaultOperator + ); + } + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random operator set ids + uint32 numOperatorSetIds = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSetIds, 0, type(uint32).max); + + // Add operator to allowlist + for (uint32 i; i < operatorSetIds.length; ++i) { + cheats.expectEmit(true, true, true, true); + emit OperatorAddedToAllowlist( + OperatorSet({avs: AVS, id: operatorSetIds[i]}), defaultOperator + ); + cheats.prank(allowlistAdmin); + avsRegistrarWithAllowlist.addOperatorToAllowlist( + OperatorSet({avs: AVS, id: operatorSetIds[i]}), defaultOperator + ); + } + + // Check the operator is in the allowlist + for (uint32 i; i < operatorSetIds.length; ++i) { + assertTrue( + avsRegistrarWithAllowlist.isOperatorAllowed( + OperatorSet({avs: AVS, id: operatorSetIds[i]}), defaultOperator + ), + "Operator not in allowlist" + ); + } + } +} + +contract AVSRegistrarWithAllowlistUnitTests_removeOperatorFromAllowlist is + AVSRegistrarWithAllowlistUnitTests +{ + using ArrayLib for *; + + function testFuzz_revert_notOwner( + address notOwner + ) public view { + cheats.assume(notOwner != allowlistAdmin); + } + + function test_revert_operatorNotInAllowlist() public { + cheats.expectRevert(OperatorNotInAllowlist.selector); + cheats.prank(allowlistAdmin); + avsRegistrarWithAllowlist.removeOperatorFromAllowlist( + OperatorSet({avs: AVS, id: 0}), defaultOperator + ); + } + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random operator set ids + uint32 numOperatorSetIds = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSetIds, 0, type(uint32).max); + + // Add operator to allowlist + _addOperatorToAllowlist(defaultOperator, operatorSetIds); + + // Remove operator from allowlist + for (uint32 i; i < operatorSetIds.length; ++i) { + cheats.expectEmit(true, true, true, true); + emit OperatorRemovedFromAllowlist( + OperatorSet({avs: AVS, id: operatorSetIds[i]}), defaultOperator + ); + cheats.prank(allowlistAdmin); + avsRegistrarWithAllowlist.removeOperatorFromAllowlist( + OperatorSet({avs: AVS, id: operatorSetIds[i]}), defaultOperator + ); + } + + // Check the operator is not in the allowlist + for (uint32 i; i < operatorSetIds.length; ++i) { + assertFalse( + avsRegistrarWithAllowlist.isOperatorAllowed( + OperatorSet({avs: AVS, id: operatorSetIds[i]}), defaultOperator + ), + "Operator still in allowlist" + ); + } + } +} + +contract AVSRegistrarAllowistUnitTest_getRegisteredOperators is + AVSRegistrarWithAllowlistUnitTests +{ + using ArrayLib for *; + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random addresses + uint32 numAddresses = r.Uint32(1, 50); + address[] memory operators = new address[](numAddresses); + for (uint32 i; i < numAddresses; ++i) { + operators[i] = r.Address(); + } + + // Generate random operator set ids + uint32 numOperatorSetIds = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSetIds, 0, type(uint32).max); + + // Add operators to allowlist + for (uint32 i; i < operators.length; ++i) { + _addOperatorToAllowlist(operators[i], operatorSetIds); + } + + // Get the allowed operators + for (uint32 i; i < operatorSetIds.length; ++i) { + // Note: although ordering is not guaranteed generally, it works here since we do not do any removes. + address[] memory allowedOperators = avsRegistrarWithAllowlist.getAllowedOperators( + OperatorSet({avs: AVS, id: operatorSetIds[i]}) + ); + assertEq( + allowedOperators.length, operators.length, "Incorrect number of allowed operators" + ); + for (uint32 j; j < allowedOperators.length; ++j) { + assertTrue(allowedOperators[j] == operators[j], "Allowed operator incorrect"); + } + } + } +} + +contract AVSRegistrarWithAllowlistUnitTests_registerOperator is + AVSRegistrarWithAllowlistUnitTests +{ + using ArrayLib for *; + + function testFuzz_revert_notAllocationManager( + address notAllocationManager + ) public filterFuzzedAddressInputs(notAllocationManager) { + cheats.assume(notAllocationManager != address(allocationManagerMock)); + + cheats.prank(notAllocationManager); + cheats.expectRevert(NotAllocationManager.selector); + avsRegistrarWithAllowlist.registerOperator( + defaultOperator, AVS, defaultOperatorSetId.toArrayU32(), "0x" + ); + } + + function test_revert_operatorNotInAllowlist() public { + // Register operator + cheats.expectRevert(OperatorNotInAllowlist.selector); + cheats.prank(address(allocationManagerMock)); + avsRegistrarWithAllowlist.registerOperator( + defaultOperator, AVS, defaultOperatorSetId.toArrayU32(), "0x" + ); + } + + function test_revert_keyNotRegistered() public { + // Add operator to allowlist + _addOperatorToAllowlist(defaultOperator, defaultOperatorSetId.toArrayU32()); + + // Register operator + cheats.expectRevert(KeyNotRegistered.selector); + cheats.prank(address(allocationManagerMock)); + avsRegistrarWithAllowlist.registerOperator( + defaultOperator, AVS, defaultOperatorSetId.toArrayU32(), "0x" + ); + } + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random operator set ids & register keys + uint32 numOperatorSetIds = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSetIds, 0, type(uint32).max); + _registerKey(defaultOperator, operatorSetIds); + + // Add operator to allowlist + _addOperatorToAllowlist(defaultOperator, operatorSetIds); + + // Register operator + cheats.expectEmit(true, true, true, true); + emit OperatorRegistered(defaultOperator, operatorSetIds); + cheats.prank(address(allocationManagerMock)); + avsRegistrarWithAllowlist.registerOperator(defaultOperator, AVS, operatorSetIds, "0x"); + } +} + +contract AVSRegistrarWithAllowlistUnitTests_deregisterOperator is + AVSRegistrarWithAllowlistUnitTests +{ + using ArrayLib for *; + + function testFuzz_revert_notAllocationManager( + address notAllocationManager + ) public { + cheats.assume(notAllocationManager != address(allocationManagerMock)); + cheats.assume(notAllocationManager != address(proxyAdmin)); + + cheats.prank(notAllocationManager); + cheats.expectRevert(NotAllocationManager.selector); + avsRegistrarWithAllowlist.deregisterOperator( + defaultOperator, AVS, defaultOperatorSetId.toArrayU32() + ); + } + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random operator set ids + uint32 numOperatorSetIds = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSetIds, 0, type(uint32).max); + + // Deregister operator + cheats.expectEmit(true, true, true, true); + emit OperatorDeregistered(defaultOperator, operatorSetIds); + cheats.prank(address(allocationManagerMock)); + avsRegistrarWithAllowlist.deregisterOperator(defaultOperator, AVS, operatorSetIds); + } +} + +contract AVSRegistrarWithAllowlistUnitTests_ViewFunctions is AVSRegistrarWithAllowlistUnitTests { + function test_supportsAVS_true() public { + // Should return true when checking against the configured AVS + assertTrue( + avsRegistrarWithAllowlist.supportsAVS(AVS), + "supportsAVS: should return true for configured AVS" + ); + } + + function test_supportsAVS_false() public { + // Should return false for any other address + assertFalse( + avsRegistrarWithAllowlist.supportsAVS(address(0)), + "supportsAVS: should return false for zero address" + ); + assertFalse( + avsRegistrarWithAllowlist.supportsAVS(address(1)), + "supportsAVS: should return false for random address" + ); + assertFalse( + avsRegistrarWithAllowlist.supportsAVS(address(avsRegistrarWithAllowlist)), + "supportsAVS: should return false for registrar address" + ); + assertFalse( + avsRegistrarWithAllowlist.supportsAVS(defaultOperator), + "supportsAVS: should return false for operator" + ); + } + + function testFuzz_supportsAVS( + address randomAddress + ) public { + if (randomAddress == AVS) { + assertTrue( + avsRegistrarWithAllowlist.supportsAVS(randomAddress), + "supportsAVS: should return true for configured AVS" + ); + } else { + assertFalse( + avsRegistrarWithAllowlist.supportsAVS(randomAddress), + "supportsAVS: should return false for non-AVS address" + ); + } + } + + function test_getAVS() public { + // Should return the configured AVS address + assertEq( + avsRegistrarWithAllowlist.getAVS(), AVS, "getAVS: should return configured AVS address" + ); + } +} diff --git a/test/unit/middlewareV2/AVSRegistrarAsIdentifierUnit.t.sol b/test/unit/middlewareV2/AVSRegistrarAsIdentifierUnit.t.sol new file mode 100644 index 000000000..046a052fe --- /dev/null +++ b/test/unit/middlewareV2/AVSRegistrarAsIdentifierUnit.t.sol @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "./AVSRegistrarBase.t.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import {IPermissionController} from + "eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol"; +import {PermissionController} from + "eigenlayer-contracts/src/contracts/permissions/PermissionController.sol"; +import {AVSRegistrarAsIdentifier} from + "src/middlewareV2/registrar/presets/AVSRegistrarAsIdentifier.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract AVSRegistrarAsIdentifierUnitTests is AVSRegistrarBase { + AVSRegistrarAsIdentifier public avsRegistrarAsIdentifier; + address public admin = address(0xAD); + string public constant METADATA_URI = "https://example.com/metadata"; + + function setUp() public virtual override { + super.setUp(); + + // Deploy the implementation + avsRegistrarImplementation = new AVSRegistrarAsIdentifier( + AVS, + IAllocationManager(address(allocationManagerMock)), + permissionController, + IKeyRegistrar(address(keyRegistrarMock)) + ); + + // Deploy the proxy + avsRegistrarAsIdentifier = AVSRegistrarAsIdentifier( + address( + new TransparentUpgradeableProxy( + address(avsRegistrarImplementation), address(proxyAdmin), "" + ) + ) + ); + } +} + +contract AVSRegistrarAsIdentifierUnitTests_constructor is AVSRegistrarAsIdentifierUnitTests { + function test_constructor() public { + // Deploy a new implementation to test constructor + AVSRegistrarAsIdentifier impl = new AVSRegistrarAsIdentifier( + AVS, + IAllocationManager(address(allocationManagerMock)), + permissionController, + IKeyRegistrar(address(keyRegistrarMock)) + ); + + // Check that the immutable variables are set correctly + assertEq( + address(impl.permissionController()), + address(permissionController), + "Constructor: permission controller incorrect" + ); + assertEq( + address(impl.allocationManager()), + address(allocationManagerMock), + "Constructor: allocation manager incorrect" + ); + assertEq( + address(impl.keyRegistrar()), + address(keyRegistrarMock), + "Constructor: key registrar incorrect" + ); + } +} + +contract AVSRegistrarAsIdentifierUnitTests_initialize is AVSRegistrarAsIdentifierUnitTests { + function test_initialize() public { + // Mock the allocationManager calls + vm.mockCall( + address(allocationManagerMock), + abi.encodeWithSelector( + IAllocationManager.updateAVSMetadataURI.selector, + address(avsRegistrarAsIdentifier), + METADATA_URI + ), + "" + ); + vm.mockCall( + address(allocationManagerMock), + abi.encodeWithSelector( + IAllocationManager.setAVSRegistrar.selector, + address(avsRegistrarAsIdentifier), + avsRegistrarAsIdentifier + ), + "" + ); + + // Mock the permissionController call + vm.mockCall( + address(permissionController), + abi.encodeWithSelector( + IPermissionController.addPendingAdmin.selector, + address(avsRegistrarAsIdentifier), + admin + ), + "" + ); + + // Expect the calls to be made + vm.expectCall( + address(allocationManagerMock), + abi.encodeWithSelector( + IAllocationManager.updateAVSMetadataURI.selector, + address(avsRegistrarAsIdentifier), + METADATA_URI + ) + ); + vm.expectCall( + address(allocationManagerMock), + abi.encodeWithSelector( + IAllocationManager.setAVSRegistrar.selector, + address(avsRegistrarAsIdentifier), + avsRegistrarAsIdentifier + ) + ); + vm.expectCall( + address(permissionController), + abi.encodeWithSelector( + IPermissionController.addPendingAdmin.selector, + address(avsRegistrarAsIdentifier), + admin + ) + ); + + // Initialize + avsRegistrarAsIdentifier.initialize(admin, METADATA_URI); + } + + function test_revert_alreadyInitialized() public { + // Initialize first + vm.mockCall( + address(allocationManagerMock), + abi.encodeWithSelector(IAllocationManager.updateAVSMetadataURI.selector), + "" + ); + vm.mockCall( + address(allocationManagerMock), + abi.encodeWithSelector(IAllocationManager.setAVSRegistrar.selector), + "" + ); + vm.mockCall( + address(permissionController), + abi.encodeWithSelector(IPermissionController.addPendingAdmin.selector), + "" + ); + + avsRegistrarAsIdentifier.initialize(admin, METADATA_URI); + + // Try to initialize again + vm.expectRevert("Initializable: contract is already initialized"); + avsRegistrarAsIdentifier.initialize(admin, METADATA_URI); + } +} + +contract AVSRegistrarAsIdentifierUnitTests_supportsAVS is AVSRegistrarAsIdentifierUnitTests { + function test_supportsAVS_true() public { + // Should return true when checking against itself + assertTrue( + avsRegistrarAsIdentifier.supportsAVS(address(avsRegistrarAsIdentifier)), + "supportsAVS: should return true for self" + ); + } + + function test_supportsAVS_false() public { + // Should return false for any other address + assertFalse( + avsRegistrarAsIdentifier.supportsAVS(AVS), + "supportsAVS: should return false for original AVS" + ); + assertFalse( + avsRegistrarAsIdentifier.supportsAVS(address(0)), + "supportsAVS: should return false for zero address" + ); + assertFalse( + avsRegistrarAsIdentifier.supportsAVS(address(1)), + "supportsAVS: should return false for random address" + ); + assertFalse( + avsRegistrarAsIdentifier.supportsAVS(admin), + "supportsAVS: should return false for admin" + ); + } + + function testFuzz_supportsAVS( + address randomAddress + ) public { + if (randomAddress == address(avsRegistrarAsIdentifier)) { + assertTrue( + avsRegistrarAsIdentifier.supportsAVS(randomAddress), + "supportsAVS: should return true for self" + ); + } else { + assertFalse( + avsRegistrarAsIdentifier.supportsAVS(randomAddress), + "supportsAVS: should return false for non-self" + ); + } + } +} + +contract AVSRegistrarAsIdentifierUnitTests_getAVS is AVSRegistrarAsIdentifierUnitTests { + function test_getAVS() public { + // Should return the proxy address (self) since AVSRegistrarAsIdentifier overrides getAVS + assertEq( + avsRegistrarAsIdentifier.getAVS(), + address(avsRegistrarAsIdentifier), + "getAVS: should return self (proxy) address" + ); + } +} + +contract AVSRegistrarAsIdentifierUnitTests_registerOperator is AVSRegistrarAsIdentifierUnitTests { + using ArrayLib for *; + + function setUp() public virtual override { + super.setUp(); + + // Initialize the contract + vm.mockCall( + address(allocationManagerMock), + abi.encodeWithSelector(IAllocationManager.updateAVSMetadataURI.selector), + "" + ); + vm.mockCall( + address(allocationManagerMock), + abi.encodeWithSelector(IAllocationManager.setAVSRegistrar.selector), + "" + ); + vm.mockCall( + address(permissionController), + abi.encodeWithSelector(IPermissionController.addPendingAdmin.selector), + "" + ); + + avsRegistrarAsIdentifier.initialize(admin, METADATA_URI); + } + + function testFuzz_revert_notAllocationManager( + address notAllocationManager + ) public filterFuzzedAddressInputs(notAllocationManager) { + cheats.assume(notAllocationManager != address(allocationManagerMock)); + + vm.prank(notAllocationManager); + vm.expectRevert(NotAllocationManager.selector); + avsRegistrarAsIdentifier.registerOperator( + defaultOperator, + address(avsRegistrarAsIdentifier), + defaultOperatorSetId.toArrayU32(), + "0x" + ); + } + + function test_revert_keyNotRegistered() public { + // Register operator without key registration + vm.expectRevert(KeyNotRegistered.selector); + vm.prank(address(allocationManagerMock)); + avsRegistrarAsIdentifier.registerOperator( + defaultOperator, + address(avsRegistrarAsIdentifier), + defaultOperatorSetId.toArrayU32(), + "0x" + ); + } + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random operator set ids & register keys + uint32 numOperatorSetIds = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSetIds, 0, type(uint32).max); + + // Register keys for the operator - Note: using AVS as the avs address in the OperatorSet + for (uint32 i; i < operatorSetIds.length; ++i) { + keyRegistrarMock.setIsRegistered( + defaultOperator, OperatorSet({avs: AVS, id: operatorSetIds[i]}), true + ); + } + + // Register operator + vm.expectEmit(true, true, true, true); + emit OperatorRegistered(defaultOperator, operatorSetIds); + vm.prank(address(allocationManagerMock)); + avsRegistrarAsIdentifier.registerOperator( + defaultOperator, address(avsRegistrarAsIdentifier), operatorSetIds, "0x" + ); + } +} + +contract AVSRegistrarAsIdentifierUnitTests_deregisterOperator is + AVSRegistrarAsIdentifierUnitTests +{ + using ArrayLib for *; + + function setUp() public virtual override { + super.setUp(); + + // Initialize the contract + vm.mockCall( + address(allocationManagerMock), + abi.encodeWithSelector(IAllocationManager.updateAVSMetadataURI.selector), + "" + ); + vm.mockCall( + address(allocationManagerMock), + abi.encodeWithSelector(IAllocationManager.setAVSRegistrar.selector), + "" + ); + vm.mockCall( + address(permissionController), + abi.encodeWithSelector(IPermissionController.addPendingAdmin.selector), + "" + ); + + avsRegistrarAsIdentifier.initialize(admin, METADATA_URI); + } + + function testFuzz_revert_notAllocationManager( + address notAllocationManager + ) public filterFuzzedAddressInputs(notAllocationManager) { + cheats.assume(notAllocationManager != address(allocationManagerMock)); + + vm.prank(notAllocationManager); + vm.expectRevert(NotAllocationManager.selector); + avsRegistrarAsIdentifier.deregisterOperator( + defaultOperator, address(avsRegistrarAsIdentifier), defaultOperatorSetId.toArrayU32() + ); + } + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random operator set ids + uint32 numOperatorSetIds = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSetIds, 0, type(uint32).max); + + // Deregister operator + vm.expectEmit(true, true, true, true); + emit OperatorDeregistered(defaultOperator, operatorSetIds); + vm.prank(address(allocationManagerMock)); + avsRegistrarAsIdentifier.deregisterOperator( + defaultOperator, address(avsRegistrarAsIdentifier), operatorSetIds + ); + } +} diff --git a/test/unit/middlewareV2/AVSRegistrarBase.t.sol b/test/unit/middlewareV2/AVSRegistrarBase.t.sol new file mode 100644 index 000000000..36c5a8e08 --- /dev/null +++ b/test/unit/middlewareV2/AVSRegistrarBase.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {MockEigenLayerDeployer} from "./MockDeployer.sol"; +import {IAVSRegistrarErrors, IAVSRegistrarEvents} from "src/interfaces/IAVSRegistrarInternal.sol"; +import {AVSRegistrar} from "src/middlewareV2/registrar/AVSRegistrar.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import { + OperatorSet, + OperatorSetLib +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {ArrayLib} from "eigenlayer-contracts/src/test/utils/ArrayLib.sol"; +import "test/utils/Random.sol"; + +abstract contract AVSRegistrarBase is + MockEigenLayerDeployer, + IAVSRegistrarErrors, + IAVSRegistrarEvents +{ + AVSRegistrar internal avsRegistrar; + AVSRegistrar internal avsRegistrarImplementation; + + address internal constant defaultOperator = address(0x123); + address internal constant AVS = address(0x456); + uint32 internal constant defaultOperatorSetId = 0; + + function setUp() public virtual { + _deployMockEigenLayer(); + } + + function _registerKey(address operator, uint32[] memory operatorSetIds) internal { + for (uint32 i; i < operatorSetIds.length; ++i) { + keyRegistrarMock.setIsRegistered( + operator, OperatorSet({avs: AVS, id: operatorSetIds[i]}), true + ); + } + } +} diff --git a/test/unit/middlewareV2/AVSRegistrarSocketUnit.t.sol b/test/unit/middlewareV2/AVSRegistrarSocketUnit.t.sol new file mode 100644 index 000000000..fe1f8027c --- /dev/null +++ b/test/unit/middlewareV2/AVSRegistrarSocketUnit.t.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import "./AVSRegistrarBase.t.sol"; +import {AVSRegistrarWithSocket} from "src/middlewareV2/registrar/presets/AVSRegistrarWithSocket.sol"; +import {ISocketRegistryEvents, ISocketRegistryErrors} from "src/interfaces/ISocketRegistryV2.sol"; + +contract AVSRegistrarSocketUnitTests is + AVSRegistrarBase, + ISocketRegistryEvents, + ISocketRegistryErrors +{ + AVSRegistrarWithSocket public avsRegistrarWithSocket; + + string defaultSocket = "Socket"; + bytes socketData; + + function setUp() public override { + super.setUp(); + + avsRegistrarImplementation = new AVSRegistrarWithSocket( + AVS, + IAllocationManager(address(allocationManagerMock)), + IKeyRegistrar(address(keyRegistrarMock)) + ); + + avsRegistrarWithSocket = AVSRegistrarWithSocket( + address( + new TransparentUpgradeableProxy( + address(avsRegistrarImplementation), address(proxyAdmin), "" + ) + ) + ); + + // Encode defaultSocker into data + socketData = abi.encode(defaultSocket); + } + + function _registerOperator( + uint32[] memory operatorSetIds + ) internal { + // Register operator + _registerKey(defaultOperator, operatorSetIds); + cheats.prank(address(allocationManagerMock)); + avsRegistrarWithSocket.registerOperator(defaultOperator, AVS, operatorSetIds, socketData); + } +} + +contract AVSRegistrarSocketUnitTests_registerOperator is AVSRegistrarSocketUnitTests { + using ArrayLib for *; + + function testFuzz_revert_notAllocationManager( + address notAllocationManager + ) public filterFuzzedAddressInputs(notAllocationManager) { + cheats.assume(notAllocationManager != address(allocationManagerMock)); + + cheats.prank(notAllocationManager); + cheats.expectRevert(NotAllocationManager.selector); + avsRegistrarWithSocket.registerOperator( + defaultOperator, AVS, defaultOperatorSetId.toArrayU32(), socketData + ); + } + + function test_revert_keyNotRegistered() public { + cheats.expectRevert(KeyNotRegistered.selector); + cheats.prank(address(allocationManagerMock)); + avsRegistrarWithSocket.registerOperator( + defaultOperator, AVS, defaultOperatorSetId.toArrayU32(), socketData + ); + } + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random operator set ids & register keys + uint32 numOperatorSetIds = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSetIds, 0, type(uint32).max); + _registerKey(defaultOperator, operatorSetIds); + + // Register operator + cheats.expectEmit(true, true, true, true); + emit OperatorSocketSet(defaultOperator, defaultSocket); + cheats.expectEmit(true, true, true, true); + emit OperatorRegistered(defaultOperator, operatorSetIds); + + cheats.prank(address(allocationManagerMock)); + avsRegistrarWithSocket.registerOperator(defaultOperator, AVS, operatorSetIds, socketData); + + // Check that the socket is set + string memory socket = avsRegistrarWithSocket.getOperatorSocket(defaultOperator); + assertEq(socket, defaultSocket, "Socket mismatch"); + } +} + +contract AVSRegistrarSocketUnitTests_DeregisterOperator is AVSRegistrarSocketUnitTests { + using ArrayLib for *; + + function testFuzz_revert_notAllocationManager( + address notAllocationManager + ) public filterFuzzedAddressInputs(notAllocationManager) { + cheats.assume(notAllocationManager != address(allocationManagerMock)); + + cheats.prank(notAllocationManager); + cheats.expectRevert(NotAllocationManager.selector); + avsRegistrarWithSocket.deregisterOperator( + defaultOperator, AVS, defaultOperatorSetId.toArrayU32() + ); + } + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random operator set ids + uint32 numOperatorSetIds = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSetIds, 0, type(uint32).max); + + _registerOperator(operatorSetIds); + + cheats.expectEmit(true, true, true, true); + emit OperatorDeregistered(defaultOperator, operatorSetIds); + cheats.prank(address(allocationManagerMock)); + avsRegistrarWithSocket.deregisterOperator(defaultOperator, AVS, operatorSetIds); + + // Check that the socket still exists + string memory socket = avsRegistrarWithSocket.getOperatorSocket(defaultOperator); + assertEq(socket, defaultSocket, "Socket mismatch"); + } +} + +contract AVSRegistrarSocketUnitTests_updateSocket is AVSRegistrarSocketUnitTests { + using ArrayLib for *; + + function testFuzz_revert_notOperator( + address notOperator + ) public { + _registerOperator(defaultOperatorSetId.toArrayU32()); + cheats.assume(notOperator != defaultOperator); + cheats.assume(notOperator != address(proxyAdmin)); + + cheats.prank(notOperator); + cheats.expectRevert(CallerNotOperator.selector); + avsRegistrarWithSocket.updateSocket(defaultOperator, defaultSocket); + } + + function test_updateSocket() public { + _registerOperator(defaultOperatorSetId.toArrayU32()); + + string memory newSocket = "NewSocket"; + + cheats.expectEmit(true, true, true, true); + emit OperatorSocketSet(defaultOperator, newSocket); + cheats.prank(defaultOperator); + avsRegistrarWithSocket.updateSocket(defaultOperator, newSocket); + + // Check that the socket is updated + string memory socket = avsRegistrarWithSocket.getOperatorSocket(defaultOperator); + assertEq(socket, newSocket, "Socket mismatch"); + } +} + +contract AVSRegistrarSocketUnitTests_ViewFunctions is AVSRegistrarSocketUnitTests { + function test_supportsAVS_true() public { + // Should return true when checking against the configured AVS + assertTrue( + avsRegistrarWithSocket.supportsAVS(AVS), + "supportsAVS: should return true for configured AVS" + ); + } + + function test_supportsAVS_false() public { + // Should return false for any other address + assertFalse( + avsRegistrarWithSocket.supportsAVS(address(0)), + "supportsAVS: should return false for zero address" + ); + assertFalse( + avsRegistrarWithSocket.supportsAVS(address(1)), + "supportsAVS: should return false for random address" + ); + assertFalse( + avsRegistrarWithSocket.supportsAVS(address(avsRegistrarWithSocket)), + "supportsAVS: should return false for registrar address" + ); + assertFalse( + avsRegistrarWithSocket.supportsAVS(defaultOperator), + "supportsAVS: should return false for operator" + ); + } + + function testFuzz_supportsAVS( + address randomAddress + ) public { + if (randomAddress == AVS) { + assertTrue( + avsRegistrarWithSocket.supportsAVS(randomAddress), + "supportsAVS: should return true for configured AVS" + ); + } else { + assertFalse( + avsRegistrarWithSocket.supportsAVS(randomAddress), + "supportsAVS: should return false for non-AVS address" + ); + } + } + + function test_getAVS() public { + // Should return the configured AVS address + assertEq( + avsRegistrarWithSocket.getAVS(), AVS, "getAVS: should return configured AVS address" + ); + } +} diff --git a/test/unit/middlewareV2/AVSRegistrarUnit.t.sol b/test/unit/middlewareV2/AVSRegistrarUnit.t.sol new file mode 100644 index 000000000..388e1630f --- /dev/null +++ b/test/unit/middlewareV2/AVSRegistrarUnit.t.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import "./AVSRegistrarBase.t.sol"; + +contract AVSRegistrarUnitTests is AVSRegistrarBase { + function setUp() public override { + super.setUp(); + + avsRegistrarImplementation = new AVSRegistrar( + AVS, + IAllocationManager(address(allocationManagerMock)), + IKeyRegistrar(address(keyRegistrarMock)) + ); + + avsRegistrar = AVSRegistrar( + address( + new TransparentUpgradeableProxy( + address(avsRegistrarImplementation), address(proxyAdmin), "" + ) + ) + ); + } +} + +contract AVSRegistrarUnitTests_RegisterOperator is AVSRegistrarUnitTests { + using ArrayLib for *; + + function testFuzz_revert_notAllocationManager( + address notAllocationManager + ) public filterFuzzedAddressInputs(notAllocationManager) { + cheats.assume(notAllocationManager != address(allocationManagerMock)); + + cheats.prank(notAllocationManager); + cheats.expectRevert(NotAllocationManager.selector); + avsRegistrar.registerOperator(defaultOperator, AVS, defaultOperatorSetId.toArrayU32(), "0x"); + } + + function test_revert_keyNotRegistered() public { + cheats.expectRevert(KeyNotRegistered.selector); + cheats.prank(address(allocationManagerMock)); + avsRegistrar.registerOperator(defaultOperator, AVS, defaultOperatorSetId.toArrayU32(), "0x"); + } + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random operator set ids & register keys + uint32 numOperatorSetIds = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSetIds, 0, type(uint32).max); + _registerKey(defaultOperator, operatorSetIds); + + // Register operator + cheats.expectEmit(true, true, true, true); + emit OperatorRegistered(defaultOperator, operatorSetIds); + cheats.prank(address(allocationManagerMock)); + avsRegistrar.registerOperator(defaultOperator, AVS, operatorSetIds, "0x"); + } +} + +contract AVSRegistrarUnitTests_DeregisterOperator is AVSRegistrarUnitTests { + using ArrayLib for *; + + function testFuzz_revert_notAllocationManager( + address notAllocationManager + ) public filterFuzzedAddressInputs(notAllocationManager) { + cheats.assume(notAllocationManager != address(allocationManagerMock)); + + cheats.prank(notAllocationManager); + cheats.expectRevert(NotAllocationManager.selector); + avsRegistrar.deregisterOperator(defaultOperator, AVS, defaultOperatorSetId.toArrayU32()); + } + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random operator set ids + uint32 numOperatorSetIds = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSetIds, 0, type(uint32).max); + + // Deregister operator + cheats.expectEmit(true, true, true, true); + emit OperatorDeregistered(defaultOperator, operatorSetIds); + cheats.prank(address(allocationManagerMock)); + avsRegistrar.deregisterOperator(defaultOperator, AVS, operatorSetIds); + } +} + +contract AVSRegistrarUnitTests_ViewFunctions is AVSRegistrarUnitTests { + function test_supportsAVS_true() public { + // Should return true when checking against the configured AVS + assertTrue( + avsRegistrar.supportsAVS(AVS), "supportsAVS: should return true for configured AVS" + ); + } + + function test_supportsAVS_false() public { + // Should return false for any other address + assertFalse( + avsRegistrar.supportsAVS(address(0)), + "supportsAVS: should return false for zero address" + ); + assertFalse( + avsRegistrar.supportsAVS(address(1)), + "supportsAVS: should return false for random address" + ); + assertFalse( + avsRegistrar.supportsAVS(address(avsRegistrar)), + "supportsAVS: should return false for registrar address" + ); + assertFalse( + avsRegistrar.supportsAVS(defaultOperator), + "supportsAVS: should return false for operator" + ); + } + + function testFuzz_supportsAVS( + address randomAddress + ) public { + if (randomAddress == AVS) { + assertTrue( + avsRegistrar.supportsAVS(randomAddress), + "supportsAVS: should return true for configured AVS" + ); + } else { + assertFalse( + avsRegistrar.supportsAVS(randomAddress), + "supportsAVS: should return false for non-AVS address" + ); + } + } + + function test_getAVS() public { + // Should return the configured AVS address + assertEq(avsRegistrar.getAVS(), AVS, "getAVS: should return configured AVS address"); + } +} diff --git a/test/unit/middlewareV2/AllowlistUnit.t.sol b/test/unit/middlewareV2/AllowlistUnit.t.sol new file mode 100644 index 000000000..7cc33f7c9 --- /dev/null +++ b/test/unit/middlewareV2/AllowlistUnit.t.sol @@ -0,0 +1,479 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +import {Allowlist} from "src/middlewareV2/registrar/modules/Allowlist.sol"; +import {IAllowlist, IAllowlistErrors, IAllowlistEvents} from "src/interfaces/IAllowlist.sol"; +import { + OperatorSet, + OperatorSetLib +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {Random, Randomness} from "test/utils/Random.sol"; + +// Concrete implementation for testing +contract AllowlistImplementation is Allowlist { + function version() external pure returns (string memory) { + return "1.0.0"; + } +} + +contract AllowlistUnitTests is Test, IAllowlistErrors, IAllowlistEvents { + using OperatorSetLib for OperatorSet; + + Vm cheats = Vm(VM_ADDRESS); + + // Contracts + AllowlistImplementation public allowlistImplementation; + AllowlistImplementation public allowlist; + ProxyAdmin public proxyAdmin; + + // Test addresses + address public allowlistOwner = address(this); + address public avs1 = address(0x1); + address public avs2 = address(0x2); + address public operator1 = address(0x3); + address public operator2 = address(0x4); + address public operator3 = address(0x5); + address public defaultOperator = operator1; + + // Test operator sets + OperatorSet defaultOperatorSet; + OperatorSet alternativeOperatorSet; + uint32 public defaultOperatorSetId = 0; + + /// @dev set the random seed for the current test + modifier rand( + Randomness r + ) { + r.set(); + _; + } + + function random() internal returns (Randomness) { + return Randomness.wrap(Random.SEED).shuffle(); + } + + function setUp() public virtual { + // Deploy proxy admin + proxyAdmin = new ProxyAdmin(); + + // Deploy implementation + allowlistImplementation = new AllowlistImplementation(); + + // Deploy proxy + allowlist = AllowlistImplementation( + address( + new TransparentUpgradeableProxy( + address(allowlistImplementation), + address(proxyAdmin), + abi.encodeWithSelector(Allowlist.initialize.selector, allowlistOwner) + ) + ) + ); + + // Set up operator sets + defaultOperatorSet = OperatorSet({avs: avs1, id: defaultOperatorSetId}); + alternativeOperatorSet = OperatorSet({avs: avs2, id: 1}); + } + + function _addOperatorToAllowlist(address operator, OperatorSet memory operatorSet) internal { + cheats.prank(allowlistOwner); + allowlist.addOperatorToAllowlist(operatorSet, operator); + } + + function _addOperatorsToAllowlist( + address[] memory operators, + OperatorSet memory operatorSet + ) internal { + for (uint256 i = 0; i < operators.length; i++) { + _addOperatorToAllowlist(operators[i], operatorSet); + } + } +} + +contract AllowlistUnitTests_initialize is AllowlistUnitTests { + function test_initialization() public view { + // Check the owner is set correctly + assertEq(allowlist.owner(), allowlistOwner, "Initialization: owner incorrect"); + } + + function test_revert_alreadyInitialized() public { + cheats.expectRevert("Initializable: contract is already initialized"); + allowlist.initialize(allowlistOwner); + } + + function testFuzz_initialization( + address randomOwner + ) public { + // Deploy new instance with random owner + AllowlistImplementation newAllowlist = AllowlistImplementation( + address( + new TransparentUpgradeableProxy( + address(allowlistImplementation), + address(proxyAdmin), + abi.encodeWithSelector(Allowlist.initialize.selector, randomOwner) + ) + ) + ); + + assertEq(newAllowlist.owner(), randomOwner, "Owner should be set to randomOwner"); + } +} + +contract AllowlistUnitTests_addOperatorToAllowlist is AllowlistUnitTests { + function testFuzz_revert_notOwner( + address notOwner + ) public { + cheats.assume(notOwner != allowlistOwner); + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(notOwner); + allowlist.addOperatorToAllowlist(defaultOperatorSet, defaultOperator); + } + + function test_revert_operatorAlreadyInAllowlist() public { + // Add operator first time + _addOperatorToAllowlist(defaultOperator, defaultOperatorSet); + + // Try to add again + cheats.expectRevert(OperatorAlreadyInAllowlist.selector); + cheats.prank(allowlistOwner); + allowlist.addOperatorToAllowlist(defaultOperatorSet, defaultOperator); + } + + function test_correctness_singleOperator() public { + // Add operator + cheats.expectEmit(true, true, true, true); + emit OperatorAddedToAllowlist(defaultOperatorSet, defaultOperator); + cheats.prank(allowlistOwner); + allowlist.addOperatorToAllowlist(defaultOperatorSet, defaultOperator); + + // Check operator is in allowlist + assertTrue( + allowlist.isOperatorAllowed(defaultOperatorSet, defaultOperator), + "Operator should be in allowlist" + ); + } + + function testFuzz_correctness_multipleOperatorSets( + Randomness r + ) public rand(r) { + // Generate random operator set ids + uint32 numOperatorSets = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSets, 0, type(uint32).max); + + // Add operator to multiple operator sets + for (uint32 i = 0; i < operatorSetIds.length; i++) { + OperatorSet memory operatorSet = OperatorSet({avs: avs1, id: operatorSetIds[i]}); + + cheats.expectEmit(true, true, true, true); + emit OperatorAddedToAllowlist(operatorSet, defaultOperator); + cheats.prank(allowlistOwner); + allowlist.addOperatorToAllowlist(operatorSet, defaultOperator); + } + + // Verify operator is in all operator sets + for (uint32 i = 0; i < operatorSetIds.length; i++) { + OperatorSet memory operatorSet = OperatorSet({avs: avs1, id: operatorSetIds[i]}); + assertTrue( + allowlist.isOperatorAllowed(operatorSet, defaultOperator), + "Operator should be in all operator sets" + ); + } + } + + function testFuzz_correctness_multipleOperators( + Randomness r + ) public rand(r) { + // Generate random operators + uint32 numOperators = r.Uint32(1, 50); + address[] memory operators = new address[](numOperators); + for (uint32 i = 0; i < numOperators; i++) { + operators[i] = r.Address(); + } + + // Add all operators to the default operator set + for (uint32 i = 0; i < operators.length; i++) { + cheats.expectEmit(true, true, true, true); + emit OperatorAddedToAllowlist(defaultOperatorSet, operators[i]); + cheats.prank(allowlistOwner); + allowlist.addOperatorToAllowlist(defaultOperatorSet, operators[i]); + } + + // Verify all operators are in the allowlist + for (uint32 i = 0; i < operators.length; i++) { + assertTrue( + allowlist.isOperatorAllowed(defaultOperatorSet, operators[i]), + "All operators should be in allowlist" + ); + } + } +} + +contract AllowlistUnitTests_removeOperatorFromAllowlist is AllowlistUnitTests { + function testFuzz_revert_notOwner( + address notOwner + ) public { + cheats.assume(notOwner != allowlistOwner); + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(notOwner); + allowlist.removeOperatorFromAllowlist(defaultOperatorSet, defaultOperator); + } + + function test_revert_operatorNotInAllowlist() public { + cheats.expectRevert(OperatorNotInAllowlist.selector); + cheats.prank(allowlistOwner); + allowlist.removeOperatorFromAllowlist(defaultOperatorSet, defaultOperator); + } + + function test_correctness_removeAfterAdd() public { + // Add operator first + _addOperatorToAllowlist(defaultOperator, defaultOperatorSet); + + // Remove operator + cheats.expectEmit(true, true, true, true); + emit OperatorRemovedFromAllowlist(defaultOperatorSet, defaultOperator); + cheats.prank(allowlistOwner); + allowlist.removeOperatorFromAllowlist(defaultOperatorSet, defaultOperator); + + // Check operator is not in allowlist + assertFalse( + allowlist.isOperatorAllowed(defaultOperatorSet, defaultOperator), + "Operator should not be in allowlist after removal" + ); + } + + function testFuzz_correctness_multipleOperatorSets( + Randomness r + ) public rand(r) { + // Generate random operator set ids + uint32 numOperatorSets = r.Uint32(1, 50); + uint32[] memory operatorSetIds = r.Uint32Array(numOperatorSets, 0, type(uint32).max); + + // Add operator to all operator sets + for (uint32 i = 0; i < operatorSetIds.length; i++) { + OperatorSet memory operatorSet = OperatorSet({avs: avs1, id: operatorSetIds[i]}); + _addOperatorToAllowlist(defaultOperator, operatorSet); + } + + // Remove operator from all operator sets + for (uint32 i = 0; i < operatorSetIds.length; i++) { + OperatorSet memory operatorSet = OperatorSet({avs: avs1, id: operatorSetIds[i]}); + + cheats.expectEmit(true, true, true, true); + emit OperatorRemovedFromAllowlist(operatorSet, defaultOperator); + cheats.prank(allowlistOwner); + allowlist.removeOperatorFromAllowlist(operatorSet, defaultOperator); + } + + // Verify operator is not in any operator set + for (uint32 i = 0; i < operatorSetIds.length; i++) { + OperatorSet memory operatorSet = OperatorSet({avs: avs1, id: operatorSetIds[i]}); + assertFalse( + allowlist.isOperatorAllowed(operatorSet, defaultOperator), + "Operator should not be in any operator set" + ); + } + } + + function test_correctness_partialRemoval() public { + // Add operator to multiple operator sets + _addOperatorToAllowlist(defaultOperator, defaultOperatorSet); + _addOperatorToAllowlist(defaultOperator, alternativeOperatorSet); + + // Remove from only one operator set + cheats.prank(allowlistOwner); + allowlist.removeOperatorFromAllowlist(defaultOperatorSet, defaultOperator); + + // Check operator is removed from one but not the other + assertFalse( + allowlist.isOperatorAllowed(defaultOperatorSet, defaultOperator), + "Operator should be removed from default operator set" + ); + assertTrue( + allowlist.isOperatorAllowed(alternativeOperatorSet, defaultOperator), + "Operator should still be in alternative operator set" + ); + } +} + +contract AllowlistUnitTests_isOperatorAllowed is AllowlistUnitTests { + function test_returnsFalse_whenNotAdded() public view { + assertFalse( + allowlist.isOperatorAllowed(defaultOperatorSet, defaultOperator), + "Should return false for operator not in allowlist" + ); + } + + function test_returnsTrue_whenAdded() public { + _addOperatorToAllowlist(defaultOperator, defaultOperatorSet); + + assertTrue( + allowlist.isOperatorAllowed(defaultOperatorSet, defaultOperator), + "Should return true for operator in allowlist" + ); + } + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random operators and operator sets + uint32 numOperators = r.Uint32(5, 20); + uint32 numOperatorSets = r.Uint32(5, 20); + + address[] memory operators = new address[](numOperators); + for (uint32 i = 0; i < numOperators; i++) { + operators[i] = r.Address(); + } + + OperatorSet[] memory operatorSets = new OperatorSet[](numOperatorSets); + for (uint32 i = 0; i < numOperatorSets; i++) { + operatorSets[i] = OperatorSet({avs: r.Address(), id: r.Uint32(0, type(uint32).max)}); + } + + // Randomly add some operators to some operator sets + for (uint32 i = 0; i < operators.length; i++) { + for (uint32 j = 0; j < operatorSets.length; j++) { + if (r.Boolean()) { + // 50% chance + _addOperatorToAllowlist(operators[i], operatorSets[j]); + } + } + } + + // Verify the state is correct by re-checking + for (uint32 i = 0; i < operators.length; i++) { + for (uint32 j = 0; j < operatorSets.length; j++) { + // The state should be consistent with what we set + bool shouldBeAllowed = allowlist.isOperatorAllowed(operatorSets[j], operators[i]); + + // If allowed, removing should work + if (shouldBeAllowed) { + cheats.prank(allowlistOwner); + allowlist.removeOperatorFromAllowlist(operatorSets[j], operators[i]); + assertFalse( + allowlist.isOperatorAllowed(operatorSets[j], operators[i]), + "After removal, operator should not be allowed" + ); + } + } + } + } +} + +contract AllowlistUnitTests_getAllowedOperators is AllowlistUnitTests { + function test_returnsEmptyArray_whenNoOperators() public view { + address[] memory allowedOperators = allowlist.getAllowedOperators(defaultOperatorSet); + assertEq(allowedOperators.length, 0, "Should return empty array when no operators"); + } + + function test_returnsSingleOperator() public { + _addOperatorToAllowlist(defaultOperator, defaultOperatorSet); + + address[] memory allowedOperators = allowlist.getAllowedOperators(defaultOperatorSet); + assertEq(allowedOperators.length, 1, "Should return array with one operator"); + assertEq(allowedOperators[0], defaultOperator, "Should return the correct operator"); + } + + function test_returnsMultipleOperators() public { + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + // Add all operators + _addOperatorsToAllowlist(operators, defaultOperatorSet); + + address[] memory allowedOperators = allowlist.getAllowedOperators(defaultOperatorSet); + assertEq(allowedOperators.length, 3, "Should return all operators"); + + // Note: Order may not be preserved, so we check membership + bool found1 = false; + bool found2 = false; + bool found3 = false; + + for (uint256 i = 0; i < allowedOperators.length; i++) { + if (allowedOperators[i] == operator1) found1 = true; + if (allowedOperators[i] == operator2) found2 = true; + if (allowedOperators[i] == operator3) found3 = true; + } + + assertTrue(found1 && found2 && found3, "All operators should be in the returned array"); + } + + function testFuzz_correctness( + Randomness r + ) public rand(r) { + // Generate random operators + uint32 numOperators = r.Uint32(1, 50); + address[] memory operators = new address[](numOperators); + for (uint32 i = 0; i < numOperators; i++) { + operators[i] = r.Address(); + } + + // Add all operators to default operator set + _addOperatorsToAllowlist(operators, defaultOperatorSet); + + // Get allowed operators + address[] memory allowedOperators = allowlist.getAllowedOperators(defaultOperatorSet); + + assertEq(allowedOperators.length, operators.length, "Should return all added operators"); + + // Check all operators are in the returned array + for (uint32 i = 0; i < operators.length; i++) { + bool found = false; + for (uint32 j = 0; j < allowedOperators.length; j++) { + if (allowedOperators[j] == operators[i]) { + found = true; + break; + } + } + assertTrue(found, "All added operators should be in the returned array"); + } + } + + function test_independentOperatorSets() public { + // Add different operators to different operator sets + _addOperatorToAllowlist(operator1, defaultOperatorSet); + _addOperatorToAllowlist(operator2, defaultOperatorSet); + _addOperatorToAllowlist(operator3, alternativeOperatorSet); + + // Check default operator set + address[] memory defaultAllowed = allowlist.getAllowedOperators(defaultOperatorSet); + assertEq(defaultAllowed.length, 2, "Default operator set should have 2 operators"); + + // Check alternative operator set + address[] memory alternativeAllowed = allowlist.getAllowedOperators(alternativeOperatorSet); + assertEq(alternativeAllowed.length, 1, "Alternative operator set should have 1 operator"); + assertEq( + alternativeAllowed[0], operator3, "Alternative operator set should contain operator3" + ); + } + + function test_afterRemoval() public { + // Add multiple operators + _addOperatorToAllowlist(operator1, defaultOperatorSet); + _addOperatorToAllowlist(operator2, defaultOperatorSet); + _addOperatorToAllowlist(operator3, defaultOperatorSet); + + // Remove one operator + cheats.prank(allowlistOwner); + allowlist.removeOperatorFromAllowlist(defaultOperatorSet, operator2); + + // Check the result + address[] memory allowedOperators = allowlist.getAllowedOperators(defaultOperatorSet); + assertEq(allowedOperators.length, 2, "Should have 2 operators after removal"); + + // Verify operator2 is not in the array + for (uint256 i = 0; i < allowedOperators.length; i++) { + assertTrue( + allowedOperators[i] != operator2, "Removed operator should not be in the array" + ); + } + } +} diff --git a/test/unit/middlewareV2/BN254TableCalculatorBaseUnit.t.sol b/test/unit/middlewareV2/BN254TableCalculatorBaseUnit.t.sol new file mode 100644 index 000000000..193d811e0 --- /dev/null +++ b/test/unit/middlewareV2/BN254TableCalculatorBaseUnit.t.sol @@ -0,0 +1,806 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import { + KeyRegistrar, + IKeyRegistrarTypes +} from "eigenlayer-contracts/src/contracts/permissions/KeyRegistrar.sol"; +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import {IOperatorTableCalculatorTypes} from + "eigenlayer-contracts/src/contracts/interfaces/IOperatorTableCalculator.sol"; +import {IBN254TableCalculator} from "../../../src/interfaces/IBN254TableCalculator.sol"; +import { + OperatorSet, + OperatorSetLib +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {BN254} from "eigenlayer-contracts/src/contracts/libraries/BN254.sol"; + +import {BN254TableCalculatorBase} from + "../../../src/middlewareV2/tableCalculator/BN254TableCalculatorBase.sol"; +import {MockEigenLayerDeployer} from "./MockDeployer.sol"; +import "test/utils/Random.sol"; + +// Mock implementation for testing abstract contract +contract BN254TableCalculatorBaseHarness is BN254TableCalculatorBase { + // Storage for mock weights + mapping(bytes32 => address[]) internal _mockOperators; + mapping(bytes32 => uint256[][]) internal _mockWeights; + + constructor( + IKeyRegistrar _keyRegistrar + ) BN254TableCalculatorBase(_keyRegistrar) {} + + function setMockOperatorWeights( + OperatorSet calldata operatorSet, + address[] memory operators, + uint256[][] memory weights + ) external { + bytes32 key = operatorSet.key(); + _mockOperators[key] = operators; + _mockWeights[key] = weights; + } + + function _getOperatorWeights( + OperatorSet calldata operatorSet + ) internal view override returns (address[] memory operators, uint256[][] memory weights) { + bytes32 key = operatorSet.key(); + operators = _mockOperators[key]; + weights = _mockWeights[key]; + } +} + +/** + * @title BN254TableCalculatorBaseUnitTests + * @notice Base contract for all BN254TableCalculatorBase unit tests + */ +contract BN254TableCalculatorBaseUnitTests is + MockEigenLayerDeployer, + IOperatorTableCalculatorTypes, + IKeyRegistrarTypes +{ + using BN254 for BN254.G1Point; + using OperatorSetLib for OperatorSet; + + // Test contracts + BN254TableCalculatorBaseHarness public calculator; + + // Test addresses + address public avs1 = address(0x1); + address public avs2 = address(0x2); + address public operator1 = address(0x3); + address public operator2 = address(0x4); + address public operator3 = address(0x5); + + // Test operator sets + OperatorSet defaultOperatorSet; + OperatorSet alternativeOperatorSet; + + // BN254 test keys + uint256 constant BN254_PRIV_KEY_1 = 69; + uint256 constant BN254_PRIV_KEY_2 = 123; + uint256 constant BN254_PRIV_KEY_3 = 456; + + BN254.G1Point bn254G1Key1; + BN254.G1Point bn254G1Key2; + BN254.G1Point bn254G1Key3; + BN254.G2Point bn254G2Key1; + BN254.G2Point bn254G2Key2; + BN254.G2Point bn254G2Key3; + + function setUp() public virtual { + _deployMockEigenLayer(); + + // Deploy calculator with KeyRegistrar + calculator = new BN254TableCalculatorBaseHarness(IKeyRegistrar(address(keyRegistrar))); + + // Set up operator sets + defaultOperatorSet = OperatorSet({avs: avs1, id: 0}); + alternativeOperatorSet = OperatorSet({avs: avs2, id: 1}); + + // Set up BN254 keys + bn254G1Key1 = BN254.generatorG1().scalar_mul(BN254_PRIV_KEY_1); + bn254G1Key2 = BN254.generatorG1().scalar_mul(BN254_PRIV_KEY_2); + + // Valid G2 points that correspond to the private keys + bn254G2Key1.X[1] = + 19101821850089705274637533855249918363070101489527618151493230256975900223847; + bn254G2Key1.X[0] = + 5334410886741819556325359147377682006012228123419628681352847439302316235957; + bn254G2Key1.Y[1] = + 354176189041917478648604979334478067325821134838555150300539079146482658331; + bn254G2Key1.Y[0] = + 4185483097059047421902184823581361466320657066600218863748375739772335928910; + + bn254G2Key2.X[1] = + 19276105129625393659655050515259006463014579919681138299520812914148935621072; + bn254G2Key2.X[0] = + 14066454060412929535985836631817650877381034334390275410072431082437297539867; + bn254G2Key2.Y[1] = + 12642665914920339463975152321804664028480770144655934937445922690262428344269; + bn254G2Key2.Y[0] = + 10109651107942685361120988628892759706059655669161016107907096760613704453218; + + // Configure operator sets in AllocationManager + allocationManagerMock.setAVSRegistrar(avs1, IAVSRegistrar(avs1)); + allocationManagerMock.setAVSRegistrar(avs2, IAVSRegistrar(avs2)); + + // Configure operator sets for BN254 + vm.prank(avs1); + keyRegistrar.configureOperatorSet(defaultOperatorSet, IKeyRegistrarTypes.CurveType.BN254); + + vm.prank(avs2); + keyRegistrar.configureOperatorSet( + alternativeOperatorSet, IKeyRegistrarTypes.CurveType.BN254 + ); + } + + // Helper functions + function _registerOperatorKey( + address operator, + OperatorSet memory operatorSet, + BN254.G1Point memory g1Key, + BN254.G2Point memory g2Key, + uint256 privKey + ) internal { + bytes memory pubkey = abi.encode(g1Key.X, g1Key.Y, g2Key.X, g2Key.Y); + bytes memory signature = _generateBN254Signature(operator, operatorSet, pubkey, privKey); + + vm.prank(operator); + keyRegistrar.registerKey(operator, operatorSet, pubkey, signature); + } + + function _generateBN254Signature( + address operator, + OperatorSet memory operatorSet, + bytes memory pubkey, + uint256 privKey + ) internal view returns (bytes memory) { + bytes32 structHash = keccak256( + abi.encode( + keyRegistrar.BN254_KEY_REGISTRATION_TYPEHASH(), + operator, + operatorSet.avs, + operatorSet.id, + keccak256(pubkey) + ) + ); + bytes32 messageHash = keyRegistrar.domainSeparator(); + messageHash = keccak256(abi.encodePacked("\x19\x01", messageHash, structHash)); + + BN254.G1Point memory msgPoint = BN254.hashToG1(messageHash); + BN254.G1Point memory signature = msgPoint.scalar_mul(privKey); + + return abi.encode(signature.X, signature.Y); + } + + function _createSingleWeightArray( + uint256 weight + ) internal pure returns (uint256[][] memory) { + uint256[][] memory weights = new uint256[][](1); + weights[0] = new uint256[](1); + weights[0][0] = weight; + return weights; + } + + function _createMultiWeightArray( + uint256[] memory weightValues + ) internal pure returns (uint256[][] memory) { + uint256[][] memory weights = new uint256[][](1); + weights[0] = weightValues; + return weights; + } + + function _addG1Points( + BN254.G1Point memory p1, + BN254.G1Point memory p2 + ) internal view returns (BN254.G1Point memory) { + if (p1.X == 0 && p1.Y == 0) return p2; + if (p2.X == 0 && p2.Y == 0) return p1; + return BN254.plus(p1, p2); + } +} + +/** + * @title BN254TableCalculatorBaseUnitTests_calculateOperatorTable + * @notice Unit tests for BN254TableCalculatorBase.calculateOperatorTable + */ +contract BN254TableCalculatorBaseUnitTests_calculateOperatorTable is + BN254TableCalculatorBaseUnitTests +{ + function test_noOperators() public { + // Set empty operators and weights + address[] memory operators = new address[](0); + uint256[][] memory weights = new uint256[][](0); + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorSetInfo memory info = calculator.calculateOperatorTable(defaultOperatorSet); + + assertEq(info.numOperators, 0, "Should have 0 operators"); + assertEq(info.totalWeights.length, 0, "Should have empty total weights"); + assertEq(info.aggregatePubkey.X, 0, "Aggregate pubkey X should be 0"); + assertEq(info.aggregatePubkey.Y, 0, "Aggregate pubkey Y should be 0"); + } + + function test_operatorsWithNoRegisteredKeys() public { + // Set operators without registering their keys + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + uint256[][] memory weights = new uint256[][](2); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorSetInfo memory info = calculator.calculateOperatorTable(defaultOperatorSet); + + // When no operators have registered keys, operatorCount should be 0 and return empty table + assertEq(info.numOperators, 0, "Should have 0 operators when none are registered"); + assertEq( + info.totalWeights.length, + 0, + "Should have empty total weights when no operators registered" + ); + assertEq(info.operatorInfoTreeRoot, bytes32(0), "Should have zero tree root"); + assertEq(info.aggregatePubkey.X, 0, "Aggregate pubkey X should be 0"); + assertEq(info.aggregatePubkey.Y, 0, "Aggregate pubkey Y should be 0"); + } + + function test_allOperatorsRegistered() public { + // Register operators + _registerOperatorKey( + operator1, defaultOperatorSet, bn254G1Key1, bn254G2Key1, BN254_PRIV_KEY_1 + ); + _registerOperatorKey( + operator2, defaultOperatorSet, bn254G1Key2, bn254G2Key2, BN254_PRIV_KEY_2 + ); + + // Set operators and weights + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + uint256[][] memory weights = new uint256[][](2); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorSetInfo memory info = calculator.calculateOperatorTable(defaultOperatorSet); + + assertEq(info.numOperators, 2, "Should have 2 operators"); + assertEq(info.totalWeights.length, 1, "Should have 1 weight type"); + assertEq(info.totalWeights[0], 300, "Total weight should be 300"); + + // Verify aggregate pubkey is correct (sum of G1 points) + BN254.G1Point memory expectedAggregate = _addG1Points(bn254G1Key1, bn254G1Key2); + assertEq(info.aggregatePubkey.X, expectedAggregate.X, "Aggregate pubkey X mismatch"); + assertEq(info.aggregatePubkey.Y, expectedAggregate.Y, "Aggregate pubkey Y mismatch"); + } + + function test_multipleWeightTypes() public { + // Register operators + _registerOperatorKey( + operator1, defaultOperatorSet, bn254G1Key1, bn254G2Key1, BN254_PRIV_KEY_1 + ); + _registerOperatorKey( + operator2, defaultOperatorSet, bn254G1Key2, bn254G2Key2, BN254_PRIV_KEY_2 + ); + + // Set operators and weights with multiple types + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + uint256[][] memory weights = new uint256[][](2); + uint256[] memory op1Weights = new uint256[](3); + op1Weights[0] = 100; + op1Weights[1] = 150; + op1Weights[2] = 50; + weights[0] = op1Weights; + + uint256[] memory op2Weights = new uint256[](3); + op2Weights[0] = 200; + op2Weights[1] = 250; + op2Weights[2] = 100; + weights[1] = op2Weights; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorSetInfo memory info = calculator.calculateOperatorTable(defaultOperatorSet); + + assertEq(info.totalWeights.length, 3, "Should have 3 weight types"); + assertEq(info.totalWeights[0], 300, "Total weight[0] should be 300"); + assertEq(info.totalWeights[1], 400, "Total weight[1] should be 400"); + assertEq(info.totalWeights[2], 150, "Total weight[2] should be 150"); + } + + function test_mixedRegistrationStatus() public { + // Register only operator1 + _registerOperatorKey( + operator1, defaultOperatorSet, bn254G1Key1, bn254G2Key1, BN254_PRIV_KEY_1 + ); + + // Set operators and weights + address[] memory operators = new address[](3); + operators[0] = operator1; // registered + operators[1] = operator2; // not registered + operators[2] = operator3; // not registered + + uint256[][] memory weights = new uint256[][](3); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + weights[2] = _createSingleWeightArray(300)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorSetInfo memory info = calculator.calculateOperatorTable(defaultOperatorSet); + + assertEq(info.numOperators, 1, "Should have 1 operator (only registered ones count)"); + assertEq(info.totalWeights[0], 100, "Total weight should be 100 (only operator1)"); + assertEq(info.aggregatePubkey.X, bn254G1Key1.X, "Aggregate pubkey X should be operator1's"); + assertEq(info.aggregatePubkey.Y, bn254G1Key1.Y, "Aggregate pubkey Y should be operator1's"); + } + + function test_singleOperatorRegistered() public { + // Test with 1 operator, 1 registered + address newOperator = address(uint160(100)); + + address[] memory operators = new address[](1); + operators[0] = newOperator; + uint256[][] memory weights = new uint256[][](1); + weights[0] = _createSingleWeightArray(100)[0]; + + _registerOperatorKey( + newOperator, defaultOperatorSet, bn254G1Key1, bn254G2Key1, BN254_PRIV_KEY_1 + ); + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorSetInfo memory info = calculator.calculateOperatorTable(defaultOperatorSet); + assertEq(info.numOperators, 1, "Should have 1 operator"); + assertEq(info.totalWeights[0], 100, "Total weight should be 100"); + assertEq(info.aggregatePubkey.X, bn254G1Key1.X, "Aggregate pubkey X mismatch"); + assertEq(info.aggregatePubkey.Y, bn254G1Key1.Y, "Aggregate pubkey Y mismatch"); + } + + function test_subsetOfOperatorsRegistered() public { + // Register operator1 and operator3, but not operator2 + _registerOperatorKey( + operator1, defaultOperatorSet, bn254G1Key1, bn254G2Key1, BN254_PRIV_KEY_1 + ); + + // Set operators and weights + address[] memory operators = new address[](3); + operators[0] = operator1; // registered + operators[1] = operator2; // not registered + + uint256[][] memory weights = new uint256[][](3); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; // This weight won't be included + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorSetInfo memory info = calculator.calculateOperatorTable(defaultOperatorSet); + + assertEq(info.numOperators, 1, "Should have 1 operator (only registered ones)"); + assertEq(info.totalWeights[0], 100, "Total weight should be 100 (100 + 300)"); + + // Verify aggregate pubkey is sum of registered operators' keys + BN254.G1Point memory expectedAggregate = bn254G1Key1; + assertEq(info.aggregatePubkey.X, expectedAggregate.X, "Aggregate pubkey X mismatch"); + assertEq(info.aggregatePubkey.Y, expectedAggregate.Y, "Aggregate pubkey Y mismatch"); + + // Verify merkle root is non-zero + assertTrue(info.operatorInfoTreeRoot != bytes32(0), "Merkle root should be non-zero"); + } + + function test_emptyOperatorSetReturnsZeroValues() public { + // Test with operators that exist but none registered + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + uint256[][] memory weights = new uint256[][](3); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + weights[2] = _createSingleWeightArray(300)[0]; + + // Don't register any operators + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorSetInfo memory info = calculator.calculateOperatorTable(defaultOperatorSet); + + // Verify all values are zero/empty when no operators are registered + assertEq(info.operatorInfoTreeRoot, bytes32(0), "Tree root should be zero"); + assertEq(info.numOperators, 0, "Should have 0 operators"); + assertEq(info.aggregatePubkey.X, 0, "Aggregate pubkey X should be 0"); + assertEq(info.aggregatePubkey.Y, 0, "Aggregate pubkey Y should be 0"); + assertEq(info.totalWeights.length, 0, "Total weights should be empty array"); + } +} + +/** + * @title BN254TableCalculatorBaseUnitTests_calculateOperatorTableBytes + * @notice Unit tests for BN254TableCalculatorBase.calculateOperatorTableBytes + */ +contract BN254TableCalculatorBaseUnitTests_calculateOperatorTableBytes is + BN254TableCalculatorBaseUnitTests +{ + function test_encodesCorrectly() public { + // Register operator + _registerOperatorKey( + operator1, defaultOperatorSet, bn254G1Key1, bn254G2Key1, BN254_PRIV_KEY_1 + ); + + // Set operators and weights + address[] memory operators = new address[](1); + operators[0] = operator1; + uint256[][] memory weights = _createSingleWeightArray(100); + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + bytes memory tableBytes = calculator.calculateOperatorTableBytes(defaultOperatorSet); + + // Decode and verify + BN254OperatorSetInfo memory decodedInfo = abi.decode(tableBytes, (BN254OperatorSetInfo)); + + assertEq(decodedInfo.numOperators, 1, "Should have 1 operator"); + assertEq(decodedInfo.totalWeights[0], 100, "Total weight should be 100"); + assertEq(decodedInfo.aggregatePubkey.X, bn254G1Key1.X, "Aggregate pubkey X mismatch"); + assertEq(decodedInfo.aggregatePubkey.Y, bn254G1Key1.Y, "Aggregate pubkey Y mismatch"); + } + + function testFuzz_encodesCorrectly( + uint256 weight + ) public { + weight = bound(weight, 1, 1e18); + + // Register operator + _registerOperatorKey( + operator1, defaultOperatorSet, bn254G1Key1, bn254G2Key1, BN254_PRIV_KEY_1 + ); + + // Set operators and weights + address[] memory operators = new address[](1); + operators[0] = operator1; + uint256[][] memory weights = _createSingleWeightArray(weight); + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + bytes memory tableBytes = calculator.calculateOperatorTableBytes(defaultOperatorSet); + + // Decode and verify + BN254OperatorSetInfo memory decodedInfo = abi.decode(tableBytes, (BN254OperatorSetInfo)); + + assertEq(decodedInfo.totalWeights[0], weight, "Weight mismatch"); + } +} + +/** + * @title BN254TableCalculatorBaseUnitTests_getOperatorSetWeights + * @notice Unit tests for BN254TableCalculatorBase.getOperatorSetWeights + */ +contract BN254TableCalculatorBaseUnitTests_getOperatorSetWeights is + BN254TableCalculatorBaseUnitTests +{ + function test_returnsImplementationResult() public { + // Set mock weights + address[] memory expectedOperators = new address[](2); + expectedOperators[0] = operator1; + expectedOperators[1] = operator2; + + uint256[][] memory expectedWeights = new uint256[][](2); + expectedWeights[0] = _createSingleWeightArray(100)[0]; + expectedWeights[1] = _createSingleWeightArray(200)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, expectedOperators, expectedWeights); + + (address[] memory operators, uint256[][] memory weights) = + calculator.getOperatorSetWeights(defaultOperatorSet); + + assertEq(operators.length, expectedOperators.length, "Operators length mismatch"); + assertEq(weights.length, expectedWeights.length, "Weights length mismatch"); + + for (uint256 i = 0; i < operators.length; i++) { + assertEq(operators[i], expectedOperators[i], "Operator address mismatch"); + assertEq(weights[i][0], expectedWeights[i][0], "Weight value mismatch"); + } + } + + function testFuzz_returnsImplementationResult( + uint8 numOperators + ) public { + numOperators = uint8(bound(numOperators, 0, 20)); + + address[] memory expectedOperators = new address[](numOperators); + uint256[][] memory expectedWeights = new uint256[][](numOperators); + + for (uint256 i = 0; i < numOperators; i++) { + expectedOperators[i] = address(uint160(i + 100)); + expectedWeights[i] = _createSingleWeightArray((i + 1) * 100)[0]; + } + + calculator.setMockOperatorWeights(defaultOperatorSet, expectedOperators, expectedWeights); + + (address[] memory operators, uint256[][] memory weights) = + calculator.getOperatorSetWeights(defaultOperatorSet); + + assertEq(operators.length, numOperators, "Operators length mismatch"); + assertEq(weights.length, numOperators, "Weights length mismatch"); + } +} + +/** + * @title BN254TableCalculatorBaseUnitTests_getOperatorWeights + * @notice Unit tests for BN254TableCalculatorBase.getOperatorWeights + */ +contract BN254TableCalculatorBaseUnitTests_getOperatorWeights is + BN254TableCalculatorBaseUnitTests +{ + function test_operatorExists() public { + // Set operators and weights + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + uint256[][] memory weights = new uint256[][](3); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + weights[2] = _createSingleWeightArray(300)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + uint256[] memory op1Weights = calculator.getOperatorWeights(defaultOperatorSet, operator1); + assertEq(op1Weights.length, 1, "Operator1 should have 1 weight type"); + assertEq(op1Weights[0], 100, "Operator1 weight mismatch"); + + uint256[] memory op2Weights = calculator.getOperatorWeights(defaultOperatorSet, operator2); + assertEq(op2Weights.length, 1, "Operator2 should have 1 weight type"); + assertEq(op2Weights[0], 200, "Operator2 weight mismatch"); + + uint256[] memory op3Weights = calculator.getOperatorWeights(defaultOperatorSet, operator3); + assertEq(op3Weights.length, 1, "Operator3 should have 1 weight type"); + assertEq(op3Weights[0], 300, "Operator3 weight mismatch"); + } + + function test_operatorDoesNotExist() public { + // Set operators and weights + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + uint256[][] memory weights = new uint256[][](2); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + uint256[] memory op3Weights = calculator.getOperatorWeights(defaultOperatorSet, operator3); + assertEq(op3Weights.length, 0, "Non-existent operator should return empty array"); + + uint256[] memory deadWeights = + calculator.getOperatorWeights(defaultOperatorSet, address(0xdead)); + assertEq(deadWeights.length, 0, "Random address should return empty array"); + } + + function test_emptyOperatorSet() public { + // Set empty operators and weights + address[] memory operators = new address[](0); + uint256[][] memory weights = new uint256[][](0); + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + uint256[] memory op1Weights = calculator.getOperatorWeights(defaultOperatorSet, operator1); + assertEq(op1Weights.length, 0, "Should return empty array for empty set"); + } + + function testFuzz_getOperatorWeights(address operator, uint256 weight) public { + weight = bound(weight, 0, 1e18); + + // Set single operator + address[] memory operators = new address[](1); + operators[0] = operator; + + uint256[][] memory weights = _createSingleWeightArray(weight); + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + uint256[] memory opWeights = calculator.getOperatorWeights(defaultOperatorSet, operator); + assertEq(opWeights.length, 1, "Should have 1 weight type"); + assertEq(opWeights[0], weight, "Weight mismatch"); + + // Different operator should return empty array + address differentOperator = address(uint160(uint256(uint160(operator)) + 1)); + uint256[] memory diffWeights = + calculator.getOperatorWeights(defaultOperatorSet, differentOperator); + assertEq(diffWeights.length, 0, "Different operator should return empty array"); + } +} + +/** + * @title BN254TableCalculatorBaseUnitTests_getOperatorInfos + * @notice Unit tests for BN254TableCalculatorBase.getOperatorInfos + */ +contract BN254TableCalculatorBaseUnitTests_getOperatorInfos is BN254TableCalculatorBaseUnitTests { + function test_noOperatorsRegistered() public { + // Set operators without registering keys + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + uint256[][] memory weights = new uint256[][](2); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorInfo[] memory infos = calculator.getOperatorInfos(defaultOperatorSet); + + assertEq(infos.length, 2, "Should have 2 operator infos"); + + // Both should have zero pubkeys since not registered + for (uint256 i = 0; i < infos.length; i++) { + assertEq(infos[i].pubkey.X, 0, "Unregistered operator pubkey X should be 0"); + assertEq(infos[i].pubkey.Y, 0, "Unregistered operator pubkey Y should be 0"); + assertEq(infos[i].weights.length, 0, "Unregistered operator weights should be empty"); + } + } + + function test_someOperatorsNotRegistered() public { + // Register only operator1 (skip operator3 to avoid pairing issues) + _registerOperatorKey( + operator1, defaultOperatorSet, bn254G1Key1, bn254G2Key1, BN254_PRIV_KEY_1 + ); + + // Set operators and weights + address[] memory operators = new address[](3); + operators[0] = operator1; // registered + operators[1] = operator2; // not registered + operators[2] = operator3; // not registered + + uint256[][] memory weights = new uint256[][](3); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + weights[2] = _createSingleWeightArray(300)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorInfo[] memory infos = calculator.getOperatorInfos(defaultOperatorSet); + + assertEq(infos.length, 3, "Should have 3 operator infos"); + + // Check operator1 (registered) + assertEq(infos[0].pubkey.X, bn254G1Key1.X, "Operator1 pubkey X mismatch"); + assertEq(infos[0].pubkey.Y, bn254G1Key1.Y, "Operator1 pubkey Y mismatch"); + assertEq(infos[0].weights[0], 100, "Operator1 weight mismatch"); + + // Check operator2 (not registered) + assertEq(infos[1].pubkey.X, 0, "Operator2 pubkey X should be 0"); + assertEq(infos[1].pubkey.Y, 0, "Operator2 pubkey Y should be 0"); + assertEq(infos[1].weights.length, 0, "Operator2 weights should be empty"); + + // Check operator3 (not registered) + assertEq(infos[2].pubkey.X, 0, "Operator3 pubkey X should be 0"); + assertEq(infos[2].pubkey.Y, 0, "Operator3 pubkey Y should be 0"); + assertEq(infos[2].weights.length, 0, "Operator3 weights should be empty"); + } + + function test_allOperatorsRegistered() public { + // Register all operators + _registerOperatorKey( + operator1, defaultOperatorSet, bn254G1Key1, bn254G2Key1, BN254_PRIV_KEY_1 + ); + _registerOperatorKey( + operator2, defaultOperatorSet, bn254G1Key2, bn254G2Key2, BN254_PRIV_KEY_2 + ); + + // Set operators and weights + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + uint256[][] memory weights = new uint256[][](2); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorInfo[] memory infos = calculator.getOperatorInfos(defaultOperatorSet); + + assertEq(infos.length, 2, "Should have 2 operator infos"); + + // Check operator1 + assertEq(infos[0].pubkey.X, bn254G1Key1.X, "Operator1 pubkey X mismatch"); + assertEq(infos[0].pubkey.Y, bn254G1Key1.Y, "Operator1 pubkey Y mismatch"); + assertEq(infos[0].weights[0], 100, "Operator1 weight mismatch"); + + // Check operator2 + assertEq(infos[1].pubkey.X, bn254G1Key2.X, "Operator2 pubkey X mismatch"); + assertEq(infos[1].pubkey.Y, bn254G1Key2.Y, "Operator2 pubkey Y mismatch"); + assertEq(infos[1].weights[0], 200, "Operator2 weight mismatch"); + } + + function test_multipleWeightTypes() public { + // Register operator + _registerOperatorKey( + operator1, defaultOperatorSet, bn254G1Key1, bn254G2Key1, BN254_PRIV_KEY_1 + ); + + // Set operators and weights with multiple types + address[] memory operators = new address[](1); + operators[0] = operator1; + + uint256[][] memory weights = new uint256[][](1); + uint256[] memory multiWeights = new uint256[](3); + multiWeights[0] = 100; + multiWeights[1] = 200; + multiWeights[2] = 300; + weights[0] = multiWeights; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorInfo[] memory infos = calculator.getOperatorInfos(defaultOperatorSet); + + assertEq(infos.length, 1, "Should have 1 operator info"); + assertEq(infos[0].weights.length, 3, "Should have 3 weight types"); + assertEq(infos[0].weights[0], 100, "Weight[0] mismatch"); + assertEq(infos[0].weights[1], 200, "Weight[1] mismatch"); + assertEq(infos[0].weights[2], 300, "Weight[2] mismatch"); + } + + function testFuzz_getOperatorInfos( + uint8 numOperators + ) public { + numOperators = uint8(bound(numOperators, 1, 5)); + + address[] memory operators = new address[](numOperators); + uint256[][] memory weights = new uint256[][](numOperators); + + // Generate operators and weights + for (uint256 i = 0; i < numOperators; i++) { + operators[i] = address(uint160(i + 100)); + weights[i] = _createSingleWeightArray((i + 1) * 100)[0]; + } + + // Register some operators with valid keys + if (numOperators >= 1) { + _registerOperatorKey( + operators[0], defaultOperatorSet, bn254G1Key1, bn254G2Key1, BN254_PRIV_KEY_1 + ); + } + if (numOperators >= 3) { + _registerOperatorKey( + operators[2], defaultOperatorSet, bn254G1Key2, bn254G2Key2, BN254_PRIV_KEY_2 + ); + } + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + BN254OperatorInfo[] memory infos = calculator.getOperatorInfos(defaultOperatorSet); + + assertEq(infos.length, numOperators, "Operator info count mismatch"); + + for (uint256 i = 0; i < numOperators; i++) { + if ((i == 0 && numOperators >= 1) || (i == 2 && numOperators >= 3)) { + // Registered operators should have weights + assertEq(infos[i].weights.length, 1, "Registered operator should have weights"); + assertEq(infos[i].weights[0], (i + 1) * 100, "Weight value mismatch"); + assertTrue( + infos[i].pubkey.X != 0 || infos[i].pubkey.Y != 0, + "Registered operator should have pubkey" + ); + } else { + // Unregistered operators should have empty weights + assertEq( + infos[i].weights.length, 0, "Unregistered operator should have empty weights" + ); + assertEq(infos[i].pubkey.X, 0, "Unregistered operator pubkey X should be 0"); + assertEq(infos[i].pubkey.Y, 0, "Unregistered operator pubkey Y should be 0"); + } + } + } +} diff --git a/test/unit/middlewareV2/BN254TableCalculatorUnit.t.sol b/test/unit/middlewareV2/BN254TableCalculatorUnit.t.sol new file mode 100644 index 000000000..889941cc5 --- /dev/null +++ b/test/unit/middlewareV2/BN254TableCalculatorUnit.t.sol @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import { + KeyRegistrar, + IKeyRegistrarTypes +} from "eigenlayer-contracts/src/contracts/permissions/KeyRegistrar.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import {IOperatorTableCalculatorTypes} from + "eigenlayer-contracts/src/contracts/interfaces/IOperatorTableCalculator.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import { + OperatorSet, + OperatorSetLib +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {BN254} from "eigenlayer-contracts/src/contracts/libraries/BN254.sol"; + +import {BN254TableCalculator} from + "../../../src/middlewareV2/tableCalculator/BN254TableCalculator.sol"; +import {MockEigenLayerDeployer} from "./MockDeployer.sol"; +import "test/utils/Random.sol"; + +// Harness to test internal functions +contract BN254TableCalculatorHarness is BN254TableCalculator { + constructor( + IKeyRegistrar _keyRegistrar, + IAllocationManager _allocationManager, + uint256 _LOOKAHEAD_BLOCKS + ) BN254TableCalculator(_keyRegistrar, _allocationManager, _LOOKAHEAD_BLOCKS) {} + + function exposed_getOperatorWeights( + OperatorSet calldata operatorSet + ) external view returns (address[] memory operators, uint256[][] memory weights) { + return _getOperatorWeights(operatorSet); + } +} + +/** + * @title BN254TableCalculatorUnitTests + * @notice Base contract for all BN254TableCalculator unit tests + */ +contract BN254TableCalculatorUnitTests is MockEigenLayerDeployer, IOperatorTableCalculatorTypes { + using OperatorSetLib for OperatorSet; + + // Test contracts + BN254TableCalculatorHarness public calculator; + + // Test addresses + address public avs1 = address(0x1); + address public avs2 = address(0x2); + address public operator1 = address(0x3); + address public operator2 = address(0x4); + address public operator3 = address(0x5); + + // Test strategies + IStrategy public strategy1 = IStrategy(address(0x100)); + IStrategy public strategy2 = IStrategy(address(0x200)); + + // Test operator sets + OperatorSet defaultOperatorSet; + OperatorSet alternativeOperatorSet; + + // Test constants + uint256 public constant TEST_LOOKAHEAD_BLOCKS = 100; + + function setUp() public virtual { + _deployMockEigenLayer(); + + // Deploy calculator with mocked AllocationManager + calculator = new BN254TableCalculatorHarness( + IKeyRegistrar(address(keyRegistrar)), + IAllocationManager(address(allocationManagerMock)), + TEST_LOOKAHEAD_BLOCKS + ); + + // Set up operator sets + defaultOperatorSet = OperatorSet({avs: avs1, id: 0}); + alternativeOperatorSet = OperatorSet({avs: avs2, id: 1}); + + // Configure operator sets in AllocationManager + allocationManagerMock.setAVSRegistrar(avs1, IAVSRegistrar(avs1)); + allocationManagerMock.setAVSRegistrar(avs2, IAVSRegistrar(avs2)); + + // Configure operator sets for BN254 + vm.prank(avs1); + keyRegistrar.configureOperatorSet(defaultOperatorSet, IKeyRegistrarTypes.CurveType.BN254); + + vm.prank(avs2); + keyRegistrar.configureOperatorSet( + alternativeOperatorSet, IKeyRegistrarTypes.CurveType.BN254 + ); + } + + // Helper functions + function _setupOperatorSet( + OperatorSet memory operatorSet, + address[] memory operators, + IStrategy[] memory strategies, + uint256[][] memory minSlashableStake + ) internal { + allocationManagerMock.setMembersInOperatorSet(operatorSet, operators); + allocationManagerMock.setStrategiesInOperatorSet(operatorSet, strategies); + allocationManagerMock.setMinimumSlashableStake( + operatorSet, operators, strategies, minSlashableStake + ); + } + + function _createSingleWeightArray( + uint256 weight + ) internal pure returns (uint256[][] memory) { + uint256[][] memory weights = new uint256[][](1); + weights[0] = new uint256[](1); + weights[0][0] = weight; + return weights; + } + + function _createMultiWeightArray( + uint256[] memory weightValues + ) internal pure returns (uint256[][] memory) { + uint256[][] memory weights = new uint256[][](1); + weights[0] = weightValues; + return weights; + } +} + +/** + * @title BN254TableCalculatorUnitTests_getOperatorWeights + * @notice Unit tests for BN254TableCalculator._getOperatorWeights + */ +contract BN254TableCalculatorUnitTests_getOperatorWeights is BN254TableCalculatorUnitTests { + function test_noOperators() public { + // Setup empty operator set + address[] memory operators = new address[](0); + IStrategy[] memory strategies = new IStrategy[](0); + uint256[][] memory minSlashableStake = new uint256[][](0); + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, 0, "Should have no operators"); + assertEq(resultWeights.length, 0, "Should have no weights"); + } + + function test_singleOperatorWithStake() public { + // Setup operator set with one operator + address[] memory operators = new address[](1); + operators[0] = operator1; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory minSlashableStake = new uint256[][](1); + minSlashableStake[0] = new uint256[](1); + minSlashableStake[0][0] = 1000; + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, 1, "Should have 1 operator"); + assertEq(resultOperators[0], operator1, "Operator mismatch"); + assertEq(resultWeights.length, 1, "Should have 1 weight array"); + assertEq(resultWeights[0][0], 1000, "Weight mismatch"); + } + + function test_singleOperatorWithZeroStake() public { + // Setup operator set with one operator with zero stake + address[] memory operators = new address[](1); + operators[0] = operator1; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory minSlashableStake = new uint256[][](1); + minSlashableStake[0] = new uint256[](1); + minSlashableStake[0][0] = 0; + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, 0, "Should have no operators with zero stake"); + assertEq(resultWeights.length, 0, "Should have no weights"); + } + + function test_multipleOperatorsWithStake() public { + // Setup operator set with multiple operators + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + IStrategy[] memory strategies = new IStrategy[](2); + strategies[0] = strategy1; + strategies[1] = strategy2; + + uint256[][] memory minSlashableStake = new uint256[][](3); + minSlashableStake[0] = new uint256[](2); + minSlashableStake[0][0] = 500; + minSlashableStake[0][1] = 300; + minSlashableStake[1] = new uint256[](2); + minSlashableStake[1][0] = 200; + minSlashableStake[1][1] = 400; + minSlashableStake[2] = new uint256[](2); + minSlashableStake[2][0] = 100; + minSlashableStake[2][1] = 150; + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, 3, "Should have 3 operators"); + assertEq(resultOperators[0], operator1, "Operator1 mismatch"); + assertEq(resultOperators[1], operator2, "Operator2 mismatch"); + assertEq(resultOperators[2], operator3, "Operator3 mismatch"); + + assertEq(resultWeights[0][0], 800, "Operator1 weight mismatch (500 + 300)"); + assertEq(resultWeights[1][0], 600, "Operator2 weight mismatch (200 + 400)"); + assertEq(resultWeights[2][0], 250, "Operator3 weight mismatch (100 + 150)"); + } + + function test_mixedOperatorsWithAndWithoutStake() public { + // Setup operator set with mixed stake amounts + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory minSlashableStake = new uint256[][](3); + minSlashableStake[0] = new uint256[](1); + minSlashableStake[0][0] = 1000; + minSlashableStake[1] = new uint256[](1); + minSlashableStake[1][0] = 0; + minSlashableStake[2] = new uint256[](1); + minSlashableStake[2][0] = 500; + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, 2, "Should have 2 operators with stake"); + assertEq(resultOperators[0], operator1, "Operator1 mismatch"); + assertEq(resultOperators[1], operator3, "Operator3 mismatch"); + assertEq(resultWeights[0][0], 1000, "Operator1 weight mismatch"); + assertEq(resultWeights[1][0], 500, "Operator3 weight mismatch"); + } + + function test_lookaheadBlocksUsed() public { + // This test ensures LOOKAHEAD_BLOCKS is used in the calculation + address[] memory operators = new address[](1); + operators[0] = operator1; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory minSlashableStake = new uint256[][](1); + minSlashableStake[0] = new uint256[](1); + minSlashableStake[0][0] = 1000; + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, 1, "Should have 1 operator"); + assertEq(resultWeights[0][0], 1000, "Weight should match"); + } + + function testFuzz_getOperatorWeights(uint8 numOperators, uint256 baseWeight) public { + numOperators = uint8(bound(numOperators, 1, 10)); + baseWeight = bound(baseWeight, 1, 1e18); + + address[] memory operators = new address[](numOperators); + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory minSlashableStake = new uint256[][](numOperators); + uint256 expectedNonZeroOperators = 0; + + for (uint256 i = 0; i < numOperators; i++) { + operators[i] = address(uint160(i + 100)); + minSlashableStake[i] = new uint256[](1); + minSlashableStake[i][0] = (i % 2 == 0) ? baseWeight * (i + 1) : 0; + if (minSlashableStake[i][0] > 0) { + expectedNonZeroOperators++; + } + } + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, expectedNonZeroOperators, "Operator count mismatch"); + assertEq(resultWeights.length, expectedNonZeroOperators, "Weight count mismatch"); + } +} diff --git a/test/unit/middlewareV2/ECDSATableCalculatorBaseUnit.t.sol b/test/unit/middlewareV2/ECDSATableCalculatorBaseUnit.t.sol new file mode 100644 index 000000000..e3a415e95 --- /dev/null +++ b/test/unit/middlewareV2/ECDSATableCalculatorBaseUnit.t.sol @@ -0,0 +1,841 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import { + KeyRegistrar, + IKeyRegistrarTypes +} from "eigenlayer-contracts/src/contracts/permissions/KeyRegistrar.sol"; +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import {IOperatorTableCalculatorTypes} from + "eigenlayer-contracts/src/contracts/interfaces/IOperatorTableCalculator.sol"; +import {IECDSATableCalculator} from "../../../src/interfaces/IECDSATableCalculator.sol"; +import { + OperatorSet, + OperatorSetLib +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {SlashingLib} from "eigenlayer-contracts/src/contracts/libraries/SlashingLib.sol"; +import {AllocationManagerMock} from "eigenlayer-contracts/src/test/mocks/AllocationManagerMock.sol"; +import {ECDSATableCalculatorBase} from + "../../../src/middlewareV2/tableCalculator/ECDSATableCalculatorBase.sol"; +import {MockEigenLayerDeployer} from "./MockDeployer.sol"; +import "test/utils/Random.sol"; + +// Mock implementation for testing abstract contract +contract ECDSATableCalculatorBaseHarness is ECDSATableCalculatorBase { + // Storage for mock weights + mapping(bytes32 => address[]) internal _mockOperators; + mapping(bytes32 => uint256[][]) internal _mockWeights; + + constructor( + IKeyRegistrar _keyRegistrar + ) ECDSATableCalculatorBase(_keyRegistrar) {} + + function setMockOperatorWeights( + OperatorSet calldata operatorSet, + address[] memory operators, + uint256[][] memory weights + ) external { + bytes32 key = operatorSet.key(); + _mockOperators[key] = operators; + _mockWeights[key] = weights; + } + + function _getOperatorWeights( + OperatorSet calldata operatorSet + ) internal view override returns (address[] memory operators, uint256[][] memory weights) { + bytes32 key = operatorSet.key(); + operators = _mockOperators[key]; + weights = _mockWeights[key]; + } +} + +/** + * @title ECDSATableCalculatorBaseUnitTests + * @notice Base contract for all ECDSATableCalculatorBase unit tests + */ +contract ECDSATableCalculatorBaseUnitTests is + MockEigenLayerDeployer, + IOperatorTableCalculatorTypes, + IKeyRegistrarTypes +{ + using OperatorSetLib for OperatorSet; + + // Test contracts + ECDSATableCalculatorBaseHarness public calculator; + + // Test addresses + address public avs1 = address(0x1); + address public avs2 = address(0x2); + address public operator1 = address(0x3); + address public operator2 = address(0x4); + address public operator3 = address(0x5); + + // Test operator sets + OperatorSet defaultOperatorSet; + OperatorSet alternativeOperatorSet; + + // ECDSA test keys (private keys for signature generation) + uint256 constant ECDSA_PRIV_KEY_1 = + 0x1234567890123456789012345678901234567890123456789012345678901234; + uint256 constant ECDSA_PRIV_KEY_2 = + 0x9876543210987654321098765432109876543210987654321098765432109876; + uint256 constant ECDSA_PRIV_KEY_3 = + 0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890; + + // ECDSA addresses (public keys) + address public ecdsaAddress1; + address public ecdsaAddress2; + address public ecdsaAddress3; + + // ECDSA key data (20-byte addresses) + bytes public ecdsaKey1; + bytes public ecdsaKey2; + bytes public ecdsaKey3; + + function setUp() public virtual { + _deployMockEigenLayer(); + + // Deploy calculator with KeyRegistrar + calculator = new ECDSATableCalculatorBaseHarness(keyRegistrar); + + // Set up operator sets + defaultOperatorSet = OperatorSet({avs: avs1, id: 0}); + alternativeOperatorSet = OperatorSet({avs: avs2, id: 1}); + + // Set up ECDSA addresses that correspond to the private keys + ecdsaAddress1 = vm.addr(ECDSA_PRIV_KEY_1); + ecdsaAddress2 = vm.addr(ECDSA_PRIV_KEY_2); + ecdsaAddress3 = vm.addr(ECDSA_PRIV_KEY_3); + + // Set up ECDSA key data (20-byte addresses) + ecdsaKey1 = abi.encodePacked(ecdsaAddress1); + ecdsaKey2 = abi.encodePacked(ecdsaAddress2); + ecdsaKey3 = abi.encodePacked(ecdsaAddress3); + + // Configure operator sets in AllocationManager + allocationManagerMock.setAVSRegistrar(avs1, IAVSRegistrar(avs1)); + allocationManagerMock.setAVSRegistrar(avs2, IAVSRegistrar(avs2)); + + // Configure operator sets for ECDSA + vm.prank(avs1); + keyRegistrar.configureOperatorSet(defaultOperatorSet, IKeyRegistrarTypes.CurveType.ECDSA); + + vm.prank(avs2); + keyRegistrar.configureOperatorSet( + alternativeOperatorSet, IKeyRegistrarTypes.CurveType.ECDSA + ); + } + + // Helper functions + function _registerOperatorKey( + address operator, + OperatorSet memory operatorSet, + address ecdsaAddress, + uint256 privKey + ) internal { + bytes memory signature = + _generateECDSASignature(operator, operatorSet, ecdsaAddress, privKey); + + vm.prank(operator); + keyRegistrar.registerKey(operator, operatorSet, abi.encodePacked(ecdsaAddress), signature); + } + + function _generateECDSASignature( + address operator, + OperatorSet memory operatorSet, + address ecdsaAddress, + uint256 privKey + ) internal view returns (bytes memory) { + bytes32 messageHash = + keyRegistrar.getECDSAKeyRegistrationMessageHash(operator, operatorSet, ecdsaAddress); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKey, messageHash); + return abi.encodePacked(r, s, v); + } + + function _createSingleWeightArray( + uint256 weight + ) internal pure returns (uint256[][] memory) { + uint256[][] memory weights = new uint256[][](1); + weights[0] = new uint256[](1); + weights[0][0] = weight; + return weights; + } + + function _createMultiWeightArray( + uint256[] memory weightValues + ) internal pure returns (uint256[][] memory) { + uint256[][] memory weights = new uint256[][](1); + weights[0] = weightValues; + return weights; + } +} + +/** + * @title ECDSATableCalculatorBaseUnitTests_calculateOperatorTable + * @notice Unit tests for ECDSATableCalculatorBase.calculateOperatorTable + */ +contract ECDSATableCalculatorBaseUnitTests_calculateOperatorTable is + ECDSATableCalculatorBaseUnitTests +{ + function test_noOperators() public { + // Set empty operators and weights + address[] memory operators = new address[](0); + uint256[][] memory weights = new uint256[][](0); + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + ECDSAOperatorInfo[] memory infos = calculator.calculateOperatorTable(defaultOperatorSet); + + assertEq(infos.length, 0, "Should have 0 operators"); + } + + function test_operatorsWithNoRegisteredKeys() public { + // Set operators without registering their keys + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + uint256[][] memory weights = new uint256[][](2); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + ECDSAOperatorInfo[] memory infos = calculator.calculateOperatorTable(defaultOperatorSet); + + // When no operators have registered keys, should return empty array + assertEq(infos.length, 0, "Should have 0 operators when none are registered"); + } + + function test_allOperatorsRegistered() public { + // Register operators + _registerOperatorKey(operator1, defaultOperatorSet, ecdsaAddress1, ECDSA_PRIV_KEY_1); + _registerOperatorKey(operator2, defaultOperatorSet, ecdsaAddress2, ECDSA_PRIV_KEY_2); + + // Set operators and weights + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + uint256[][] memory weights = new uint256[][](2); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + ECDSAOperatorInfo[] memory infos = calculator.calculateOperatorTable(defaultOperatorSet); + + assertEq(infos.length, 2, "Should have 2 operators"); + assertEq(infos[0].pubkey, ecdsaAddress1, "Operator1 pubkey mismatch"); + assertEq(infos[0].weights[0], 100, "Operator1 weight mismatch"); + assertEq(infos[1].pubkey, ecdsaAddress2, "Operator2 pubkey mismatch"); + assertEq(infos[1].weights[0], 200, "Operator2 weight mismatch"); + } + + function test_multipleWeightTypes() public { + // Register operators + _registerOperatorKey(operator1, defaultOperatorSet, ecdsaAddress1, ECDSA_PRIV_KEY_1); + _registerOperatorKey(operator2, defaultOperatorSet, ecdsaAddress2, ECDSA_PRIV_KEY_2); + + // Set operators and weights with multiple types + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + uint256[][] memory weights = new uint256[][](2); + uint256[] memory op1Weights = new uint256[](3); + op1Weights[0] = 100; + op1Weights[1] = 150; + op1Weights[2] = 50; + weights[0] = op1Weights; + + uint256[] memory op2Weights = new uint256[](3); + op2Weights[0] = 200; + op2Weights[1] = 250; + op2Weights[2] = 100; + weights[1] = op2Weights; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + ECDSAOperatorInfo[] memory infos = calculator.calculateOperatorTable(defaultOperatorSet); + + assertEq(infos.length, 2, "Should have 2 operators"); + assertEq(infos[0].weights.length, 3, "Operator1 should have 3 weight types"); + assertEq(infos[0].weights[0], 100, "Operator1 weight[0] mismatch"); + assertEq(infos[0].weights[1], 150, "Operator1 weight[1] mismatch"); + assertEq(infos[0].weights[2], 50, "Operator1 weight[2] mismatch"); + assertEq(infos[1].weights.length, 3, "Operator2 should have 3 weight types"); + assertEq(infos[1].weights[0], 200, "Operator2 weight[0] mismatch"); + assertEq(infos[1].weights[1], 250, "Operator2 weight[1] mismatch"); + assertEq(infos[1].weights[2], 100, "Operator2 weight[2] mismatch"); + } + + function test_mixedRegistrationStatus() public { + // Register only operator1 + _registerOperatorKey(operator1, defaultOperatorSet, ecdsaAddress1, ECDSA_PRIV_KEY_1); + + // Set operators and weights + address[] memory operators = new address[](3); + operators[0] = operator1; // registered + operators[1] = operator2; // not registered + operators[2] = operator3; // not registered + + uint256[][] memory weights = new uint256[][](3); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + weights[2] = _createSingleWeightArray(300)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + ECDSAOperatorInfo[] memory infos = calculator.calculateOperatorTable(defaultOperatorSet); + + assertEq(infos.length, 1, "Should have 1 operator (only registered ones count)"); + assertEq(infos[0].pubkey, ecdsaAddress1, "Operator1 pubkey mismatch"); + assertEq(infos[0].weights[0], 100, "Operator1 weight mismatch"); + } + + function test_singleOperatorRegistered() public { + // Test with 1 operator, 1 registered + address newOperator = address(uint160(100)); + + address[] memory operators = new address[](1); + operators[0] = newOperator; + uint256[][] memory weights = new uint256[][](1); + weights[0] = _createSingleWeightArray(100)[0]; + + _registerOperatorKey(newOperator, defaultOperatorSet, ecdsaAddress1, ECDSA_PRIV_KEY_1); + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + ECDSAOperatorInfo[] memory infos = calculator.calculateOperatorTable(defaultOperatorSet); + assertEq(infos.length, 1, "Should have 1 operator"); + assertEq(infos[0].pubkey, ecdsaAddress1, "Operator pubkey mismatch"); + assertEq(infos[0].weights[0], 100, "Operator weight mismatch"); + } + + function test_subsetRegisteredToAVS() public { + // Register all operator keys + _registerOperatorKey(operator1, defaultOperatorSet, ecdsaAddress1, ECDSA_PRIV_KEY_1); + _registerOperatorKey(operator2, defaultOperatorSet, ecdsaAddress2, ECDSA_PRIV_KEY_2); + _registerOperatorKey(operator3, defaultOperatorSet, ecdsaAddress3, ECDSA_PRIV_KEY_3); // Not in actual operator set + + // Set operators and weights + address[] memory operators = new address[](2); + operators[0] = operator1; // registered + operators[1] = operator2; // registered + + uint256[][] memory weights = new uint256[][](2); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + ECDSAOperatorInfo[] memory infos = calculator.calculateOperatorTable(defaultOperatorSet); + + assertEq(infos.length, 2, "Should have 2 operators"); + assertEq(infos[0].pubkey, ecdsaAddress1, "Operator1 pubkey mismatch"); + assertEq(infos[0].weights[0], 100, "Operator1 weight mismatch"); + assertEq(infos[1].pubkey, ecdsaAddress2, "Operator2 pubkey mismatch"); + assertEq(infos[1].weights[0], 200, "Operator2 weight mismatch"); + } + + function test_subsetRegisteredKey() public { + // Register operator1 and operator3, but not operator2 + _registerOperatorKey(operator1, defaultOperatorSet, ecdsaAddress1, ECDSA_PRIV_KEY_1); + _registerOperatorKey(operator3, defaultOperatorSet, ecdsaAddress3, ECDSA_PRIV_KEY_3); + + // Set operators and weights + address[] memory operators = new address[](3); + operators[0] = operator1; // registered + operators[1] = operator2; // not registered + operators[2] = operator3; // registered + + uint256[][] memory weights = new uint256[][](3); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; // This weight won't be included + weights[2] = _createSingleWeightArray(300)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + ECDSAOperatorInfo[] memory infos = calculator.calculateOperatorTable(defaultOperatorSet); + + assertEq(infos.length, 2, "Should have 2 operators (only registered ones)"); + assertEq(infos[0].pubkey, ecdsaAddress1, "Operator1 pubkey mismatch"); + assertEq(infos[0].weights[0], 100, "Operator1 weight mismatch"); + assertEq(infos[1].pubkey, ecdsaAddress3, "Operator3 pubkey mismatch"); + assertEq(infos[1].weights[0], 300, "Operator3 weight mismatch"); + } + + function test_emptyOperatorSetReturnsEmptyArray() public { + // Test with operators that exist but none registered + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + uint256[][] memory weights = new uint256[][](3); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + weights[2] = _createSingleWeightArray(300)[0]; + + // Don't register any operators + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + ECDSAOperatorInfo[] memory infos = calculator.calculateOperatorTable(defaultOperatorSet); + + // Verify empty array when no operators are registered + assertEq(infos.length, 0, "Should have 0 operators when none are registered"); + } + + function testFuzz_calculateOperatorTable( + Randomness r, + uint8 numOperators, + uint8 numRegistered + ) public rand(r) { + numOperators = uint8(r.Uint256() % 10 + 1); // 1-10 operators + numRegistered = uint8(r.Uint256() % (numOperators + 1)); // 0 to numOperators registered + + address[] memory operators = new address[](numOperators); + uint256[][] memory weights = new uint256[][](numOperators); + + // Generate random operators and weights + for (uint256 i = 0; i < numOperators; i++) { + operators[i] = address(uint160(r.Uint256())); + weights[i] = _createSingleWeightArray(r.Uint256() % 1000 + 1)[0]; + } + + // Register random subset of operators + uint256[] memory registeredIndices = new uint256[](numRegistered); + for (uint256 i = 0; i < numRegistered; i++) { + uint256 idx = r.Uint256() % numOperators; + // Ensure unique indices + bool unique = true; + for (uint256 j = 0; j < i; j++) { + if (registeredIndices[j] == idx) { + unique = false; + break; + } + } + if (unique) { + registeredIndices[i] = idx; + address ecdsaAddr = vm.addr(uint256(keccak256(abi.encode(operators[idx], i)))); + _registerOperatorKey( + operators[idx], + defaultOperatorSet, + ecdsaAddr, + uint256(keccak256(abi.encode(operators[idx], i))) + ); + } + } + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + ECDSAOperatorInfo[] memory infos = calculator.calculateOperatorTable(defaultOperatorSet); + + // Count actual registered operators + uint256 actualRegistered = 0; + for (uint256 i = 0; i < numOperators; i++) { + if (keyRegistrar.isRegistered(defaultOperatorSet, operators[i])) actualRegistered++; + } + + assertEq( + infos.length, actualRegistered, "Should have correct number of registered operators" + ); + } +} + +/** + * @title ECDSATableCalculatorBaseUnitTests_calculateOperatorTableBytes + * @notice Unit tests for ECDSATableCalculatorBase.calculateOperatorTableBytes + */ +contract ECDSATableCalculatorBaseUnitTests_calculateOperatorTableBytes is + ECDSATableCalculatorBaseUnitTests +{ + function test_encodesCorrectly() public { + // Register operator + _registerOperatorKey(operator1, defaultOperatorSet, ecdsaAddress1, ECDSA_PRIV_KEY_1); + + // Set operators and weights + address[] memory operators = new address[](1); + operators[0] = operator1; + uint256[][] memory weights = _createSingleWeightArray(100); + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + bytes memory tableBytes = calculator.calculateOperatorTableBytes(defaultOperatorSet); + + // Decode and verify + ECDSAOperatorInfo[] memory decodedInfos = abi.decode(tableBytes, (ECDSAOperatorInfo[])); + + assertEq(decodedInfos.length, 1, "Should have 1 operator"); + assertEq(decodedInfos[0].weights[0], 100, "Total weight should be 100"); + assertEq(decodedInfos[0].pubkey, ecdsaAddress1, "Pubkey mismatch"); + } + + function test_multipleOperatorsEncodedCorrectly() public { + // Register multiple operators + _registerOperatorKey(operator1, defaultOperatorSet, ecdsaAddress1, ECDSA_PRIV_KEY_1); + _registerOperatorKey(operator2, defaultOperatorSet, ecdsaAddress2, ECDSA_PRIV_KEY_2); + _registerOperatorKey(operator3, defaultOperatorSet, ecdsaAddress3, ECDSA_PRIV_KEY_3); + + // Set operators and weights + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + uint256[][] memory weights = new uint256[][](3); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + weights[2] = _createSingleWeightArray(300)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + bytes memory tableBytes = calculator.calculateOperatorTableBytes(defaultOperatorSet); + + // Decode and verify + ECDSAOperatorInfo[] memory decodedInfos = abi.decode(tableBytes, (ECDSAOperatorInfo[])); + + assertEq(decodedInfos.length, 3, "Should have 3 operators"); + assertEq(decodedInfos[0].pubkey, ecdsaAddress1, "Operator1 pubkey mismatch"); + assertEq(decodedInfos[0].weights[0], 100, "Operator1 weight mismatch"); + assertEq(decodedInfos[1].pubkey, ecdsaAddress2, "Operator2 pubkey mismatch"); + assertEq(decodedInfos[1].weights[0], 200, "Operator2 weight mismatch"); + assertEq(decodedInfos[2].pubkey, ecdsaAddress3, "Operator3 pubkey mismatch"); + assertEq(decodedInfos[2].weights[0], 300, "Operator3 weight mismatch"); + } + + function test_emptyOperatorSetEncodesEmptyArray() public { + // Don't register any operators + address[] memory operators = new address[](0); + uint256[][] memory weights = new uint256[][](0); + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + bytes memory tableBytes = calculator.calculateOperatorTableBytes(defaultOperatorSet); + + // Decode and verify + ECDSAOperatorInfo[] memory decodedInfos = abi.decode(tableBytes, (ECDSAOperatorInfo[])); + + assertEq(decodedInfos.length, 0, "Should encode empty array"); + } + + function testFuzz_encodesCorrectly(Randomness r, uint256 weight) public rand(r) { + weight = r.Uint256() % 1e18 + 1; // 1 to 1e18 + + // Register operator + _registerOperatorKey(operator1, defaultOperatorSet, ecdsaAddress1, ECDSA_PRIV_KEY_1); + + // Set operators and weights + address[] memory operators = new address[](1); + operators[0] = operator1; + uint256[][] memory weights = _createSingleWeightArray(weight); + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + bytes memory tableBytes = calculator.calculateOperatorTableBytes(defaultOperatorSet); + + // Decode and verify + ECDSAOperatorInfo[] memory decodedInfos = abi.decode(tableBytes, (ECDSAOperatorInfo[])); + + assertEq(decodedInfos[0].weights[0], weight, "Weight mismatch"); + } + + function testFuzz_multipleWeightTypesEncoded( + Randomness r, + uint8 numWeightTypes + ) public rand(r) { + numWeightTypes = uint8(r.Uint256() % 5 + 1); // 1-5 weight types + + // Register operator + _registerOperatorKey(operator1, defaultOperatorSet, ecdsaAddress1, ECDSA_PRIV_KEY_1); + + // Create random weights + uint256[] memory weightValues = new uint256[](numWeightTypes); + for (uint256 i = 0; i < numWeightTypes; i++) { + weightValues[i] = r.Uint256() % 1000 + 1; + } + + address[] memory operators = new address[](1); + operators[0] = operator1; + uint256[][] memory weights = new uint256[][](1); + weights[0] = weightValues; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + bytes memory tableBytes = calculator.calculateOperatorTableBytes(defaultOperatorSet); + + // Decode and verify + ECDSAOperatorInfo[] memory decodedInfos = abi.decode(tableBytes, (ECDSAOperatorInfo[])); + + assertEq(decodedInfos.length, 1, "Should have 1 operator"); + assertEq(decodedInfos[0].weights.length, numWeightTypes, "Weight types mismatch"); + + for (uint256 i = 0; i < numWeightTypes; i++) { + assertEq(decodedInfos[0].weights[i], weightValues[i], "Weight value mismatch"); + } + } +} + +/** + * @title ECDSATableCalculatorBaseUnitTests_getOperatorSetWeights + * @notice Unit tests for ECDSATableCalculatorBase.getOperatorSetWeights + */ +contract ECDSATableCalculatorBaseUnitTests_getOperatorSetWeights is + ECDSATableCalculatorBaseUnitTests +{ + function test_returnsImplementationResult() public { + // Set mock weights + address[] memory expectedOperators = new address[](2); + expectedOperators[0] = operator1; + expectedOperators[1] = operator2; + + uint256[][] memory expectedWeights = new uint256[][](2); + expectedWeights[0] = _createSingleWeightArray(100)[0]; + expectedWeights[1] = _createSingleWeightArray(200)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, expectedOperators, expectedWeights); + + (address[] memory operators, uint256[][] memory weights) = + calculator.getOperatorSetWeights(defaultOperatorSet); + + assertEq(operators.length, expectedOperators.length, "Operators length mismatch"); + assertEq(weights.length, expectedWeights.length, "Weights length mismatch"); + + for (uint256 i = 0; i < operators.length; i++) { + assertEq(operators[i], expectedOperators[i], "Operator address mismatch"); + assertEq(weights[i][0], expectedWeights[i][0], "Weight value mismatch"); + } + } + + function test_emptyOperatorSet() public { + // Set empty operators and weights + address[] memory expectedOperators = new address[](0); + uint256[][] memory expectedWeights = new uint256[][](0); + + calculator.setMockOperatorWeights(defaultOperatorSet, expectedOperators, expectedWeights); + + (address[] memory operators, uint256[][] memory weights) = + calculator.getOperatorSetWeights(defaultOperatorSet); + + assertEq(operators.length, 0, "Should return empty operators array"); + assertEq(weights.length, 0, "Should return empty weights array"); + } + + function test_alternativeOperatorSet() public { + // Set weights for alternative operator set + address[] memory expectedOperators = new address[](1); + expectedOperators[0] = operator3; + + uint256[][] memory expectedWeights = new uint256[][](1); + expectedWeights[0] = _createSingleWeightArray(500)[0]; + + calculator.setMockOperatorWeights( + alternativeOperatorSet, expectedOperators, expectedWeights + ); + + (address[] memory operators, uint256[][] memory weights) = + calculator.getOperatorSetWeights(alternativeOperatorSet); + + assertEq(operators.length, 1, "Operators length mismatch"); + assertEq(operators[0], operator3, "Operator address mismatch"); + assertEq(weights[0][0], 500, "Weight value mismatch"); + } + + function testFuzz_returnsImplementationResult( + Randomness r, + uint8 numOperators + ) public rand(r) { + numOperators = uint8(r.Uint256() % 20); // 0-19 operators + + address[] memory expectedOperators = new address[](numOperators); + uint256[][] memory expectedWeights = new uint256[][](numOperators); + + for (uint256 i = 0; i < numOperators; i++) { + expectedOperators[i] = address(uint160(r.Uint256())); + expectedWeights[i] = _createSingleWeightArray(r.Uint256() % 1000 + 1)[0]; + } + + calculator.setMockOperatorWeights(defaultOperatorSet, expectedOperators, expectedWeights); + + (address[] memory operators, uint256[][] memory weights) = + calculator.getOperatorSetWeights(defaultOperatorSet); + + assertEq(operators.length, numOperators, "Operators length mismatch"); + assertEq(weights.length, numOperators, "Weights length mismatch"); + + for (uint256 i = 0; i < numOperators; i++) { + assertEq(operators[i], expectedOperators[i], "Operator address mismatch"); + assertEq(weights[i][0], expectedWeights[i][0], "Weight value mismatch"); + } + } +} + +/** + * @title ECDSATableCalculatorBaseUnitTests_getOperatorWeights + * @notice Unit tests for ECDSATableCalculatorBase.getOperatorWeights + */ +contract ECDSATableCalculatorBaseUnitTests_getOperatorWeights is + ECDSATableCalculatorBaseUnitTests +{ + function test_operatorExists() public { + // Set operators and weights + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + uint256[][] memory weights = new uint256[][](3); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + weights[2] = _createSingleWeightArray(300)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + uint256[] memory op1Weights = calculator.getOperatorWeights(defaultOperatorSet, operator1); + assertEq(op1Weights.length, 1, "Operator1 should have 1 weight type"); + assertEq(op1Weights[0], 100, "Operator1 weight mismatch"); + + uint256[] memory op2Weights = calculator.getOperatorWeights(defaultOperatorSet, operator2); + assertEq(op2Weights.length, 1, "Operator2 should have 1 weight type"); + assertEq(op2Weights[0], 200, "Operator2 weight mismatch"); + + uint256[] memory op3Weights = calculator.getOperatorWeights(defaultOperatorSet, operator3); + assertEq(op3Weights.length, 1, "Operator3 should have 1 weight type"); + assertEq(op3Weights[0], 300, "Operator3 weight mismatch"); + } + + function test_operatorDoesNotExist() public { + // Set operators and weights + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + uint256[][] memory weights = new uint256[][](2); + weights[0] = _createSingleWeightArray(100)[0]; + weights[1] = _createSingleWeightArray(200)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + uint256[] memory op3Weights = calculator.getOperatorWeights(defaultOperatorSet, operator3); + assertEq(op3Weights.length, 0, "Non-existent operator should return empty array"); + + uint256[] memory deadWeights = + calculator.getOperatorWeights(defaultOperatorSet, address(0xdead)); + assertEq(deadWeights.length, 0, "Random address should return empty array"); + } + + function test_emptyOperatorSet() public { + // Set empty operators and weights + address[] memory operators = new address[](0); + uint256[][] memory weights = new uint256[][](0); + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + uint256[] memory op1Weights = calculator.getOperatorWeights(defaultOperatorSet, operator1); + assertEq(op1Weights.length, 0, "Should return empty array for empty set"); + } + + function test_zeroWeight() public { + // Set operator with zero weight + address[] memory operators = new address[](1); + operators[0] = operator1; + + uint256[][] memory weights = new uint256[][](1); + weights[0] = _createSingleWeightArray(0)[0]; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + uint256[] memory op1Weights = calculator.getOperatorWeights(defaultOperatorSet, operator1); + assertEq(op1Weights.length, 1, "Should have 1 weight type"); + assertEq(op1Weights[0], 0, "Should return 0 for zero weight"); + } + + function test_multipleWeightTypes() public { + // Set operator with multiple weight types + address[] memory operators = new address[](1); + operators[0] = operator1; + + uint256[][] memory weights = new uint256[][](1); + uint256[] memory multiWeights = new uint256[](3); + multiWeights[0] = 100; + multiWeights[1] = 200; + multiWeights[2] = 300; + weights[0] = multiWeights; + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + // getOperatorWeights returns all weight types + uint256[] memory op1Weights = calculator.getOperatorWeights(defaultOperatorSet, operator1); + assertEq(op1Weights.length, 3, "Should have 3 weight types"); + assertEq(op1Weights[0], 100, "First weight type mismatch"); + assertEq(op1Weights[1], 200, "Second weight type mismatch"); + assertEq(op1Weights[2], 300, "Third weight type mismatch"); + } + + function testFuzz_getOperatorWeights( + Randomness r, + address operator, + uint256 weight + ) public rand(r) { + weight = r.Uint256() % 1e18; // 0 to 1e18 + + // Set single operator + address[] memory operators = new address[](1); + operators[0] = operator; + + uint256[][] memory weights = _createSingleWeightArray(weight); + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + uint256[] memory opWeights = calculator.getOperatorWeights(defaultOperatorSet, operator); + assertEq(opWeights.length, 1, "Should have 1 weight type"); + assertEq(opWeights[0], weight, "Weight mismatch"); + + // Different operator should return empty array + address differentOperator = address(uint160(uint256(uint160(operator)) + 1)); + uint256[] memory diffWeights = + calculator.getOperatorWeights(defaultOperatorSet, differentOperator); + assertEq(diffWeights.length, 0, "Different operator should return empty array"); + } + + function testFuzz_multipleOperators(Randomness r, uint8 numOperators) public rand(r) { + numOperators = uint8(r.Uint256() % 10 + 1); // 1-10 operators + + address[] memory operators = new address[](numOperators); + uint256[][] memory weights = new uint256[][](numOperators); + uint256[] memory expectedWeights = new uint256[](numOperators); + + for (uint256 i = 0; i < numOperators; i++) { + operators[i] = address(uint160(r.Uint256())); + expectedWeights[i] = r.Uint256() % 1000 + 1; + weights[i] = _createSingleWeightArray(expectedWeights[i])[0]; + } + + calculator.setMockOperatorWeights(defaultOperatorSet, operators, weights); + + // Verify each operator's weight + for (uint256 i = 0; i < numOperators; i++) { + uint256[] memory opWeights = + calculator.getOperatorWeights(defaultOperatorSet, operators[i]); + assertEq(opWeights.length, 1, "Should have 1 weight type"); + assertEq(opWeights[0], expectedWeights[i], "Weight mismatch"); + } + + // Non-existent operator should return empty array + address nonExistent = address(uint160(r.Uint256())); + bool exists = false; + for (uint256 i = 0; i < numOperators; i++) { + if (operators[i] == nonExistent) { + exists = true; + break; + } + } + if (!exists) { + uint256[] memory nonExistentWeights = + calculator.getOperatorWeights(defaultOperatorSet, nonExistent); + assertEq( + nonExistentWeights.length, 0, "Non-existent operator should return empty array" + ); + } + } +} diff --git a/test/unit/middlewareV2/ECDSATableCalculatorUnit.t.sol b/test/unit/middlewareV2/ECDSATableCalculatorUnit.t.sol new file mode 100644 index 000000000..1260f3aeb --- /dev/null +++ b/test/unit/middlewareV2/ECDSATableCalculatorUnit.t.sol @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import { + KeyRegistrar, + IKeyRegistrarTypes +} from "eigenlayer-contracts/src/contracts/permissions/KeyRegistrar.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import {IOperatorTableCalculatorTypes} from + "eigenlayer-contracts/src/contracts/interfaces/IOperatorTableCalculator.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import { + OperatorSet, + OperatorSetLib +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; + +import {ECDSATableCalculator} from + "../../../src/middlewareV2/tableCalculator/ECDSATableCalculator.sol"; +import {MockEigenLayerDeployer} from "./MockDeployer.sol"; +import "test/utils/Random.sol"; + +// Harness to test internal functions +contract ECDSATableCalculatorHarness is ECDSATableCalculator { + constructor( + IKeyRegistrar _keyRegistrar, + IAllocationManager _allocationManager, + uint256 _LOOKAHEAD_BLOCKS + ) ECDSATableCalculator(_keyRegistrar, _allocationManager, _LOOKAHEAD_BLOCKS) {} + + function exposed_getOperatorWeights( + OperatorSet calldata operatorSet + ) external view returns (address[] memory operators, uint256[][] memory weights) { + return _getOperatorWeights(operatorSet); + } +} + +/** + * @title ECDSATableCalculatorUnitTests + * @notice Base contract for all ECDSATableCalculator unit tests + */ +contract ECDSATableCalculatorUnitTests is MockEigenLayerDeployer, IOperatorTableCalculatorTypes { + using OperatorSetLib for OperatorSet; + + // Test contracts + ECDSATableCalculatorHarness public calculator; + + // Test addresses + address public avs1 = address(0x1); + address public avs2 = address(0x2); + address public operator1 = address(0x3); + address public operator2 = address(0x4); + address public operator3 = address(0x5); + + // Test strategies + IStrategy public strategy1 = IStrategy(address(0x100)); + IStrategy public strategy2 = IStrategy(address(0x200)); + + // Test operator sets + OperatorSet defaultOperatorSet; + OperatorSet alternativeOperatorSet; + + // Test constants + uint256 public constant TEST_LOOKAHEAD_BLOCKS = 100; + + function setUp() public virtual { + _deployMockEigenLayer(); + + // Deploy calculator with mocked AllocationManager + calculator = new ECDSATableCalculatorHarness( + IKeyRegistrar(address(keyRegistrar)), + IAllocationManager(address(allocationManagerMock)), + TEST_LOOKAHEAD_BLOCKS + ); + + // Set up operator sets + defaultOperatorSet = OperatorSet({avs: avs1, id: 0}); + alternativeOperatorSet = OperatorSet({avs: avs2, id: 1}); + + // Configure operator sets in AllocationManager + allocationManagerMock.setAVSRegistrar(avs1, IAVSRegistrar(avs1)); + allocationManagerMock.setAVSRegistrar(avs2, IAVSRegistrar(avs2)); + + // Configure operator sets for ECDSA + vm.prank(avs1); + keyRegistrar.configureOperatorSet(defaultOperatorSet, IKeyRegistrarTypes.CurveType.ECDSA); + + vm.prank(avs2); + keyRegistrar.configureOperatorSet( + alternativeOperatorSet, IKeyRegistrarTypes.CurveType.ECDSA + ); + } + + // Helper functions + function _setupOperatorSet( + OperatorSet memory operatorSet, + address[] memory operators, + IStrategy[] memory strategies, + uint256[][] memory minSlashableStake + ) internal { + allocationManagerMock.setMembersInOperatorSet(operatorSet, operators); + allocationManagerMock.setStrategiesInOperatorSet(operatorSet, strategies); + allocationManagerMock.setMinimumSlashableStake( + operatorSet, operators, strategies, minSlashableStake + ); + } + + function _createSingleWeightArray( + uint256 weight + ) internal pure returns (uint256[][] memory) { + uint256[][] memory weights = new uint256[][](1); + weights[0] = new uint256[](1); + weights[0][0] = weight; + return weights; + } + + function _createMultiWeightArray( + uint256[] memory weightValues + ) internal pure returns (uint256[][] memory) { + uint256[][] memory weights = new uint256[][](1); + weights[0] = weightValues; + return weights; + } + + function _registerOperatorECDSAKey( + address operator, + OperatorSet memory operatorSet, + address ecdsaAddress + ) internal { + bytes memory pubkey = abi.encode(ecdsaAddress); + bytes memory signature = ""; + + vm.prank(operator); + keyRegistrar.registerKey(operator, operatorSet, pubkey, signature); + } +} + +/** + * @title ECDSATableCalculatorUnitTests_getOperatorWeights + * @notice Unit tests for ECDSATableCalculator._getOperatorWeights + */ +contract ECDSATableCalculatorUnitTests_getOperatorWeights is ECDSATableCalculatorUnitTests { + function test_noOperators() public { + // Setup empty operator set + address[] memory operators = new address[](0); + IStrategy[] memory strategies = new IStrategy[](0); + uint256[][] memory minSlashableStake = new uint256[][](0); + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, 0, "Should have no operators"); + assertEq(resultWeights.length, 0, "Should have no weights"); + } + + function test_singleOperatorWithStake() public { + // Setup operator set with one operator + address[] memory operators = new address[](1); + operators[0] = operator1; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory minSlashableStake = new uint256[][](1); + minSlashableStake[0] = new uint256[](1); + minSlashableStake[0][0] = 1000; + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, 1, "Should have 1 operator"); + assertEq(resultOperators[0], operator1, "Operator mismatch"); + assertEq(resultWeights.length, 1, "Should have 1 weight array"); + assertEq(resultWeights[0][0], 1000, "Weight mismatch"); + } + + function test_singleOperatorWithZeroStake() public { + // Setup operator set with one operator with zero stake + address[] memory operators = new address[](1); + operators[0] = operator1; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory minSlashableStake = new uint256[][](1); + minSlashableStake[0] = new uint256[](1); + minSlashableStake[0][0] = 0; + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, 0, "Should have no operators with zero stake"); + assertEq(resultWeights.length, 0, "Should have no weights"); + } + + function test_multipleOperatorsWithStake() public { + // Setup operator set with multiple operators + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + IStrategy[] memory strategies = new IStrategy[](2); + strategies[0] = strategy1; + strategies[1] = strategy2; + + uint256[][] memory minSlashableStake = new uint256[][](3); + minSlashableStake[0] = new uint256[](2); + minSlashableStake[0][0] = 500; + minSlashableStake[0][1] = 300; + minSlashableStake[1] = new uint256[](2); + minSlashableStake[1][0] = 200; + minSlashableStake[1][1] = 400; + minSlashableStake[2] = new uint256[](2); + minSlashableStake[2][0] = 100; + minSlashableStake[2][1] = 150; + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, 3, "Should have 3 operators"); + assertEq(resultOperators[0], operator1, "Operator1 mismatch"); + assertEq(resultOperators[1], operator2, "Operator2 mismatch"); + assertEq(resultOperators[2], operator3, "Operator3 mismatch"); + + assertEq(resultWeights[0][0], 800, "Operator1 weight mismatch (500 + 300)"); + assertEq(resultWeights[1][0], 600, "Operator2 weight mismatch (200 + 400)"); + assertEq(resultWeights[2][0], 250, "Operator3 weight mismatch (100 + 150)"); + } + + function test_mixedOperatorsWithAndWithoutStake() public { + // Setup operator set with mixed stake amounts + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory minSlashableStake = new uint256[][](3); + minSlashableStake[0] = new uint256[](1); + minSlashableStake[0][0] = 1000; + minSlashableStake[1] = new uint256[](1); + minSlashableStake[1][0] = 0; + minSlashableStake[2] = new uint256[](1); + minSlashableStake[2][0] = 500; + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, 2, "Should have 2 operators with stake"); + assertEq(resultOperators[0], operator1, "Operator1 mismatch"); + assertEq(resultOperators[1], operator3, "Operator3 mismatch"); + assertEq(resultWeights[0][0], 1000, "Operator1 weight mismatch"); + assertEq(resultWeights[1][0], 500, "Operator3 weight mismatch"); + } + + function test_lookaheadBlocksUsed() public { + // This test ensures LOOKAHEAD_BLOCKS is used in the calculation + address[] memory operators = new address[](1); + operators[0] = operator1; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory minSlashableStake = new uint256[][](1); + minSlashableStake[0] = new uint256[](1); + minSlashableStake[0][0] = 1000; + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + // Verify that the correct futureBlock is used + uint32 expectedFutureBlock = uint32(block.number + TEST_LOOKAHEAD_BLOCKS); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, 1, "Should have 1 operator"); + assertEq(resultWeights[0][0], 1000, "Weight should match"); + } + + function testFuzz_getOperatorWeights(uint8 numOperators, uint256 baseWeight) public { + numOperators = uint8(bound(numOperators, 1, 10)); + baseWeight = bound(baseWeight, 1, 1e18); + + address[] memory operators = new address[](numOperators); + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory minSlashableStake = new uint256[][](numOperators); + uint256 expectedNonZeroOperators = 0; + + for (uint256 i = 0; i < numOperators; i++) { + operators[i] = address(uint160(i + 100)); + minSlashableStake[i] = new uint256[](1); + minSlashableStake[i][0] = (i % 2 == 0) ? baseWeight * (i + 1) : 0; + if (minSlashableStake[i][0] > 0) { + expectedNonZeroOperators++; + } + } + + _setupOperatorSet(defaultOperatorSet, operators, strategies, minSlashableStake); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(defaultOperatorSet); + + assertEq(resultOperators.length, expectedNonZeroOperators, "Operator count mismatch"); + assertEq(resultWeights.length, expectedNonZeroOperators, "Weight count mismatch"); + } +} diff --git a/test/unit/middlewareV2/MockDeployer.sol b/test/unit/middlewareV2/MockDeployer.sol new file mode 100644 index 000000000..ba73cb71e --- /dev/null +++ b/test/unit/middlewareV2/MockDeployer.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +import {KeyRegistrar} from "eigenlayer-contracts/src/contracts/permissions/KeyRegistrar.sol"; +import {PermissionController} from + "eigenlayer-contracts/src/contracts/permissions/PermissionController.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; + +import {AllocationManagerMock} from "test/mocks/AllocationManagerMock.sol"; +import "test/mocks/KeyRegistrarMock.sol"; +import "test/utils/Random.sol"; + +import "forge-std/Test.sol"; + +abstract contract MockEigenLayerDeployer is Test { + Vm cheats = Vm(VM_ADDRESS); + + /// @dev addresses that should be excluded from fuzzing + mapping(address => bool) public isExcludedFuzzAddress; + + modifier filterFuzzedAddressInputs( + address addr + ) { + cheats.assume(!isExcludedFuzzAddress[addr]); + _; + } + + /// @dev set the random seed for the current test + modifier rand( + Randomness r + ) { + r.set(); + _; + } + + function random() internal returns (Randomness) { + return Randomness.wrap(Random.SEED).shuffle(); + } + + // State Variables + ProxyAdmin public proxyAdmin; + AllocationManagerMock public allocationManagerMock; + KeyRegistrarMock public keyRegistrarMock; + + /// @dev In order to test key functionality, for the table calculators, we also deploy the actual KeyRegistrar implementation + PermissionController permissionController; + PermissionController permissionControllerImplementation; + KeyRegistrar keyRegistrarImplementation; + KeyRegistrar keyRegistrar; + + function _deployMockEigenLayer() internal { + // Deploy the proxy admin + proxyAdmin = new ProxyAdmin(); + + // Deploy mocks + allocationManagerMock = new AllocationManagerMock(); + keyRegistrarMock = new KeyRegistrarMock(); + + // Deploy the actual PermissionController & KeyRegistrar implementations + permissionControllerImplementation = new PermissionController("9.9.9"); + permissionController = PermissionController( + address( + new TransparentUpgradeableProxy( + address(permissionControllerImplementation), address(proxyAdmin), "" + ) + ) + ); + + keyRegistrarImplementation = new KeyRegistrar( + permissionController, IAllocationManager(address(allocationManagerMock)), "9.9.9" + ); + keyRegistrar = KeyRegistrar( + address( + new TransparentUpgradeableProxy( + address(keyRegistrarImplementation), address(proxyAdmin), "" + ) + ) + ); + + // Filter our proxyAdmin from fuzzing + isExcludedFuzzAddress[address(proxyAdmin)] = true; + } +} diff --git a/test/utils/CoreDeployLib.sol b/test/utils/CoreDeployLib.sol index dff9a02b3..ddd67decd 100644 --- a/test/utils/CoreDeployLib.sol +++ b/test/utils/CoreDeployLib.sol @@ -170,6 +170,7 @@ library CoreDeployLib { address strategyManagerImpl = address( new StrategyManager( + IAllocationManager(deployments.allocationManager), IDelegationManager(deployments.delegationManager), IPauserRegistry(deployments.pauserRegistry), "1.0.0" @@ -179,6 +180,7 @@ library CoreDeployLib { address allocationManagerImpl = address( new AllocationManager( IDelegationManager(deployments.delegationManager), + IStrategy(address(0)), // TODO: update this to the eigenStrategy, IPauserRegistry(deployments.pauserRegistry), IPermissionController(deployments.permissionController), config.allocationManager.deallocationDelay, @@ -223,16 +225,14 @@ library CoreDeployLib { ); upgradeCall = abi.encodeCall( - DelegationManager.initialize, - (config.delegationManager.initialOwner, config.delegationManager.initPausedStatus) + DelegationManager.initialize, (config.delegationManager.initPausedStatus) ); UpgradeableProxyLib.upgradeAndCall( deployments.delegationManager, delegationManagerImpl, upgradeCall ); upgradeCall = abi.encodeCall( - AllocationManager.initialize, - (config.allocationManager.initialOwner, config.allocationManager.initPausedStatus) + AllocationManager.initialize, (config.allocationManager.initPausedStatus) ); UpgradeableProxyLib.upgradeAndCall( deployments.allocationManager, allocationManagerImpl, upgradeCall @@ -262,9 +262,6 @@ library CoreDeployLib { new EigenPod( IETHPOSDeposit(ethPOSDeposit), IEigenPodManager(deployments.eigenPodManager), - config.eigenPod.genesisTimestamp == 0 - ? uint64(block.timestamp) - : config.eigenPod.genesisTimestamp, "1.0.0" ) ); diff --git a/test/utils/MockAVSDeployer.sol b/test/utils/MockAVSDeployer.sol index ad1547c65..d770bf9ae 100644 --- a/test/utils/MockAVSDeployer.sol +++ b/test/utils/MockAVSDeployer.sol @@ -275,6 +275,7 @@ contract MockAVSDeployer is Test { allocationManagerImplementation = new AllocationManager( delegationMock, + IStrategy(address(0)), // TODO: update this to the eigenStrategy pauserRegistry, permissionControllerMock, uint32(7 days), // DEALLOCATION_DELAY diff --git a/test/utils/Random.sol b/test/utils/Random.sol new file mode 100644 index 000000000..bb5a2bcc5 --- /dev/null +++ b/test/utils/Random.sol @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +type Randomness is uint256; + +using Random for Randomness global; + +library Random { + /// ----------------------------------------------------------------------- + /// Constants + /// ----------------------------------------------------------------------- + + /// @dev Equivalent to: `uint256(keccak256("RANDOMNESS.SEED"))`. + uint256 constant SEED = 0x93bfe7cafd9427243dc4fe8c6e706851eb6696ba8e48960dd74ecc96544938ce; + + /// @dev Equivalent to: `uint256(keccak256("RANDOMNESS.SLOT"))`. + uint256 constant SLOT = 0xd0660badbab446a974e6a19901c78a2ad88d7e4f1710b85e1cfc0878477344fd; + + /// ----------------------------------------------------------------------- + /// Helpers + /// ----------------------------------------------------------------------- + + function set( + Randomness r + ) internal returns (Randomness) { + /// @solidity memory-safe-assembly + assembly { + sstore(SLOT, r) + } + return r; + } + + function shuffle( + Randomness r + ) internal returns (Randomness) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, sload(SLOT)) + mstore(0x20, r) + r := keccak256(0x00, 0x20) + } + return r.set(); + } + + /// ----------------------------------------------------------------------- + /// Native Types + /// ----------------------------------------------------------------------- + + function Int256(Randomness r, int256 min, int256 max) internal returns (int256) { + return max <= min ? min : r.Int256() % (max - min) + min; + } + + function Int256( + Randomness r + ) internal returns (int256) { + return r.unwrap() % 2 == 0 ? int256(r.Uint256()) : -int256(r.Uint256()); + } + + function Int128(Randomness r, int128 min, int128 max) internal returns (int128) { + return int128(Int256(r, min, max)); + } + + function Int128( + Randomness r + ) internal returns (int128) { + return int128(Int256(r)); + } + + function Int64(Randomness r, int64 min, int64 max) internal returns (int64) { + return int64(Int256(r, min, max)); + } + + function Int64( + Randomness r + ) internal returns (int64) { + return int64(Int256(r)); + } + + function Int32(Randomness r, int32 min, int32 max) internal returns (int32) { + return int32(Int256(r, min, max)); + } + + function Uint256(Randomness r, uint256 min, uint256 max) internal returns (uint256) { + return max <= min ? min : r.Uint256() % (max - min) + min; + } + + function Uint256( + Randomness r + ) internal returns (uint256) { + return r.shuffle().unwrap(); + } + + function Uint128(Randomness r, uint128 min, uint128 max) internal returns (uint128) { + return uint128(Uint256(r, min, max)); + } + + function Uint128( + Randomness r + ) internal returns (uint128) { + return uint128(Uint256(r)); + } + + function Uint64(Randomness r, uint64 min, uint64 max) internal returns (uint64) { + return uint64(Uint256(r, min, max)); + } + + function Uint64( + Randomness r + ) internal returns (uint64) { + return uint64(Uint256(r)); + } + + function Uint32(Randomness r, uint32 min, uint32 max) internal returns (uint32) { + return uint32(Uint256(r, min, max)); + } + + function Uint32( + Randomness r + ) internal returns (uint32) { + return uint32(Uint256(r)); + } + + function Bytes32( + Randomness r + ) internal returns (bytes32) { + return bytes32(r.Uint256()); + } + + function Address( + Randomness r + ) internal returns (address) { + return address(uint160(r.Uint256(1, type(uint160).max))); + } + + function Boolean( + Randomness r + ) internal returns (bool) { + return r.Uint256() % 2 == 0; + } + + /// ----------------------------------------------------------------------- + /// Arrays + /// ----------------------------------------------------------------------- + + function Int256Array( + Randomness r, + uint256 len, + int256 min, + int256 max + ) internal returns (int256[] memory arr) { + arr = new int256[](len); + for (uint256 i; i < len; ++i) { + arr[i] = r.Int256(min, max); + } + } + + function Int128Array( + Randomness r, + uint256 len, + int128 min, + int128 max + ) internal returns (int128[] memory arr) { + arr = new int128[](len); + for (uint256 i; i < len; ++i) { + arr[i] = r.Int128(min, max); + } + } + + function Int64Array( + Randomness r, + uint256 len, + int64 min, + int64 max + ) internal returns (int64[] memory arr) { + arr = new int64[](len); + for (uint256 i; i < len; ++i) { + arr[i] = r.Int64(min, max); + } + } + + function Int32Array( + Randomness r, + uint256 len, + int32 min, + int32 max + ) internal returns (int32[] memory arr) { + arr = new int32[](len); + for (uint256 i; i < len; ++i) { + arr[i] = r.Int32(min, max); + } + } + + function Uint256Array( + Randomness r, + uint256 len, + uint256 min, + uint256 max + ) internal returns (uint256[] memory arr) { + arr = new uint256[](len); + for (uint256 i; i < len; ++i) { + arr[i] = uint256(r.Uint256(min, max)); + } + } + + function Uint32Array( + Randomness r, + uint256 len, + uint32 min, + uint32 max + ) internal returns (uint32[] memory arr) { + arr = new uint32[](len); + for (uint256 i; i < len; ++i) { + arr[i] = r.Uint32(min, max); + } + } + + /// ----------------------------------------------------------------------- + /// Helpers + /// ----------------------------------------------------------------------- + + function wrap( + uint256 r + ) internal pure returns (Randomness) { + return Randomness.wrap(r); + } + + function unwrap( + Randomness r + ) internal pure returns (uint256) { + return Randomness.unwrap(r); + } +}