Skip to content

Commit 426e3c8

Browse files
gzeonethyahgwai
andauthored
feat: ResourceConstraintManager (#389)
* chore: checkout resource-constrait-precompiles * wip: PricingManager * chore: rename ResourceConstraintManager * fix: use proper interface * fix: nConstraints * feat: ResourceConstraintManager.revoke * feat: startingBacklogValue must be 0 * chore: better error * chore: manager * test: ResourceConstraintManagerTest * feat: added pricing exponent check and tests * style: formatting * fix: remove unused error * test: fixed failing test * test: added better revert check --------- Co-authored-by: Chris Buckland <cpbuckland88@gmail.com>
1 parent 4abf3a5 commit 426e3c8

File tree

2 files changed

+346
-0
lines changed

2 files changed

+346
-0
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2022-2025, Offchain Labs, Inc.
2+
// For license information, see https://github.com/nitro/blob/master/LICENSE
3+
// SPDX-License-Identifier: BUSL-1.1
4+
5+
pragma solidity ^0.8.0;
6+
7+
import "../precompiles/ArbOwner.sol";
8+
import "../precompiles/ArbGasInfo.sol";
9+
import "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
10+
11+
contract ResourceConstraintManager is AccessControlEnumerable {
12+
ArbOwner internal constant ARB_OWNER = ArbOwner(address(0x70));
13+
ArbGasInfo internal constant ARB_GAS_INFO = ArbGasInfo(address(0x6c));
14+
15+
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
16+
uint256 public expiryTimestamp;
17+
18+
error TooManyConstraints();
19+
error InvalidPeriod(
20+
uint64 gasTargetPerSec, uint64 adjustmentWindowSecs, uint64 startingBacklogValue
21+
);
22+
error InvalidTarget(
23+
uint64 gasTargetPerSec, uint64 adjustmentWindowSecs, uint64 startingBacklogValue
24+
);
25+
error PricingExponentTooHigh(uint64 pricingExponent);
26+
error NotExpired();
27+
28+
constructor(address admin, address manager, uint256 _expiryTimestamp) {
29+
_setupRole(DEFAULT_ADMIN_ROLE, admin);
30+
_setupRole(MANAGER_ROLE, manager);
31+
expiryTimestamp = _expiryTimestamp;
32+
}
33+
34+
/// @notice Removes the contract from the list of chain owners after the expiry timestamp
35+
function revoke() external {
36+
if (block.timestamp < expiryTimestamp) {
37+
revert NotExpired();
38+
}
39+
ARB_OWNER.removeChainOwner(address(this));
40+
}
41+
42+
/// @notice Sets the list of gas pricing constraints for the multi-constraint pricing model.
43+
/// See ArbOwner.setGasPricingConstraints interface for more information.
44+
/// @param constraints Array of triples (gas_target_per_second, adjustment_window_seconds, starting_backlog_value)
45+
/// - gas_target_per_second: target gas usage per second for the constraint (uint64, gas/sec)
46+
/// - adjustment_window_seconds: time over which the price will rise by a factor of e if demand is 2x the target (uint64, seconds)
47+
/// - starting_backlog_value: initial backlog for this constraint (uint64, gas units)
48+
function setGasPricingConstraints(
49+
uint64[3][] calldata constraints
50+
) external onlyRole(MANAGER_ROLE) {
51+
// If zero constraints are provided, the chain uses the single-constraint pricing model
52+
uint256 nConstraints = constraints.length;
53+
if (nConstraints > 10) {
54+
revert TooManyConstraints();
55+
}
56+
uint64 pricingExponent = 0;
57+
for (uint256 i = 0; i < nConstraints; ++i) {
58+
uint64 gasTargetPerSec = constraints[i][0];
59+
uint64 adjustmentWindowSecs = constraints[i][1];
60+
uint64 startingBacklogValue = constraints[i][2];
61+
if (gasTargetPerSec < 7_000_000 || gasTargetPerSec > 100_000_000) {
62+
revert InvalidTarget(gasTargetPerSec, adjustmentWindowSecs, startingBacklogValue);
63+
}
64+
if (adjustmentWindowSecs < 5 || adjustmentWindowSecs > 86400) {
65+
revert InvalidPeriod(gasTargetPerSec, adjustmentWindowSecs, startingBacklogValue);
66+
}
67+
// we scale by 1000 to improve precision in calculating the exponent
68+
// since this division will round down, it's always possible for the real exponent to be up to
69+
// the number of constraints greater than the value we measure
70+
// for instance
71+
// if n = 10, and we check with precision 1 against threshold 8, then the real exponent might actually be up to 18
72+
// if n = 10, and we check with precision 1000 against threshold 8000, then the real exponent might actually be up to 8010 / 1000
73+
pricingExponent +=
74+
(startingBacklogValue * 1000) / (gasTargetPerSec * adjustmentWindowSecs);
75+
}
76+
77+
// this calculated pricing exponent will by used by nitro to calculate the gas price
78+
// we check that the pricing exponent is below some reasonable number to avoid setting the gas price astronomically high
79+
// 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
80+
// eg. these constraints can be changed again, or the sec council can send admin transactions
81+
// with min base fee of 0.02, exponent of 8 (scaled by 1000) corresponds to a gas price of ~60 Gwei
82+
if (pricingExponent > 8000) {
83+
revert PricingExponentTooHigh(pricingExponent);
84+
}
85+
86+
ARB_OWNER.setGasPricingConstraints(constraints);
87+
}
88+
}
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.4;
3+
4+
import "forge-std/Test.sol";
5+
import "../../src/chain/ResourceConstraintManager.sol";
6+
7+
contract ResourceConstraintManagerTest is Test {
8+
ResourceConstraintManager public resourceConstraintManager;
9+
ArbOwnerMock internal constant ARB_OWNER = ArbOwnerMock(address(0x70));
10+
11+
address constant admin = address(1337);
12+
address constant manager = address(7331);
13+
uint256 constant expiryTimestamp = 12345678;
14+
15+
constructor() {
16+
resourceConstraintManager = new ResourceConstraintManager(admin, manager, expiryTimestamp);
17+
vm.etch(address(ARB_OWNER), type(ArbOwnerMock).runtimeCode);
18+
}
19+
20+
function test_revoke() external {
21+
// Test before expiry
22+
vm.warp(expiryTimestamp - 1);
23+
vm.expectRevert(ResourceConstraintManager.NotExpired.selector);
24+
resourceConstraintManager.revoke();
25+
26+
// Test after expiry
27+
vm.warp(expiryTimestamp);
28+
assertFalse(ARB_OWNER.removeChainOwnerCalled());
29+
resourceConstraintManager.revoke();
30+
assertTrue(ARB_OWNER.removeChainOwnerCalled());
31+
}
32+
33+
function test_setGasPricingConstraints_success() external {
34+
// Test with valid single constraint
35+
uint64[3][] memory constraints = new uint64[3][](1);
36+
constraints[0] = [uint64(10_000_000), uint64(100), uint64(0)];
37+
38+
vm.prank(manager);
39+
resourceConstraintManager.setGasPricingConstraints(constraints);
40+
41+
// Test with multiple valid constraints
42+
uint64[3][] memory multipleConstraints = new uint64[3][](3);
43+
multipleConstraints[0] = [uint64(7_000_000), uint64(5), uint64(0)];
44+
multipleConstraints[1] = [uint64(50_000_000), uint64(1000), uint64(1)];
45+
multipleConstraints[2] = [uint64(100_000_000), uint64(86400), uint64(10000)];
46+
47+
vm.prank(manager);
48+
resourceConstraintManager.setGasPricingConstraints(multipleConstraints);
49+
50+
// Test with empty constraints array (switch to single-constraint model)
51+
uint64[3][] memory emptyConstraints = new uint64[3][](0);
52+
vm.prank(manager);
53+
resourceConstraintManager.setGasPricingConstraints(emptyConstraints);
54+
}
55+
56+
function test_setGasPricingConstraints_pricingExponentTooHigh() external {
57+
// create constraints on the limit of the pricing exponent
58+
uint64[3][] memory multipleConstraints = new uint64[3][](3);
59+
multipleConstraints[0] = [uint64(7_000_000), uint64(5), uint64(35_000_000)]; // 1
60+
multipleConstraints[1] = [uint64(50_000_000), uint64(1000), uint64(300_000_000_000)]; // 6
61+
multipleConstraints[2] = [uint64(100_000_000), uint64(86400), uint64(8_640_000_000_000)]; // 1
62+
63+
vm.prank(manager);
64+
resourceConstraintManager.setGasPricingConstraints(multipleConstraints);
65+
66+
// up to the limit
67+
multipleConstraints[1][2] = uint64(300_049_999_999);
68+
vm.prank(manager);
69+
resourceConstraintManager.setGasPricingConstraints(multipleConstraints);
70+
71+
// over the limit
72+
multipleConstraints[1][2] = uint64(300_050_000_000);
73+
vm.prank(manager);
74+
vm.expectRevert(
75+
abi.encodeWithSelector(ResourceConstraintManager.PricingExponentTooHigh.selector, 8001)
76+
);
77+
resourceConstraintManager.setGasPricingConstraints(multipleConstraints);
78+
}
79+
80+
function test_setGasPricingConstraints_accessControl() external {
81+
uint64[3][] memory constraints = new uint64[3][](1);
82+
constraints[0] = [uint64(10_000_000), uint64(100), uint64(0)];
83+
84+
// Test non-manager cannot call
85+
vm.expectRevert();
86+
resourceConstraintManager.setGasPricingConstraints(constraints);
87+
88+
// Test admin without manager role cannot call
89+
vm.prank(admin);
90+
vm.expectRevert();
91+
resourceConstraintManager.setGasPricingConstraints(constraints);
92+
93+
// Test manager can call
94+
vm.prank(manager);
95+
resourceConstraintManager.setGasPricingConstraints(constraints);
96+
}
97+
98+
function test_setGasPricingConstraints_tooManyConstraints() external {
99+
// Test exactly 10 constraints (should succeed)
100+
uint64[3][] memory tenConstraints = new uint64[3][](10);
101+
for (uint256 i = 0; i < 10; i++) {
102+
tenConstraints[i] = [uint64(10_000_000), uint64(100), uint64(0)];
103+
}
104+
vm.prank(manager);
105+
resourceConstraintManager.setGasPricingConstraints(tenConstraints);
106+
107+
// Test 11 constraints (should revert)
108+
uint64[3][] memory elevenConstraints = new uint64[3][](11);
109+
for (uint256 i = 0; i < 11; i++) {
110+
elevenConstraints[i] = [uint64(10_000_000), uint64(100), uint64(0)];
111+
}
112+
vm.prank(manager);
113+
vm.expectRevert(ResourceConstraintManager.TooManyConstraints.selector);
114+
resourceConstraintManager.setGasPricingConstraints(elevenConstraints);
115+
}
116+
117+
function test_setGasPricingConstraints_invalidTarget() external {
118+
// Test gas target below minimum (6,999,999)
119+
uint64[3][] memory constraintsLowTarget = new uint64[3][](1);
120+
constraintsLowTarget[0] = [uint64(6_999_999), uint64(100), uint64(0)];
121+
122+
vm.prank(manager);
123+
vm.expectRevert(
124+
abi.encodeWithSelector(
125+
ResourceConstraintManager.InvalidTarget.selector,
126+
uint64(6_999_999),
127+
uint64(100),
128+
uint64(0)
129+
)
130+
);
131+
resourceConstraintManager.setGasPricingConstraints(constraintsLowTarget);
132+
133+
// Test gas target above maximum (100,000,001)
134+
uint64[3][] memory constraintsHighTarget = new uint64[3][](1);
135+
constraintsHighTarget[0] = [uint64(100_000_001), uint64(100), uint64(0)];
136+
137+
vm.prank(manager);
138+
vm.expectRevert(
139+
abi.encodeWithSelector(
140+
ResourceConstraintManager.InvalidTarget.selector,
141+
uint64(100_000_001),
142+
uint64(100),
143+
uint64(0)
144+
)
145+
);
146+
resourceConstraintManager.setGasPricingConstraints(constraintsHighTarget);
147+
148+
// Test edge cases (exactly at boundaries should succeed)
149+
uint64[3][] memory constraintsMinTarget = new uint64[3][](1);
150+
constraintsMinTarget[0] = [uint64(7_000_000), uint64(100), uint64(0)];
151+
vm.prank(manager);
152+
resourceConstraintManager.setGasPricingConstraints(constraintsMinTarget);
153+
154+
uint64[3][] memory constraintsMaxTarget = new uint64[3][](1);
155+
constraintsMaxTarget[0] = [uint64(100_000_000), uint64(100), uint64(0)];
156+
vm.prank(manager);
157+
resourceConstraintManager.setGasPricingConstraints(constraintsMaxTarget);
158+
}
159+
160+
function test_setGasPricingConstraints_invalidPeriod() external {
161+
// Test adjustment window below minimum (4 seconds)
162+
uint64[3][] memory constraintsLowPeriod = new uint64[3][](1);
163+
constraintsLowPeriod[0] = [uint64(10_000_000), uint64(4), uint64(0)];
164+
165+
vm.prank(manager);
166+
vm.expectRevert(
167+
abi.encodeWithSelector(
168+
ResourceConstraintManager.InvalidPeriod.selector,
169+
uint64(10_000_000),
170+
uint64(4),
171+
uint64(0)
172+
)
173+
);
174+
resourceConstraintManager.setGasPricingConstraints(constraintsLowPeriod);
175+
176+
// Test adjustment window above maximum (86401 seconds)
177+
uint64[3][] memory constraintsHighPeriod = new uint64[3][](1);
178+
constraintsHighPeriod[0] = [uint64(10_000_000), uint64(86401), uint64(0)];
179+
180+
vm.prank(manager);
181+
vm.expectRevert(
182+
abi.encodeWithSelector(
183+
ResourceConstraintManager.InvalidPeriod.selector,
184+
uint64(10_000_000),
185+
uint64(86401),
186+
uint64(0)
187+
)
188+
);
189+
resourceConstraintManager.setGasPricingConstraints(constraintsHighPeriod);
190+
191+
// Test edge cases (exactly at boundaries should succeed)
192+
uint64[3][] memory constraintsMinPeriod = new uint64[3][](1);
193+
constraintsMinPeriod[0] = [uint64(10_000_000), uint64(5), uint64(0)];
194+
vm.prank(manager);
195+
resourceConstraintManager.setGasPricingConstraints(constraintsMinPeriod);
196+
197+
uint64[3][] memory constraintsMaxPeriod = new uint64[3][](1);
198+
constraintsMaxPeriod[0] = [uint64(10_000_000), uint64(86400), uint64(0)];
199+
vm.prank(manager);
200+
resourceConstraintManager.setGasPricingConstraints(constraintsMaxPeriod);
201+
}
202+
203+
function test_setGasPricingConstraints_multipleConstraintValidation() external {
204+
// Test that all constraints are validated (not just the first one)
205+
uint64[3][] memory constraints = new uint64[3][](3);
206+
constraints[0] = [uint64(10_000_000), uint64(100), uint64(0)]; // Valid
207+
constraints[1] = [uint64(20_000_000), uint64(200), uint64(0)]; // Valid
208+
constraints[2] = [uint64(5_000_000), uint64(100), uint64(0)]; // Invalid target
209+
210+
vm.prank(manager);
211+
vm.expectRevert(
212+
abi.encodeWithSelector(
213+
ResourceConstraintManager.InvalidTarget.selector,
214+
uint64(5_000_000),
215+
uint64(100),
216+
uint64(0)
217+
)
218+
);
219+
resourceConstraintManager.setGasPricingConstraints(constraints);
220+
221+
// Test with invalid period in middle
222+
constraints[0] = [uint64(10_000_000), uint64(100), uint64(0)]; // Valid
223+
constraints[1] = [uint64(20_000_000), uint64(3), uint64(0)]; // Invalid period
224+
constraints[2] = [uint64(30_000_000), uint64(100), uint64(0)]; // Valid
225+
226+
vm.prank(manager);
227+
vm.expectRevert(
228+
abi.encodeWithSelector(
229+
ResourceConstraintManager.InvalidPeriod.selector,
230+
uint64(20_000_000),
231+
uint64(3),
232+
uint64(0)
233+
)
234+
);
235+
resourceConstraintManager.setGasPricingConstraints(constraints);
236+
}
237+
}
238+
239+
contract ArbOwnerMock {
240+
bool public removeChainOwnerCalled;
241+
uint64[3][] public lastConstraints;
242+
243+
function removeChainOwner(
244+
address ownerToRemove
245+
) external {
246+
removeChainOwnerCalled = true;
247+
}
248+
249+
function setGasPricingConstraints(
250+
uint64[3][] calldata constraints
251+
) external {
252+
lastConstraints = constraints;
253+
}
254+
255+
function getLastConstraints() external view returns (uint64[3][] memory) {
256+
return lastConstraints;
257+
}
258+
}

0 commit comments

Comments
 (0)