diff --git a/CHANGELOG/CHANGELOG-1.5.0.md b/CHANGELOG/CHANGELOG-1.5.0.md new file mode 100644 index 00000000..c463db83 --- /dev/null +++ b/CHANGELOG/CHANGELOG-1.5.0.md @@ -0,0 +1,26 @@ +# v1.5.0 Hourglass + +The Hourglass release consists of a framework that supports the creation of task-based AVSs. The task-based AVSs are enabled through a `TaskMailbox` core contract deployed to all chains that support a `CertificateVerifier`. Additionally AVSs deploy their `TaskAVSRegistrar`. 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](https://github.com/Layr-Labs/hourglass-monorepo/blob/master/README.md). + +## Release Manager + +@0xrajath + +## Highlights + +This hourglass release only introduces new contracts. As a result, there are no breaking changes or deprecations. + +🚀 New Features + +- `TaskAVSRegistrar`: An instanced (per-AVS) eigenlayer middleware contract on L1 that is responsible for handling operator registration for specific operator sets of your AVS and providing the offchain components with socket endpoints for the Aggregator and Executor operators. It also keeps track of which operator sets are the aggregator and executors. It works by default, but can be extended to include additional onchain logic for your AVS. + +## Changelog + +- chore: bump up core deps +- feat: hourglass [PR #507](https://github.com/layr-labs/eigenlayer-middleware/pull/507) diff --git a/lib/eigenlayer-contracts b/lib/eigenlayer-contracts index fbfd00ca..a77bd0f0 160000 --- a/lib/eigenlayer-contracts +++ b/lib/eigenlayer-contracts @@ -1 +1 @@ -Subproject commit fbfd00ca3cb212d8fb4a95f2451616de9d1882fe +Subproject commit a77bd0f037bfb136065770f281e2dd34fba74866 diff --git a/src/avs/task/TaskAVSRegistrarBase.sol b/src/avs/task/TaskAVSRegistrarBase.sol new file mode 100644 index 00000000..5550ae29 --- /dev/null +++ b/src/avs/task/TaskAVSRegistrarBase.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {OwnableUpgradeable} from "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import {AVSRegistrarWithSocket} from + "../../middlewareV2/registrar/presets/AVSRegistrarWithSocket.sol"; +import {ITaskAVSRegistrarBase} from "../../interfaces/ITaskAVSRegistrarBase.sol"; +import {TaskAVSRegistrarBaseStorage} from "./TaskAVSRegistrarBaseStorage.sol"; + +/** + * @title TaskAVSRegistrarBase + * @author Layr Labs, Inc. + * @notice Abstract AVS Registrar for task-based AVSs + */ +abstract contract TaskAVSRegistrarBase is + Initializable, + OwnableUpgradeable, + AVSRegistrarWithSocket, + TaskAVSRegistrarBaseStorage +{ + /** + * @dev Constructor that passes parameters to parent + * @param _avs The address of the AVS + * @param _allocationManager The AllocationManager contract address + * @param _keyRegistrar The KeyRegistrar contract address + */ + constructor( + address _avs, + IAllocationManager _allocationManager, + IKeyRegistrar _keyRegistrar + ) AVSRegistrarWithSocket(_avs, _allocationManager, _keyRegistrar) { + _disableInitializers(); + } + + /** + * @dev Initializer for the upgradeable contract + * @param _owner The owner of the contract + * @param _initialConfig The initial AVS configuration + */ + function __TaskAVSRegistrarBase_init( + address _owner, + AvsConfig memory _initialConfig + ) internal onlyInitializing { + __Ownable_init(); + _transferOwnership(_owner); + _setAvsConfig(_initialConfig); + } + + /// @inheritdoc ITaskAVSRegistrarBase + function setAvsConfig( + AvsConfig memory config + ) external onlyOwner { + _setAvsConfig(config); + } + + /// @inheritdoc ITaskAVSRegistrarBase + function getAvsConfig() external view returns (AvsConfig memory) { + return avsConfig; + } + + /** + * @notice Internal function to set the AVS configuration + * @param config The AVS configuration to set + * @dev The executorOperatorSetIds must be monotonically increasing. + */ + function _setAvsConfig( + AvsConfig memory config + ) internal { + // Require at least one executor operator set + require(config.executorOperatorSetIds.length > 0, ExecutorOperatorSetIdsEmpty()); + + // Check monotonically increasing order and no aggregator overlap in one pass + for (uint256 i = 0; i < config.executorOperatorSetIds.length; i++) { + require( + config.aggregatorOperatorSetId != config.executorOperatorSetIds[i], + InvalidAggregatorOperatorSetId() + ); + require( + i == 0 || config.executorOperatorSetIds[i] > config.executorOperatorSetIds[i - 1], + DuplicateExecutorOperatorSetId() + ); + } + + avsConfig = config; + emit AvsConfigSet(config.aggregatorOperatorSetId, config.executorOperatorSetIds); + } +} diff --git a/src/avs/task/TaskAVSRegistrarBaseStorage.sol b/src/avs/task/TaskAVSRegistrarBaseStorage.sol new file mode 100644 index 00000000..da0afcc7 --- /dev/null +++ b/src/avs/task/TaskAVSRegistrarBaseStorage.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {ITaskAVSRegistrarBase} from "../../interfaces/ITaskAVSRegistrarBase.sol"; + +/** + * @title TaskAVSRegistrarBaseStorage + * @author Layr Labs, Inc. + * @notice Storage contract for TaskAVSRegistrarBase + * @dev This contract holds the storage variables for TaskAVSRegistrarBase + */ +abstract contract TaskAVSRegistrarBaseStorage is ITaskAVSRegistrarBase { + /// @notice Configuration for this AVS + AvsConfig public avsConfig; + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[48] private __gap; +} diff --git a/src/interfaces/ITaskAVSRegistrarBase.sol b/src/interfaces/ITaskAVSRegistrarBase.sol new file mode 100644 index 00000000..cfc42b1e --- /dev/null +++ b/src/interfaces/ITaskAVSRegistrarBase.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IAVSRegistrarInternal} from "./IAVSRegistrarInternal.sol"; +import {ISocketRegistry} from "./ISocketRegistryV2.sol"; + +/** + * @title ITaskAVSRegistrarBaseTypes + * @notice Interface defining the type structures used in the TaskAVSRegistrarBase + */ +interface ITaskAVSRegistrarBaseTypes { + /** + * @notice Configuration for the Task-based AVS + * @param aggregatorOperatorSetId The operator set ID responsible for aggregating results + * @param executorOperatorSetIds Array of operator set IDs responsible for executing tasks + */ + struct AvsConfig { + uint32 aggregatorOperatorSetId; + uint32[] executorOperatorSetIds; + } +} + +/** + * @title ITaskAVSRegistrarBaseErrors + * @notice Interface defining errors that can be thrown by the TaskAVSRegistrarBase + */ +interface ITaskAVSRegistrarBaseErrors { + /// @notice Thrown when an aggregator operator set id is also an executor operator set id + error InvalidAggregatorOperatorSetId(); + + /// @notice Thrown when executor operator set ids are not in monotonically increasing order (duplicate or unsorted) + error DuplicateExecutorOperatorSetId(); + + /// @notice Thrown when executor operator set ids are empty + error ExecutorOperatorSetIdsEmpty(); +} + +/** + * @title ITaskAVSRegistrarBaseEvents + * @notice Interface defining events emitted by the TaskAVSRegistrarBase + */ +interface ITaskAVSRegistrarBaseEvents is ITaskAVSRegistrarBaseTypes { + /** + * @notice Emitted when the AVS configuration is set + * @param aggregatorOperatorSetId The operator set ID responsible for aggregating results + * @param executorOperatorSetIds Array of operator set IDs responsible for executing tasks + */ + event AvsConfigSet(uint32 aggregatorOperatorSetId, uint32[] executorOperatorSetIds); +} + +/** + * @title ITaskAVSRegistrarBase + * @author Layr Labs, Inc. + * @notice Interface for TaskAVSRegistrarBase contract that manages AVS configuration + */ +interface ITaskAVSRegistrarBase is + ITaskAVSRegistrarBaseErrors, + ITaskAVSRegistrarBaseEvents, + IAVSRegistrar, + IAVSRegistrarInternal, + ISocketRegistry +{ + /** + * @notice Sets the configuration for this AVS + * @param config Configuration for the AVS + * @dev The executorOperatorSetIds must be monotonically increasing. + */ + function setAvsConfig( + AvsConfig memory config + ) external; + + /** + * @notice Gets the configuration for this AVS + * @return Configuration for the AVS + */ + function getAvsConfig() external view returns (AvsConfig memory); +} diff --git a/test/mocks/MockTaskAVSRegistrar.sol b/test/mocks/MockTaskAVSRegistrar.sol new file mode 100644 index 00000000..3466b988 --- /dev/null +++ b/test/mocks/MockTaskAVSRegistrar.sol @@ -0,0 +1,31 @@ +// 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 {TaskAVSRegistrarBase} from "../../src/avs/task/TaskAVSRegistrarBase.sol"; + +contract MockTaskAVSRegistrar is TaskAVSRegistrarBase { + /** + * @dev Constructor that passes parameters to parent TaskAVSRegistrarBase + * @param _avs The address of the AVS + * @param _allocationManager The AllocationManager contract address + * @param _keyRegistrar The KeyRegistrar contract address + */ + constructor( + address _avs, + IAllocationManager _allocationManager, + IKeyRegistrar _keyRegistrar + ) TaskAVSRegistrarBase(_avs, _allocationManager, _keyRegistrar) {} + + /** + * @dev Initializer that calls parent initializer + * @param _owner The owner of the contract + * @param _initialConfig The initial AVS configuration + */ + function initialize(address _owner, AvsConfig memory _initialConfig) external initializer { + __TaskAVSRegistrarBase_init(_owner, _initialConfig); + } +} diff --git a/test/unit/TaskAVSRegistrarBaseUnit.t.sol b/test/unit/TaskAVSRegistrarBaseUnit.t.sol new file mode 100644 index 00000000..4bf0fc2f --- /dev/null +++ b/test/unit/TaskAVSRegistrarBaseUnit.t.sol @@ -0,0 +1,694 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {Test} from "forge-std/Test.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ITransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; + +import {TaskAVSRegistrarBase} from "../../src/avs/task/TaskAVSRegistrarBase.sol"; +import {ITaskAVSRegistrarBase} from "../../src/interfaces/ITaskAVSRegistrarBase.sol"; +import {ITaskAVSRegistrarBaseTypes} from "../../src/interfaces/ITaskAVSRegistrarBase.sol"; +import {ITaskAVSRegistrarBaseErrors} from "../../src/interfaces/ITaskAVSRegistrarBase.sol"; +import {ITaskAVSRegistrarBaseEvents} from "../../src/interfaces/ITaskAVSRegistrarBase.sol"; +import {MockTaskAVSRegistrar} from "../mocks/MockTaskAVSRegistrar.sol"; +import {AllocationManagerMock} from "../mocks/AllocationManagerMock.sol"; +import {KeyRegistrarMock} from "../mocks/KeyRegistrarMock.sol"; + +// Base test contract with common setup +contract TaskAVSRegistrarBaseUnitTests is + Test, + ITaskAVSRegistrarBaseTypes, + ITaskAVSRegistrarBaseErrors, + ITaskAVSRegistrarBaseEvents +{ + // Test addresses + address public avs = address(0x1); + address public owner = address(0x4); + address public nonOwner = address(0x5); + + // Mock contracts + AllocationManagerMock public allocationManager; + KeyRegistrarMock public keyRegistrar; + + // Test operator set IDs + uint32 public constant AGGREGATOR_OPERATOR_SET_ID = 1; + uint32 public constant EXECUTOR_OPERATOR_SET_ID_1 = 2; + uint32 public constant EXECUTOR_OPERATOR_SET_ID_2 = 3; + uint32 public constant EXECUTOR_OPERATOR_SET_ID_3 = 4; + + // Contract under test + MockTaskAVSRegistrar public registrar; + ProxyAdmin public proxyAdmin; + + function setUp() public virtual { + // Deploy mock contracts + allocationManager = new AllocationManagerMock(); + keyRegistrar = new KeyRegistrarMock(); + + // Create initial valid config + AvsConfig memory initialConfig = _createValidAvsConfig(); + + // Deploy the registrar with proxy pattern + proxyAdmin = new ProxyAdmin(); + MockTaskAVSRegistrar registrarImpl = new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(registrarImpl), + address(proxyAdmin), + abi.encodeWithSelector(MockTaskAVSRegistrar.initialize.selector, owner, initialConfig) + ); + registrar = MockTaskAVSRegistrar(address(proxy)); + } + + // Helper function to create a valid AVS config + function _createValidAvsConfig() internal pure returns (AvsConfig memory) { + uint32[] memory executorOperatorSetIds = new uint32[](2); + executorOperatorSetIds[0] = EXECUTOR_OPERATOR_SET_ID_1; + executorOperatorSetIds[1] = EXECUTOR_OPERATOR_SET_ID_2; + + return AvsConfig({ + aggregatorOperatorSetId: AGGREGATOR_OPERATOR_SET_ID, + executorOperatorSetIds: executorOperatorSetIds + }); + } + + // Helper function to create config with empty executor set + function _createEmptyExecutorSetConfig() internal pure returns (AvsConfig memory) { + uint32[] memory executorOperatorSetIds = new uint32[](0); + + return AvsConfig({ + aggregatorOperatorSetId: AGGREGATOR_OPERATOR_SET_ID, + executorOperatorSetIds: executorOperatorSetIds + }); + } + + // Helper function to create config with duplicate executor IDs + function _createDuplicateExecutorConfig() internal pure returns (AvsConfig memory) { + uint32[] memory executorOperatorSetIds = new uint32[](3); + executorOperatorSetIds[0] = EXECUTOR_OPERATOR_SET_ID_1; + executorOperatorSetIds[1] = EXECUTOR_OPERATOR_SET_ID_2; + executorOperatorSetIds[2] = EXECUTOR_OPERATOR_SET_ID_2; // Duplicate + + return AvsConfig({ + aggregatorOperatorSetId: AGGREGATOR_OPERATOR_SET_ID, + executorOperatorSetIds: executorOperatorSetIds + }); + } + + // Helper function to create config with unsorted executor IDs + function _createUnsortedExecutorConfig() internal pure returns (AvsConfig memory) { + uint32[] memory executorOperatorSetIds = new uint32[](3); + executorOperatorSetIds[0] = EXECUTOR_OPERATOR_SET_ID_2; + executorOperatorSetIds[1] = EXECUTOR_OPERATOR_SET_ID_1; // Not sorted + executorOperatorSetIds[2] = EXECUTOR_OPERATOR_SET_ID_3; + + return AvsConfig({ + aggregatorOperatorSetId: AGGREGATOR_OPERATOR_SET_ID, + executorOperatorSetIds: executorOperatorSetIds + }); + } + + // Helper function to create config where aggregator ID matches executor ID + function _createAggregatorMatchingExecutorConfig() internal pure returns (AvsConfig memory) { + uint32[] memory executorOperatorSetIds = new uint32[](2); + executorOperatorSetIds[0] = AGGREGATOR_OPERATOR_SET_ID; // Same as aggregator + executorOperatorSetIds[1] = EXECUTOR_OPERATOR_SET_ID_2; + + return AvsConfig({ + aggregatorOperatorSetId: AGGREGATOR_OPERATOR_SET_ID, + executorOperatorSetIds: executorOperatorSetIds + }); + } +} + +// Test contract for constructor +contract TaskAVSRegistrarBaseUnitTests_Constructor is TaskAVSRegistrarBaseUnitTests { + function test_Constructor() public { + // Create config for new deployment + AvsConfig memory config = _createValidAvsConfig(); + + // Deploy new registrar with proxy pattern + ProxyAdmin newProxyAdmin = new ProxyAdmin(); + MockTaskAVSRegistrar newRegistrarImpl = new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ); + TransparentUpgradeableProxy newProxy = new TransparentUpgradeableProxy( + address(newRegistrarImpl), + address(newProxyAdmin), + abi.encodeWithSelector(MockTaskAVSRegistrar.initialize.selector, owner, config) + ); + MockTaskAVSRegistrar newRegistrar = MockTaskAVSRegistrar(address(newProxy)); + + // Verify owner was set + assertEq(newRegistrar.owner(), owner); + + // Verify config was set + AvsConfig memory storedConfig = newRegistrar.getAvsConfig(); + assertEq(storedConfig.aggregatorOperatorSetId, config.aggregatorOperatorSetId); + assertEq(storedConfig.executorOperatorSetIds.length, config.executorOperatorSetIds.length); + for (uint256 i = 0; i < config.executorOperatorSetIds.length; i++) { + assertEq(storedConfig.executorOperatorSetIds[i], config.executorOperatorSetIds[i]); + } + } + + function test_Constructor_EmitsAvsConfigSet() public { + AvsConfig memory config = _createValidAvsConfig(); + + // Deploy implementation + ProxyAdmin newProxyAdmin = new ProxyAdmin(); + MockTaskAVSRegistrar newRegistrarImpl = new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ); + + // Expect event during initialization + vm.expectEmit(true, true, true, true); + emit AvsConfigSet(config.aggregatorOperatorSetId, config.executorOperatorSetIds); + + // Deploy proxy with initialization + new TransparentUpgradeableProxy( + address(newRegistrarImpl), + address(newProxyAdmin), + abi.encodeWithSelector(MockTaskAVSRegistrar.initialize.selector, owner, config) + ); + } + + function test_Revert_Constructor_EmptyExecutorSet() public { + AvsConfig memory config = _createEmptyExecutorSetConfig(); + + // Deploy implementation + ProxyAdmin newProxyAdmin = new ProxyAdmin(); + MockTaskAVSRegistrar newRegistrarImpl = new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ); + + // Expect revert during initialization + vm.expectRevert(ExecutorOperatorSetIdsEmpty.selector); + new TransparentUpgradeableProxy( + address(newRegistrarImpl), + address(newProxyAdmin), + abi.encodeWithSelector(MockTaskAVSRegistrar.initialize.selector, owner, config) + ); + } + + function test_Revert_Constructor_InvalidAggregatorId() public { + AvsConfig memory config = _createAggregatorMatchingExecutorConfig(); + + // Deploy implementation + ProxyAdmin newProxyAdmin = new ProxyAdmin(); + MockTaskAVSRegistrar newRegistrarImpl = new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ); + + // Expect revert during initialization + vm.expectRevert(InvalidAggregatorOperatorSetId.selector); + new TransparentUpgradeableProxy( + address(newRegistrarImpl), + address(newProxyAdmin), + abi.encodeWithSelector(MockTaskAVSRegistrar.initialize.selector, owner, config) + ); + } + + function test_Revert_Constructor_DuplicateExecutorId() public { + AvsConfig memory config = _createDuplicateExecutorConfig(); + + // Deploy implementation + ProxyAdmin newProxyAdmin = new ProxyAdmin(); + MockTaskAVSRegistrar newRegistrarImpl = new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ); + + // Expect revert during initialization + vm.expectRevert(DuplicateExecutorOperatorSetId.selector); + new TransparentUpgradeableProxy( + address(newRegistrarImpl), + address(newProxyAdmin), + abi.encodeWithSelector(MockTaskAVSRegistrar.initialize.selector, owner, config) + ); + } + + function test_Revert_Constructor_UnsortedExecutorIds() public { + AvsConfig memory config = _createUnsortedExecutorConfig(); + + // Deploy implementation + ProxyAdmin newProxyAdmin = new ProxyAdmin(); + MockTaskAVSRegistrar newRegistrarImpl = new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ); + + // Expect revert during initialization + vm.expectRevert(DuplicateExecutorOperatorSetId.selector); + new TransparentUpgradeableProxy( + address(newRegistrarImpl), + address(newProxyAdmin), + abi.encodeWithSelector(MockTaskAVSRegistrar.initialize.selector, owner, config) + ); + } +} + +// Test contract for setAvsConfig +contract TaskAVSRegistrarBaseUnitTests_setAvsConfig is TaskAVSRegistrarBaseUnitTests { + function test_setAvsConfig() public { + // Create new config + uint32[] memory newExecutorIds = new uint32[](3); + newExecutorIds[0] = 10; + newExecutorIds[1] = 20; + newExecutorIds[2] = 30; + + AvsConfig memory newConfig = + AvsConfig({aggregatorOperatorSetId: 5, executorOperatorSetIds: newExecutorIds}); + + // Expect event + vm.expectEmit(true, true, true, true, address(registrar)); + emit AvsConfigSet(newConfig.aggregatorOperatorSetId, newConfig.executorOperatorSetIds); + + // Set config as owner + vm.prank(owner); + registrar.setAvsConfig(newConfig); + + // Verify config was updated + AvsConfig memory storedConfig = registrar.getAvsConfig(); + assertEq(storedConfig.aggregatorOperatorSetId, newConfig.aggregatorOperatorSetId); + assertEq( + storedConfig.executorOperatorSetIds.length, newConfig.executorOperatorSetIds.length + ); + for (uint256 i = 0; i < newConfig.executorOperatorSetIds.length; i++) { + assertEq(storedConfig.executorOperatorSetIds[i], newConfig.executorOperatorSetIds[i]); + } + } + + function test_setAvsConfig_SingleExecutor() public { + // Create config with single executor + uint32[] memory executorIds = new uint32[](1); + executorIds[0] = 10; + + AvsConfig memory config = + AvsConfig({aggregatorOperatorSetId: 5, executorOperatorSetIds: executorIds}); + + vm.prank(owner); + registrar.setAvsConfig(config); + + // Verify config was updated + AvsConfig memory storedConfig = registrar.getAvsConfig(); + assertEq(storedConfig.executorOperatorSetIds.length, 1); + assertEq(storedConfig.executorOperatorSetIds[0], 10); + } + + function test_Revert_setAvsConfig_NotOwner() public { + AvsConfig memory config = _createValidAvsConfig(); + + vm.prank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + registrar.setAvsConfig(config); + } + + function test_Revert_setAvsConfig_EmptyExecutorSet() public { + AvsConfig memory config = _createEmptyExecutorSetConfig(); + + vm.prank(owner); + vm.expectRevert(ExecutorOperatorSetIdsEmpty.selector); + registrar.setAvsConfig(config); + } + + function test_Revert_setAvsConfig_InvalidAggregatorId_FirstElement() public { + AvsConfig memory config = _createAggregatorMatchingExecutorConfig(); + + vm.prank(owner); + vm.expectRevert(InvalidAggregatorOperatorSetId.selector); + registrar.setAvsConfig(config); + } + + function test_Revert_setAvsConfig_InvalidAggregatorId_MiddleElement() public { + uint32[] memory executorIds = new uint32[](3); + executorIds[0] = 10; + executorIds[1] = 20; // This will be the aggregator ID + executorIds[2] = 30; + + AvsConfig memory config = AvsConfig({ + aggregatorOperatorSetId: 20, // Matches middle executor + executorOperatorSetIds: executorIds + }); + + vm.prank(owner); + vm.expectRevert(InvalidAggregatorOperatorSetId.selector); + registrar.setAvsConfig(config); + } + + function test_Revert_setAvsConfig_InvalidAggregatorId_LastElement() public { + uint32[] memory executorIds = new uint32[](3); + executorIds[0] = 10; + executorIds[1] = 20; + executorIds[2] = 30; // This will be the aggregator ID + + AvsConfig memory config = AvsConfig({ + aggregatorOperatorSetId: 30, // Matches last executor + executorOperatorSetIds: executorIds + }); + + vm.prank(owner); + vm.expectRevert(InvalidAggregatorOperatorSetId.selector); + registrar.setAvsConfig(config); + } + + function test_Revert_setAvsConfig_DuplicateExecutorId() public { + AvsConfig memory config = _createDuplicateExecutorConfig(); + + vm.prank(owner); + vm.expectRevert(DuplicateExecutorOperatorSetId.selector); + registrar.setAvsConfig(config); + } + + function test_Revert_setAvsConfig_UnsortedExecutorIds() public { + AvsConfig memory config = _createUnsortedExecutorConfig(); + + vm.prank(owner); + vm.expectRevert(DuplicateExecutorOperatorSetId.selector); + registrar.setAvsConfig(config); + } + + function testFuzz_setAvsConfig(uint32 aggregatorId, uint8 numExecutors) public { + // Bound inputs + vm.assume(numExecutors > 0 && numExecutors <= 10); + vm.assume(aggregatorId > 0); + // Ensure we have room for executor IDs without overflow + vm.assume(aggregatorId < type(uint32).max - (uint32(numExecutors) * 10)); + + // Create executor IDs that don't conflict with aggregator + uint32[] memory executorIds = new uint32[](numExecutors); + uint32 currentId = aggregatorId + 1; + for (uint8 i = 0; i < numExecutors; i++) { + executorIds[i] = currentId; + currentId += 10; // Ensure monotonic increase + } + + AvsConfig memory config = + AvsConfig({aggregatorOperatorSetId: aggregatorId, executorOperatorSetIds: executorIds}); + + vm.prank(owner); + registrar.setAvsConfig(config); + + // Verify + AvsConfig memory storedConfig = registrar.getAvsConfig(); + assertEq(storedConfig.aggregatorOperatorSetId, aggregatorId); + assertEq(storedConfig.executorOperatorSetIds.length, numExecutors); + } +} + +// Test contract for upgradeable functionality +contract TaskAVSRegistrarBaseUnitTests_Upgradeable is TaskAVSRegistrarBaseUnitTests { + function test_Initialize_OnlyOnce() public { + // Try to initialize again, should revert + vm.expectRevert("Initializable: contract is already initialized"); + registrar.initialize(address(0x9999), _createValidAvsConfig()); + } + + function test_Implementation_CannotBeInitialized() public { + // Deploy a new implementation + MockTaskAVSRegistrar newImpl = new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ); + + // Try to initialize the implementation directly, should revert + vm.expectRevert("Initializable: contract is already initialized"); + newImpl.initialize(owner, _createValidAvsConfig()); + } + + function test_ProxyUpgrade() public { + address newOwner = address(0x1234); + + // First, make some state changes + AvsConfig memory newConfig = + AvsConfig({aggregatorOperatorSetId: 5, executorOperatorSetIds: new uint32[](2)}); + newConfig.executorOperatorSetIds[0] = 6; + newConfig.executorOperatorSetIds[1] = 7; + + vm.prank(owner); + registrar.setAvsConfig(newConfig); + + // Deploy new implementation (could have new functions/logic) + MockTaskAVSRegistrar newImpl = new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ); + + // Upgrade proxy to new implementation + proxyAdmin.upgrade(ITransparentUpgradeableProxy(address(registrar)), address(newImpl)); + + // Verify state is preserved (config should still be the same) + AvsConfig memory storedConfig = registrar.getAvsConfig(); + assertEq(storedConfig.aggregatorOperatorSetId, newConfig.aggregatorOperatorSetId); + assertEq( + storedConfig.executorOperatorSetIds.length, newConfig.executorOperatorSetIds.length + ); + for (uint256 i = 0; i < newConfig.executorOperatorSetIds.length; i++) { + assertEq(storedConfig.executorOperatorSetIds[i], newConfig.executorOperatorSetIds[i]); + } + + // Verify owner is still the same + assertEq(registrar.owner(), owner); + } + + function test_ProxyAdmin_OnlyOwnerCanUpgrade() public { + address attacker = address(0x9999); + + // Deploy new implementation + MockTaskAVSRegistrar newImpl = new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ); + + // Try to upgrade from non-owner, should revert + vm.prank(attacker); + vm.expectRevert("Ownable: caller is not the owner"); + proxyAdmin.upgrade(ITransparentUpgradeableProxy(address(registrar)), address(newImpl)); + } + + function test_Initialization_SetsCorrectValues() public { + // Already tested in setUp, but let's verify again explicitly + assertEq(registrar.owner(), owner); + + // Verify initial config + AvsConfig memory config = registrar.getAvsConfig(); + assertEq(config.aggregatorOperatorSetId, AGGREGATOR_OPERATOR_SET_ID); + assertEq(config.executorOperatorSetIds.length, 2); + assertEq(config.executorOperatorSetIds[0], EXECUTOR_OPERATOR_SET_ID_1); + assertEq(config.executorOperatorSetIds[1], EXECUTOR_OPERATOR_SET_ID_2); + } + + function test_ProxyAdmin_CannotCallImplementation() public { + // ProxyAdmin should not be able to call implementation functions + vm.prank(address(proxyAdmin)); + vm.expectRevert("TransparentUpgradeableProxy: admin cannot fallback to proxy target"); + MockTaskAVSRegistrar(payable(address(registrar))).owner(); + } + + function test_StorageSlotConsistency_AfterUpgrade() public { + address newOwner = address(0x1234); + + // First, transfer ownership to track a state change + vm.prank(owner); + registrar.transferOwnership(newOwner); + assertEq(registrar.owner(), newOwner); + + // Set a different config + AvsConfig memory newConfig = + AvsConfig({aggregatorOperatorSetId: 10, executorOperatorSetIds: new uint32[](3)}); + newConfig.executorOperatorSetIds[0] = 11; + newConfig.executorOperatorSetIds[1] = 12; + newConfig.executorOperatorSetIds[2] = 13; + + vm.prank(newOwner); + registrar.setAvsConfig(newConfig); + + // Deploy new implementation + MockTaskAVSRegistrar newImpl = new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ); + + // Upgrade + vm.prank(address(this)); // proxyAdmin owner + proxyAdmin.upgrade(ITransparentUpgradeableProxy(address(registrar)), address(newImpl)); + + // Verify all state is preserved after upgrade + assertEq(registrar.owner(), newOwner); + + // Verify the config is still there + AvsConfig memory configAfterUpgrade = registrar.getAvsConfig(); + assertEq(configAfterUpgrade.aggregatorOperatorSetId, newConfig.aggregatorOperatorSetId); + assertEq( + configAfterUpgrade.executorOperatorSetIds.length, + newConfig.executorOperatorSetIds.length + ); + for (uint256 i = 0; i < newConfig.executorOperatorSetIds.length; i++) { + assertEq( + configAfterUpgrade.executorOperatorSetIds[i], newConfig.executorOperatorSetIds[i] + ); + } + } + + function test_InitializerModifier_PreventsReinitialization() public { + // Deploy a new proxy without initialization data + TransparentUpgradeableProxy uninitializedProxy = new TransparentUpgradeableProxy( + address( + new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ) + ), + address(new ProxyAdmin()), + "" + ); + + MockTaskAVSRegistrar uninitializedRegistrar = + MockTaskAVSRegistrar(address(uninitializedProxy)); + + // Initialize it once + AvsConfig memory config = _createValidAvsConfig(); + uninitializedRegistrar.initialize(owner, config); + assertEq(uninitializedRegistrar.owner(), owner); + + // Try to initialize again, should fail + vm.expectRevert("Initializable: contract is already initialized"); + uninitializedRegistrar.initialize(address(0x9999), config); + } + + function test_DisableInitializers_InImplementation() public { + // This test verifies that the implementation contract has initializers disabled + MockTaskAVSRegistrar impl = new MockTaskAVSRegistrar( + avs, + IAllocationManager(address(allocationManager)), + IKeyRegistrar(address(keyRegistrar)) + ); + + // Try to initialize the implementation, should revert + vm.expectRevert("Initializable: contract is already initialized"); + impl.initialize(owner, _createValidAvsConfig()); + } +} + +// Test contract for getAvsConfig +contract TaskAVSRegistrarBaseUnitTests_getAvsConfig is TaskAVSRegistrarBaseUnitTests { + function test_getAvsConfig() public { + // Get initial config + AvsConfig memory config = registrar.getAvsConfig(); + + // Verify it matches what was set in constructor + assertEq(config.aggregatorOperatorSetId, AGGREGATOR_OPERATOR_SET_ID); + assertEq(config.executorOperatorSetIds.length, 2); + assertEq(config.executorOperatorSetIds[0], EXECUTOR_OPERATOR_SET_ID_1); + assertEq(config.executorOperatorSetIds[1], EXECUTOR_OPERATOR_SET_ID_2); + } + + function test_getAvsConfig_AfterUpdate() public { + // Update config + uint32[] memory newExecutorIds = new uint32[](1); + newExecutorIds[0] = 100; + + AvsConfig memory newConfig = + AvsConfig({aggregatorOperatorSetId: 50, executorOperatorSetIds: newExecutorIds}); + + vm.prank(owner); + registrar.setAvsConfig(newConfig); + + // Get updated config + AvsConfig memory config = registrar.getAvsConfig(); + + // Verify it matches the update + assertEq(config.aggregatorOperatorSetId, 50); + assertEq(config.executorOperatorSetIds.length, 1); + assertEq(config.executorOperatorSetIds[0], 100); + } + + function test_getAvsConfig_CalledByNonOwner() public { + // Anyone should be able to read the config + vm.prank(nonOwner); + AvsConfig memory config = registrar.getAvsConfig(); + + // Verify it returns correct data + assertEq(config.aggregatorOperatorSetId, AGGREGATOR_OPERATOR_SET_ID); + assertEq(config.executorOperatorSetIds.length, 2); + } +} + +// Test contract for access control +contract TaskAVSRegistrarBaseUnitTests_AccessControl is TaskAVSRegistrarBaseUnitTests { + function test_Owner() public { + assertEq(registrar.owner(), owner); + } + + function test_OnlyOwnerCanSetConfig() public { + AvsConfig memory config = _createValidAvsConfig(); + + // Owner can set config + vm.prank(owner); + registrar.setAvsConfig(config); + + // Non-owner cannot + vm.prank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + registrar.setAvsConfig(config); + } + + function test_TransferOwnership() public { + address newOwner = address(0x123); + + // Transfer ownership + vm.prank(owner); + registrar.transferOwnership(newOwner); + + // Verify new owner + assertEq(registrar.owner(), newOwner); + + // Old owner can no longer set config + AvsConfig memory config = _createValidAvsConfig(); + vm.prank(owner); + vm.expectRevert("Ownable: caller is not the owner"); + registrar.setAvsConfig(config); + + // New owner can set config + vm.prank(newOwner); + registrar.setAvsConfig(config); + } + + function test_RenounceOwnership() public { + // Renounce ownership + vm.prank(owner); + registrar.renounceOwnership(); + + // Verify owner is zero address + assertEq(registrar.owner(), address(0)); + + // No one can set config anymore + AvsConfig memory config = _createValidAvsConfig(); + vm.prank(owner); + vm.expectRevert("Ownable: caller is not the owner"); + registrar.setAvsConfig(config); + } +}