From 6863e434437889b277eea59237731b319f700022 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Fri, 27 Feb 2026 15:13:21 +0000 Subject: [PATCH 01/15] Update precompiles pin --- src/precompiles | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/precompiles b/src/precompiles index f49a4889..d92f4788 160000 --- a/src/precompiles +++ b/src/precompiles @@ -1 +1 @@ -Subproject commit f49a4889b486fd804a7901203f5f663cfd1581c8 +Subproject commit d92f4788293c1c3c5612761428968d08f638c5e4 From 4a56e5857196bd359b8a0f97aaf3ec1836e9eb63 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Fri, 27 Feb 2026 15:42:16 +0000 Subject: [PATCH 02/15] Update precompiles pin to a better commit --- src/precompiles | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/precompiles b/src/precompiles index d92f4788..a60077f4 160000 --- a/src/precompiles +++ b/src/precompiles @@ -1 +1 @@ -Subproject commit d92f4788293c1c3c5612761428968d08f638c5e4 +Subproject commit a60077f472b26686d4ee9bb1cf60cfd2d3eada55 From 40f7aed958edc705dbf7b7678abb0b178b313bfd Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Fri, 6 Mar 2026 12:33:44 +0000 Subject: [PATCH 03/15] Add function to manage setMultiGasPricingConstraints from the ResourceConstraintManager --- src/chain/ResourceConstraintManager.sol | 78 +++++ test/foundry/ResourceConstraintManager.t.sol | 302 +++++++++++++++++++ 2 files changed, 380 insertions(+) diff --git a/src/chain/ResourceConstraintManager.sol b/src/chain/ResourceConstraintManager.sol index b7ed4816..586e69f8 100644 --- a/src/chain/ResourceConstraintManager.sol +++ b/src/chain/ResourceConstraintManager.sol @@ -12,6 +12,14 @@ 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_CONSTRAINTS = 10; + 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; @@ -85,4 +93,74 @@ contract ResourceConstraintManager is AccessControlEnumerable { 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 + /// - 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 + // Starting from ArbOS 60, there's no limit to the number of constraints to set + uint256 nConstraints = constraints.length; + + // 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); + } + 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 by 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/test/foundry/ResourceConstraintManager.t.sol b/test/foundry/ResourceConstraintManager.t.sol index 0ab94eaa..f09017bc 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,11 +237,290 @@ 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: Computation (weight 2) + StorageAccess (weight 3) + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources2 = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + resources2[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 1 + }); + resources2[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.StorageAccess, + weight: 1 + }); + multipleConstraints[1] = _createMultiGasConstraintWithResources(50_000_000, 1000, 1, resources2); + + // Multiple resources: HistoryGrowth (weight 5) + L1Calldata (weight 1) + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources3 = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + resources3[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.WasmComputation, + weight: 5 + }); + resources3[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.L1Calldata, + weight: 1 + }); + 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 + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory multipleConstraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](3); + multipleConstraints[0] = _createMultiGasConstraint(7_000_000, 5, 35_000_000); // 1000 + multipleConstraints[1] = _createMultiGasConstraint(50_000_000, 1000, 300_000_000_000); // 6000 + multipleConstraints[2] = _createMultiGasConstraint(100_000_000, 86400, 8_640_000_000_000); // 1000 + + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(multipleConstraints); + + // up to the limit + multipleConstraints[1] = _createMultiGasConstraint(50_000_000, 1000, 300_049_999_999); + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(multipleConstraints); + + // over the limit + 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); + } + + 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_noConstraintLimit() external { + // Starting from ArbOS 60, there's no limit to the number of constraints + // Test 10 constraints (should succeed) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory tenConstraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](10); + for (uint256 i = 0; i < 10; i++) { + tenConstraints[i] = _createMultiGasConstraint(10_000_000, 100, 0); + } + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(tenConstraints); + + // Test 11 constraints (should also succeed) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory elevenConstraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](11); + for (uint256 i = 0; i < 11; i++) { + elevenConstraints[i] = _createMultiGasConstraint(10_000_000, 100, 0); + } + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(elevenConstraints); + } + + 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); + } } contract ArbOwnerMock { bool public removeChainOwnerCalled; uint64[3][] public lastConstraints; + bytes internal lastMultiGasConstraintsEncoded; function removeChainOwner( address ownerToRemove @@ -255,4 +537,24 @@ contract ArbOwnerMock { function getLastConstraints() external view returns (uint64[3][] memory) { return lastConstraints; } + + function setMultiGasPricingConstraints( + ArbMultiGasConstraintsTypes.ResourceConstraint[] calldata constraints + ) external { + lastMultiGasConstraintsEncoded = abi.encode(constraints); + } + + function getLastMultiGasConstraints() + external + view + returns (ArbMultiGasConstraintsTypes.ResourceConstraint[] memory) + { + if (lastMultiGasConstraintsEncoded.length == 0) { + return new ArbMultiGasConstraintsTypes.ResourceConstraint[](0); + } + return abi.decode( + lastMultiGasConstraintsEncoded, + (ArbMultiGasConstraintsTypes.ResourceConstraint[]) + ); + } } From e641bc2487fc111e45a39151d299cd33818c926c Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Fri, 6 Mar 2026 12:34:51 +0000 Subject: [PATCH 04/15] Format --- src/chain/ResourceConstraintManager.sol | 11 ++++++++--- test/foundry/ResourceConstraintManager.t.sol | 13 ++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/chain/ResourceConstraintManager.sol b/src/chain/ResourceConstraintManager.sol index 586e69f8..4962bb17 100644 --- a/src/chain/ResourceConstraintManager.sol +++ b/src/chain/ResourceConstraintManager.sol @@ -118,7 +118,10 @@ contract ResourceConstraintManager is AccessControlEnumerable { 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) { + if ( + adjustmentWindowSecs < MIN_ADJUSTMENT_WINDOW_SECS + || adjustmentWindowSecs > MAX_ADJUSTMENT_WINDOW_SECS + ) { revert InvalidPeriod(targetPerSec, adjustmentWindowSecs, startingBacklogValue); } if (startingBacklogValue > 0) { @@ -134,7 +137,8 @@ contract ResourceConstraintManager is AccessControlEnumerable { 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); + 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 @@ -144,7 +148,8 @@ contract ResourceConstraintManager is AccessControlEnumerable { 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); + pricingExponents[kind] += + uint64(uint256(startingBacklogValue) * uint256(weight) * 1000 / divisor); } } } diff --git a/test/foundry/ResourceConstraintManager.t.sol b/test/foundry/ResourceConstraintManager.t.sol index f09017bc..66de8102 100644 --- a/test/foundry/ResourceConstraintManager.t.sol +++ b/test/foundry/ResourceConstraintManager.t.sol @@ -266,7 +266,9 @@ contract ResourceConstraintManagerTest is Test { resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, weight: 1 }); - return _createMultiGasConstraintWithResources(targetPerSec, adjustmentWindowSecs, backlog, resources); + return _createMultiGasConstraintWithResources( + targetPerSec, adjustmentWindowSecs, backlog, resources + ); } function test_setMultiGasPricingConstraints_success() external { @@ -296,7 +298,8 @@ contract ResourceConstraintManagerTest is Test { resource: ArbMultiGasConstraintsTypes.ResourceKind.StorageAccess, weight: 1 }); - multipleConstraints[1] = _createMultiGasConstraintWithResources(50_000_000, 1000, 1, resources2); + multipleConstraints[1] = + _createMultiGasConstraintWithResources(50_000_000, 1000, 1, resources2); // Multiple resources: HistoryGrowth (weight 5) + L1Calldata (weight 1) ArbMultiGasConstraintsTypes.WeightedResource[] memory resources3 = @@ -309,7 +312,8 @@ contract ResourceConstraintManagerTest is Test { resource: ArbMultiGasConstraintsTypes.ResourceKind.L1Calldata, weight: 1 }); - multipleConstraints[2] = _createMultiGasConstraintWithResources(100_000_000, 86400, 10000, resources3); + multipleConstraints[2] = + _createMultiGasConstraintWithResources(100_000_000, 86400, 10000, resources3); vm.prank(manager); resourceConstraintManager.setMultiGasPricingConstraints(multipleConstraints); @@ -553,8 +557,7 @@ contract ArbOwnerMock { return new ArbMultiGasConstraintsTypes.ResourceConstraint[](0); } return abi.decode( - lastMultiGasConstraintsEncoded, - (ArbMultiGasConstraintsTypes.ResourceConstraint[]) + lastMultiGasConstraintsEncoded, (ArbMultiGasConstraintsTypes.ResourceConstraint[]) ); } } From 7f8f60fa79eb21eab19aaa1cfd61f244ce8b2623 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Tue, 10 Mar 2026 09:53:54 +0000 Subject: [PATCH 05/15] Use constants in previous function and cleanup comments and unused code --- src/chain/ResourceConstraintManager.sol | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/chain/ResourceConstraintManager.sol b/src/chain/ResourceConstraintManager.sol index 4962bb17..15e45d3e 100644 --- a/src/chain/ResourceConstraintManager.sol +++ b/src/chain/ResourceConstraintManager.sol @@ -5,12 +5,10 @@ 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_CONSTRAINTS = 10; @@ -58,7 +56,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_CONSTRAINTS) { revert TooManyConstraints(); } uint64 pricingExponent = 0; @@ -66,10 +64,10 @@ 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 @@ -82,12 +80,12 @@ 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); } @@ -155,7 +153,7 @@ contract ResourceConstraintManager is AccessControlEnumerable { } } - // 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 From a25646251278a84947314a99d34584e0aafca193 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Tue, 10 Mar 2026 10:41:58 +0000 Subject: [PATCH 06/15] Add one more test and clean up unused and unrelevant code --- test/foundry/ResourceConstraintManager.t.sol | 100 ++++++++++--------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/test/foundry/ResourceConstraintManager.t.sol b/test/foundry/ResourceConstraintManager.t.sol index 66de8102..f8b72ba1 100644 --- a/test/foundry/ResourceConstraintManager.t.sol +++ b/test/foundry/ResourceConstraintManager.t.sol @@ -287,21 +287,21 @@ contract ResourceConstraintManagerTest is Test { // Single resource: Computation with weight 1 multipleConstraints[0] = _createMultiGasConstraint(7_000_000, 5, 0); - // Multiple resources: Computation (weight 2) + StorageAccess (weight 3) + // Multiple resources: HistoryGrowth (weight 2) + StorageAccess (weight 3) ArbMultiGasConstraintsTypes.WeightedResource[] memory resources2 = new ArbMultiGasConstraintsTypes.WeightedResource[](2); resources2[0] = ArbMultiGasConstraintsTypes.WeightedResource({ resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, - weight: 1 + weight: 2 }); resources2[1] = ArbMultiGasConstraintsTypes.WeightedResource({ resource: ArbMultiGasConstraintsTypes.ResourceKind.StorageAccess, - weight: 1 + weight: 3 }); multipleConstraints[1] = _createMultiGasConstraintWithResources(50_000_000, 1000, 1, resources2); - // Multiple resources: HistoryGrowth (weight 5) + L1Calldata (weight 1) + // Multiple resources: WasmComputation (weight 5) + L1Calldata (weight 1) ArbMultiGasConstraintsTypes.WeightedResource[] memory resources3 = new ArbMultiGasConstraintsTypes.WeightedResource[](2); resources3[0] = ArbMultiGasConstraintsTypes.WeightedResource({ @@ -326,28 +326,68 @@ contract ResourceConstraintManagerTest is Test { } function test_setMultiGasPricingConstraints_pricingExponentTooHigh() external { - // create constraints on the limit of the pricing exponent + // 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); - multipleConstraints[0] = _createMultiGasConstraint(7_000_000, 5, 35_000_000); // 1000 - multipleConstraints[1] = _createMultiGasConstraint(50_000_000, 1000, 300_000_000_000); // 6000 - multipleConstraints[2] = _createMultiGasConstraint(100_000_000, 86400, 8_640_000_000_000); // 1000 + // 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); - // up to the limit + // 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); - // over the limit + // 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 { @@ -369,27 +409,6 @@ contract ResourceConstraintManagerTest is Test { resourceConstraintManager.setMultiGasPricingConstraints(constraints); } - function test_setMultiGasPricingConstraints_noConstraintLimit() external { - // Starting from ArbOS 60, there's no limit to the number of constraints - // Test 10 constraints (should succeed) - ArbMultiGasConstraintsTypes.ResourceConstraint[] memory tenConstraints = - new ArbMultiGasConstraintsTypes.ResourceConstraint[](10); - for (uint256 i = 0; i < 10; i++) { - tenConstraints[i] = _createMultiGasConstraint(10_000_000, 100, 0); - } - vm.prank(manager); - resourceConstraintManager.setMultiGasPricingConstraints(tenConstraints); - - // Test 11 constraints (should also succeed) - ArbMultiGasConstraintsTypes.ResourceConstraint[] memory elevenConstraints = - new ArbMultiGasConstraintsTypes.ResourceConstraint[](11); - for (uint256 i = 0; i < 11; i++) { - elevenConstraints[i] = _createMultiGasConstraint(10_000_000, 100, 0); - } - vm.prank(manager); - resourceConstraintManager.setMultiGasPricingConstraints(elevenConstraints); - } - function test_setMultiGasPricingConstraints_invalidTarget() external { // Test gas target below minimum (6,999,999) ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraintsLowTarget = @@ -527,7 +546,7 @@ contract ArbOwnerMock { bytes internal lastMultiGasConstraintsEncoded; function removeChainOwner( - address ownerToRemove + address ) external { removeChainOwnerCalled = true; } @@ -538,26 +557,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); } - - function getLastMultiGasConstraints() - external - view - returns (ArbMultiGasConstraintsTypes.ResourceConstraint[] memory) - { - if (lastMultiGasConstraintsEncoded.length == 0) { - return new ArbMultiGasConstraintsTypes.ResourceConstraint[](0); - } - return abi.decode( - lastMultiGasConstraintsEncoded, (ArbMultiGasConstraintsTypes.ResourceConstraint[]) - ); - } } From 8a6f7db0978121892460cad725306aca1e73bd9b Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Tue, 10 Mar 2026 10:42:18 +0000 Subject: [PATCH 07/15] Format --- src/chain/ResourceConstraintManager.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/chain/ResourceConstraintManager.sol b/src/chain/ResourceConstraintManager.sol index 15e45d3e..d537a12b 100644 --- a/src/chain/ResourceConstraintManager.sol +++ b/src/chain/ResourceConstraintManager.sol @@ -64,10 +64,15 @@ contract ResourceConstraintManager is AccessControlEnumerable { uint64 gasTargetPerSec = constraints[i][0]; uint64 adjustmentWindowSecs = constraints[i][1]; uint64 startingBacklogValue = constraints[i][2]; - if (gasTargetPerSec < MIN_GAS_TARGET_PER_SEC || gasTargetPerSec > MAX_GAS_TARGET_PER_SEC) { + if ( + gasTargetPerSec < MIN_GAS_TARGET_PER_SEC || gasTargetPerSec > MAX_GAS_TARGET_PER_SEC + ) { revert InvalidTarget(gasTargetPerSec, adjustmentWindowSecs, startingBacklogValue); } - if (adjustmentWindowSecs < MIN_ADJUSTMENT_WINDOW_SECS || adjustmentWindowSecs > MAX_ADJUSTMENT_WINDOW_SECS) { + 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 From 4d3803be090489a273e4f8a454130f984c4ecc5b Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Thu, 12 Mar 2026 10:28:50 +0000 Subject: [PATCH 08/15] Prevent duplication of resources --- src/chain/ResourceConstraintManager.sol | 20 +++++ test/foundry/ResourceConstraintManager.t.sol | 87 ++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/src/chain/ResourceConstraintManager.sol b/src/chain/ResourceConstraintManager.sol index d537a12b..521937b8 100644 --- a/src/chain/ResourceConstraintManager.sol +++ b/src/chain/ResourceConstraintManager.sol @@ -29,6 +29,7 @@ contract ResourceConstraintManager is AccessControlEnumerable { uint64 gasTargetPerSec, uint64 adjustmentWindowSecs, uint64 startingBacklogValue ); error PricingExponentTooHigh(uint64 pricingExponent); + error DuplicateResourceKind(uint8 resourceKind); error NotExpired(); constructor(address admin, address manager, uint256 _expiryTimestamp) { @@ -127,6 +128,25 @@ contract ResourceConstraintManager is AccessControlEnumerable { ) { revert InvalidPeriod(targetPerSec, adjustmentWindowSecs, startingBacklogValue); } + { + // Check for duplicate resource kinds within this constraint + // Using bit comparison for efficient calculation (supports up to 256 kinds) + uint256 seenKinds; + uint256 nResources = constraints[i].resources.length; + for (uint256 j = 0; j < nResources; ++j) { + uint8 kind = uint8(constraints[i].resources[j].resource); + // Shifting 1 by the resource kind value + // (example: kind = 1, kindBit = ...0010) + // (example: kind = 2, kindBit = ...0100) + uint256 kindBit = 1 << kind; + // Bitwise AND comparison + if ((seenKinds & kindBit) != 0) { + revert DuplicateResourceKind(kind); + } + // Bitwise OR to add kind to seenKinds + seenKinds = seenKinds | kindBit; + } + } if (startingBacklogValue > 0) { // Find the maximum weight among all resources in this constraint uint64 maxWeight = 0; diff --git a/test/foundry/ResourceConstraintManager.t.sol b/test/foundry/ResourceConstraintManager.t.sol index f8b72ba1..9be4d712 100644 --- a/test/foundry/ResourceConstraintManager.t.sol +++ b/test/foundry/ResourceConstraintManager.t.sol @@ -538,6 +538,93 @@ contract ResourceConstraintManagerTest is Test { ); 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, 0, resources1); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.DuplicateResourceKind.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints1); + + // Test duplicate Unknown (kind=0) in a single constraint + // Special case in case bitwise operations are modified and the zero value is not handled correctly + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources2 = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + resources2[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Unknown, + weight: 1 + }); + resources2[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Unknown, + weight: 3 + }); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints2 = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints2[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 0, resources2); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.DuplicateResourceKind.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Unknown) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints2); + + // Test constraint with two pairs of duplicates: [Computation, HistoryGrowth, Computation, HistoryGrowth] + // Should revert on the first duplicate found (Computation at index 2) + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources3 = + new ArbMultiGasConstraintsTypes.WeightedResource[](4); + resources3[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 1 + }); + resources3[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 2 + }); + resources3[2] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 3 + }); + resources3[3] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 4 + }); + + 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.DuplicateResourceKind.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints3); + } } contract ArbOwnerMock { From dd9041d51c9d718860611e2e153e935c8aacb95c Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Thu, 12 Mar 2026 10:33:34 +0000 Subject: [PATCH 09/15] Explicitly initialize variable to 0 --- src/chain/ResourceConstraintManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chain/ResourceConstraintManager.sol b/src/chain/ResourceConstraintManager.sol index 521937b8..f2a2afb9 100644 --- a/src/chain/ResourceConstraintManager.sol +++ b/src/chain/ResourceConstraintManager.sol @@ -131,7 +131,7 @@ contract ResourceConstraintManager is AccessControlEnumerable { { // Check for duplicate resource kinds within this constraint // Using bit comparison for efficient calculation (supports up to 256 kinds) - uint256 seenKinds; + uint256 seenKinds = 0; uint256 nResources = constraints[i].resources.length; for (uint256 j = 0; j < nResources; ++j) { uint8 kind = uint8(constraints[i].resources[j].resource); From de3c14c83d9a82264059656b3bcb4ad5f95771a6 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Thu, 12 Mar 2026 12:41:57 +0000 Subject: [PATCH 10/15] Modify duplication and unsorted check --- src/chain/ResourceConstraintManager.sol | 25 ++- test/foundry/ResourceConstraintManager.t.sol | 174 +++++++++++++++++-- 2 files changed, 168 insertions(+), 31 deletions(-) diff --git a/src/chain/ResourceConstraintManager.sol b/src/chain/ResourceConstraintManager.sol index f2a2afb9..cbd66db9 100644 --- a/src/chain/ResourceConstraintManager.sol +++ b/src/chain/ResourceConstraintManager.sol @@ -28,8 +28,8 @@ contract ResourceConstraintManager is AccessControlEnumerable { error InvalidTarget( uint64 gasTargetPerSec, uint64 adjustmentWindowSecs, uint64 startingBacklogValue ); + error InvalidResources(uint8 resourceKind); error PricingExponentTooHigh(uint64 pricingExponent); - error DuplicateResourceKind(uint8 resourceKind); error NotExpired(); constructor(address admin, address manager, uint256 _expiryTimestamp) { @@ -101,7 +101,7 @@ contract ResourceConstraintManager is AccessControlEnumerable { /// @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 + /// - 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) @@ -129,22 +129,19 @@ contract ResourceConstraintManager is AccessControlEnumerable { revert InvalidPeriod(targetPerSec, adjustmentWindowSecs, startingBacklogValue); } { - // Check for duplicate resource kinds within this constraint - // Using bit comparison for efficient calculation (supports up to 256 kinds) - uint256 seenKinds = 0; + // 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) + uint8 lastResourceKind = 0; uint256 nResources = constraints[i].resources.length; for (uint256 j = 0; j < nResources; ++j) { uint8 kind = uint8(constraints[i].resources[j].resource); - // Shifting 1 by the resource kind value - // (example: kind = 1, kindBit = ...0010) - // (example: kind = 2, kindBit = ...0100) - uint256 kindBit = 1 << kind; - // Bitwise AND comparison - if ((seenKinds & kindBit) != 0) { - revert DuplicateResourceKind(kind); + // check that resource kinds are sorted and contain no duplicates + if (j > 0 && kind <= lastResourceKind) { + revert InvalidResources(kind); } - // Bitwise OR to add kind to seenKinds - seenKinds = seenKinds | kindBit; + lastResourceKind = kind; } } if (startingBacklogValue > 0) { diff --git a/test/foundry/ResourceConstraintManager.t.sol b/test/foundry/ResourceConstraintManager.t.sol index 9be4d712..aa1235c7 100644 --- a/test/foundry/ResourceConstraintManager.t.sol +++ b/test/foundry/ResourceConstraintManager.t.sol @@ -301,17 +301,17 @@ contract ResourceConstraintManagerTest is Test { multipleConstraints[1] = _createMultiGasConstraintWithResources(50_000_000, 1000, 1, resources2); - // Multiple resources: WasmComputation (weight 5) + L1Calldata (weight 1) + // Multiple resources: L1Calldata (weight 1) + WasmComputation (weight 5) ArbMultiGasConstraintsTypes.WeightedResource[] memory resources3 = new ArbMultiGasConstraintsTypes.WeightedResource[](2); resources3[0] = ArbMultiGasConstraintsTypes.WeightedResource({ - resource: ArbMultiGasConstraintsTypes.ResourceKind.WasmComputation, - weight: 5 - }); - resources3[1] = ArbMultiGasConstraintsTypes.WeightedResource({ resource: ArbMultiGasConstraintsTypes.ResourceKind.L1Calldata, weight: 1 }); + resources3[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.WasmComputation, + weight: 5 + }); multipleConstraints[2] = _createMultiGasConstraintWithResources(100_000_000, 86400, 10000, resources3); @@ -539,6 +539,19 @@ contract ResourceConstraintManagerTest is Test { resourceConstraintManager.setMultiGasPricingConstraints(constraints); } + 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 = @@ -554,19 +567,18 @@ contract ResourceConstraintManagerTest is Test { ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints1 = new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); - constraints1[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 0, resources1); + constraints1[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, resources1); vm.prank(manager); vm.expectRevert( abi.encodeWithSelector( - ResourceConstraintManager.DuplicateResourceKind.selector, + ResourceConstraintManager.InvalidResources.selector, uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) ) ); resourceConstraintManager.setMultiGasPricingConstraints(constraints1); - + // Test duplicate Unknown (kind=0) in a single constraint - // Special case in case bitwise operations are modified and the zero value is not handled correctly ArbMultiGasConstraintsTypes.WeightedResource[] memory resources2 = new ArbMultiGasConstraintsTypes.WeightedResource[](2); resources2[0] = ArbMultiGasConstraintsTypes.WeightedResource({ @@ -580,19 +592,19 @@ contract ResourceConstraintManagerTest is Test { ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints2 = new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); - constraints2[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 0, resources2); + constraints2[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, resources2); vm.prank(manager); vm.expectRevert( abi.encodeWithSelector( - ResourceConstraintManager.DuplicateResourceKind.selector, + ResourceConstraintManager.InvalidResources.selector, uint8(ArbMultiGasConstraintsTypes.ResourceKind.Unknown) ) ); resourceConstraintManager.setMultiGasPricingConstraints(constraints2); - // Test constraint with two pairs of duplicates: [Computation, HistoryGrowth, Computation, HistoryGrowth] - // Should revert on the first duplicate found (Computation at index 2) + // Test constraint with adjacent duplicates in sorted order: [Computation, HistoryGrowth, HistoryGrowth, StorageAccess] + // Should revert on the duplicate HistoryGrowth at index 2 ArbMultiGasConstraintsTypes.WeightedResource[] memory resources3 = new ArbMultiGasConstraintsTypes.WeightedResource[](4); resources3[0] = ArbMultiGasConstraintsTypes.WeightedResource({ @@ -601,15 +613,143 @@ contract ResourceConstraintManagerTest is Test { }); resources3[1] = ArbMultiGasConstraintsTypes.WeightedResource({ resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, - weight: 2 + weight: 3 }); resources3[2] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, + weight: 4 + }); + resources3[3] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.StorageAccess, + weight: 2 + }); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints3 = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints3[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, resources3); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidResources.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints3); + + // Duplicate resources with zero backlog + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources4 = + new ArbMultiGasConstraintsTypes.WeightedResource[](2); + resources4[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 1 + }); + resources4[1] = ArbMultiGasConstraintsTypes.WeightedResource({ resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 2 + }); + + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints4 = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); + constraints4[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 0, resources4); + + vm.prank(manager); + vm.expectRevert( + abi.encodeWithSelector( + ResourceConstraintManager.InvalidResources.selector, + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) + ) + ); + resourceConstraintManager.setMultiGasPricingConstraints(constraints4); + } + + function test_setMultiGasPricingConstraints_zeroWeightsWithNonZeroBacklog() external { + // Edge case: all-zero weights with non-zero backlog: sorted check runs but no exponent is added + 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); + 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), StorageAccess(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.StorageAccess, + weight: 2 + }); + resources2[2] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, weight: 3 }); - resources3[3] = ArbMultiGasConstraintsTypes.WeightedResource({ + + 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: 4 + weight: 1 + }); + resources3[1] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + weight: 2 }); ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints3 = @@ -619,7 +759,7 @@ contract ResourceConstraintManagerTest is Test { vm.prank(manager); vm.expectRevert( abi.encodeWithSelector( - ResourceConstraintManager.DuplicateResourceKind.selector, + ResourceConstraintManager.InvalidResources.selector, uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) ) ); From cd1f823d68d419512c16e8f53a8b810f8650442c Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Tue, 17 Mar 2026 10:04:41 +0000 Subject: [PATCH 11/15] Add limit to number of constraints --- src/chain/ResourceConstraintManager.sol | 10 +++++++--- test/foundry/ResourceConstraintManager.t.sol | 21 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/chain/ResourceConstraintManager.sol b/src/chain/ResourceConstraintManager.sol index cbd66db9..e349beaa 100644 --- a/src/chain/ResourceConstraintManager.sol +++ b/src/chain/ResourceConstraintManager.sol @@ -11,7 +11,8 @@ contract ResourceConstraintManager is AccessControlEnumerable { ArbOwner internal constant ARB_OWNER = ArbOwner(address(0x70)); // Constraint parameters boundaries - uint256 public constant MAX_CONSTRAINTS = 10; + uint256 public constant MAX_SINGLE_GAS_CONSTRAINTS = 10; + uint256 public constant MAX_MULTI_GAS_CONSTRAINTS = 70; 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; @@ -57,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 > MAX_CONSTRAINTS) { + if (nConstraints > MAX_SINGLE_GAS_CONSTRAINTS) { revert TooManyConstraints(); } uint64 pricingExponent = 0; @@ -109,8 +110,11 @@ contract ResourceConstraintManager is AccessControlEnumerable { ArbMultiGasConstraintsTypes.ResourceConstraint[] calldata constraints ) external onlyRole(MANAGER_ROLE) { // If zero constraints are provided, the chain uses the single-constraint pricing model - // Starting from ArbOS 60, there's no limit to the number of constraints to set + // 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; diff --git a/test/foundry/ResourceConstraintManager.t.sol b/test/foundry/ResourceConstraintManager.t.sol index aa1235c7..9b3c10c6 100644 --- a/test/foundry/ResourceConstraintManager.t.sol +++ b/test/foundry/ResourceConstraintManager.t.sol @@ -539,6 +539,27 @@ contract ResourceConstraintManagerTest is Test { resourceConstraintManager.setMultiGasPricingConstraints(constraints); } + function test_setMultiGasPricingConstraints_tooManyConstraints() external { + // Test exactly 70 constraints (should succeed) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory seventyConstraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](70); + for (uint256 i = 0; i < 70; i++) { + seventyConstraints[i] = _createMultiGasConstraint(10_000_000, 100, 0); + } + vm.prank(manager); + resourceConstraintManager.setMultiGasPricingConstraints(seventyConstraints); + + // Test 71 constraints (should revert) + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory seventyOneConstraints = + new ArbMultiGasConstraintsTypes.ResourceConstraint[](71); + for (uint256 i = 0; i < 71; i++) { + seventyOneConstraints[i] = _createMultiGasConstraint(10_000_000, 100, 0); + } + vm.prank(manager); + vm.expectRevert(ResourceConstraintManager.TooManyConstraints.selector); + resourceConstraintManager.setMultiGasPricingConstraints(seventyOneConstraints); + } + function test_setMultiGasPricingConstraints_emptyResources() external { // Edge case: a constraint with an empty resources array should succeed (no-op constraint) ArbMultiGasConstraintsTypes.WeightedResource[] memory emptyResources = From aed24d5b024fcc40faac4ea5b36d5811ed1958f2 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Tue, 17 Mar 2026 10:12:06 +0000 Subject: [PATCH 12/15] Reduce maximum to 60 --- src/chain/ResourceConstraintManager.sol | 2 +- test/foundry/ResourceConstraintManager.t.sol | 25 ++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/chain/ResourceConstraintManager.sol b/src/chain/ResourceConstraintManager.sol index e349beaa..85ee6811 100644 --- a/src/chain/ResourceConstraintManager.sol +++ b/src/chain/ResourceConstraintManager.sol @@ -12,7 +12,7 @@ contract ResourceConstraintManager is AccessControlEnumerable { // Constraint parameters boundaries uint256 public constant MAX_SINGLE_GAS_CONSTRAINTS = 10; - uint256 public constant MAX_MULTI_GAS_CONSTRAINTS = 70; + uint256 public constant MAX_MULTI_GAS_CONSTRAINTS = 60; 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; diff --git a/test/foundry/ResourceConstraintManager.t.sol b/test/foundry/ResourceConstraintManager.t.sol index 9b3c10c6..b266caf6 100644 --- a/test/foundry/ResourceConstraintManager.t.sol +++ b/test/foundry/ResourceConstraintManager.t.sol @@ -540,24 +540,25 @@ contract ResourceConstraintManagerTest is Test { } function test_setMultiGasPricingConstraints_tooManyConstraints() external { - // Test exactly 70 constraints (should succeed) - ArbMultiGasConstraintsTypes.ResourceConstraint[] memory seventyConstraints = - new ArbMultiGasConstraintsTypes.ResourceConstraint[](70); - for (uint256 i = 0; i < 70; i++) { - seventyConstraints[i] = _createMultiGasConstraint(10_000_000, 100, 0); + uint256 maxConstraints = 60; + // 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(seventyConstraints); + resourceConstraintManager.setMultiGasPricingConstraints(maxConstraintsArray); - // Test 71 constraints (should revert) - ArbMultiGasConstraintsTypes.ResourceConstraint[] memory seventyOneConstraints = - new ArbMultiGasConstraintsTypes.ResourceConstraint[](71); - for (uint256 i = 0; i < 71; i++) { - seventyOneConstraints[i] = _createMultiGasConstraint(10_000_000, 100, 0); + // 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(seventyOneConstraints); + resourceConstraintManager.setMultiGasPricingConstraints(tooManyConstraints); } function test_setMultiGasPricingConstraints_emptyResources() external { From cd266f185c5ba3471f01ca36f76e100e0ef7be36 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Tue, 17 Mar 2026 10:13:45 +0000 Subject: [PATCH 13/15] Change max constraints to 100 --- src/chain/ResourceConstraintManager.sol | 2 +- test/foundry/ResourceConstraintManager.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chain/ResourceConstraintManager.sol b/src/chain/ResourceConstraintManager.sol index 85ee6811..338f63c9 100644 --- a/src/chain/ResourceConstraintManager.sol +++ b/src/chain/ResourceConstraintManager.sol @@ -12,7 +12,7 @@ contract ResourceConstraintManager is AccessControlEnumerable { // Constraint parameters boundaries uint256 public constant MAX_SINGLE_GAS_CONSTRAINTS = 10; - uint256 public constant MAX_MULTI_GAS_CONSTRAINTS = 60; + 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; diff --git a/test/foundry/ResourceConstraintManager.t.sol b/test/foundry/ResourceConstraintManager.t.sol index b266caf6..7c25a598 100644 --- a/test/foundry/ResourceConstraintManager.t.sol +++ b/test/foundry/ResourceConstraintManager.t.sol @@ -540,7 +540,7 @@ contract ResourceConstraintManagerTest is Test { } function test_setMultiGasPricingConstraints_tooManyConstraints() external { - uint256 maxConstraints = 60; + uint256 maxConstraints = 100; // Test exactly maxConstraints (should succeed) ArbMultiGasConstraintsTypes.ResourceConstraint[] memory maxConstraintsArray = new ArbMultiGasConstraintsTypes.ResourceConstraint[](maxConstraints); From 80c00808a9ad9d61a0e6400320cf149d24ab418b Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Tue, 17 Mar 2026 13:01:55 +0000 Subject: [PATCH 14/15] Update precompiles pin --- src/precompiles | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/precompiles b/src/precompiles index a60077f4..3d46aa39 160000 --- a/src/precompiles +++ b/src/precompiles @@ -1 +1 @@ -Subproject commit a60077f472b26686d4ee9bb1cf60cfd2d3eada55 +Subproject commit 3d46aa39ae44802a6656bc5fd393b6751c9347fd From e266f6fd3d942bf09f0a63bd8f739ee8459aef56 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Fri, 20 Mar 2026 09:48:40 +0000 Subject: [PATCH 15/15] Update resource kinds and include extra validation --- src/chain/ResourceConstraintManager.sol | 10 +- test/foundry/ResourceConstraintManager.t.sol | 118 ++++++++++++------- 2 files changed, 87 insertions(+), 41 deletions(-) diff --git a/src/chain/ResourceConstraintManager.sol b/src/chain/ResourceConstraintManager.sol index 338f63c9..67a6f61e 100644 --- a/src/chain/ResourceConstraintManager.sol +++ b/src/chain/ResourceConstraintManager.sol @@ -137,12 +137,20 @@ contract ResourceConstraintManager is AccessControlEnumerable { // 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 - if (j > 0 && kind <= lastResourceKind) { + // 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; diff --git a/test/foundry/ResourceConstraintManager.t.sol b/test/foundry/ResourceConstraintManager.t.sol index 7c25a598..0b4055a2 100644 --- a/test/foundry/ResourceConstraintManager.t.sol +++ b/test/foundry/ResourceConstraintManager.t.sol @@ -287,7 +287,7 @@ contract ResourceConstraintManagerTest is Test { // Single resource: Computation with weight 1 multipleConstraints[0] = _createMultiGasConstraint(7_000_000, 5, 0); - // Multiple resources: HistoryGrowth (weight 2) + StorageAccess (weight 3) + // Multiple resources: HistoryGrowth (weight 2) + StorageAccessRead (weight 3) ArbMultiGasConstraintsTypes.WeightedResource[] memory resources2 = new ArbMultiGasConstraintsTypes.WeightedResource[](2); resources2[0] = ArbMultiGasConstraintsTypes.WeightedResource({ @@ -295,17 +295,17 @@ contract ResourceConstraintManagerTest is Test { weight: 2 }); resources2[1] = ArbMultiGasConstraintsTypes.WeightedResource({ - resource: ArbMultiGasConstraintsTypes.ResourceKind.StorageAccess, + resource: ArbMultiGasConstraintsTypes.ResourceKind.StorageAccessRead, weight: 3 }); multipleConstraints[1] = _createMultiGasConstraintWithResources(50_000_000, 1000, 1, resources2); - // Multiple resources: L1Calldata (weight 1) + WasmComputation (weight 5) + // Multiple resources: SingleDim (weight 1) + WasmComputation (weight 5) ArbMultiGasConstraintsTypes.WeightedResource[] memory resources3 = new ArbMultiGasConstraintsTypes.WeightedResource[](2); resources3[0] = ArbMultiGasConstraintsTypes.WeightedResource({ - resource: ArbMultiGasConstraintsTypes.ResourceKind.L1Calldata, + resource: ArbMultiGasConstraintsTypes.ResourceKind.SingleDim, weight: 1 }); resources3[1] = ArbMultiGasConstraintsTypes.WeightedResource({ @@ -600,17 +600,26 @@ contract ResourceConstraintManagerTest is Test { ); resourceConstraintManager.setMultiGasPricingConstraints(constraints1); - // Test duplicate Unknown (kind=0) in a single constraint + // 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[](2); + new ArbMultiGasConstraintsTypes.WeightedResource[](4); resources2[0] = ArbMultiGasConstraintsTypes.WeightedResource({ - resource: ArbMultiGasConstraintsTypes.ResourceKind.Unknown, + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, weight: 1 }); resources2[1] = ArbMultiGasConstraintsTypes.WeightedResource({ - resource: ArbMultiGasConstraintsTypes.ResourceKind.Unknown, + 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); @@ -620,73 +629,61 @@ contract ResourceConstraintManagerTest is Test { vm.expectRevert( abi.encodeWithSelector( ResourceConstraintManager.InvalidResources.selector, - uint8(ArbMultiGasConstraintsTypes.ResourceKind.Unknown) + uint8(ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth) ) ); resourceConstraintManager.setMultiGasPricingConstraints(constraints2); - // Test constraint with adjacent duplicates in sorted order: [Computation, HistoryGrowth, HistoryGrowth, StorageAccess] - // Should revert on the duplicate HistoryGrowth at index 2 + // Duplicate resources with zero backlog ArbMultiGasConstraintsTypes.WeightedResource[] memory resources3 = - new ArbMultiGasConstraintsTypes.WeightedResource[](4); + new ArbMultiGasConstraintsTypes.WeightedResource[](2); resources3[0] = ArbMultiGasConstraintsTypes.WeightedResource({ resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, weight: 1 }); resources3[1] = ArbMultiGasConstraintsTypes.WeightedResource({ - resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, - weight: 3 - }); - resources3[2] = ArbMultiGasConstraintsTypes.WeightedResource({ - resource: ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth, - weight: 4 - }); - resources3[3] = ArbMultiGasConstraintsTypes.WeightedResource({ - resource: ArbMultiGasConstraintsTypes.ResourceKind.StorageAccess, + resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, weight: 2 }); ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints3 = new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); - constraints3[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, resources3); + constraints3[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 0, resources3); vm.prank(manager); vm.expectRevert( abi.encodeWithSelector( ResourceConstraintManager.InvalidResources.selector, - uint8(ArbMultiGasConstraintsTypes.ResourceKind.HistoryGrowth) + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) ) ); resourceConstraintManager.setMultiGasPricingConstraints(constraints3); + } - // Duplicate resources with zero backlog - ArbMultiGasConstraintsTypes.WeightedResource[] memory resources4 = - new ArbMultiGasConstraintsTypes.WeightedResource[](2); - resources4[0] = ArbMultiGasConstraintsTypes.WeightedResource({ - resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, + function test_setMultiGasPricingConstraints_unknownResourceKind() external { + ArbMultiGasConstraintsTypes.WeightedResource[] memory resources = + new ArbMultiGasConstraintsTypes.WeightedResource[](1); + resources[0] = ArbMultiGasConstraintsTypes.WeightedResource({ + resource: ArbMultiGasConstraintsTypes.ResourceKind.Unknown, weight: 1 }); - resources4[1] = ArbMultiGasConstraintsTypes.WeightedResource({ - resource: ArbMultiGasConstraintsTypes.ResourceKind.Computation, - weight: 2 - }); - ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints4 = + ArbMultiGasConstraintsTypes.ResourceConstraint[] memory constraints = new ArbMultiGasConstraintsTypes.ResourceConstraint[](1); - constraints4[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 0, resources4); + constraints[0] = _createMultiGasConstraintWithResources(10_000_000, 100, 1, resources); vm.prank(manager); vm.expectRevert( abi.encodeWithSelector( ResourceConstraintManager.InvalidResources.selector, - uint8(ArbMultiGasConstraintsTypes.ResourceKind.Computation) + uint8(ArbMultiGasConstraintsTypes.ResourceKind.Unknown) ) ); - resourceConstraintManager.setMultiGasPricingConstraints(constraints4); + resourceConstraintManager.setMultiGasPricingConstraints(constraints); } - function test_setMultiGasPricingConstraints_zeroWeightsWithNonZeroBacklog() external { - // Edge case: all-zero weights with non-zero backlog: sorted check runs but no exponent is added + 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({ @@ -703,6 +700,47 @@ contract ResourceConstraintManagerTest is Test { 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); } @@ -733,7 +771,7 @@ contract ResourceConstraintManagerTest is Test { ); resourceConstraintManager.setMultiGasPricingConstraints(constraints); - // Larger unsorted array: [Computation(1), StorageAccess(3), HistoryGrowth(2)] + // Larger unsorted array: [Computation(1), StorageAccessRead(3), HistoryGrowth(2)] ArbMultiGasConstraintsTypes.WeightedResource[] memory resources2 = new ArbMultiGasConstraintsTypes.WeightedResource[](3); resources2[0] = ArbMultiGasConstraintsTypes.WeightedResource({ @@ -741,7 +779,7 @@ contract ResourceConstraintManagerTest is Test { weight: 1 }); resources2[1] = ArbMultiGasConstraintsTypes.WeightedResource({ - resource: ArbMultiGasConstraintsTypes.ResourceKind.StorageAccess, + resource: ArbMultiGasConstraintsTypes.ResourceKind.StorageAccessRead, weight: 2 }); resources2[2] = ArbMultiGasConstraintsTypes.WeightedResource({