diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index af4cb7f0..c81cd4ad 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -101,14 +101,6 @@ jobs: run-coverage: name: Coverage runs-on: ubuntu-latest - # Only run coverage checks on dev, testnet-holesky, and mainnet branches, or PRs targeting these branches - if: | - github.ref == 'refs/heads/dev' || - github.ref == 'refs/heads/testnet-holesky' || - github.ref == 'refs/heads/mainnet' || - github.base_ref == 'dev' || - github.base_ref == 'testnet-holesky' || - github.base_ref == 'mainnet' strategy: fail-fast: true steps: diff --git a/test/unit/BLSSignatureCheckerUnit.t.sol b/test/unit/BLSSignatureCheckerUnit.t.sol index ef13c575..9f3ea9c0 100644 --- a/test/unit/BLSSignatureCheckerUnit.t.sol +++ b/test/unit/BLSSignatureCheckerUnit.t.sol @@ -582,4 +582,75 @@ contract BLSSignatureCheckerUnitTests is BLSMockAVSDeployer { msgHash, quorumNumbers, referenceBlockNumber, nonSignerStakesAndSignature ); } + + function test_trySignatureAndApkVerification_success() public { + uint256 numNonSigners = 0; + uint256 quorumBitmap = 1; + ( + uint32 referenceBlockNumber, + BLSSignatureChecker.NonSignerStakesAndSignature memory nonSignerStakesAndSignature + ) = _registerSignatoriesAndGetNonSignerStakeAndSignatureRandom( + 1, numNonSigners, quorumBitmap + ); + + (bool pairingSuccessful, bool signatureIsValid) = blsSignatureChecker + .trySignatureAndApkVerification( + msgHash, + nonSignerStakesAndSignature.quorumApks[0], + nonSignerStakesAndSignature.apkG2, + nonSignerStakesAndSignature.sigma + ); + + assertTrue(pairingSuccessful, "Pairing should be successful"); + assertTrue(signatureIsValid, "Signature should be valid"); + } + + function test_trySignatureAndApkVerification_invalidSignature() public { + uint256 numNonSigners = 0; + uint256 quorumBitmap = 1; + ( + uint32 referenceBlockNumber, + BLSSignatureChecker.NonSignerStakesAndSignature memory nonSignerStakesAndSignature + ) = _registerSignatoriesAndGetNonSignerStakeAndSignatureRandom( + 1, numNonSigners, quorumBitmap + ); + + // Modify sigma to make it invalid + nonSignerStakesAndSignature.sigma.X++; + + cheats.expectRevert(); + blsSignatureChecker.trySignatureAndApkVerification( + msgHash, + nonSignerStakesAndSignature.quorumApks[0], + nonSignerStakesAndSignature.apkG2, + nonSignerStakesAndSignature.sigma + ); + } + + function test_trySignatureAndApkVerification_invalidPairing() public { + uint256 numNonSigners = 0; + uint256 quorumBitmap = 1; + ( + uint32 referenceBlockNumber, + BLSSignatureChecker.NonSignerStakesAndSignature memory nonSignerStakesAndSignature + ) = _registerSignatoriesAndGetNonSignerStakeAndSignatureRandom( + 1, numNonSigners, quorumBitmap + ); + + // Create invalid G2 point + BN254.G2Point memory invalidG2Point = BN254.G2Point( + [type(uint256).max, type(uint256).max], [type(uint256).max, type(uint256).max] + ); + + (bool pairingSuccessful, bool signatureIsValid) = blsSignatureChecker + .trySignatureAndApkVerification( + msgHash, + nonSignerStakesAndSignature.quorumApks[0], + invalidG2Point, + nonSignerStakesAndSignature.sigma + ); + + assertFalse(pairingSuccessful, "Pairing should fail"); + assertFalse(signatureIsValid, "Signature should be invalid"); + } } diff --git a/test/unit/OperatorStateRetrieverUnit.t.sol b/test/unit/OperatorStateRetrieverUnit.t.sol index 79a93988..4020604f 100644 --- a/test/unit/OperatorStateRetrieverUnit.t.sol +++ b/test/unit/OperatorStateRetrieverUnit.t.sol @@ -649,4 +649,82 @@ contract OperatorStateRetrieverUnitTests is MockAVSDeployer { } } } + + function test_getBatchOperatorId_emptyArray() public { + address[] memory operators = new address[](0); + bytes32[] memory operatorIds = + operatorStateRetriever.getBatchOperatorId(registryCoordinator, operators); + assertEq(operatorIds.length, 0, "Should return empty array for empty input"); + } + + function test_getBatchOperatorId_unregisteredOperators() public { + address[] memory operators = new address[](2); + operators[0] = address(1); + operators[1] = address(2); + + bytes32[] memory operatorIds = + operatorStateRetriever.getBatchOperatorId(registryCoordinator, operators); + + assertEq(operatorIds.length, 2, "Should return array of same length as input"); + assertEq(operatorIds[0], bytes32(0), "Unregistered operator should return 0"); + assertEq(operatorIds[1], bytes32(0), "Unregistered operator should return 0"); + } + + function test_getBatchOperatorId_mixedRegistration() public { + // Register one operator + cheats.roll(registrationBlockNumber); + _registerOperatorWithCoordinator(defaultOperator, 1, defaultPubKey); + + // Create test array with one registered and one unregistered operator + address[] memory operators = new address[](2); + operators[0] = defaultOperator; + operators[1] = address(2); // unregistered + + bytes32[] memory operatorIds = + operatorStateRetriever.getBatchOperatorId(registryCoordinator, operators); + + assertEq(operatorIds.length, 2, "Should return array of same length as input"); + assertEq( + operatorIds[0], defaultOperatorId, "Should return correct ID for registered operator" + ); + assertEq(operatorIds[1], bytes32(0), "Should return 0 for unregistered operator"); + } + + function test_getBatchOperatorFromId_emptyArray() public { + bytes32[] memory operatorIds = new bytes32[](0); + address[] memory operators = + operatorStateRetriever.getBatchOperatorFromId(registryCoordinator, operatorIds); + assertEq(operators.length, 0, "Should return empty array for empty input"); + } + + function test_getBatchOperatorFromId_unregisteredIds() public { + bytes32[] memory operatorIds = new bytes32[](2); + operatorIds[0] = bytes32(uint256(1)); + operatorIds[1] = bytes32(uint256(2)); + + address[] memory operators = + operatorStateRetriever.getBatchOperatorFromId(registryCoordinator, operatorIds); + + assertEq(operators.length, 2, "Should return array of same length as input"); + assertEq(operators[0], address(0), "Unregistered ID should return address(0)"); + assertEq(operators[1], address(0), "Unregistered ID should return address(0)"); + } + + function test_getBatchOperatorFromId_mixedRegistration() public { + // Register one operator + cheats.roll(registrationBlockNumber); + _registerOperatorWithCoordinator(defaultOperator, 1, defaultPubKey); + + // Create test array with one registered and one unregistered operator ID + bytes32[] memory operatorIds = new bytes32[](2); + operatorIds[0] = defaultOperatorId; + operatorIds[1] = bytes32(uint256(2)); // unregistered + + address[] memory operators = + operatorStateRetriever.getBatchOperatorFromId(registryCoordinator, operatorIds); + + assertEq(operators.length, 2, "Should return array of same length as input"); + assertEq(operators[0], defaultOperator, "Should return correct address for registered ID"); + assertEq(operators[1], address(0), "Should return address(0) for unregistered ID"); + } } diff --git a/test/unit/ServiceManagerBase.t.sol b/test/unit/ServiceManagerBase.t.sol index b2494e64..72cb562e 100644 --- a/test/unit/ServiceManagerBase.t.sol +++ b/test/unit/ServiceManagerBase.t.sol @@ -15,6 +15,11 @@ import {IStrategyManager} from "eigenlayer-contracts/src/contracts/interfaces/IS import {IServiceManagerBaseEvents} from "../events/IServiceManagerBaseEvents.sol"; import {IServiceManagerErrors} from "../../src/interfaces/IServiceManager.sol"; +import { + IAllocationManagerTypes, + IAllocationManager +} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; + import "../utils/MockAVSDeployer.sol"; contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEvents { @@ -544,4 +549,383 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve cheats.prank(caller); serviceManager.setRewardsInitiator(newRewardsInitiator); } + + function testFuzz_addPendingAdmin( + address admin + ) public filterFuzzedAddressInputs(admin) { + // Mock the expected call to permissionController + cheats.expectCall( + address(permissionControllerMock), + abi.encodeCall(PermissionController.addPendingAdmin, (address(serviceManager), admin)) + ); + + // Call should only work from owner + cheats.prank(serviceManagerOwner); + serviceManager.addPendingAdmin(admin); + } + + function testFuzz_addPendingAdmin_revert_notOwner( + address admin, + address caller + ) public filterFuzzedAddressInputs(admin) filterFuzzedAddressInputs(caller) { + cheats.assume(caller != serviceManagerOwner); + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(caller); + serviceManager.addPendingAdmin(admin); + } + + function testFuzz_removePendingAdmin( + address pendingAdmin + ) public filterFuzzedAddressInputs(pendingAdmin) { + // Mock the expected call to permissionController + cheats.expectCall( + address(permissionControllerMock), + abi.encodeCall( + PermissionController.removePendingAdmin, (address(serviceManager), pendingAdmin) + ) + ); + + // Call should only work from owner + cheats.prank(serviceManagerOwner); + serviceManager.removePendingAdmin(pendingAdmin); + } + + function testFuzz_removePendingAdmin_revert_notOwner( + address pendingAdmin, + address caller + ) public filterFuzzedAddressInputs(pendingAdmin) filterFuzzedAddressInputs(caller) { + cheats.assume(caller != serviceManagerOwner); + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(caller); + serviceManager.removePendingAdmin(pendingAdmin); + } + + function testFuzz_removeAdmin( + address admin + ) public filterFuzzedAddressInputs(admin) { + // Mock the expected call to permissionController + cheats.expectCall( + address(permissionControllerMock), + abi.encodeCall(PermissionController.removeAdmin, (address(serviceManager), admin)) + ); + + // Call should only work from owner + cheats.prank(serviceManagerOwner); + serviceManager.removeAdmin(admin); + } + + function testFuzz_removeAdmin_revert_notOwner( + address admin, + address caller + ) public filterFuzzedAddressInputs(admin) filterFuzzedAddressInputs(caller) { + cheats.assume(caller != serviceManagerOwner); + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(caller); + serviceManager.removeAdmin(admin); + } + + function testFuzz_removeAppointee( + address appointee, + address target, + bytes4 selector + ) public filterFuzzedAddressInputs(appointee) filterFuzzedAddressInputs(target) { + // Mock the expected call to permissionController + cheats.expectCall( + address(permissionControllerMock), + abi.encodeCall( + PermissionController.removeAppointee, + (address(serviceManager), appointee, target, selector) + ) + ); + + // Call should only work from owner + cheats.prank(serviceManagerOwner); + serviceManager.removeAppointee(appointee, target, selector); + } + + function testFuzz_removeAppointee_revert_notOwner( + address appointee, + address target, + bytes4 selector, + address caller + ) + public + filterFuzzedAddressInputs(appointee) + filterFuzzedAddressInputs(target) + filterFuzzedAddressInputs(caller) + { + cheats.assume(caller != serviceManagerOwner); + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(caller); + serviceManager.removeAppointee(appointee, target, selector); + } + + function testFuzz_createOperatorDirectedAVSRewardsSubmission_Revert_WhenNotOwner( + address caller + ) public filterFuzzedAddressInputs(caller) { + cheats.assume(caller != rewardsInitiator); + IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory rewardsSubmissions; + + cheats.prank(caller); + cheats.expectRevert(IServiceManagerErrors.OnlyRewardsInitiator.selector); + serviceManager.createOperatorDirectedAVSRewardsSubmission(rewardsSubmissions); + } + + function test_createOperatorDirectedAVSRewardsSubmission_Revert_WhenERC20NotApproved() public { + IERC20 token = new ERC20PresetFixedSupply( + "dog wif hat", "MOCK1", mockTokenInitialSupply, rewardsInitiator + ); + + IRewardsCoordinatorTypes.OperatorReward[] memory operatorRewards = + new IRewardsCoordinatorTypes.OperatorReward[](1); + operatorRewards[0] = + IRewardsCoordinatorTypes.OperatorReward({operator: address(0x1), amount: 100}); + + IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory rewardsSubmissions = + new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1); + rewardsSubmissions[0] = IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: token, + operatorRewards: operatorRewards, + startTimestamp: uint32(block.timestamp), + duration: uint32(1 weeks), + description: "Test Rewards" + }); + + cheats.prank(rewardsInitiator); + cheats.expectRevert("ERC20: insufficient allowance"); + serviceManager.createOperatorDirectedAVSRewardsSubmission(rewardsSubmissions); + } + + function testFuzz_createOperatorDirectedAVSRewardsSubmission_SingleSubmission( + uint256 startTimestamp, + uint256 duration, + uint256 amount + ) public { + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply( + "dog wif hat", "MOCK1", mockTokenInitialSupply, rewardsInitiator + ); + amount = bound(amount, 1, MAX_REWARDS_AMOUNT); + duration = bound(duration, 0, MAX_REWARDS_DURATION); + duration = duration - (duration % CALCULATION_INTERVAL_SECONDS); + startTimestamp = bound( + startTimestamp, + uint256( + _maxTimestamp( + GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + ) + ) + CALCULATION_INTERVAL_SECONDS - 1, + block.timestamp + uint256(MAX_FUTURE_LENGTH) + ); + startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS); + + vm.warp(startTimestamp + duration + 1); + + // 2. Create reward submission input param + // Create operator rewards array + IRewardsCoordinatorTypes.OperatorReward[] memory operatorRewards = + new IRewardsCoordinatorTypes.OperatorReward[](1); + operatorRewards[0] = + IRewardsCoordinatorTypes.OperatorReward({operator: address(0x1), amount: amount}); + + // Create rewards submission + IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory rewardsSubmissions = + new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1); + rewardsSubmissions[0] = IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: operatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "Test Rewards" + }); + + // 3. Approve serviceManager for ERC20 + cheats.startPrank(rewardsInitiator); + rewardToken.approve(address(serviceManager), amount); + + // 4. call createAVSRewardsSubmission() with expected event emitted + uint256 rewardsInitiatorBalanceBefore = rewardToken.balanceOf(address(rewardsInitiator)); + uint256 rewardsCoordinatorBalanceBefore = rewardToken.balanceOf(address(rewardsCoordinator)); + + rewardToken.approve(address(rewardsCoordinator), amount); + uint256 currSubmissionNonce = rewardsCoordinator.submissionNonce(address(serviceManager)); + bytes32 avsSubmissionHash = keccak256( + abi.encode(address(serviceManager), currSubmissionNonce, rewardsSubmissions[0]) + ); + + // cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); + // emit AVSRewardsSubmissionCreated( + // address(serviceManager), currSubmissionNonce, avsSubmissionHash, rewardsSubmissions[0] + // ); + serviceManager.createOperatorDirectedAVSRewardsSubmission(rewardsSubmissions); + cheats.stopPrank(); + + assertTrue( + rewardsCoordinator.isOperatorDirectedAVSRewardsSubmissionHash( + address(serviceManager), avsSubmissionHash + ), + "reward submission hash not submitted" + ); + assertEq( + currSubmissionNonce + 1, + rewardsCoordinator.submissionNonce(address(serviceManager)), + "submission nonce not incremented" + ); + assertEq( + rewardsInitiatorBalanceBefore - amount, + rewardToken.balanceOf(rewardsInitiator), + "rewardsInitiator balance not decremented by amount of reward submission" + ); + assertEq( + rewardsCoordinatorBalanceBefore + amount, + rewardToken.balanceOf(address(rewardsCoordinator)), + "RewardsCoordinator balance not incremented by amount of reward submission" + ); + } + + function testFuzz_createOperatorDirectedAVSRewardsSubmission_MultipleSubmissions( + uint256 startTimestamp, + uint256 duration, + uint256 amount, + uint256 numSubmissions + ) public { + numSubmissions = bound(numSubmissions, 2, 10); + cheats.prank(rewardsCoordinator.owner()); + + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] memory rewardsSubmissions = + new IRewardsCoordinator.OperatorDirectedRewardsSubmission[](numSubmissions); + bytes32[] memory avsSubmissionHashes = new bytes32[](numSubmissions); + uint256 startSubmissionNonce = rewardsCoordinator.submissionNonce(address(serviceManager)); + _deployMockRewardTokens(rewardsInitiator, numSubmissions); + + uint256[] memory avsBalancesBefore = _getBalanceForTokens(rewardTokens, rewardsInitiator); + uint256[] memory rewardsCoordinatorBalancesBefore = + _getBalanceForTokens(rewardTokens, address(rewardsCoordinator)); + // uint256[] memory amounts = new uint256[](numSubmissions); + + uint256 latestStartTimestamp = 0; + uint256 longestDuration = 0; + + // Create multiple rewards submissions and their expected event + for (uint256 i = 0; i < numSubmissions; ++i) { + // 1. Bound fuzz inputs to valid ranges and amounts using randSeed for each + amount = bound(amount + i, 1, MAX_REWARDS_AMOUNT); + // amounts[i] = amount; + duration = bound(duration + i, 0, MAX_REWARDS_DURATION); + duration = duration - (duration % CALCULATION_INTERVAL_SECONDS); + startTimestamp = bound( + startTimestamp + i, + uint256( + _maxTimestamp( + GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + ) + ) + CALCULATION_INTERVAL_SECONDS - 1, + block.timestamp - 1 // Must be in past for operator directed rewards + ); + startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS); + + // loop and find the latest startTimestamp and the longest duration, then warp start + duration + 1 + + if (startTimestamp > latestStartTimestamp) { + latestStartTimestamp = startTimestamp; + } + if (duration > longestDuration) { + longestDuration = duration; + } + + // 2. Create reward submission input param + IRewardsCoordinatorTypes.OperatorReward[] memory operatorRewards = + new IRewardsCoordinatorTypes.OperatorReward[](1); + operatorRewards[0] = + IRewardsCoordinatorTypes.OperatorReward({operator: address(0x1), amount: amount}); + + IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory rewardsSubmission = + IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardTokens[i], + operatorRewards: operatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "Test Rewards" + }); + rewardsSubmissions[i] = rewardsSubmission; + + // 3. expected event emitted for this rewardsSubmission + avsSubmissionHashes[i] = keccak256( + abi.encode(address(serviceManager), startSubmissionNonce + i, rewardsSubmissions[i]) + ); + } + + vm.warp(latestStartTimestamp + longestDuration + 1); + + // 4. call createOperatorDirectedAVSRewardsSubmission() + cheats.prank(rewardsInitiator); + serviceManager.createOperatorDirectedAVSRewardsSubmission(rewardsSubmissions); + + // 5. Check for submissionNonce() and avsSubmissionHashes being set + assertEq( + startSubmissionNonce + numSubmissions, + rewardsCoordinator.submissionNonce(address(serviceManager)), + "avs submission nonce not incremented properly" + ); + + for (uint256 i = 0; i < numSubmissions; ++i) { + assertTrue( + rewardsCoordinator.isOperatorDirectedAVSRewardsSubmissionHash( + address(serviceManager), avsSubmissionHashes[i] + ), + "rewards submission hash not submitted" + ); + // assertEq( + // avsBalancesBefore[i] - amounts[i], + // rewardTokens[i].balanceOf(rewardsInitiator), + // "AVS balance not decremented by amount of rewards submission" + // ); + // assertEq( + // rewardsCoordinatorBalancesBefore[i] + amounts[i], + // rewardTokens[i].balanceOf(address(rewardsCoordinator)), + // "RewardsCoordinator balance not incremented by amount of rewards submission" + // ); + } + } + + function testFuzz_deregisterOperatorFromOperatorSets( + address operator, + uint32[] memory operatorSetIds + ) public { + // Mock the expected call to allocationManager + IAllocationManagerTypes.DeregisterParams memory expectedParams = IAllocationManagerTypes + .DeregisterParams({ + operator: operator, + avs: address(serviceManager), + operatorSetIds: operatorSetIds + }); + + cheats.expectCall( + address(allocationManagerMock), + abi.encodeCall(IAllocationManager.deregisterFromOperatorSets, (expectedParams)) + ); + + // Call should only work from registryCoordinator + cheats.prank(address(registryCoordinatorImplementation)); + serviceManager.deregisterOperatorFromOperatorSets(operator, operatorSetIds); + } + + function testFuzz_deregisterOperatorFromOperatorSets_revert_notRegistryCoordinator( + address operator, + uint32[] memory operatorSetIds, + address caller + ) public filterFuzzedAddressInputs(caller) { + cheats.assume(caller != address(registryCoordinatorImplementation)); + + cheats.prank(caller); + cheats.expectRevert(IServiceManagerErrors.OnlyRegistryCoordinator.selector); + serviceManager.deregisterOperatorFromOperatorSets(operator, operatorSetIds); + } }