diff --git a/src/chain/ResourceConstraintManager.sol b/src/chain/ResourceConstraintManager.sol index b7ed4816..67a6f61e 100644 --- a/src/chain/ResourceConstraintManager.sol +++ b/src/chain/ResourceConstraintManager.sol @@ -5,12 +5,19 @@ pragma solidity ^0.8.0; import "../precompiles/ArbOwner.sol"; -import "../precompiles/ArbGasInfo.sol"; import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; contract ResourceConstraintManager is AccessControlEnumerable { ArbOwner internal constant ARB_OWNER = ArbOwner(address(0x70)); - ArbGasInfo internal constant ARB_GAS_INFO = ArbGasInfo(address(0x6c)); + + // Constraint parameters boundaries + uint256 public constant MAX_SINGLE_GAS_CONSTRAINTS = 10; + uint256 public constant MAX_MULTI_GAS_CONSTRAINTS = 100; + uint64 public constant MIN_GAS_TARGET_PER_SEC = 7_000_000; + uint64 public constant MAX_GAS_TARGET_PER_SEC = 100_000_000; + uint32 public constant MIN_ADJUSTMENT_WINDOW_SECS = 5; + uint32 public constant MAX_ADJUSTMENT_WINDOW_SECS = 86400; + uint64 public constant MAX_PRICING_EXPONENT = 8000; // scaled by 1000 to allow for fractional exponents bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); uint256 public expiryTimestamp; @@ -22,6 +29,7 @@ contract ResourceConstraintManager is AccessControlEnumerable { error InvalidTarget( uint64 gasTargetPerSec, uint64 adjustmentWindowSecs, uint64 startingBacklogValue ); + error InvalidResources(uint8 resourceKind); error PricingExponentTooHigh(uint64 pricingExponent); error NotExpired(); @@ -50,7 +58,7 @@ contract ResourceConstraintManager is AccessControlEnumerable { ) external onlyRole(MANAGER_ROLE) { // If zero constraints are provided, the chain uses the single-constraint pricing model uint256 nConstraints = constraints.length; - if (nConstraints > 10) { + if (nConstraints > MAX_SINGLE_GAS_CONSTRAINTS) { revert TooManyConstraints(); } uint64 pricingExponent = 0; @@ -58,10 +66,15 @@ contract ResourceConstraintManager is AccessControlEnumerable { uint64 gasTargetPerSec = constraints[i][0]; uint64 adjustmentWindowSecs = constraints[i][1]; uint64 startingBacklogValue = constraints[i][2]; - if (gasTargetPerSec < 7_000_000 || gasTargetPerSec > 100_000_000) { + if ( + gasTargetPerSec < MIN_GAS_TARGET_PER_SEC || gasTargetPerSec > MAX_GAS_TARGET_PER_SEC + ) { revert InvalidTarget(gasTargetPerSec, adjustmentWindowSecs, startingBacklogValue); } - if (adjustmentWindowSecs < 5 || adjustmentWindowSecs > 86400) { + if ( + adjustmentWindowSecs < MIN_ADJUSTMENT_WINDOW_SECS + || adjustmentWindowSecs > MAX_ADJUSTMENT_WINDOW_SECS + ) { revert InvalidPeriod(gasTargetPerSec, adjustmentWindowSecs, startingBacklogValue); } // we scale by 1000 to improve precision in calculating the exponent @@ -74,15 +87,117 @@ contract ResourceConstraintManager is AccessControlEnumerable { (startingBacklogValue * 1000) / (gasTargetPerSec * adjustmentWindowSecs); } - // this calculated pricing exponent will by used by nitro to calculate the gas price + // this calculated pricing exponent will be used by nitro to calculate the gas price // we check that the pricing exponent is below some reasonable number to avoid setting the gas price astronomically high // as long as the gas price is not so high that no-one at all can send a transaction the chain will be able to function // eg. these constraints can be changed again, or the sec council can send admin transactions // with min base fee of 0.02, exponent of 8 (scaled by 1000) corresponds to a gas price of ~60 Gwei - if (pricingExponent > 8000) { + if (pricingExponent > MAX_PRICING_EXPONENT) { revert PricingExponentTooHigh(pricingExponent); } ARB_OWNER.setGasPricingConstraints(constraints); } + + /// @notice Sets the list of multi-gas pricing constraints for the multi-dimensional multi-constraint pricing model. + /// See ArbOwner.setMultiGasPricingConstraints interface for more information. + /// @param constraints Array of ResourceConstraint structs, each containing: + /// - resources: list of (ResourceKind, weight) pairs. Must be sorted by ResourceKind and contain no duplicate ResourceKinds. (see ArbMultiGasConstraintsTypes for struct definitions) + /// - adjustmentWindowSecs: time window (seconds) over which the price will rise by a factor of e if demand is 2x the target (uint32, seconds) + /// - targetPerSec: target gas usage per second for this constraint (uint64, gas/sec) + /// - backlog: initial backlog value for this constraint (uint64, gas units) + function setMultiGasPricingConstraints( + ArbMultiGasConstraintsTypes.ResourceConstraint[] calldata constraints + ) external onlyRole(MANAGER_ROLE) { + // If zero constraints are provided, the chain uses the single-constraint pricing model + // Each constraint adds a small amount of overhead to the gas cost of each transaction and block, so we limit the number of constraints that can be set + uint256 nConstraints = constraints.length; + if (nConstraints > MAX_MULTI_GAS_CONSTRAINTS) { + revert TooManyConstraints(); + } + + // We calculate the implied pricing exponent for each resource kind + uint8 numResourceKinds = uint8(type(ArbMultiGasConstraintsTypes.ResourceKind).max) + 1; + uint64[] memory pricingExponents = new uint64[](numResourceKinds); + for (uint256 i = 0; i < nConstraints; ++i) { + uint64 targetPerSec = constraints[i].targetPerSec; + uint32 adjustmentWindowSecs = constraints[i].adjustmentWindowSecs; + uint64 startingBacklogValue = constraints[i].backlog; + if (targetPerSec < MIN_GAS_TARGET_PER_SEC || targetPerSec > MAX_GAS_TARGET_PER_SEC) { + revert InvalidTarget(targetPerSec, adjustmentWindowSecs, startingBacklogValue); + } + if ( + adjustmentWindowSecs < MIN_ADJUSTMENT_WINDOW_SECS + || adjustmentWindowSecs > MAX_ADJUSTMENT_WINDOW_SECS + ) { + revert InvalidPeriod(targetPerSec, adjustmentWindowSecs, startingBacklogValue); + } + { + // Check for unsorted or duplicate resource kinds within this constraint + // The check is performed here instead of in the loop below (for calculating pricing exponents) + // to prevent bypassing the check when setting a starting backlog value of zero + // (in that case, nitro would only store the last of the duplicated entries) + // lastResourceKind starts at 0 so that Unknown (kind=0) is rejected by the sorted check + // out-of-range enum values are automatically rejected + uint8 lastResourceKind = 0; + uint256 nResources = constraints[i].resources.length; + for (uint256 j = 0; j < nResources; ++j) { + uint8 kind = uint8(constraints[i].resources[j].resource); + // check that resource kinds are sorted and contain no duplicates + // we disallow the resource kind "Unknown" (kind=0), so this condition also applies during the first iteration + if (kind <= lastResourceKind) { + revert InvalidResources(kind); + } + + // check that resources have non-zero weights + if (constraints[i].resources[j].weight == 0) { + revert InvalidResources(kind); + } + lastResourceKind = kind; + } + } + if (startingBacklogValue > 0) { + // Find the maximum weight among all resources in this constraint + uint64 maxWeight = 0; + uint256 nResources = constraints[i].resources.length; + for (uint256 j = 0; j < nResources; ++j) { + uint64 weight = constraints[i].resources[j].weight; + if (weight > maxWeight) { + maxWeight = weight; + } + } + + if (maxWeight > 0) { + // Neither of these values can be zero due to the earlier checks, so this division is safe + uint256 divisor = + uint256(adjustmentWindowSecs) * uint256(targetPerSec) * uint256(maxWeight); + + // Calculate per-resource-kind exponent contribution + // we scale by 1000 to improve precision in calculating the exponent + // since this division will round down, it's always possible for the real exponent to be up to + // the number of constraints greater than the value we measure + // Operation is performed in uint256 to avoid overflow, but the result is guaranteed to fit in uint64 due to the earlier check + for (uint256 j = 0; j < nResources; ++j) { + uint8 kind = uint8(constraints[i].resources[j].resource); + uint64 weight = constraints[i].resources[j].weight; + pricingExponents[kind] += + uint64(uint256(startingBacklogValue) * uint256(weight) * 1000 / divisor); + } + } + } + } + + // this calculated pricing exponent will be used by nitro to calculate the gas price + // we check that the pricing exponent is below some reasonable number to avoid setting the gas price astronomically high + // as long as the gas price is not so high that no-one at all can send a transaction the chain will be able to function + // eg. these constraints can be changed again, or the sec council can send admin transactions + // with min base fee of 0.02, exponent of 8 (scaled by 1000) corresponds to a gas price of ~60 Gwei + for (uint8 k = 0; k < numResourceKinds; ++k) { + if (pricingExponents[k] > MAX_PRICING_EXPONENT) { + revert PricingExponentTooHigh(pricingExponents[k]); + } + } + + ARB_OWNER.setMultiGasPricingConstraints(constraints); + } } diff --git a/src/precompiles b/src/precompiles index f49a4889..3d46aa39 160000 --- a/src/precompiles +++ b/src/precompiles @@ -1 +1 @@ -Subproject commit f49a4889b486fd804a7901203f5f663cfd1581c8 +Subproject commit 3d46aa39ae44802a6656bc5fd393b6751c9347fd diff --git a/test/foundry/ResourceConstraintManager.t.sol b/test/foundry/ResourceConstraintManager.t.sol index 0ab94eaa..0b4055a2 100644 --- a/test/foundry/ResourceConstraintManager.t.sol +++ b/test/foundry/ResourceConstraintManager.t.sol @@ -30,6 +30,9 @@ contract ResourceConstraintManagerTest is Test { assertTrue(ARB_OWNER.removeChainOwnerCalled()); } + // + // --- setGasPricingConstraints tests --- + // function test_setGasPricingConstraints_success() external { // Test with valid single constraint uint64[3][] memory constraints = new uint64[3][](1); @@ -234,14 +237,603 @@ contract ResourceConstraintManagerTest is Test { ); resourceConstraintManager.setGasPricingConstraints(constraints); } + + // + // --- setMultiGasPricingConstraints tests --- + // + function _createMultiGasConstraintWithResources( + uint64 targetPerSec, + uint32 adjustmentWindowSecs, + uint64 backlog, + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources + ) internal pure returns (ArbMultiGasConstraintsTypes.ResourceConstraint memory) { + return ArbMultiGasConstraintsTypes.ResourceConstraint({ + resources: resources, + adjustmentWindowSecs: adjustmentWindowSecs, + targetPerSec: targetPerSec, + backlog: backlog + }); + } + + function _createMultiGasConstraint( + uint64 targetPerSec, + uint32 adjustmentWindowSecs, + uint64 backlog + ) internal pure returns (ArbMultiGasConstraintsTypes.ResourceConstraint memory) { + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources = + new ArbMultiGasConstraintsTypes.WeightedResource[](1); + resources[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 1 + }); + return _createMultiGasConstraintWithResources( + targetPerSec, adjustmentWindowSecs, backlog, resources + ); + } + + function test_setMultiGasPricingConstraints_success() external { + // Test with valid single constraint using single resource kind + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints[0] = _createMultiGasConstraint(10_000_000, 100, 0); + + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); + + // Test with multiple constraints using different resource kinds and weights + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory multipleConstraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](3); + + // Single resource: Computation with weight 1 + multipleConstraints[0] = _createMultiGasConstraint(7_000_000, 5, 0); + + // Multiple resources: HistoryGrowth (weight 2) + StorageAccessRead (weight 3) + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources2 = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + resources2[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 2 + }); + resources2[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.StorageAccessRead, + weight: 3 + }); + multipleConstraints[1] = + _createMultiGasConstraintWithResources(50_000_000, 1000, 1, resources2); + + // Multiple resources: SingleDim (weight 1) + WasmComputation (weight 5) + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources3 = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + resources3[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.SingleDim, + weight: 1 + }); + resources3[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.WasmComputation, + weight: 5 + }); + multipleConstraints[2] = + _createMultiGasConstraintWithResources(100_000_000, 86400, 10000, resources3); + + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(multipleConstraints); + + // Test with empty constraints array (switch to previous pricing model) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory emptyConstraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](0); + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(emptyConstraints); + } + + function test_setMultiGasPricingConstraints_pricingExponentTooHigh() external { + // Create constraints on the limit of the pricing exponent + // Computation (weight=1), maxWeight=1 + // exponent = backlog * 1 * 1000 / (targetPerSec * adjustmentWindowSecs * 1) = backlog * 1000 / (targetPerSec * adjustmentWindowSecs) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory multipleConstraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](3); + // 35_000_000 * 1000 / (7_000_000 * 5) = 1000 + multipleConstraints[0] = _createMultiGasConstraint(7_000_000, 5, 35_000_000); + // 300_000_000_000 * 1000 / (50_000_000 * 1000) = 6000 + multipleConstraints[1] = _createMultiGasConstraint(50_000_000, 1000, 300_000_000_000); + // 8_640_000_000_000 * 1000 / (100_000_000 * 86400) = 1000 + multipleConstraints[2] = _createMultiGasConstraint(100_000_000, 86400, 8_640_000_000_000); + // total = 1000 + 6000 + 1000 = 8000 + + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(multipleConstraints); + + // Modify constraint and set it up to the limit: 300_049_999_999 * 1000 / 50_000_000_000 = 6000, total = 8000 + multipleConstraints[1] = _createMultiGasConstraint(50_000_000, 1000, 300_049_999_999); + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(multipleConstraints); + + // Modify constraint and set it over the limit: 300_050_000_000 * 1000 / 50_000_000_000 = 6001, total = 8001 + multipleConstraints[1] = _createMultiGasConstraint(50_000_000, 1000, 300_050_000_000); + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector(ResourceConstraintManager.PricingExponentTooHigh.selector, 8001) + ); + resourceConstraintManager.setMultiGasPricingConstraints(multipleConstraints); + + // Test with multi-resource constraint (with one resource over the limit) + // Computation (weight=2) and HistoryGrowth (weight=1), targetPerSec=10_000_000, adjustmentWindowSecs=100 + // maxWeight=2, divisor=2_000_000_000 + // Computation exponent = backlog * 2 * 1000 / 2_000_000_000 = backlog / 1_000_000 + // HistoryGrowth exponent = backlog * 1 * 1000 / 2_000_000_000 = backlog / 2_000_000 + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + resources[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 2 + }); + resources[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 1 + }); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory multiResourceConstraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + + // Constraint up to the limit: Computation exponent = 8000, HistoryGrowth exponent = 4000 + multiResourceConstraints[0] = + _createMultiGasConstraintWithResources(10_000_000, 100, 8_000_000_000, resources); + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(multiResourceConstraints); + + // Constraint over the limit: Computation exponent = 8001, HistoryGrowth exponent = 4000 + multiResourceConstraints[0] = + _createMultiGasConstraintWithResources(10_000_000, 100, 8_001_000_000, resources); + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector(ResourceConstraintManager.PricingExponentTooHigh.selector, 8001) + ); + resourceConstraintManager.setMultiGasPricingConstraints(multiResourceConstraints); + } + + function test_setMultiGasPricingConstraints_accessControl() external { + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints[0] = _createMultiGasConstraint(10_000_000, 100, 0); + + // Test non-manager cannot call + vm.expectRevert(); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); + + // Test admin without manager role cannot call + vm.prank(admin); + vm.expectRevert(); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); + + // Test manager can call + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); + } + + function test_setMultiGasPricingConstraints_invalidTarget() external { + // Test gas target below minimum (6,999,999) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraintsLowTarget = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraintsLowTarget[0] = _createMultiGasConstraint(6_999_999, 100, 0); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidTarget.selector, + uint64(6_999_999), + uint64(100), + uint64(0) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraintsLowTarget); + + // Test gas target above maximum (100,000,001) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraintsHighTarget = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraintsHighTarget[0] = _createMultiGasConstraint(100_000_001, 100, 0); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidTarget.selector, + uint64(100_000_001), + uint64(100), + uint64(0) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraintsHighTarget); + + // Test edge cases (exactly at boundaries should succeed) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraintsMinTarget = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraintsMinTarget[0] = _createMultiGasConstraint(7_000_000, 100, 0); + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(constraintsMinTarget); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraintsMaxTarget = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraintsMaxTarget[0] = _createMultiGasConstraint(100_000_000, 100, 0); + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(constraintsMaxTarget); + } + + function test_setMultiGasPricingConstraints_invalidPeriod() external { + // Test adjustment window below minimum (4 seconds) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraintsLowPeriod = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraintsLowPeriod[0] = _createMultiGasConstraint(10_000_000, 4, 0); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidPeriod.selector, + uint64(10_000_000), + uint64(4), + uint64(0) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraintsLowPeriod); + + // Test adjustment window above maximum (86401 seconds) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraintsHighPeriod = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraintsHighPeriod[0] = _createMultiGasConstraint(10_000_000, 86401, 0); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidPeriod.selector, + uint64(10_000_000), + uint64(86401), + uint64(0) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraintsHighPeriod); + + // Test edge cases (exactly at boundaries should succeed) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraintsMinPeriod = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraintsMinPeriod[0] = _createMultiGasConstraint(10_000_000, 5, 0); + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(constraintsMinPeriod); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraintsMaxPeriod = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraintsMaxPeriod[0] = _createMultiGasConstraint(10_000_000, 86400, 0); + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(constraintsMaxPeriod); + } + + function test_setMultiGasPricingConstraints_multipleConstraintValidation() external { + // Test that all constraints are validated (not just the first one) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](3); + constraints[0] = _createMultiGasConstraint(10_000_000, 100, 0); // Valid + constraints[1] = _createMultiGasConstraint(20_000_000, 200, 0); // Valid + constraints[2] = _createMultiGasConstraint(5_000_000, 100, 0); // Invalid target + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidTarget.selector, + uint64(5_000_000), + uint64(100), + uint64(0) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); + + // Test with invalid period in middle + constraints[0] = _createMultiGasConstraint(10_000_000, 100, 0); // Valid + constraints[1] = _createMultiGasConstraint(20_000_000, 3, 0); // Invalid period + constraints[2] = _createMultiGasConstraint(30_000_000, 100, 0); // Valid + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidPeriod.selector, + uint64(20_000_000), + uint64(3), + uint64(0) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); + } + + function test_setMultiGasPricingConstraints_tooManyConstraints() external { + uint256 maxConstraints = 100; + // Test exactly maxConstraints (should succeed) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory maxConstraintsArray = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](maxConstraints); + for (uint256 i = 0; i < maxConstraints; i++) { + maxConstraintsArray[i] = _createMultiGasConstraint(10_000_000, 100, 0); + } + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(maxConstraintsArray); + + // Test maxConstraints + 1 constraints (should revert) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory tooManyConstraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](maxConstraints + 1); + for (uint256 i = 0; i < maxConstraints + 1; i++) { + tooManyConstraints[i] = _createMultiGasConstraint(10_000_000, 100, 0); + } + vm.prank(manager); + vm.expectRevert(ResourceConstraintManager.TooManyConstraints.selector); + resourceConstraintManager.setMultiGasPricingConstraints(tooManyConstraints); + } + + function test_setMultiGasPricingConstraints_emptyResources() external { + // Edge case: a constraint with an empty resources array should succeed (no-op constraint) + ArbMultiGasConstraintsTypes.WeightedResource[] memory emptyResources = + new ArbMultiGasConstraintsTypes.WeightedResource[](0); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, emptyResources); + + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); + } + + function test_setMultiGasPricingConstraints_duplicateResourceKind() external { + // Test duplicate Computation (kind=1) in a single constraint + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources1 = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + resources1[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 1 + }); + resources1[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 2 + }); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints1 = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints1[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, resources1); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidResources.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints1); + + // Test constraint with adjacent duplicates in sorted order: [Computation, HistoryGrowth, HistoryGrowth, StorageAccessRead] + // Should revert on the duplicate HistoryGrowth at index 2 + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources2 = + new ArbMultiGasConstraintsTypes.WeightedResource[](4); + resources2[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 1 + }); + resources2[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 3 + }); + resources2[2] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 4 + }); + resources2[3] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.StorageAccessRead, + weight: 2 + }); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints2 = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints2[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, resources2); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidResources.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints2); + + // Duplicate resources with zero backlog + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources3 = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + resources3[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 1 + }); + resources3[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 2 + }); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints3 = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints3[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 0, resources3); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidResources.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints3); + } + + function test_setMultiGasPricingConstraints_unknownResourceKind() external { + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources = + new ArbMultiGasConstraintsTypes.WeightedResource[](1); + resources[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Unknown, + weight: 1 + }); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, resources); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidResources.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Unknown) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); + } + + function test_setMultiGasPricingConstraints_zeroWeight() external { + // Zero weight should revert even with non-zero backlog + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + resources[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 0 + }); + resources[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 0 + }); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, resources); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidResources.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); + + // Zero weight should also revert with zero backlog + constraints[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 0, resources); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidResources.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); + + // Only one zero weight among valid weights should revert + ArbMultiGasConstraintsTypes.WeightedResource[] memory mixedResources = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + mixedResources[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 1 + }); + mixedResources[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 0 + }); + + constraints[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, mixedResources); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidResources.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); + } + + function test_setMultiGasPricingConstraints_unsortedResources() external { + // Resources not in ascending order by kind should revert + // HistoryGrowth (kind=2) before Computation (kind=1) + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + resources[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 1 + }); + resources[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 2 + }); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, resources); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidResources.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); + + // Larger unsorted array: [Computation(1), StorageAccessRead(3), HistoryGrowth(2)] + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources2 = + new ArbMultiGasConstraintsTypes.WeightedResource[](3); + resources2[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 1 + }); + resources2[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.StorageAccessRead, + weight: 2 + }); + resources2[2] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 3 + }); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints2 = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints2[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, resources2); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidResources.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints2); + + // Unsorted resources should revert even with zero backlog + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources3 = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + resources3[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 1 + }); + resources3[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 2 + }); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints3 = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints3[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 0, resources3); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidResources.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints3); + } } contract ArbOwnerMock { bool public removeChainOwnerCalled; uint64[3][] public lastConstraints; + bytes internal lastMultiGasConstraintsEncoded; function removeChainOwner( - address ownerToRemove + address ) external { removeChainOwnerCalled = true; } @@ -252,7 +844,9 @@ contract ArbOwnerMock { lastConstraints = constraints; } - function getLastConstraints() external view returns (uint64[3][] memory) { - return lastConstraints; + function setMultiGasPricingConstraints( + ArbMultiGasConstraintsTypes.ResourceConstraint[] calldata constraints + ) external { + lastMultiGasConstraintsEncoded = abi.encode(constraints); } }