diff --git a/src/middlewareV2/tableCalculator/unaudited/BN254TableCalculatorWithCaps.sol b/src/middlewareV2/tableCalculator/unaudited/BN254TableCalculatorWithCaps.sol new file mode 100644 index 00000000..77054e88 --- /dev/null +++ b/src/middlewareV2/tableCalculator/unaudited/BN254TableCalculatorWithCaps.sol @@ -0,0 +1,163 @@ +// 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 {IPermissionController} from + "eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol"; +import {PermissionControllerMixin} from + "eigenlayer-contracts/src/contracts/mixins/PermissionControllerMixin.sol"; + +import "../BN254TableCalculatorBase.sol"; +import {WeightCapUtils} from "../../../unaudited/libraries/WeightCapUtils.sol"; + +/** + * @title BN254TableCalculatorWithCaps + * @notice BN254 table calculator with configurable weight caps + * @dev Extends the basic table calculator to cap operator weights + */ +contract BN254TableCalculatorWithCaps is BN254TableCalculatorBase, PermissionControllerMixin { + // 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; + + // Storage + /// @notice Mapping from operatorSet key to weight caps per stake type (0 = no cap) + /// @dev Index 0 represents the total weight cap for backwards compatibility + mapping(bytes32 => uint256[]) public weightCaps; + + // Events + /// @notice Emitted when weight caps are set for an operator set + event WeightCapsSet(OperatorSet indexed operatorSet, uint256[] maxWeights); + + constructor( + IKeyRegistrar _keyRegistrar, + IAllocationManager _allocationManager, + IPermissionController _permissionController, + uint256 _LOOKAHEAD_BLOCKS + ) BN254TableCalculatorBase(_keyRegistrar) PermissionControllerMixin(_permissionController) { + allocationManager = _allocationManager; + LOOKAHEAD_BLOCKS = _LOOKAHEAD_BLOCKS; + } + + /** + * @notice Set weight caps for a given operator set + * @param operatorSet The operator set to set caps for + * @param maxWeights Array of maximum allowed weights per stake type (0 = no cap) + * Index 0 is the total weight cap for backwards compatibility + * @dev Only the AVS can set caps for their operator sets + */ + function setWeightCaps( + OperatorSet calldata operatorSet, + uint256[] calldata maxWeights + ) external checkCanCall(operatorSet.avs) { + require(maxWeights.length > 0, "BN254TableCalculatorWithCaps: empty weight caps array"); + + bytes32 operatorSetKey = operatorSet.key(); + weightCaps[operatorSetKey] = maxWeights; + + emit WeightCapsSet(operatorSet, maxWeights); + } + + /** + * @notice Set the total weight cap for a given operator set (backwards compatibility) + * @param operatorSet The operator set to set the cap for + * @param maxWeight Maximum allowed total weight per operator (0 = no cap) + * @dev Only the AVS can set caps for their operator sets + */ + function setWeightCap( + OperatorSet calldata operatorSet, + uint256 maxWeight + ) external checkCanCall(operatorSet.avs) { + bytes32 operatorSetKey = operatorSet.key(); + uint256[] memory caps = new uint256[](1); + caps[0] = maxWeight; + weightCaps[operatorSetKey] = caps; + + emit WeightCapsSet(operatorSet, caps); + } + + /** + * @notice Get weight caps for a given operator set + * @param operatorSet The operator set to get caps for + * @return maxWeights Array of maximum weight caps per stake type (0 = no cap) + */ + function getWeightCaps( + OperatorSet calldata operatorSet + ) external view returns (uint256[] memory maxWeights) { + bytes32 operatorSetKey = operatorSet.key(); + return weightCaps[operatorSetKey]; + } + + /** + * @notice Get the total weight cap for a given operator set (backwards compatibility) + * @param operatorSet The operator set to get the cap for + * @return maxWeight The maximum weight cap (0 = no cap) + */ + function getWeightCap( + OperatorSet calldata operatorSet + ) external view returns (uint256 maxWeight) { + bytes32 operatorSetKey = operatorSet.key(); + uint256[] storage caps = weightCaps[operatorSetKey]; + return caps.length > 0 ? caps[0] : 0; + } + + /** + * @notice Get operator weights with caps applied + * @param operatorSet The operator set to calculate weights for + * @return operators Array of operator addresses + * @return weights Array of weights per operator + */ + 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) { + uint256 totalWeight; + for (uint256 stratIndex = 0; stratIndex < strategies.length; ++stratIndex) { + totalWeight += minSlashableStake[i][stratIndex]; + } + + if (totalWeight > 0) { + weights[operatorCount] = new uint256[](1); + weights[operatorCount][0] = totalWeight; + operators[operatorCount] = registeredOperators[i]; + operatorCount++; + } + } + + assembly { + mstore(operators, operatorCount) + mstore(weights, operatorCount) + } + + // Apply weight caps if configured + bytes32 operatorSetKey = operatorSet.key(); + uint256[] storage maxWeights = weightCaps[operatorSetKey]; + + if (maxWeights.length > 0) { + (operators, weights) = WeightCapUtils.applyWeightCaps(operators, weights, maxWeights); + } + + return (operators, weights); + } +} diff --git a/src/middlewareV2/tableCalculator/unaudited/BN254WeightedTableCalculator.sol b/src/middlewareV2/tableCalculator/unaudited/BN254WeightedTableCalculator.sol new file mode 100644 index 00000000..d40f32a4 --- /dev/null +++ b/src/middlewareV2/tableCalculator/unaudited/BN254WeightedTableCalculator.sol @@ -0,0 +1,167 @@ +// 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 {IPermissionController} from + "eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol"; +import {PermissionControllerMixin} from + "eigenlayer-contracts/src/contracts/mixins/PermissionControllerMixin.sol"; + +import "../BN254TableCalculatorBase.sol"; + +/** + * @title BN254WeightedTableCalculator + * @notice Implementation that calculates BN254 operator tables using custom multipliers for different strategies + * @dev This contract allows AVSs to set custom multipliers for each strategy instead of weighting all strategies equally. + */ +contract BN254WeightedTableCalculator is BN254TableCalculatorBase, PermissionControllerMixin { + // Constants + /// @notice Default multiplier in basis points (10000 = 1x) + uint256 public constant DEFAULT_STRATEGY_MULTIPLIER = 10000; + + // 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; + + // Storage + /// @notice Mapping from operatorSet hash to strategy to multiplier (in basis points, 10000 = 1x) + mapping(bytes32 => mapping(IStrategy => uint256)) public strategyMultipliers; + + // Events + /// @notice Emitted when strategy multipliers are updated for an operator set + event StrategyMultipliersUpdated( + OperatorSet indexed operatorSet, IStrategy[] strategies, uint256[] multipliers + ); + + // Errors + error ArrayLengthMismatch(); + + constructor( + IKeyRegistrar _keyRegistrar, + IAllocationManager _allocationManager, + IPermissionController _permissionController, + uint256 _LOOKAHEAD_BLOCKS + ) BN254TableCalculatorBase(_keyRegistrar) PermissionControllerMixin(_permissionController) { + allocationManager = _allocationManager; + LOOKAHEAD_BLOCKS = _LOOKAHEAD_BLOCKS; + } + + /** + * @notice Set strategy multipliers for a given operator set + * @param operatorSet The operator set to set multipliers for + * @param strategies Array of strategies to set multipliers for + * @param multipliers Array of multipliers in basis points (10000 = 1x) + * @dev Only the AVS can set multipliers for their operator sets + */ + function setStrategyMultipliers( + OperatorSet calldata operatorSet, + IStrategy[] calldata strategies, + uint256[] calldata multipliers + ) external checkCanCall(operatorSet.avs) { + // Validate input arrays + require(strategies.length == multipliers.length, ArrayLengthMismatch()); + + bytes32 operatorSetKey = operatorSet.key(); + + // Set multipliers for each strategy + for (uint256 i = 0; i < strategies.length; i++) { + strategyMultipliers[operatorSetKey][strategies[i]] = multipliers[i]; + strategyMultipliersSet[operatorSetKey][strategies[i]] = true; + } + + emit StrategyMultipliersUpdated(operatorSet, strategies, multipliers); + } + + // Storage to track which strategies have been explicitly set + mapping(bytes32 => mapping(IStrategy => bool)) public strategyMultipliersSet; + + /** + * @notice Get the strategy multiplier for a given operator set and strategy + * @param operatorSet The operator set + * @param strategy The strategy + * @return multiplier The multiplier in basis points (returns 10000 if not set) + */ + function getStrategyMultiplier( + OperatorSet calldata operatorSet, + IStrategy strategy + ) external view returns (uint256 multiplier) { + bytes32 operatorSetKey = operatorSet.key(); + if (strategyMultipliersSet[operatorSetKey][strategy]) { + multiplier = strategyMultipliers[operatorSetKey][strategy]; + } else { + multiplier = DEFAULT_STRATEGY_MULTIPLIER; // Default 1x multiplier + } + } + + /** + * @notice Get the operator weights for a given operatorSet based on weighted 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 weighted 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) + }); + + bytes32 operatorSetKey = operatorSet.key(); + + 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 apply multipliers before summing + uint256 totalWeight; + for (uint256 stratIndex = 0; stratIndex < strategies.length; ++stratIndex) { + uint256 stakeAmount = minSlashableStake[i][stratIndex]; + + // Get the multiplier for this strategy (default to DEFAULT_STRATEGY_MULTIPLIER if not set) + uint256 multiplier; + if (strategyMultipliersSet[operatorSetKey][strategies[stratIndex]]) { + multiplier = strategyMultipliers[operatorSetKey][strategies[stratIndex]]; + } else { + multiplier = DEFAULT_STRATEGY_MULTIPLIER; // Default 1x multiplier + } + + // Apply multiplier (divide by DEFAULT_STRATEGY_MULTIPLIER to convert from basis points) + totalWeight += (stakeAmount * multiplier) / DEFAULT_STRATEGY_MULTIPLIER; + } + + // If the operator has nonzero weighted stake, add them to the operators array + if (totalWeight > 0) { + // Initialize operator weights array of length 1 just for weighted 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 weighted stake + assembly { + mstore(operators, operatorCount) + mstore(weights, operatorCount) + } + + return (operators, weights); + } +} diff --git a/src/unaudited/libraries/WeightCapUtils.sol b/src/unaudited/libraries/WeightCapUtils.sol new file mode 100644 index 00000000..3572b323 --- /dev/null +++ b/src/unaudited/libraries/WeightCapUtils.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +/** + * @title WeightCapUtils + * @notice Utility library for applying weight caps to operator weights + */ +library WeightCapUtils { + /** + * @notice Apply single weight cap to operator weights (backwards compatibility) + * @param operators Array of operator addresses + * @param weights 2D array of weights for each operator + * @param maxWeight Maximum allowed total weight per operator (0 = no cap) + * @return cappedOperators Array of operators after applying caps + * @return cappedWeights Array of weights after applying caps + */ + function applyWeightCap( + address[] memory operators, + uint256[][] memory weights, + uint256 maxWeight + ) internal pure returns (address[] memory cappedOperators, uint256[][] memory cappedWeights) { + uint256[] memory maxWeights = new uint256[](1); + maxWeights[0] = maxWeight; + return applyWeightCaps(operators, weights, maxWeights); + } + /** + * @notice Apply weight caps to operator weights + * @param operators Array of operator addresses + * @param weights 2D array of weights for each operator + * @param maxWeights Array of maximum allowed weights per stake type (0 = no cap) + * Index 0 is treated as total weight cap if array length is 1 + * @return cappedOperators Array of operators after filtering + * @return cappedWeights Array of weights after applying caps + * @dev For single cap: truncates to cap (first weight = cap, rest = 0) and filters zero-weight operators. For multi-cap: applies per-stake-type caps. + */ + + function applyWeightCaps( + address[] memory operators, + uint256[][] memory weights, + uint256[] memory maxWeights + ) internal pure returns (address[] memory cappedOperators, uint256[][] memory cappedWeights) { + require( + operators.length == weights.length, "WeightCapUtils: operators/weights length mismatch" + ); + + if (maxWeights.length == 0 || operators.length == 0) { + return (operators, weights); + } + + if (maxWeights.length == 1 && maxWeights[0] == 0) { + return (operators, weights); + } + + // Count operators with non-zero weights for filtering + uint256 validOperatorCount = 0; + bool[] memory isValid = new bool[](operators.length); + + for (uint256 i = 0; i < operators.length; i++) { + uint256 totalWeight = 0; + for (uint256 j = 0; j < weights[i].length; j++) { + totalWeight += weights[i][j]; + } + + if (totalWeight > 0) { + isValid[i] = true; + validOperatorCount++; + } + } + + // Initialize result arrays with only valid operators + cappedOperators = new address[](validOperatorCount); + cappedWeights = new uint256[][](validOperatorCount); + + uint256 resultIndex = 0; + for (uint256 i = 0; i < operators.length; i++) { + if (!isValid[i]) continue; + + cappedOperators[resultIndex] = operators[i]; + cappedWeights[resultIndex] = new uint256[](weights[i].length); + + if (maxWeights.length == 1 && maxWeights[0] > 0) { + // Legacy mode: single total weight cap with truncation behavior + uint256 totalWeight = 0; + for (uint256 j = 0; j < weights[i].length; j++) { + totalWeight += weights[i][j]; + } + + if (totalWeight <= maxWeights[0]) { + // No cap needed + for (uint256 j = 0; j < weights[i].length; j++) { + cappedWeights[resultIndex][j] = weights[i][j]; + } + } else { + // Cap with truncation: set first weight to cap, zero out rest + cappedWeights[resultIndex][0] = maxWeights[0]; + for (uint256 j = 1; j < weights[i].length; j++) { + cappedWeights[resultIndex][j] = 0; + } + } + } else { + // Per-stake-type caps + for (uint256 j = 0; j < weights[i].length; j++) { + if (j < maxWeights.length && maxWeights[j] > 0) { + cappedWeights[resultIndex][j] = + weights[i][j] > maxWeights[j] ? maxWeights[j] : weights[i][j]; + } else { + cappedWeights[resultIndex][j] = weights[i][j]; + } + } + } + + resultIndex++; + } + } +} diff --git a/test/unit/libraries/WeightCapUtilsUnit.t.sol b/test/unit/libraries/WeightCapUtilsUnit.t.sol new file mode 100644 index 00000000..bde23105 --- /dev/null +++ b/test/unit/libraries/WeightCapUtilsUnit.t.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; +import {WeightCapUtils} from "../../../src/unaudited/libraries/WeightCapUtils.sol"; + +/** + * @title WeightCapUtilsUnitTests + * @notice Unit tests for WeightCapUtils library + */ +contract WeightCapUtilsUnitTests is Test { + // Test addresses + address public operator1 = address(0x1); + address public operator2 = address(0x2); + address public operator3 = address(0x3); + + function _createSingleWeights( + uint256[] memory weights + ) internal pure returns (uint256[][] memory) { + uint256[][] memory result = new uint256[][](weights.length); + for (uint256 i = 0; i < weights.length; i++) { + result[i] = new uint256[](1); + result[i][0] = weights[i]; + } + return result; + } + + function _extractTotalWeights( + uint256[][] memory weights + ) internal pure returns (uint256[] memory) { + uint256[] memory totals = new uint256[](weights.length); + for (uint256 i = 0; i < weights.length; i++) { + for (uint256 j = 0; j < weights[i].length; j++) { + totals[i] += weights[i][j]; + } + } + return totals; + } + + function test_applyWeightCap_noCap() public { + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + uint256[] memory weights = new uint256[](2); + weights[0] = 100 ether; + weights[1] = 200 ether; + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + WeightCapUtils.applyWeightCap(operators, _createSingleWeights(weights), 0); + + // Should be unchanged with no cap + assertEq(resultOperators.length, 2); + assertEq(resultOperators[0], operator1); + assertEq(resultOperators[1], operator2); + + uint256[] memory resultTotals = _extractTotalWeights(resultWeights); + assertEq(resultTotals[0], 100 ether); + assertEq(resultTotals[1], 200 ether); + } + + function test_applyWeightCap_someOperatorsCapped() public { + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + uint256[] memory weights = new uint256[](3); + weights[0] = 50 ether; // Under cap + weights[1] = 150 ether; // Over cap + weights[2] = 200 ether; // Over cap + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + WeightCapUtils.applyWeightCap(operators, _createSingleWeights(weights), 100 ether); + + assertEq(resultOperators.length, 3); + assertEq(resultOperators[0], operator1); + assertEq(resultOperators[1], operator2); + assertEq(resultOperators[2], operator3); + uint256[] memory resultTotals = _extractTotalWeights(resultWeights); + assertEq(resultTotals[0], 50 ether); // Unchanged (under cap) + assertEq(resultTotals[1], 100 ether); // Capped from 150 + assertEq(resultTotals[2], 100 ether); // Capped from 200 + } + + function test_applyWeightCap_allOperatorsUnderCap() public { + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + uint256[] memory weights = new uint256[](3); + weights[0] = 50 ether; + weights[1] = 75 ether; + weights[2] = 90 ether; + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + WeightCapUtils.applyWeightCap(operators, _createSingleWeights(weights), 100 ether); + + assertEq(resultOperators.length, 3); + + uint256[] memory resultTotals = _extractTotalWeights(resultWeights); + assertEq(resultTotals[0], 50 ether); + assertEq(resultTotals[1], 75 ether); + assertEq(resultTotals[2], 90 ether); + } + + function test_applyWeightCap_zeroWeightOperatorsFiltered() public { + address[] memory operators = new address[](4); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + operators[3] = address(0x4); + + uint256[] memory weights = new uint256[](4); + weights[0] = 100 ether; + weights[1] = 0; // Zero weight + weights[2] = 150 ether; + weights[3] = 0; // Zero weight + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + WeightCapUtils.applyWeightCap(operators, _createSingleWeights(weights), 120 ether); + + // Only non-zero weight operators should remain + assertEq(resultOperators.length, 2); + assertEq(resultOperators[0], operator1); + assertEq(resultOperators[1], operator3); + + uint256[] memory resultTotals = _extractTotalWeights(resultWeights); + assertEq(resultTotals[0], 100 ether); // Under cap + assertEq(resultTotals[1], 120 ether); // Capped from 150 + } + + function test_applyWeightCap_emptyOperators() public { + address[] memory operators = new address[](0); + uint256[][] memory weights = new uint256[][](0); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + WeightCapUtils.applyWeightCap(operators, weights, 100 ether); + + assertEq(resultOperators.length, 0); + assertEq(resultWeights.length, 0); + } + + function test_applyWeightCap_multiDimensionalWeights() public { + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + // Create 2D weights where each operator has 2 weight types + uint256[][] memory weights = new uint256[][](2); + weights[0] = new uint256[](2); + weights[0][0] = 60 ether; // operator1: 60 + 40 = 100 total (at cap) + weights[0][1] = 40 ether; + weights[1] = new uint256[](2); + weights[1][0] = 120 ether; // operator2: 120 + 80 = 200 total (over cap) + weights[1][1] = 80 ether; + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + WeightCapUtils.applyWeightCap(operators, weights, 100 ether); + + assertEq(resultOperators.length, 2); + + // operator1 should be unchanged (total = 100, exactly at cap) + assertEq(resultWeights[0][0], 60 ether); + assertEq(resultWeights[0][1], 40 ether); + + // operator2 should be capped: primary weight = 100, secondary = 0 + assertEq(resultWeights[1][0], 100 ether); // Capped to maxWeight + assertEq(resultWeights[1][1], 0); // Zeroed out + + // Verify total weights + uint256[] memory resultTotals = _extractTotalWeights(resultWeights); + assertEq(resultTotals[0], 100 ether); + assertEq(resultTotals[1], 100 ether); + } + + function test_applyWeightCap_simpleTruncation() public { + address[] memory operators = new address[](1); + operators[0] = operator1; + + // Create weights that exceed the cap + uint256[][] memory weights = new uint256[][](1); + weights[0] = new uint256[](3); + weights[0][0] = 300 ether; // Primary weight + weights[0][1] = 200 ether; // Secondary weight + weights[0][2] = 100 ether; // Tertiary weight + // Total: 600 ether, should be capped to 150 ether + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + WeightCapUtils.applyWeightCap(operators, weights, 150 ether); + + assertEq(resultOperators.length, 1); + + // Check simple truncation: primary weight = cap, others = 0 + assertEq(resultWeights[0][0], 150 ether); // Capped to maxWeight + assertEq(resultWeights[0][1], 0); // Zeroed out + assertEq(resultWeights[0][2], 0); // Zeroed out + + // Verify total + uint256 total = resultWeights[0][0] + resultWeights[0][1] + resultWeights[0][2]; + assertEq(total, 150 ether); + } +} diff --git a/test/unit/middlewareV2/BN254TableCalculatorWithCapsUnit.t.sol b/test/unit/middlewareV2/BN254TableCalculatorWithCapsUnit.t.sol new file mode 100644 index 00000000..4890001d --- /dev/null +++ b/test/unit/middlewareV2/BN254TableCalculatorWithCapsUnit.t.sol @@ -0,0 +1,325 @@ +// 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 {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import {IPermissionController} from + "eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import { + OperatorSet, + OperatorSetLib +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {PermissionControllerMixin} from + "eigenlayer-contracts/src/contracts/mixins/PermissionControllerMixin.sol"; + +import {BN254TableCalculatorWithCaps} from + "../../../src/middlewareV2/tableCalculator/unaudited/BN254TableCalculatorWithCaps.sol"; +import {BN254TableCalculatorBase} from + "../../../src/middlewareV2/tableCalculator/BN254TableCalculatorBase.sol"; +import {MockEigenLayerDeployer} from "./MockDeployer.sol"; + +/** + * @title BN254TableCalculatorWithCapsUnitTests + * @notice Unit tests for BN254TableCalculatorWithCaps + */ +contract BN254TableCalculatorWithCapsUnitTests is MockEigenLayerDeployer { + using OperatorSetLib for OperatorSet; + + // Test contracts + BN254TableCalculatorWithCaps public calculator; + + IStrategy public strategy1 = IStrategy(address(0x100)); + IStrategy public strategy2 = IStrategy(address(0x200)); + OperatorSet public operatorSet; + + // Test addresses + address public avs1 = address(0x1); + address public operator1 = address(0x3); + address public operator2 = address(0x4); + address public operator3 = address(0x5); + address public unauthorizedCaller = address(0x999); + + // Test constants + uint256 public constant TEST_LOOKAHEAD_BLOCKS = 100; + + event WeightCapsSet(OperatorSet indexed operatorSet, uint256[] maxWeights); + + function setUp() public virtual { + _deployMockEigenLayer(); + + // Deploy calculator + calculator = new BN254TableCalculatorWithCaps( + IKeyRegistrar(address(keyRegistrarMock)), + IAllocationManager(address(allocationManagerMock)), + IPermissionController(address(permissionController)), + TEST_LOOKAHEAD_BLOCKS + ); + + // Set up operator set + operatorSet = OperatorSet({avs: avs1, id: 1}); + } + + function _setupOperatorSet( + OperatorSet memory opSet, + address[] memory operators, + IStrategy[] memory strategies, + uint256[][] memory minSlashableStake + ) internal { + allocationManagerMock.setMembersInOperatorSet(opSet, operators); + allocationManagerMock.setStrategiesInOperatorSet(opSet, strategies); + allocationManagerMock.setMinimumSlashableStake( + opSet, operators, strategies, minSlashableStake + ); + } + + /*////////////////////////////////////////////////////////////// + WEIGHT CAP CONFIGURATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_setWeightCap_success() public { + uint256 maxWeight = 100 ether; + + uint256[] memory expectedWeights = new uint256[](1); + expectedWeights[0] = maxWeight; + + vm.expectEmit(true, false, false, true); + emit WeightCapsSet(operatorSet, expectedWeights); + + vm.prank(avs1); + calculator.setWeightCap(operatorSet, maxWeight); + assertEq(calculator.getWeightCap(operatorSet), maxWeight); + } + + function test_setWeightCap_revertsOnUnauthorized() public { + vm.prank(unauthorizedCaller); + vm.expectRevert(PermissionControllerMixin.InvalidPermissions.selector); + calculator.setWeightCap(operatorSet, 100 ether); + } + + function test_setWeightCap_allowsZeroCap() public { + // Set a cap first + vm.prank(avs1); + calculator.setWeightCap(operatorSet, 100 ether); + assertEq(calculator.getWeightCap(operatorSet), 100 ether); + + // Remove the cap by setting to 0 + uint256[] memory expectedZeroWeights = new uint256[](1); + expectedZeroWeights[0] = 0; + + vm.expectEmit(true, false, false, true); + emit WeightCapsSet(operatorSet, expectedZeroWeights); + + vm.prank(avs1); + calculator.setWeightCap(operatorSet, 0); + + assertEq(calculator.getWeightCap(operatorSet), 0); + } + + function test_getWeightCap_returnsZeroByDefault() public { + uint256 cap = calculator.getWeightCap(operatorSet); + assertEq(cap, 0); + } + + /*////////////////////////////////////////////////////////////// + WEIGHT CALCULATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_getOperatorWeights_withoutCap() public { + // Setup operators and strategies + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + // Set up stakes: different amounts + uint256[][] memory stakes = new uint256[][](3); + stakes[0] = new uint256[](1); + stakes[0][0] = 50 ether; + stakes[1] = new uint256[](1); + stakes[1][0] = 150 ether; + stakes[2] = new uint256[](1); + stakes[2][0] = 300 ether; + + _setupOperatorSet(operatorSet, operators, strategies, stakes); + + // Set key registrations + keyRegistrarMock.setIsRegistered(operator1, operatorSet, true); + keyRegistrarMock.setIsRegistered(operator2, operatorSet, true); + keyRegistrarMock.setIsRegistered(operator3, operatorSet, true); + + // No cap set (default 0) + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.getOperatorSetWeights(operatorSet); + + // Should return uncapped weights + assertEq(resultOperators.length, 3); + assertEq(resultOperators[0], operator1); + assertEq(resultOperators[1], operator2); + assertEq(resultOperators[2], operator3); + assertEq(resultWeights[0][0], 50 ether); + assertEq(resultWeights[1][0], 150 ether); + assertEq(resultWeights[2][0], 300 ether); + } + + function test_getOperatorWeights_withCap() public { + // Setup operators and strategies + address[] memory operators = new address[](3); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + // Set up stakes: different amounts + uint256[][] memory stakes = new uint256[][](3); + stakes[0] = new uint256[](1); + stakes[0][0] = 50 ether; // Under cap + stakes[1] = new uint256[](1); + stakes[1][0] = 150 ether; // Over cap + stakes[2] = new uint256[](1); + stakes[2][0] = 300 ether; // Way over cap + + _setupOperatorSet(operatorSet, operators, strategies, stakes); + + // Set key registrations + keyRegistrarMock.setIsRegistered(operator1, operatorSet, true); + keyRegistrarMock.setIsRegistered(operator2, operatorSet, true); + keyRegistrarMock.setIsRegistered(operator3, operatorSet, true); + + // Set weight cap + vm.prank(avs1); + calculator.setWeightCap(operatorSet, 100 ether); + + // Get weights (should be capped automatically) + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.getOperatorSetWeights(operatorSet); + + // Should return capped weights + assertEq(resultOperators.length, 3); + assertEq(resultOperators[0], operator1); + assertEq(resultOperators[1], operator2); + assertEq(resultOperators[2], operator3); + assertEq(resultWeights[0][0], 50 ether); // Unchanged (under cap) + assertEq(resultWeights[1][0], 100 ether); // Capped from 150 + assertEq(resultWeights[2][0], 100 ether); // Capped from 300 + } + + function test_getOperatorWeights_zeroWeightOperatorsFiltered() public { + // Setup operators where one has zero weight + 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 stakes = new uint256[][](3); + stakes[0] = new uint256[](1); + stakes[0][0] = 100 ether; + stakes[1] = new uint256[](1); + stakes[1][0] = 0; // Zero weight + stakes[2] = new uint256[](1); + stakes[2][0] = 200 ether; + + _setupOperatorSet(operatorSet, operators, strategies, stakes); + + // Set key registrations + keyRegistrarMock.setIsRegistered(operator1, operatorSet, true); + keyRegistrarMock.setIsRegistered(operator2, operatorSet, true); + keyRegistrarMock.setIsRegistered(operator3, operatorSet, true); + + // Set weight cap + vm.prank(avs1); + calculator.setWeightCap(operatorSet, 150 ether); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.getOperatorSetWeights(operatorSet); + + // Should only include non-zero weight operators, with caps applied + assertEq(resultOperators.length, 2); + assertEq(resultOperators[0], operator1); + assertEq(resultOperators[1], operator3); + assertEq(resultWeights[0][0], 100 ether); // Under cap + assertEq(resultWeights[1][0], 150 ether); // Capped from 200 + } + + /*////////////////////////////////////////////////////////////// + INTEGRATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_calculateOperatorTable_withCaps() public { + // Setup a complete scenario with caps applied + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory stakes = new uint256[][](2); + stakes[0] = new uint256[](1); + stakes[0][0] = 80 ether; // Under cap + stakes[1] = new uint256[](1); + stakes[1][0] = 200 ether; // Over cap + + _setupOperatorSet(operatorSet, operators, strategies, stakes); + keyRegistrarMock.setIsRegistered(operator1, operatorSet, true); + keyRegistrarMock.setIsRegistered(operator2, operatorSet, true); + + // Set weight cap + vm.prank(avs1); + calculator.setWeightCap(operatorSet, 100 ether); + + // Calculate operator table (should apply caps automatically) + BN254TableCalculatorBase.BN254OperatorSetInfo memory operatorSetInfo = + calculator.calculateOperatorTable(operatorSet); + + assertEq(operatorSetInfo.numOperators, 2); + assertEq(operatorSetInfo.totalWeights.length, 1); + // Total weight should be: 80 + 100 = 180 (operator2 capped) + assertEq(operatorSetInfo.totalWeights[0], 180 ether); + + // Verify operator tree root is not empty + assertTrue(operatorSetInfo.operatorInfoTreeRoot != bytes32(0)); + } + + function test_calculateOperatorTable_noCapsSet() public { + // Same test but without caps to verify normal behavior + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory stakes = new uint256[][](2); + stakes[0] = new uint256[](1); + stakes[0][0] = 80 ether; + stakes[1] = new uint256[](1); + stakes[1][0] = 200 ether; + + _setupOperatorSet(operatorSet, operators, strategies, stakes); + keyRegistrarMock.setIsRegistered(operator1, operatorSet, true); + keyRegistrarMock.setIsRegistered(operator2, operatorSet, true); + + // No caps set (default 0) + BN254TableCalculatorBase.BN254OperatorSetInfo memory operatorSetInfo = + calculator.calculateOperatorTable(operatorSet); + + assertEq(operatorSetInfo.numOperators, 2); + assertEq(operatorSetInfo.totalWeights.length, 1); + // Total weight should be: 80 + 200 = 280 (no caps) + assertEq(operatorSetInfo.totalWeights[0], 280 ether); + } +} diff --git a/test/unit/middlewareV2/BN254WeightedTableCalculatorUnit.t.sol b/test/unit/middlewareV2/BN254WeightedTableCalculatorUnit.t.sol new file mode 100644 index 00000000..1883df8d --- /dev/null +++ b/test/unit/middlewareV2/BN254WeightedTableCalculatorUnit.t.sol @@ -0,0 +1,423 @@ +// 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 {IKeyRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IKeyRegistrar.sol"; +import {IPermissionController} from + "eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import { + OperatorSet, + OperatorSetLib +} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; +import {PermissionControllerMixin} from + "eigenlayer-contracts/src/contracts/mixins/PermissionControllerMixin.sol"; + +import {BN254WeightedTableCalculator} from + "../../../src/middlewareV2/tableCalculator/unaudited/BN254WeightedTableCalculator.sol"; +import {BN254TableCalculatorBase} from + "../../../src/middlewareV2/tableCalculator/BN254TableCalculatorBase.sol"; +import {MockEigenLayerDeployer} from "./MockDeployer.sol"; + +// Harness to test internal functions +contract BN254WeightedTableCalculatorHarness is BN254WeightedTableCalculator { + constructor( + IKeyRegistrar _keyRegistrar, + IAllocationManager _allocationManager, + IPermissionController _permissionController, + uint256 _LOOKAHEAD_BLOCKS + ) + BN254WeightedTableCalculator( + _keyRegistrar, + _allocationManager, + _permissionController, + _LOOKAHEAD_BLOCKS + ) + {} + + function exposed_getOperatorWeights( + OperatorSet calldata operatorSet + ) external view returns (address[] memory operators, uint256[][] memory weights) { + return _getOperatorWeights(operatorSet); + } +} + +/** + * @title BN254WeightedTableCalculatorUnitTests + * @notice Unit tests for BN254WeightedTableCalculator + */ +contract BN254WeightedTableCalculatorUnitTests is MockEigenLayerDeployer { + using OperatorSetLib for OperatorSet; + + // Test contracts + BN254WeightedTableCalculatorHarness public calculator; + + // Test strategies (simple address casting like the original tests) + IStrategy public strategy1 = IStrategy(address(0x100)); + IStrategy public strategy2 = IStrategy(address(0x200)); + OperatorSet public operatorSet; + + // Test addresses + address public avs1 = address(0x1); + address public operator1 = address(0x3); + address public operator2 = address(0x4); + address public operator3 = address(0x5); + address public unauthorizedCaller = address(0x999); + + // Test constants + uint256 public constant TEST_LOOKAHEAD_BLOCKS = 100; + + event StrategyMultipliersUpdated( + OperatorSet indexed operatorSet, IStrategy[] strategies, uint256[] multipliers + ); + + function setUp() public virtual { + _deployMockEigenLayer(); + + // Deploy calculator + calculator = new BN254WeightedTableCalculatorHarness( + IKeyRegistrar(address(keyRegistrarMock)), + IAllocationManager(address(allocationManagerMock)), + IPermissionController(address(permissionController)), + TEST_LOOKAHEAD_BLOCKS + ); + + // Set up operator set + operatorSet = OperatorSet({avs: avs1, id: 1}); + + // Configure operator set for BN254 if needed for some tests + vm.prank(avs1); + keyRegistrar.configureOperatorSet(operatorSet, IKeyRegistrarTypes.CurveType.BN254); + } + + // Helper functions + function _setupOperatorSet( + OperatorSet memory opSet, + address[] memory operators, + IStrategy[] memory strategies, + uint256[][] memory minSlashableStake + ) internal { + allocationManagerMock.setMembersInOperatorSet(opSet, operators); + allocationManagerMock.setStrategiesInOperatorSet(opSet, strategies); + allocationManagerMock.setMinimumSlashableStake( + opSet, 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; + } + + /*////////////////////////////////////////////////////////////// + MULTIPLIER TESTS + //////////////////////////////////////////////////////////////*/ + + function test_setStrategyMultipliers_success() public { + IStrategy[] memory strategies = new IStrategy[](2); + strategies[0] = strategy1; + strategies[1] = strategy2; + + uint256[] memory multipliers = new uint256[](2); + multipliers[0] = 20000; // 2x + multipliers[1] = 5000; // 0.5x + + // Expect event emission + vm.expectEmit(true, false, false, true); + emit StrategyMultipliersUpdated(operatorSet, strategies, multipliers); + + vm.prank(avs1); + calculator.setStrategyMultipliers(operatorSet, strategies, multipliers); + + // Verify multipliers were set + assertEq(calculator.getStrategyMultiplier(operatorSet, strategies[0]), 20000); + assertEq(calculator.getStrategyMultiplier(operatorSet, strategies[1]), 5000); + } + + function test_setStrategyMultipliers_revertsOnUnauthorized() public { + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[] memory multipliers = new uint256[](1); + multipliers[0] = 15000; + + vm.prank(unauthorizedCaller); + vm.expectRevert(PermissionControllerMixin.InvalidPermissions.selector); + calculator.setStrategyMultipliers(operatorSet, strategies, multipliers); + } + + function test_setStrategyMultipliers_revertsOnArrayLengthMismatch() public { + IStrategy[] memory strategies = new IStrategy[](2); + strategies[0] = strategy1; + strategies[1] = strategy2; + + uint256[] memory multipliers = new uint256[](1); // Wrong length + multipliers[0] = 10000; + + vm.prank(avs1); + vm.expectRevert(BN254WeightedTableCalculator.ArrayLengthMismatch.selector); + calculator.setStrategyMultipliers(operatorSet, strategies, multipliers); + } + + function test_getStrategyMultiplier_returnsDefaultForUnset() public { + uint256 multiplier = calculator.getStrategyMultiplier(operatorSet, strategy1); + assertEq(multiplier, 10000); // Default 1x multiplier + } + + function test_setStrategyMultipliers_allowsZeroMultiplier() public { + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[] memory multipliers = new uint256[](1); + multipliers[0] = 0; // Zero multiplier + + vm.prank(avs1); + calculator.setStrategyMultipliers(operatorSet, strategies, multipliers); + + // Should return 0, not default + assertEq(calculator.getStrategyMultiplier(operatorSet, strategies[0]), 0); + } + + function test_setStrategyMultipliers_allowsExtremeMultipliers() public { + IStrategy[] memory strategies = new IStrategy[](2); + strategies[0] = strategy1; + strategies[1] = strategy2; + + uint256[] memory multipliers = new uint256[](2); + multipliers[0] = 1; // Very small + multipliers[1] = type(uint256).max; // Very large + + vm.prank(avs1); + calculator.setStrategyMultipliers(operatorSet, strategies, multipliers); + + assertEq(calculator.getStrategyMultiplier(operatorSet, strategies[0]), 1); + assertEq(calculator.getStrategyMultiplier(operatorSet, strategies[1]), type(uint256).max); + } + + /*////////////////////////////////////////////////////////////// + WEIGHT CALCULATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_getOperatorWeights_withMultipliers() public { + // Setup operators and strategies + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + IStrategy[] memory strategies = new IStrategy[](2); + strategies[0] = strategy1; + strategies[1] = strategy2; + + // Set up stakes: operator1 has 100 in each strategy, operator2 has 200 in each + uint256[][] memory stakes = new uint256[][](2); + stakes[0] = new uint256[](2); + stakes[0][0] = 100 ether; // operator1, strategy1 + stakes[0][1] = 100 ether; // operator1, strategy2 + stakes[1] = new uint256[](2); + stakes[1][0] = 200 ether; // operator2, strategy1 + stakes[1][1] = 200 ether; // operator2, strategy2 + + _setupOperatorSet(operatorSet, operators, strategies, stakes); + + // Set key registrations + keyRegistrarMock.setIsRegistered(operator1, operatorSet, true); + keyRegistrarMock.setIsRegistered(operator2, operatorSet, true); + + // Set multipliers: strategy1 = 2x, strategy2 = 0.5x + IStrategy[] memory strategiesForMultiplier = new IStrategy[](2); + strategiesForMultiplier[0] = strategy1; + strategiesForMultiplier[1] = strategy2; + + uint256[] memory multipliers = new uint256[](2); + multipliers[0] = 20000; // 2x + multipliers[1] = 5000; // 0.5x + + vm.prank(avs1); + calculator.setStrategyMultipliers(operatorSet, strategiesForMultiplier, multipliers); + + // Calculate weights + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(operatorSet); + + // Verify results + assertEq(resultOperators.length, 2); + assertEq(resultOperators[0], operator1); + assertEq(resultOperators[1], operator2); + + // Expected weights: + // operator1: (100 * 20000/10000) + (100 * 5000/10000) = 200 + 50 = 250 + // operator2: (200 * 20000/10000) + (200 * 5000/10000) = 400 + 100 = 500 + assertEq(resultWeights[0][0], 250 ether); + assertEq(resultWeights[1][0], 500 ether); + } + + function test_getOperatorWeights_withoutMultipliers() public { + // Setup without setting any multipliers (should use default 1x) + address[] memory operators = new address[](1); + operators[0] = operator1; + + IStrategy[] memory strategies = new IStrategy[](2); + strategies[0] = strategy1; + strategies[1] = strategy2; + + uint256[][] memory stakes = new uint256[][](1); + stakes[0] = new uint256[](2); + stakes[0][0] = 100 ether; + stakes[0][1] = 200 ether; + + _setupOperatorSet(operatorSet, operators, strategies, stakes); + keyRegistrarMock.setIsRegistered(operator1, operatorSet, true); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(operatorSet); + + // Should sum with default 1x multipliers: 100 + 200 = 300 + assertEq(resultOperators.length, 1); + assertEq(resultOperators[0], operator1); + assertEq(resultWeights[0][0], 300 ether); + } + + function test_getOperatorWeights_excludesUnregisteredOperators() public { + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory stakes = new uint256[][](2); + stakes[0] = new uint256[](1); + stakes[0][0] = 100 ether; + stakes[1] = new uint256[](1); + stakes[1][0] = 200 ether; + + _setupOperatorSet(operatorSet, operators, strategies, stakes); + + // Only register operator1 + keyRegistrarMock.setIsRegistered(operator1, operatorSet, true); + keyRegistrarMock.setIsRegistered(operator2, operatorSet, false); + + // Use calculateOperatorTable which does check registration, not just weights + BN254TableCalculatorBase.BN254OperatorSetInfo memory operatorSetInfo = + calculator.calculateOperatorTable(operatorSet); + + // Should only include operator1 (registered) + assertEq(operatorSetInfo.numOperators, 1); + assertEq(operatorSetInfo.totalWeights.length, 1); + assertEq(operatorSetInfo.totalWeights[0], 100 ether); + } + + function test_getOperatorWeights_withZeroStake() public { + address[] memory operators = new address[](1); + operators[0] = operator1; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + // Set zero stake + uint256[][] memory stakes = new uint256[][](1); + stakes[0] = new uint256[](1); + stakes[0][0] = 0; + + _setupOperatorSet(operatorSet, operators, strategies, stakes); + keyRegistrarMock.setIsRegistered(operator1, operatorSet, true); + + (address[] memory resultOperators, uint256[][] memory resultWeights) = + calculator.exposed_getOperatorWeights(operatorSet); + + // Should exclude operators with zero total weight + assertEq(resultOperators.length, 0); + } + + /*////////////////////////////////////////////////////////////// + INTEGRATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_calculateOperatorTable_integration() public { + // Setup a complete scenario and verify the full operator table calculation + address[] memory operators = new address[](1); + operators[0] = operator1; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + uint256[][] memory stakes = new uint256[][](1); + stakes[0] = new uint256[](1); + stakes[0][0] = 100 ether; + + _setupOperatorSet(operatorSet, operators, strategies, stakes); + keyRegistrarMock.setIsRegistered(operator1, operatorSet, true); + + // This should work without reverting and return a valid operator set info + BN254TableCalculatorBase.BN254OperatorSetInfo memory operatorSetInfo = + calculator.calculateOperatorTable(operatorSet); + + assertEq(operatorSetInfo.numOperators, 1); + assertEq(operatorSetInfo.totalWeights.length, 1); + assertEq(operatorSetInfo.totalWeights[0], 100 ether); + } + + function test_calculateOperatorTable_withSomeUnregisteredOperators() public { + // This test catches the audit issue where operatorInfoLeaves[i] was used instead of operatorInfoLeaves[operatorCount] + // When some operators are in allocation manager but not registered with key registrar + + address operator4 = address(0x6); + + // Setup 4 operators in allocation manager + address[] memory operators = new address[](4); + operators[0] = operator1; + operators[1] = operator2; + operators[2] = operator3; + operators[3] = operator4; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy1; + + // All operators have stake + uint256[][] memory stakes = new uint256[][](4); + for (uint256 i = 0; i < 4; i++) { + stakes[i] = new uint256[](1); + stakes[i][0] = (i + 1) * 100 ether; // 100, 200, 300, 400 + } + + _setupOperatorSet(operatorSet, operators, strategies, stakes); + + // Only register operators 1 and 3 with key registrar (skip 2 and 4) + keyRegistrarMock.setIsRegistered(operator1, operatorSet, true); + keyRegistrarMock.setIsRegistered(operator2, operatorSet, false); + keyRegistrarMock.setIsRegistered(operator3, operatorSet, true); + keyRegistrarMock.setIsRegistered(operator4, operatorSet, false); + + // Set multiplier for strategy1 + IStrategy[] memory strategiesForMultiplier = new IStrategy[](1); + strategiesForMultiplier[0] = strategy1; + uint256[] memory multipliers = new uint256[](1); + multipliers[0] = 20000; // 2x multiplier + + vm.prank(avs1); + calculator.setStrategyMultipliers(operatorSet, strategiesForMultiplier, multipliers); + + // Calculate operator table - this would fail with the audit bug because of array indexing mismatch + BN254TableCalculatorBase.BN254OperatorSetInfo memory operatorSetInfo = + calculator.calculateOperatorTable(operatorSet); + + // Should only include registered operators (1 and 3) + assertEq(operatorSetInfo.numOperators, 2); + assertEq(operatorSetInfo.totalWeights.length, 1); + + // Total weight should be: (100 * 2) + (300 * 2) = 200 + 600 = 800 + assertEq(operatorSetInfo.totalWeights[0], 800 ether); + + // Verify operator tree root is not empty (proves merkle tree was built correctly) + assertTrue(operatorSetInfo.operatorInfoTreeRoot != bytes32(0)); + } +}