arb_precompiles/
arbowner.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{keccak256, Address, B256, U256};
3use revm::{
4    precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult},
5    primitives::Log,
6};
7
8use crate::storage_slot::{
9    derive_subspace_key, map_slot, map_slot_b256, root_slot, subspace_slot, ARBOS_STATE_ADDRESS,
10    CACHE_MANAGERS_KEY, CHAIN_CONFIG_SUBSPACE, CHAIN_OWNER_SUBSPACE, FEATURES_SUBSPACE,
11    FILTERED_FUNDS_RECIPIENT_OFFSET, L1_PRICING_SUBSPACE, L2_PRICING_SUBSPACE,
12    NATIVE_TOKEN_ENABLED_FROM_TIME_OFFSET, NATIVE_TOKEN_SUBSPACE, PROGRAMS_SUBSPACE,
13    ROOT_STORAGE_KEY, TRANSACTION_FILTERER_SUBSPACE, TX_FILTERING_ENABLED_FROM_TIME_OFFSET,
14};
15
16/// ArbOwner precompile address (0x70).
17pub const ARBOWNER_ADDRESS: Address = Address::new([
18    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
19    0x00, 0x00, 0x00, 0x70,
20]);
21
22// ── Selectors ────────────────────────────────────────────────────────
23
24// Getters (also on ArbOwner in Go, though most are on ArbOwnerPublic)
25const GET_NETWORK_FEE_ACCOUNT: [u8; 4] = [0x3e, 0x7a, 0x47, 0xb1];
26const GET_INFRA_FEE_ACCOUNT: [u8; 4] = [0x74, 0x33, 0x16, 0x04];
27const IS_CHAIN_OWNER: [u8; 4] = [0x26, 0xef, 0x69, 0x9d];
28const GET_ALL_CHAIN_OWNERS: [u8; 4] = [0x51, 0x6b, 0xaf, 0x03];
29
30// Chain owner management
31const ADD_CHAIN_OWNER: [u8; 4] = [0x48, 0x1f, 0x8d, 0xbf];
32const REMOVE_CHAIN_OWNER: [u8; 4] = [0x87, 0x92, 0x70, 0x1a];
33
34// Setters — ArbOS root state
35const SET_NETWORK_FEE_ACCOUNT: [u8; 4] = [0xe1, 0xa3, 0x5b, 0x12];
36const SET_INFRA_FEE_ACCOUNT: [u8; 4] = [0x0b, 0x6c, 0xf6, 0x99];
37const SCHEDULE_ARBOS_UPGRADE: [u8; 4] = [0xe3, 0x88, 0xb3, 0x81];
38const SET_BROTLI_COMPRESSION_LEVEL: [u8; 4] = [0x86, 0x47, 0x23, 0x97];
39const SET_CHAIN_CONFIG: [u8; 4] = [0xf5, 0xb7, 0x78, 0x63];
40
41// Setters — L2 pricing
42const SET_SPEED_LIMIT: [u8; 4] = [0x2e, 0x09, 0xca, 0x2e];
43const SET_L2_BASE_FEE: [u8; 4] = [0x72, 0xbc, 0x8c, 0x42];
44const SET_MINIMUM_L2_BASE_FEE: [u8; 4] = [0xa7, 0x47, 0x14, 0x0c];
45const SET_MAX_BLOCK_GAS_LIMIT: [u8; 4] = [0x20, 0x2c, 0xbf, 0xbd];
46const SET_MAX_TX_GAS_LIMIT: [u8; 4] = [0xa3, 0xb1, 0xb3, 0x1d];
47const SET_L2_GAS_PRICING_INERTIA: [u8; 4] = [0x08, 0x88, 0x56, 0x1e];
48const SET_L2_GAS_BACKLOG_TOLERANCE: [u8; 4] = [0x1e, 0xda, 0xbd, 0xa6];
49const SET_GAS_BACKLOG: [u8; 4] = [0x50, 0x52, 0x48, 0x93];
50const SET_GAS_PRICING_CONSTRAINTS: [u8; 4] = [0xea, 0xe0, 0x29, 0x95];
51const SET_MULTI_GAS_PRICING_CONSTRAINTS: [u8; 4] = [0x9c, 0x04, 0x2d, 0x8e];
52
53// Setters — L1 pricing
54const SET_L1_PRICING_EQUILIBRATION_UNITS: [u8; 4] = [0x69, 0x2c, 0xeb, 0x1e];
55const SET_L1_PRICING_INERTIA: [u8; 4] = [0x77, 0x6d, 0xbb, 0x4e];
56const SET_L1_PRICING_REWARD_RECIPIENT: [u8; 4] = [0xca, 0x27, 0x9e, 0x4e];
57const SET_L1_PRICING_REWARD_RATE: [u8; 4] = [0xee, 0x65, 0x86, 0xc6];
58const SET_L1_PRICE_PER_UNIT: [u8; 4] = [0x63, 0xbe, 0x3f, 0x93];
59const SET_PARENT_GAS_FLOOR_PER_TOKEN: [u8; 4] = [0x07, 0x71, 0xbb, 0xc7];
60const SET_PER_BATCH_GAS_CHARGE: [u8; 4] = [0x8f, 0x69, 0xb8, 0x12];
61const SET_AMORTIZED_COST_CAP_BIPS: [u8; 4] = [0xa4, 0xb8, 0xdb, 0x1e];
62const RELEASE_L1_PRICER_SURPLUS_FUNDS: [u8; 4] = [0xbf, 0xc5, 0x21, 0xee];
63const SET_L1_BASEFEE_ESTIMATE_INERTIA: [u8; 4] = [0x11, 0xc4, 0x8a, 0x7e];
64
65// Setters — Stylus/Wasm
66const SET_INK_PRICE: [u8; 4] = [0x8a, 0x0c, 0x4b, 0x6d];
67const SET_WASM_MAX_STACK_DEPTH: [u8; 4] = [0xf2, 0x41, 0x05, 0xca];
68const SET_WASM_FREE_PAGES: [u8; 4] = [0x53, 0x09, 0xac, 0xc8];
69const SET_WASM_PAGE_GAS: [u8; 4] = [0x82, 0x0b, 0x2b, 0x3d];
70const SET_WASM_PAGE_LIMIT: [u8; 4] = [0x30, 0xc3, 0xb8, 0x41];
71const SET_WASM_MIN_INIT_GAS: [u8; 4] = [0xd2, 0x56, 0x91, 0x32];
72const SET_WASM_INIT_COST_SCALAR: [u8; 4] = [0x7a, 0xaf, 0x8c, 0xa6];
73const SET_WASM_EXPIRY_DAYS: [u8; 4] = [0xd9, 0x13, 0xea, 0x35];
74const SET_WASM_KEEPALIVE_DAYS: [u8; 4] = [0x15, 0x8c, 0x34, 0x18];
75const SET_WASM_BLOCK_CACHE_SIZE: [u8; 4] = [0xce, 0x6e, 0x7e, 0x24];
76const SET_WASM_MAX_SIZE: [u8; 4] = [0x67, 0x00, 0xbb, 0x59];
77const ADD_WASM_CACHE_MANAGER: [u8; 4] = [0x48, 0x28, 0x2e, 0xaf];
78const REMOVE_WASM_CACHE_MANAGER: [u8; 4] = [0x1e, 0xc8, 0xd5, 0x8e];
79const SET_MAX_STYLUS_CONTRACT_FRAGMENTS: [u8; 4] = [0x79, 0xaf, 0xf2, 0x99];
80const SET_CALLDATA_PRICE_INCREASE: [u8; 4] = [0x03, 0x27, 0x40, 0x3c];
81
82// Transaction filtering / native token
83const ADD_TRANSACTION_FILTERER: [u8; 4] = [0x84, 0x36, 0x3d, 0xbf];
84const REMOVE_TRANSACTION_FILTERER: [u8; 4] = [0xd8, 0x60, 0xf6, 0xc5];
85const GET_ALL_TRANSACTION_FILTERERS: [u8; 4] = [0x3d, 0xbb, 0x43, 0x98];
86const IS_TRANSACTION_FILTERER: [u8; 4] = [0xa5, 0x3f, 0xef, 0x64];
87const SET_TRANSACTION_FILTERING_FROM: [u8; 4] = [0x08, 0x36, 0x96, 0x1e];
88const SET_FILTERED_FUNDS_RECIPIENT: [u8; 4] = [0x4a, 0xc0, 0xa0, 0x45];
89const GET_FILTERED_FUNDS_RECIPIENT: [u8; 4] = [0x8b, 0x00, 0x16, 0x72];
90const SET_NATIVE_TOKEN_MANAGEMENT_FROM: [u8; 4] = [0x1b, 0x25, 0x67, 0xaa];
91const ADD_NATIVE_TOKEN_OWNER: [u8; 4] = [0xc2, 0x5d, 0xfe, 0xbb];
92const REMOVE_NATIVE_TOKEN_OWNER: [u8; 4] = [0x52, 0x2e, 0xf9, 0xad];
93const GET_ALL_NATIVE_TOKEN_OWNERS: [u8; 4] = [0xf5, 0xc8, 0x16, 0x7a];
94const IS_NATIVE_TOKEN_OWNER: [u8; 4] = [0x40, 0xb6, 0x62, 0x08];
95
96// ArbOS state offsets
97const NETWORK_FEE_ACCOUNT_OFFSET: u64 = 3;
98const INFRA_FEE_ACCOUNT_OFFSET: u64 = 6;
99const BROTLI_COMPRESSION_LEVEL_OFFSET: u64 = 7;
100const UPGRADE_VERSION_OFFSET: u64 = 1;
101const UPGRADE_TIMESTAMP_OFFSET: u64 = 2;
102
103// L1 pricing field offsets
104const L1_PAY_REWARDS_TO: u64 = 0;
105const L1_EQUILIBRATION_UNITS: u64 = 1;
106const L1_INERTIA: u64 = 2;
107const L1_PER_UNIT_REWARD: u64 = 3;
108const L1_PRICE_PER_UNIT: u64 = 7;
109const L1_PER_BATCH_GAS_COST: u64 = 9;
110const L1_AMORTIZED_COST_CAP_BIPS: u64 = 10;
111const L1_FEES_AVAILABLE: u64 = 11;
112const L1_GAS_FLOOR_PER_TOKEN: u64 = 12;
113
114// L2 pricing field offsets
115const L2_SPEED_LIMIT: u64 = 0;
116const L2_PER_BLOCK_GAS_LIMIT: u64 = 1;
117const L2_BASE_FEE: u64 = 2;
118const L2_MIN_BASE_FEE: u64 = 3;
119const L2_GAS_BACKLOG: u64 = 4;
120const L2_PRICING_INERTIA: u64 = 5;
121const L2_BACKLOG_TOLERANCE: u64 = 6;
122const L2_PER_TX_GAS_LIMIT: u64 = 7;
123
124/// L1 pricer funds pool address.
125const L1_PRICER_FUNDS_POOL_ADDRESS: Address = Address::new([
126    0xa4, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
127    0x00, 0x00, 0x00, 0xf6,
128]);
129
130const SLOAD_GAS: u64 = 800;
131const SSTORE_GAS: u64 = 20_000;
132const COPY_GAS: u64 = 3;
133
134pub fn create_arbowner_precompile() -> DynPrecompile {
135    DynPrecompile::new_stateful(PrecompileId::custom("arbowner"), handler)
136}
137
138fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
139    let gas_limit = input.gas;
140    let data = input.data;
141    if data.len() < 4 {
142        return Err(PrecompileError::other("input too short"));
143    }
144
145    // Verify the caller is a chain owner.
146    verify_owner(&mut input)?;
147
148    let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
149
150    let result = match selector {
151        // ── Getters ──────────────────────────────────────────────
152        GET_NETWORK_FEE_ACCOUNT => read_root_field(&mut input, NETWORK_FEE_ACCOUNT_OFFSET),
153        // GetInfraFeeAccount: ArbOS >= 5
154        GET_INFRA_FEE_ACCOUNT => {
155            if let Some(r) = crate::check_method_version(5, 0) {
156                return r;
157            }
158            read_root_field(&mut input, INFRA_FEE_ACCOUNT_OFFSET)
159        }
160        // GetFilteredFundsRecipient: ArbOS >= 60
161        GET_FILTERED_FUNDS_RECIPIENT => {
162            if let Some(r) = crate::check_method_version(60, 0) {
163                return r;
164            }
165            read_root_field(&mut input, FILTERED_FUNDS_RECIPIENT_OFFSET)
166        }
167        IS_CHAIN_OWNER => handle_is_chain_owner(&mut input),
168        GET_ALL_CHAIN_OWNERS => handle_get_all_chain_owners(&mut input),
169        // GetAllTransactionFilterers: ArbOS >= 60
170        GET_ALL_TRANSACTION_FILTERERS => {
171            if let Some(r) = crate::check_method_version(60, 0) {
172                return r;
173            }
174            handle_get_all_members(&mut input, TRANSACTION_FILTERER_SUBSPACE)
175        }
176        // GetAllNativeTokenOwners: ArbOS >= 41
177        GET_ALL_NATIVE_TOKEN_OWNERS => {
178            if let Some(r) = crate::check_method_version(41, 0) {
179                return r;
180            }
181            handle_get_all_members(&mut input, NATIVE_TOKEN_SUBSPACE)
182        }
183        // IsTransactionFilterer: ArbOS >= 60
184        IS_TRANSACTION_FILTERER => {
185            if let Some(r) = crate::check_method_version(60, 0) {
186                return r;
187            }
188            handle_is_member(&mut input, TRANSACTION_FILTERER_SUBSPACE)
189        }
190        // IsNativeTokenOwner: ArbOS >= 41
191        IS_NATIVE_TOKEN_OWNER => {
192            if let Some(r) = crate::check_method_version(41, 0) {
193                return r;
194            }
195            handle_is_member(&mut input, NATIVE_TOKEN_SUBSPACE)
196        }
197
198        // ── Chain owner management ─────────────────────────────────
199        ADD_CHAIN_OWNER => handle_add_chain_owner(&mut input),
200        REMOVE_CHAIN_OWNER => handle_remove_chain_owner(&mut input),
201
202        // ── Root state setters ───────────────────────────────────
203        SET_NETWORK_FEE_ACCOUNT => write_root_field(&mut input, NETWORK_FEE_ACCOUNT_OFFSET),
204        // SetInfraFeeAccount: ArbOS >= 5
205        SET_INFRA_FEE_ACCOUNT => {
206            if let Some(r) = crate::check_method_version(5, 0) {
207                return r;
208            }
209            write_root_field(&mut input, INFRA_FEE_ACCOUNT_OFFSET)
210        }
211        // SetBrotliCompressionLevel: ArbOS >= 20
212        SET_BROTLI_COMPRESSION_LEVEL => {
213            if let Some(r) = crate::check_method_version(20, 0) {
214                return r;
215            }
216            write_root_field(&mut input, BROTLI_COMPRESSION_LEVEL_OFFSET)
217        }
218        SCHEDULE_ARBOS_UPGRADE => handle_schedule_upgrade(&mut input),
219
220        // ── L2 pricing setters ───────────────────────────────────
221        SET_SPEED_LIMIT => {
222            let val = U256::from_be_slice(
223                input
224                    .data
225                    .get(4..36)
226                    .ok_or_else(|| PrecompileError::other("input too short"))?,
227            );
228            if val.is_zero() {
229                return Err(PrecompileError::other("speed limit must be nonzero"));
230            }
231            write_l2_field(&mut input, L2_SPEED_LIMIT)
232        }
233        SET_L2_BASE_FEE => write_l2_field(&mut input, L2_BASE_FEE),
234        SET_MINIMUM_L2_BASE_FEE => write_l2_field(&mut input, L2_MIN_BASE_FEE),
235        // SetMaxBlockGasLimit: ArbOS >= 50
236        SET_MAX_BLOCK_GAS_LIMIT => {
237            if let Some(r) = crate::check_method_version(50, 0) {
238                return r;
239            }
240            write_l2_field(&mut input, L2_PER_BLOCK_GAS_LIMIT)
241        }
242        SET_MAX_TX_GAS_LIMIT => {
243            // ArbOS < 50: write to per-block gas limit; >= 50: per-tx gas limit.
244            let version_slot = root_slot(0); // VERSION_OFFSET
245            load_arbos(&mut input)?;
246            let raw_version = sload_field(&mut input, version_slot)?.to::<u64>();
247            let arbos_version = raw_version + 55;
248            let offset = if arbos_version < 50 {
249                L2_PER_BLOCK_GAS_LIMIT
250            } else {
251                L2_PER_TX_GAS_LIMIT
252            };
253            write_l2_field(&mut input, offset)
254        }
255        SET_L2_GAS_PRICING_INERTIA => {
256            let val = U256::from_be_slice(
257                input
258                    .data
259                    .get(4..36)
260                    .ok_or_else(|| PrecompileError::other("input too short"))?,
261            );
262            if val.is_zero() {
263                return Err(PrecompileError::other("price inertia must be nonzero"));
264            }
265            write_l2_field(&mut input, L2_PRICING_INERTIA)
266        }
267        SET_L2_GAS_BACKLOG_TOLERANCE => write_l2_field(&mut input, L2_BACKLOG_TOLERANCE),
268        // SetGasBacklog: ArbOS >= 50
269        SET_GAS_BACKLOG => {
270            if let Some(r) = crate::check_method_version(50, 0) {
271                return r;
272            }
273            write_l2_field(&mut input, L2_GAS_BACKLOG)
274        }
275
276        // ── L1 pricing setters ───────────────────────────────────
277        SET_L1_PRICING_EQUILIBRATION_UNITS => write_l1_field(&mut input, L1_EQUILIBRATION_UNITS),
278        SET_L1_PRICING_INERTIA => write_l1_field(&mut input, L1_INERTIA),
279        SET_L1_PRICING_REWARD_RECIPIENT => write_l1_field(&mut input, L1_PAY_REWARDS_TO),
280        SET_L1_PRICING_REWARD_RATE => write_l1_field(&mut input, L1_PER_UNIT_REWARD),
281        SET_L1_PRICE_PER_UNIT => write_l1_field(&mut input, L1_PRICE_PER_UNIT),
282        // SetParentGasFloorPerToken: ArbOS >= 50
283        SET_PARENT_GAS_FLOOR_PER_TOKEN => {
284            if let Some(r) = crate::check_method_version(50, 0) {
285                return r;
286            }
287            write_l1_field(&mut input, L1_GAS_FLOOR_PER_TOKEN)
288        }
289        SET_PER_BATCH_GAS_CHARGE => write_l1_field(&mut input, L1_PER_BATCH_GAS_COST),
290        SET_AMORTIZED_COST_CAP_BIPS => write_l1_field(&mut input, L1_AMORTIZED_COST_CAP_BIPS),
291        SET_L1_BASEFEE_ESTIMATE_INERTIA => write_l1_field(&mut input, L1_INERTIA),
292        // ReleaseL1PricerSurplusFunds: ArbOS >= 10
293        RELEASE_L1_PRICER_SURPLUS_FUNDS => {
294            if let Some(r) = crate::check_method_version(10, 0) {
295                return r;
296            }
297            handle_release_l1_pricer_surplus_funds(&mut input)
298        }
299
300        // ── Stylus/Wasm parameter setters (all require ArbOS >= 30) ──
301        SET_INK_PRICE => {
302            if let Some(r) = crate::check_method_version(30, 0) {
303                return r;
304            }
305            let val = read_u32_param(data)?;
306            if val == 0 || val > 0xFF_FFFF {
307                return Err(PrecompileError::other(
308                    "ink price must be a positive uint24",
309                ));
310            }
311            write_stylus_param(&mut input, StylusField::InkPrice, val as u64)
312        }
313        SET_WASM_MAX_STACK_DEPTH => {
314            if let Some(r) = crate::check_method_version(30, 0) {
315                return r;
316            }
317            let val = read_u32_param(data)?;
318            write_stylus_param(&mut input, StylusField::MaxStackDepth, val as u64)
319        }
320        SET_WASM_FREE_PAGES => {
321            if let Some(r) = crate::check_method_version(30, 0) {
322                return r;
323            }
324            let val = read_u32_param(data)?;
325            write_stylus_param(&mut input, StylusField::FreePages, val as u64)
326        }
327        SET_WASM_PAGE_GAS => {
328            if let Some(r) = crate::check_method_version(30, 0) {
329                return r;
330            }
331            let val = read_u32_param(data)?;
332            write_stylus_param(&mut input, StylusField::PageGas, val as u64)
333        }
334        SET_WASM_PAGE_LIMIT => {
335            if let Some(r) = crate::check_method_version(30, 0) {
336                return r;
337            }
338            let val = read_u32_param(data)?;
339            write_stylus_param(&mut input, StylusField::PageLimit, val as u64)
340        }
341        SET_WASM_MIN_INIT_GAS => {
342            if let Some(r) = crate::check_method_version(30, 0) {
343                return r;
344            }
345            let val = read_u32_param(data)?;
346            write_stylus_param(&mut input, StylusField::MinInitGas, val as u64)
347        }
348        SET_WASM_INIT_COST_SCALAR => {
349            if let Some(r) = crate::check_method_version(30, 0) {
350                return r;
351            }
352            let val = read_u32_param(data)?;
353            write_stylus_param(&mut input, StylusField::InitCostScalar, val as u64)
354        }
355        SET_WASM_EXPIRY_DAYS => {
356            if let Some(r) = crate::check_method_version(30, 0) {
357                return r;
358            }
359            let val = read_u32_param(data)?;
360            write_stylus_param(&mut input, StylusField::ExpiryDays, val as u64)
361        }
362        SET_WASM_KEEPALIVE_DAYS => {
363            if let Some(r) = crate::check_method_version(30, 0) {
364                return r;
365            }
366            let val = read_u32_param(data)?;
367            write_stylus_param(&mut input, StylusField::KeepaliveDays, val as u64)
368        }
369        SET_WASM_BLOCK_CACHE_SIZE => {
370            if let Some(r) = crate::check_method_version(30, 0) {
371                return r;
372            }
373            let val = read_u32_param(data)?;
374            write_stylus_param(&mut input, StylusField::BlockCacheSize, val as u64)
375        }
376        // SetWasmMaxSize: ArbOS >= 40
377        SET_WASM_MAX_SIZE => {
378            if let Some(r) = crate::check_method_version(40, 0) {
379                return r;
380            }
381            let val = read_u32_param(data)?;
382            write_stylus_param(&mut input, StylusField::MaxWasmSize, val as u64)
383        }
384        // SetMaxStylusContractFragments: ArbOS >= 60
385        SET_MAX_STYLUS_CONTRACT_FRAGMENTS => {
386            if let Some(r) = crate::check_method_version(60, 0) {
387                return r;
388            }
389            let val = read_u32_param(data)?;
390            write_stylus_param(&mut input, StylusField::MaxFragmentCount, val as u64)
391        }
392        // AddWasmCacheManager: ArbOS >= 30
393        ADD_WASM_CACHE_MANAGER => {
394            if let Some(r) = crate::check_method_version(30, 0) {
395                return r;
396            }
397            handle_add_cache_manager(&mut input)
398        }
399        // RemoveWasmCacheManager: ArbOS >= 30
400        REMOVE_WASM_CACHE_MANAGER => {
401            if let Some(r) = crate::check_method_version(30, 0) {
402                return r;
403            }
404            handle_remove_cache_manager(&mut input)
405        }
406        // SetCalldataPriceIncrease: ArbOS >= 40
407        SET_CALLDATA_PRICE_INCREASE => {
408            if let Some(r) = crate::check_method_version(40, 0) {
409                return r;
410            }
411            handle_set_calldata_price_increase(&mut input)
412        }
413
414        // ── Transaction filtering (all ArbOS >= 60) ──────────────
415        ADD_TRANSACTION_FILTERER => {
416            if let Some(r) = crate::check_method_version(60, 0) {
417                return r;
418            }
419            handle_add_to_set_with_feature_check(
420                &mut input,
421                TRANSACTION_FILTERER_SUBSPACE,
422                TX_FILTERING_ENABLED_FROM_TIME_OFFSET,
423            )
424        }
425        REMOVE_TRANSACTION_FILTERER => {
426            if let Some(r) = crate::check_method_version(60, 0) {
427                return r;
428            }
429            handle_remove_from_set(&mut input, TRANSACTION_FILTERER_SUBSPACE)
430        }
431        SET_TRANSACTION_FILTERING_FROM => {
432            if let Some(r) = crate::check_method_version(60, 0) {
433                return r;
434            }
435            handle_set_feature_time(&mut input, TX_FILTERING_ENABLED_FROM_TIME_OFFSET)
436        }
437        // SetFilteredFundsRecipient: ArbOS >= 60
438        SET_FILTERED_FUNDS_RECIPIENT => {
439            if let Some(r) = crate::check_method_version(60, 0) {
440                return r;
441            }
442            write_root_field(&mut input, FILTERED_FUNDS_RECIPIENT_OFFSET)
443        }
444
445        // ── Native token management (all ArbOS >= 41) ─────────────
446        SET_NATIVE_TOKEN_MANAGEMENT_FROM => {
447            if let Some(r) = crate::check_method_version(41, 0) {
448                return r;
449            }
450            handle_set_feature_time(&mut input, NATIVE_TOKEN_ENABLED_FROM_TIME_OFFSET)
451        }
452        ADD_NATIVE_TOKEN_OWNER => {
453            if let Some(r) = crate::check_method_version(41, 0) {
454                return r;
455            }
456            handle_add_to_set_with_feature_check(
457                &mut input,
458                NATIVE_TOKEN_SUBSPACE,
459                NATIVE_TOKEN_ENABLED_FROM_TIME_OFFSET,
460            )
461        }
462        REMOVE_NATIVE_TOKEN_OWNER => {
463            if let Some(r) = crate::check_method_version(41, 0) {
464                return r;
465            }
466            handle_remove_from_set(&mut input, NATIVE_TOKEN_SUBSPACE)
467        }
468
469        // ── Gas pricing constraints ──────────────────────────────
470        // SetGasPricingConstraints: ArbOS >= 50
471        SET_GAS_PRICING_CONSTRAINTS => {
472            if let Some(r) = crate::check_method_version(50, 0) {
473                return r;
474            }
475            handle_set_gas_pricing_constraints(&mut input)
476        }
477        // SetMultiGasPricingConstraints: ArbOS >= 60
478        SET_MULTI_GAS_PRICING_CONSTRAINTS => {
479            if let Some(r) = crate::check_method_version(60, 0) {
480                return r;
481            }
482            handle_set_multi_gas_pricing_constraints(&mut input)
483        }
484
485        // ── Chain config (ArbOS >= 11) ──────────────────────────
486        SET_CHAIN_CONFIG => {
487            if let Some(r) = crate::check_method_version(11, 0) {
488                return r;
489            }
490            handle_set_chain_config(&mut input)
491        }
492
493        _ => Err(PrecompileError::other("unknown ArbOwner selector")),
494    };
495    // OwnerPrecompile wrapper: all successful calls are free (gas_used = 0).
496    // Emit OwnerActs event on success. In Nitro, this is automatic for all
497    // owner-only calls. For ArbOS < 11: emit for ALL calls (read+write).
498    // For ArbOS >= 11: emit only for write calls (not read-only getters).
499    let result = result.map(|output| {
500        let arbos_version = crate::get_arbos_version();
501        let is_read_only = matches!(
502            selector,
503            GET_NETWORK_FEE_ACCOUNT
504                | GET_INFRA_FEE_ACCOUNT
505                | IS_CHAIN_OWNER
506                | GET_ALL_CHAIN_OWNERS
507                | IS_TRANSACTION_FILTERER
508                | GET_ALL_TRANSACTION_FILTERERS
509                | IS_NATIVE_TOKEN_OWNER
510                | GET_ALL_NATIVE_TOKEN_OWNERS
511                | GET_FILTERED_FUNDS_RECIPIENT
512        );
513        if !is_read_only || arbos_version < 11 {
514            emit_owner_acts(&mut input, &selector, data);
515        }
516        PrecompileOutput::new(0, output.bytes)
517    });
518    crate::gas_check(gas_limit, result)
519}
520
521// ── Owner verification ───────────────────────────────────────────────
522
523fn verify_owner(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
524    let caller = input.caller;
525    load_arbos(input)?;
526
527    // Chain owners are stored in an AddressSet in the CHAIN_OWNER_SUBSPACE.
528    // AddressSet.byAddress is at sub-storage key [0] within the set's storage.
529    let set_key = derive_subspace_key(ROOT_STORAGE_KEY, CHAIN_OWNER_SUBSPACE);
530    let by_address_key = derive_subspace_key(set_key.as_slice(), &[0]);
531
532    let addr_as_b256 = alloy_primitives::B256::left_padding_from(caller.as_slice());
533    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_as_b256);
534
535    let value = sload_field(input, member_slot)?;
536    if value == U256::ZERO {
537        return Err(PrecompileError::other(
538            "ArbOwner: caller is not a chain owner",
539        ));
540    }
541    Ok(())
542}
543
544// ── Storage helpers ──────────────────────────────────────────────────
545
546fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
547    input
548        .internals_mut()
549        .load_account(ARBOS_STATE_ADDRESS)
550        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
551    Ok(())
552}
553
554fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
555    let val = input
556        .internals_mut()
557        .sload(ARBOS_STATE_ADDRESS, slot)
558        .map_err(|_| PrecompileError::other("sload failed"))?;
559    Ok(val.data)
560}
561
562fn sstore_field(
563    input: &mut PrecompileInput<'_>,
564    slot: U256,
565    value: U256,
566) -> Result<(), PrecompileError> {
567    input
568        .internals_mut()
569        .sstore(ARBOS_STATE_ADDRESS, slot, value)
570        .map_err(|_| PrecompileError::other("sstore failed"))?;
571    Ok(())
572}
573
574fn read_root_field(input: &mut PrecompileInput<'_>, offset: u64) -> PrecompileResult {
575    let gas_limit = input.gas;
576    let value = sload_field(input, root_slot(offset))?;
577    Ok(PrecompileOutput::new(
578        (SLOAD_GAS + COPY_GAS).min(gas_limit),
579        value.to_be_bytes::<32>().to_vec().into(),
580    ))
581}
582
583fn write_root_field(input: &mut PrecompileInput<'_>, offset: u64) -> PrecompileResult {
584    let data = input.data;
585    if data.len() < 36 {
586        return Err(PrecompileError::other("input too short"));
587    }
588    let gas_limit = input.gas;
589    let value = U256::from_be_slice(&data[4..36]);
590    sstore_field(input, root_slot(offset), value)?;
591    Ok(PrecompileOutput::new(
592        (SSTORE_GAS + COPY_GAS).min(gas_limit),
593        Vec::new().into(),
594    ))
595}
596
597fn write_l1_field(input: &mut PrecompileInput<'_>, offset: u64) -> PrecompileResult {
598    let data = input.data;
599    if data.len() < 36 {
600        return Err(PrecompileError::other("input too short"));
601    }
602    let gas_limit = input.gas;
603    let value = U256::from_be_slice(&data[4..36]);
604    let field_slot = subspace_slot(L1_PRICING_SUBSPACE, offset);
605    sstore_field(input, field_slot, value)?;
606    Ok(PrecompileOutput::new(
607        (SSTORE_GAS + COPY_GAS).min(gas_limit),
608        Vec::new().into(),
609    ))
610}
611
612fn write_l2_field(input: &mut PrecompileInput<'_>, offset: u64) -> PrecompileResult {
613    let data = input.data;
614    if data.len() < 36 {
615        return Err(PrecompileError::other("input too short"));
616    }
617    let gas_limit = input.gas;
618    let value = U256::from_be_slice(&data[4..36]);
619    let field_slot = subspace_slot(L2_PRICING_SUBSPACE, offset);
620    sstore_field(input, field_slot, value)?;
621    Ok(PrecompileOutput::new(
622        (SSTORE_GAS + COPY_GAS).min(gas_limit),
623        Vec::new().into(),
624    ))
625}
626
627fn handle_schedule_upgrade(input: &mut PrecompileInput<'_>) -> PrecompileResult {
628    let data = input.data;
629    if data.len() < 68 {
630        return Err(PrecompileError::other("input too short"));
631    }
632    let gas_limit = input.gas;
633    let new_version = U256::from_be_slice(&data[4..36]);
634    let timestamp = U256::from_be_slice(&data[36..68]);
635    sstore_field(input, root_slot(UPGRADE_VERSION_OFFSET), new_version)?;
636    sstore_field(input, root_slot(UPGRADE_TIMESTAMP_OFFSET), timestamp)?;
637    Ok(PrecompileOutput::new(
638        (2 * SSTORE_GAS + COPY_GAS).min(gas_limit),
639        Vec::new().into(),
640    ))
641}
642
643/// Emit the OwnerActs event: OwnerActs(bytes4 method, address owner, bytes data).
644/// Matches Nitro's automatic OwnerActs emission for all owner-only calls.
645fn emit_owner_acts(input: &mut PrecompileInput<'_>, selector: &[u8; 4], calldata: &[u8]) {
646    use alloy_primitives::{keccak256, Log, B256};
647
648    // event OwnerActs(bytes4 indexed method, address indexed owner, bytes data)
649    let topic0 = keccak256("OwnerActs(bytes4,address,bytes)");
650    let mut method_topic = [0u8; 32];
651    method_topic[..4].copy_from_slice(selector);
652    let topic1 = B256::from(method_topic);
653    let topic2 = B256::left_padding_from(input.caller.as_slice());
654
655    // ABI-encode calldata as bytes: offset(32) + length(32) + data (padded)
656    let mut log_data = Vec::with_capacity(64 + calldata.len().div_ceil(32) * 32);
657    log_data.extend_from_slice(&U256::from(32).to_be_bytes::<32>()); // offset
658    log_data.extend_from_slice(&U256::from(calldata.len()).to_be_bytes::<32>()); // length
659    log_data.extend_from_slice(calldata);
660    // Pad to 32-byte boundary
661    let pad = (32 - (calldata.len() % 32)) % 32;
662    log_data.extend(std::iter::repeat_n(0u8, pad));
663
664    input.internals_mut().log(Log::new_unchecked(
665        ARBOWNER_ADDRESS,
666        vec![topic0, topic1, topic2],
667        log_data.into(),
668    ));
669}
670
671// ── AddressSet helpers ──────────────────────────────────────────────
672
673/// Check if an address is a chain owner.
674fn handle_is_chain_owner(input: &mut PrecompileInput<'_>) -> PrecompileResult {
675    handle_is_member(input, CHAIN_OWNER_SUBSPACE)
676}
677
678/// Get all chain owners. Returns ABI-encoded dynamic address array.
679fn handle_get_all_chain_owners(input: &mut PrecompileInput<'_>) -> PrecompileResult {
680    handle_get_all_members(input, CHAIN_OWNER_SUBSPACE)
681}
682
683/// Add a chain owner. Emits ChainOwnerAdded event for ArbOS >= 60.
684fn handle_add_chain_owner(input: &mut PrecompileInput<'_>) -> PrecompileResult {
685    let data = input.data;
686    if data.len() < 36 {
687        return Err(PrecompileError::other("input too short"));
688    }
689    let gas_limit = input.gas;
690    let addr = Address::from_slice(&data[16..36]);
691
692    address_set_add(input, address_set_key(CHAIN_OWNER_SUBSPACE), addr)?;
693
694    // Emit ChainOwnerAdded event for ArbOS >= 60.
695    let arbos_version = read_arbos_version(input)? + 55;
696    if arbos_version >= 60 {
697        let topic0 = keccak256("ChainOwnerAdded(address)");
698        let topic1 = B256::left_padding_from(addr.as_slice());
699        input.internals_mut().log(Log::new_unchecked(
700            ARBOWNER_ADDRESS,
701            vec![topic0, topic1],
702            alloy_primitives::Bytes::new(),
703        ));
704    }
705
706    let gas_used = 2 * SLOAD_GAS + 3 * SSTORE_GAS + COPY_GAS;
707    Ok(PrecompileOutput::new(
708        gas_used.min(gas_limit),
709        Vec::new().into(),
710    ))
711}
712
713/// Remove a chain owner. Emits ChainOwnerRemoved event for ArbOS >= 60.
714fn handle_remove_chain_owner(input: &mut PrecompileInput<'_>) -> PrecompileResult {
715    let data = input.data;
716    if data.len() < 36 {
717        return Err(PrecompileError::other("input too short"));
718    }
719    let gas_limit = input.gas;
720    let addr = Address::from_slice(&data[16..36]);
721
722    let set_key = address_set_key(CHAIN_OWNER_SUBSPACE);
723    if !is_member_of(input, set_key, addr)? {
724        return Err(PrecompileError::other("tried to remove non-owner"));
725    }
726    address_set_remove(input, set_key, addr)?;
727
728    // Emit ChainOwnerRemoved event for ArbOS >= 60.
729    let arbos_version = read_arbos_version(input)? + 55;
730    if arbos_version >= 60 {
731        let topic0 = keccak256("ChainOwnerRemoved(address)");
732        let topic1 = B256::left_padding_from(addr.as_slice());
733        input.internals_mut().log(Log::new_unchecked(
734            ARBOWNER_ADDRESS,
735            vec![topic0, topic1],
736            alloy_primitives::Bytes::new(),
737        ));
738    }
739
740    let gas_used = 3 * SLOAD_GAS + 4 * SSTORE_GAS + COPY_GAS;
741    Ok(PrecompileOutput::new(
742        gas_used.min(gas_limit),
743        Vec::new().into(),
744    ))
745}
746
747/// Release surplus L1 pricer funds.
748///
749/// surplus = pool_balance - recognized_fees; capped by maxWeiToRelease.
750/// Adds the released amount to L1FeesAvailable rather than zeroing it.
751fn handle_release_l1_pricer_surplus_funds(input: &mut PrecompileInput<'_>) -> PrecompileResult {
752    let data = input.data;
753    if data.len() < 36 {
754        return Err(PrecompileError::other("input too short"));
755    }
756    let gas_limit = input.gas;
757    let max_wei = U256::from_be_slice(&data[4..36]);
758
759    // Read pool account balance.
760    let pool_balance = {
761        let acct = input
762            .internals_mut()
763            .load_account(L1_PRICER_FUNDS_POOL_ADDRESS)
764            .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
765        acct.data.info.balance
766    };
767
768    // Read recognized fees (L1FeesAvailable).
769    load_arbos(input)?;
770    let avail_slot = subspace_slot(L1_PRICING_SUBSPACE, L1_FEES_AVAILABLE);
771    let recognized = sload_field(input, avail_slot)?;
772
773    // Compute surplus = pool_balance - recognized.
774    if pool_balance <= recognized {
775        // No surplus.
776        return Ok(PrecompileOutput::new(
777            (SLOAD_GAS + COPY_GAS + 100).min(gas_limit),
778            U256::ZERO.to_be_bytes::<32>().to_vec().into(),
779        ));
780    }
781
782    let mut wei_to_transfer = pool_balance - recognized;
783    if wei_to_transfer > max_wei {
784        wei_to_transfer = max_wei;
785    }
786
787    // Add to L1FeesAvailable.
788    let new_available = recognized + wei_to_transfer;
789    sstore_field(input, avail_slot, new_available)?;
790
791    Ok(PrecompileOutput::new(
792        (SLOAD_GAS + SSTORE_GAS + COPY_GAS + 100).min(gas_limit),
793        wei_to_transfer.to_be_bytes::<32>().to_vec().into(),
794    ))
795}
796
797/// Derive the storage key for an AddressSet at the given subspace.
798fn address_set_key(subspace: &[u8]) -> B256 {
799    derive_subspace_key(ROOT_STORAGE_KEY, subspace)
800}
801
802/// Check if an address is a member of the AddressSet with the given set key.
803fn is_member_of(
804    input: &mut PrecompileInput<'_>,
805    set_key: B256,
806    addr: Address,
807) -> Result<bool, PrecompileError> {
808    let by_address_key = derive_subspace_key(set_key.as_slice(), &[0]);
809    let addr_hash = B256::left_padding_from(addr.as_slice());
810    let slot = map_slot_b256(by_address_key.as_slice(), &addr_hash);
811    let val = sload_field(input, slot)?;
812    Ok(val != U256::ZERO)
813}
814
815/// Handle IS_MEMBER check for an address set.
816fn handle_is_member(input: &mut PrecompileInput<'_>, subspace: &[u8]) -> PrecompileResult {
817    let data = input.data;
818    if data.len() < 36 {
819        return Err(PrecompileError::other("input too short"));
820    }
821    let gas_limit = input.gas;
822    let addr = Address::from_slice(&data[16..36]);
823    let is_member = is_member_of(input, address_set_key(subspace), addr)?;
824    let result = if is_member {
825        U256::from(1u64)
826    } else {
827        U256::ZERO
828    };
829    Ok(PrecompileOutput::new(
830        (SLOAD_GAS + COPY_GAS).min(gas_limit),
831        result.to_be_bytes::<32>().to_vec().into(),
832    ))
833}
834
835/// Handle GET_ALL_MEMBERS for an address set. Returns ABI-encoded dynamic array.
836fn handle_get_all_members(input: &mut PrecompileInput<'_>, subspace: &[u8]) -> PrecompileResult {
837    let gas_limit = input.gas;
838    let set_key = address_set_key(subspace);
839    let size_slot = map_slot(set_key.as_slice(), 0);
840    let size = sload_field(input, size_slot)?;
841    let count: u64 = size
842        .try_into()
843        .map_err(|_| PrecompileError::other("invalid address set size"))?;
844    const MAX_MEMBERS: u64 = 65536;
845    let count = count.min(MAX_MEMBERS);
846
847    let mut addresses = Vec::with_capacity(count as usize);
848    for i in 1..=count {
849        let member_slot = map_slot(set_key.as_slice(), i);
850        let val = sload_field(input, member_slot)?;
851        addresses.push(val);
852    }
853
854    let mut out = Vec::with_capacity(64 + 32 * addresses.len());
855    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
856    out.extend_from_slice(&U256::from(count).to_be_bytes::<32>());
857    for addr_val in &addresses {
858        out.extend_from_slice(&addr_val.to_be_bytes::<32>());
859    }
860
861    let gas_used = (1 + count) * SLOAD_GAS + COPY_GAS;
862    Ok(PrecompileOutput::new(gas_used.min(gas_limit), out.into()))
863}
864
865/// Add an address to an AddressSet. Returns true if newly added.
866fn address_set_add(
867    input: &mut PrecompileInput<'_>,
868    set_key: B256,
869    addr: Address,
870) -> Result<bool, PrecompileError> {
871    let by_address_key = derive_subspace_key(set_key.as_slice(), &[0]);
872    let addr_hash = B256::left_padding_from(addr.as_slice());
873    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_hash);
874    let existing = sload_field(input, member_slot)?;
875
876    if existing != U256::ZERO {
877        return Ok(false); // already a member
878    }
879
880    // Read size and increment.
881    let size_slot = map_slot(set_key.as_slice(), 0);
882    let size = sload_field(input, size_slot)?;
883    let size_u64: u64 = size
884        .try_into()
885        .map_err(|_| PrecompileError::other("invalid address set size"))?;
886    let new_size = size_u64 + 1;
887
888    // Store address at position (1 + old_size).
889    let new_pos_slot = map_slot(set_key.as_slice(), new_size);
890    let addr_as_u256 = U256::from_be_slice(addr.as_slice());
891    sstore_field(input, new_pos_slot, addr_as_u256)?;
892
893    // Store in byAddress: addr_hash → 1-based position.
894    sstore_field(input, member_slot, U256::from(new_size))?;
895
896    // Update size.
897    sstore_field(input, size_slot, U256::from(new_size))?;
898
899    Ok(true)
900}
901
902/// Remove an address from an AddressSet using swap-with-last.
903fn address_set_remove(
904    input: &mut PrecompileInput<'_>,
905    set_key: B256,
906    addr: Address,
907) -> Result<(), PrecompileError> {
908    let by_address_key = derive_subspace_key(set_key.as_slice(), &[0]);
909    let addr_hash = B256::left_padding_from(addr.as_slice());
910    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_hash);
911
912    // Get the 1-based position of the address.
913    let position = sload_field(input, member_slot)?;
914    if position == U256::ZERO {
915        return Err(PrecompileError::other("address not in set"));
916    }
917    let pos_u64: u64 = position
918        .try_into()
919        .map_err(|_| PrecompileError::other("invalid position"))?;
920
921    // Clear the byAddress entry.
922    sstore_field(input, member_slot, U256::ZERO)?;
923
924    // Read current size.
925    let size_slot = map_slot(set_key.as_slice(), 0);
926    let size = sload_field(input, size_slot)?;
927    let size_u64: u64 = size
928        .try_into()
929        .map_err(|_| PrecompileError::other("invalid size"))?;
930
931    // If not the last element, swap with last.
932    if pos_u64 < size_u64 {
933        let last_slot = map_slot(set_key.as_slice(), size_u64);
934        let last_val = sload_field(input, last_slot)?;
935
936        // Move last to removed position.
937        let removed_slot = map_slot(set_key.as_slice(), pos_u64);
938        sstore_field(input, removed_slot, last_val)?;
939
940        // Update byAddress for the swapped address.
941        let last_bytes = last_val.to_be_bytes::<32>();
942        let last_hash = B256::from(last_bytes);
943        let last_member_slot = map_slot_b256(by_address_key.as_slice(), &last_hash);
944        sstore_field(input, last_member_slot, U256::from(pos_u64))?;
945    }
946
947    // Clear last position.
948    let last_pos_slot = map_slot(set_key.as_slice(), size_u64);
949    sstore_field(input, last_pos_slot, U256::ZERO)?;
950
951    // Decrement size.
952    sstore_field(input, size_slot, U256::from(size_u64 - 1))?;
953
954    Ok(())
955}
956
957// ── Stylus param helpers ─────────────────────────────────────────────
958
959/// Field identifiers in the packed StylusParams storage word.
960///
961/// Packed layout (byte offsets within a 32-byte storage word):
962///   [0-1]   version (u16)
963///   [2-4]   ink_price (uint24)
964///   [5-8]   max_stack_depth (u32)
965///   [9-10]  free_pages (u16)
966///   [11-12] page_gas (u16)
967///   [13-14] page_limit (u16)
968///   [15]    min_init_gas (u8)
969///   [16]    min_cached_init_gas (u8)
970///   [17]    init_cost_scalar (u8)
971///   [18]    cached_cost_scalar (u8)
972///   [19-20] expiry_days (u16)
973///   [21-22] keepalive_days (u16)
974///   [23-24] block_cache_size (u16)
975///   [25-28] max_wasm_size (u32) -- arbos >= 40
976///   [29]    max_fragment_count (u8) -- arbos >= 41
977enum StylusField {
978    InkPrice,         // bytes 2..5 (uint24)
979    MaxStackDepth,    // bytes 5..9 (u32)
980    FreePages,        // bytes 9..11 (u16)
981    PageGas,          // bytes 11..13 (u16)
982    PageLimit,        // bytes 13..15 (u16)
983    MinInitGas,       // byte 15 (u8)
984    InitCostScalar,   // byte 17 (u8)
985    ExpiryDays,       // bytes 19..21 (u16)
986    KeepaliveDays,    // bytes 21..23 (u16)
987    BlockCacheSize,   // bytes 23..25 (u16)
988    MaxWasmSize,      // bytes 25..29 (u32)
989    MaxFragmentCount, // byte 29 (u8)
990}
991
992impl StylusField {
993    fn byte_range(&self) -> (usize, usize) {
994        match self {
995            Self::InkPrice => (2, 5),
996            Self::MaxStackDepth => (5, 9),
997            Self::FreePages => (9, 11),
998            Self::PageGas => (11, 13),
999            Self::PageLimit => (13, 15),
1000            Self::MinInitGas => (15, 16),
1001            Self::InitCostScalar => (17, 18),
1002            Self::ExpiryDays => (19, 21),
1003            Self::KeepaliveDays => (21, 23),
1004            Self::BlockCacheSize => (23, 25),
1005            Self::MaxWasmSize => (25, 29),
1006            Self::MaxFragmentCount => (29, 30),
1007        }
1008    }
1009}
1010
1011/// Compute the storage slot for the programs params word (slot 0).
1012fn programs_params_slot() -> U256 {
1013    // Path: root → PROGRAMS_SUBSPACE → PROGRAMS_PARAMS_KEY → slot 0
1014    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
1015    let params_key = derive_subspace_key(programs_key.as_slice(), &[0]); // PARAMS_KEY = [0]
1016    map_slot(params_key.as_slice(), 0)
1017}
1018
1019/// Read the packed programs params word from storage.
1020fn read_stylus_params_word(input: &mut PrecompileInput<'_>) -> Result<[u8; 32], PrecompileError> {
1021    let slot = programs_params_slot();
1022    let val = sload_field(input, slot)?;
1023    Ok(val.to_be_bytes::<32>())
1024}
1025
1026/// Write a modified field in the packed programs params storage word.
1027fn write_stylus_param(
1028    input: &mut PrecompileInput<'_>,
1029    field: StylusField,
1030    value: u64,
1031) -> PrecompileResult {
1032    let gas_limit = input.gas;
1033    let slot = programs_params_slot();
1034    let mut word = read_stylus_params_word(input)?;
1035
1036    let (start, end) = field.byte_range();
1037    let len = end - start;
1038    let bytes = value.to_be_bytes();
1039    // Copy the least significant `len` bytes from the u64.
1040    word[start..end].copy_from_slice(&bytes[8 - len..]);
1041
1042    sstore_field(input, slot, U256::from_be_bytes(word))?;
1043    Ok(PrecompileOutput::new(
1044        (SLOAD_GAS + SSTORE_GAS + COPY_GAS).min(gas_limit),
1045        Vec::new().into(),
1046    ))
1047}
1048
1049/// Parse a u32 parameter from ABI calldata (first arg after selector).
1050fn read_u32_param(data: &[u8]) -> Result<u32, PrecompileError> {
1051    if data.len() < 36 {
1052        return Err(PrecompileError::other("input too short"));
1053    }
1054    let val = U256::from_be_slice(&data[4..36]);
1055    val.try_into()
1056        .map_err(|_| PrecompileError::other("value overflow"))
1057}
1058
1059// ── Cache manager helpers ────────────────────────────────────────────
1060
1061/// Derive the AddressSet key for cache managers (PROGRAMS → CACHE_MANAGERS).
1062fn cache_managers_set_key() -> B256 {
1063    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
1064    derive_subspace_key(programs_key.as_slice(), CACHE_MANAGERS_KEY)
1065}
1066
1067fn handle_add_cache_manager(input: &mut PrecompileInput<'_>) -> PrecompileResult {
1068    let data = input.data;
1069    if data.len() < 36 {
1070        return Err(PrecompileError::other("input too short"));
1071    }
1072    let gas_limit = input.gas;
1073    let addr = Address::from_slice(&data[16..36]);
1074    address_set_add(input, cache_managers_set_key(), addr)?;
1075    let gas_used = 2 * SLOAD_GAS + 3 * SSTORE_GAS + COPY_GAS;
1076    Ok(PrecompileOutput::new(
1077        gas_used.min(gas_limit),
1078        Vec::new().into(),
1079    ))
1080}
1081
1082fn handle_remove_cache_manager(input: &mut PrecompileInput<'_>) -> PrecompileResult {
1083    let data = input.data;
1084    if data.len() < 36 {
1085        return Err(PrecompileError::other("input too short"));
1086    }
1087    let gas_limit = input.gas;
1088    let addr = Address::from_slice(&data[16..36]);
1089    let set_key = cache_managers_set_key();
1090    if !is_member_of(input, set_key, addr)? {
1091        return Err(PrecompileError::other("address is not a cache manager"));
1092    }
1093    address_set_remove(input, set_key, addr)?;
1094    let gas_used = 3 * SLOAD_GAS + 4 * SSTORE_GAS + COPY_GAS;
1095    Ok(PrecompileOutput::new(
1096        gas_used.min(gas_limit),
1097        Vec::new().into(),
1098    ))
1099}
1100
1101/// One week in seconds.
1102const FEATURE_ENABLE_DELAY: u64 = 7 * 24 * 60 * 60;
1103
1104/// Handle setting a feature enabled-from timestamp with validation.
1105fn handle_set_feature_time(input: &mut PrecompileInput<'_>, time_offset: u64) -> PrecompileResult {
1106    let data = input.data;
1107    if data.len() < 36 {
1108        return Err(PrecompileError::other("input too short"));
1109    }
1110    let gas_limit = input.gas;
1111    let timestamp: u64 = U256::from_be_slice(&data[4..36])
1112        .try_into()
1113        .map_err(|_| PrecompileError::other("timestamp too large"))?;
1114
1115    if timestamp == 0 {
1116        // Disable the feature.
1117        sstore_field(input, root_slot(time_offset), U256::ZERO)?;
1118        return Ok(PrecompileOutput::new(
1119            (SSTORE_GAS + COPY_GAS).min(gas_limit),
1120            Vec::new().into(),
1121        ));
1122    }
1123
1124    let stored_val = sload_field(input, root_slot(time_offset))?;
1125    let stored: u64 = stored_val.try_into().unwrap_or(0);
1126    let now = input
1127        .internals_mut()
1128        .block_timestamp()
1129        .try_into()
1130        .unwrap_or(0u64);
1131
1132    // Validate timing constraints.
1133    if (stored > now + FEATURE_ENABLE_DELAY || stored == 0)
1134        && timestamp < now + FEATURE_ENABLE_DELAY
1135    {
1136        return Err(PrecompileError::other(
1137            "feature must be enabled at least 7 days in the future",
1138        ));
1139    }
1140    if stored > now && stored <= now + FEATURE_ENABLE_DELAY && timestamp < stored {
1141        return Err(PrecompileError::other(
1142            "feature cannot be updated to a time earlier than the current scheduled enable time",
1143        ));
1144    }
1145
1146    sstore_field(input, root_slot(time_offset), U256::from(timestamp))?;
1147    Ok(PrecompileOutput::new(
1148        (SLOAD_GAS + SSTORE_GAS + COPY_GAS).min(gas_limit),
1149        Vec::new().into(),
1150    ))
1151}
1152
1153/// Add an address to an AddressSet, requiring that the feature is enabled.
1154fn handle_add_to_set_with_feature_check(
1155    input: &mut PrecompileInput<'_>,
1156    subspace: &[u8],
1157    time_offset: u64,
1158) -> PrecompileResult {
1159    let data = input.data;
1160    if data.len() < 36 {
1161        return Err(PrecompileError::other("input too short"));
1162    }
1163    let gas_limit = input.gas;
1164    let addr = Address::from_slice(&data[16..36]);
1165
1166    // Check feature is enabled.
1167    let enabled_time_val = sload_field(input, root_slot(time_offset))?;
1168    let enabled_time: u64 = enabled_time_val.try_into().unwrap_or(0);
1169    let now: u64 = input
1170        .internals_mut()
1171        .block_timestamp()
1172        .try_into()
1173        .unwrap_or(0u64);
1174
1175    if enabled_time == 0 || enabled_time > now {
1176        return Err(PrecompileError::other("feature is not enabled yet"));
1177    }
1178
1179    address_set_add(input, address_set_key(subspace), addr)?;
1180
1181    let gas_used = 2 * SLOAD_GAS + 3 * SSTORE_GAS + COPY_GAS;
1182    Ok(PrecompileOutput::new(
1183        gas_used.min(gas_limit),
1184        Vec::new().into(),
1185    ))
1186}
1187
1188/// Remove an address from an AddressSet.
1189fn handle_remove_from_set(input: &mut PrecompileInput<'_>, subspace: &[u8]) -> PrecompileResult {
1190    let data = input.data;
1191    if data.len() < 36 {
1192        return Err(PrecompileError::other("input too short"));
1193    }
1194    let gas_limit = input.gas;
1195    let addr = Address::from_slice(&data[16..36]);
1196
1197    // Verify membership first.
1198    let set_key = address_set_key(subspace);
1199    if !is_member_of(input, set_key, addr)? {
1200        return Err(PrecompileError::other("address is not a member"));
1201    }
1202
1203    address_set_remove(input, set_key, addr)?;
1204
1205    let gas_used = 3 * SLOAD_GAS + 4 * SSTORE_GAS + COPY_GAS;
1206    Ok(PrecompileOutput::new(
1207        gas_used.min(gas_limit),
1208        Vec::new().into(),
1209    ))
1210}
1211
1212// ── Gas constraint storage helpers ───────────────────────────────────
1213
1214const GC_KEY: &[u8] = b"gc";
1215const MGC_KEY: &[u8] = b"mgc";
1216const CONSTRAINT_TARGET: u64 = 0;
1217const CONSTRAINT_WINDOW: u64 = 1;
1218const CONSTRAINT_BACKLOG: u64 = 2;
1219const MGC_MAX_WEIGHT: u64 = 3;
1220const MGC_WEIGHTS_BASE: u64 = 4;
1221const NUM_RESOURCE_KINDS: u64 = 8;
1222const GAS_CONSTRAINTS_MAX_NUM: usize = 20;
1223const MAX_PRICING_EXPONENT_BIPS: u64 = 85_000;
1224
1225/// Derive the L2 pricing sub-storage key.
1226fn l2_pricing_key() -> B256 {
1227    derive_subspace_key(ROOT_STORAGE_KEY, L2_PRICING_SUBSPACE)
1228}
1229
1230/// Derive the gas constraints vector sub-storage key.
1231fn gc_vector_key() -> B256 {
1232    derive_subspace_key(l2_pricing_key().as_slice(), GC_KEY)
1233}
1234
1235/// Derive the multi-gas constraints vector sub-storage key.
1236fn mgc_vector_key() -> B256 {
1237    derive_subspace_key(l2_pricing_key().as_slice(), MGC_KEY)
1238}
1239
1240/// SubStorageVector length slot (offset 0 from vector base key).
1241fn vector_length_slot(vector_key: B256) -> U256 {
1242    map_slot(vector_key.as_slice(), 0)
1243}
1244
1245/// Sub-storage key for element at given index within a vector.
1246fn vector_element_key(vector_key: B256, index: u64) -> B256 {
1247    derive_subspace_key(vector_key.as_slice(), &index.to_be_bytes())
1248}
1249
1250/// Storage slot for a field within a constraint element.
1251fn constraint_field_slot(element_key: B256, field_offset: u64) -> U256 {
1252    map_slot(element_key.as_slice(), field_offset)
1253}
1254
1255/// Read ArbOS version from root state.
1256fn read_arbos_version(input: &mut PrecompileInput<'_>) -> Result<u64, PrecompileError> {
1257    let val = sload_field(input, root_slot(0))?; // VERSION_OFFSET = 0
1258    val.try_into()
1259        .map_err(|_| PrecompileError::other("invalid ArbOS version"))
1260}
1261
1262/// Clear all constraints in a SubStorageVector of gas constraints.
1263fn clear_gas_constraints_vector(
1264    input: &mut PrecompileInput<'_>,
1265    vector_key: B256,
1266    fields_per_element: u64,
1267) -> Result<u64, PrecompileError> {
1268    let len_slot = vector_length_slot(vector_key);
1269    let len: u64 = sload_field(input, len_slot)?.try_into().unwrap_or(0);
1270
1271    // Clear each constraint's fields.
1272    for i in 0..len {
1273        let elem_key = vector_element_key(vector_key, i);
1274        for f in 0..fields_per_element {
1275            sstore_field(input, constraint_field_slot(elem_key, f), U256::ZERO)?;
1276        }
1277    }
1278
1279    // Reset length to 0.
1280    sstore_field(input, len_slot, U256::ZERO)?;
1281
1282    Ok(len)
1283}
1284
1285/// Clear all multi-gas constraints, including resource weights.
1286fn clear_multi_gas_constraints(input: &mut PrecompileInput<'_>) -> Result<u64, PrecompileError> {
1287    let vector_key = mgc_vector_key();
1288    let len_slot = vector_length_slot(vector_key);
1289    let len: u64 = sload_field(input, len_slot)?.try_into().unwrap_or(0);
1290
1291    for i in 0..len {
1292        let elem_key = vector_element_key(vector_key, i);
1293        // Clear target, window, backlog, max_weight.
1294        for f in 0..4 {
1295            sstore_field(input, constraint_field_slot(elem_key, f), U256::ZERO)?;
1296        }
1297        // Clear resource weights.
1298        for r in 0..NUM_RESOURCE_KINDS {
1299            sstore_field(
1300                input,
1301                constraint_field_slot(elem_key, MGC_WEIGHTS_BASE + r),
1302                U256::ZERO,
1303            )?;
1304        }
1305    }
1306
1307    sstore_field(input, len_slot, U256::ZERO)?;
1308    Ok(len)
1309}
1310
1311// ── SetGasPricingConstraints ─────────────────────────────────────────
1312
1313/// ABI: `setGasPricingConstraints(uint64[3][])`
1314///
1315/// Clears existing constraints, validates count for certain ArbOS versions,
1316/// then adds each constraint (target, adjustment_window, starting_backlog).
1317fn handle_set_gas_pricing_constraints(input: &mut PrecompileInput<'_>) -> PrecompileResult {
1318    let data = input.data;
1319    // Minimum: selector(4) + offset(32) + length(32) = 68 bytes
1320    if data.len() < 68 {
1321        return Err(PrecompileError::other("input too short"));
1322    }
1323    let gas_limit = input.gas;
1324
1325    // Parse array length.
1326    let count: u64 = U256::from_be_slice(&data[36..68])
1327        .try_into()
1328        .map_err(|_| PrecompileError::other("array length overflow"))?;
1329
1330    // Each element is 3 × 32 bytes = 96 bytes.
1331    let expected_len = 68 + (count as usize) * 96;
1332    if data.len() < expected_len {
1333        return Err(PrecompileError::other("input too short for constraints"));
1334    }
1335
1336    // Clear existing constraints.
1337    let vector_key = gc_vector_key();
1338    clear_gas_constraints_vector(input, vector_key, 3)?;
1339
1340    // Version check for max constraint count.
1341    let arbos_version = read_arbos_version(input)?;
1342    use arb_chainspec::arbos_version as arb_ver;
1343    if (arb_ver::ARBOS_VERSION_MULTI_CONSTRAINT_FIX..arb_ver::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS)
1344        .contains(&arbos_version)
1345        && (count as usize) > GAS_CONSTRAINTS_MAX_NUM
1346    {
1347        return Err(PrecompileError::other("too many constraints"));
1348    }
1349
1350    // Add each constraint.
1351    let len_slot = vector_length_slot(vector_key);
1352    for i in 0..count {
1353        let base = 68 + (i as usize) * 96;
1354        let target: u64 = U256::from_be_slice(&data[base..base + 32])
1355            .try_into()
1356            .unwrap_or(0);
1357        let window: u64 = U256::from_be_slice(&data[base + 32..base + 64])
1358            .try_into()
1359            .unwrap_or(0);
1360        let backlog: u64 = U256::from_be_slice(&data[base + 64..base + 96])
1361            .try_into()
1362            .unwrap_or(0);
1363
1364        if target == 0 || window == 0 {
1365            return Err(PrecompileError::other("invalid constraint parameters"));
1366        }
1367
1368        // Write constraint fields.
1369        let elem_key = vector_element_key(vector_key, i);
1370        sstore_field(
1371            input,
1372            constraint_field_slot(elem_key, CONSTRAINT_TARGET),
1373            U256::from(target),
1374        )?;
1375        sstore_field(
1376            input,
1377            constraint_field_slot(elem_key, CONSTRAINT_WINDOW),
1378            U256::from(window),
1379        )?;
1380        sstore_field(
1381            input,
1382            constraint_field_slot(elem_key, CONSTRAINT_BACKLOG),
1383            U256::from(backlog),
1384        )?;
1385
1386        // Increment vector length.
1387        sstore_field(input, len_slot, U256::from(i + 1))?;
1388    }
1389
1390    let gas_used = (count * 4 + 2) * SSTORE_GAS + count * SLOAD_GAS + COPY_GAS;
1391    Ok(PrecompileOutput::new(
1392        gas_used.min(gas_limit),
1393        Vec::new().into(),
1394    ))
1395}
1396
1397// ── SetMultiGasPricingConstraints ────────────────────────────────────
1398
1399/// ABI: `setMultiGasPricingConstraints((uint64,uint64,uint64,(uint8,uint64)[])[])`
1400///
1401/// Clears existing multi-gas constraints, then adds each constraint
1402/// with per-resource-kind weights. Validates pricing exponents after each add.
1403fn handle_set_multi_gas_pricing_constraints(input: &mut PrecompileInput<'_>) -> PrecompileResult {
1404    let data = input.data;
1405    if data.len() < 68 {
1406        return Err(PrecompileError::other("input too short"));
1407    }
1408    let gas_limit = input.gas;
1409
1410    // Clear existing multi-gas constraints.
1411    clear_multi_gas_constraints(input)?;
1412
1413    // The outer array offset.
1414    let _outer_offset: usize = U256::from_be_slice(&data[4..36])
1415        .try_into()
1416        .unwrap_or(0usize);
1417    // Array length.
1418    let count: u64 = U256::from_be_slice(&data[36..68])
1419        .try_into()
1420        .map_err(|_| PrecompileError::other("array length overflow"))?;
1421
1422    // Parse each constraint. ABI encodes dynamic structs with offsets.
1423    let array_data_start = 68; // after selector + offset + length
1424
1425    // Read offsets to each struct.
1426    let mut struct_offsets = Vec::with_capacity(count as usize);
1427    for i in 0..count as usize {
1428        let offset_pos = array_data_start + i * 32;
1429        if data.len() < offset_pos + 32 {
1430            return Err(PrecompileError::other("input too short for struct offsets"));
1431        }
1432        let offset: usize = U256::from_be_slice(&data[offset_pos..offset_pos + 32])
1433            .try_into()
1434            .unwrap_or(0);
1435        // Offset is relative to array data start.
1436        struct_offsets.push(array_data_start + offset);
1437    }
1438
1439    let vector_key = mgc_vector_key();
1440    let len_slot = vector_length_slot(vector_key);
1441
1442    for (i, &struct_start) in struct_offsets.iter().enumerate() {
1443        // Each struct: target(32) + window(32) + backlog(32) + resources_offset(32) = 128 min
1444        if data.len() < struct_start + 128 {
1445            return Err(PrecompileError::other(
1446                "input too short for constraint struct",
1447            ));
1448        }
1449
1450        let target: u64 = U256::from_be_slice(&data[struct_start..struct_start + 32])
1451            .try_into()
1452            .unwrap_or(0);
1453        let window: u64 = U256::from_be_slice(&data[struct_start + 32..struct_start + 64])
1454            .try_into()
1455            .unwrap_or(0);
1456        let backlog: u64 = U256::from_be_slice(&data[struct_start + 64..struct_start + 96])
1457            .try_into()
1458            .unwrap_or(0);
1459
1460        if target == 0 || window == 0 {
1461            return Err(PrecompileError::other("invalid constraint parameters"));
1462        }
1463
1464        // Parse resources offset (relative to struct start).
1465        let resources_offset: usize =
1466            U256::from_be_slice(&data[struct_start + 96..struct_start + 128])
1467                .try_into()
1468                .unwrap_or(0);
1469        let resources_start = struct_start + resources_offset;
1470
1471        if data.len() < resources_start + 32 {
1472            return Err(PrecompileError::other(
1473                "input too short for resources array",
1474            ));
1475        }
1476
1477        let num_resources: usize =
1478            U256::from_be_slice(&data[resources_start..resources_start + 32])
1479                .try_into()
1480                .unwrap_or(0);
1481
1482        // Parse resource weights.
1483        let mut weights = [0u64; 8];
1484        let mut max_weight = 0u64;
1485        for r in 0..num_resources {
1486            let r_start = resources_start + 32 + r * 64;
1487            if data.len() < r_start + 64 {
1488                return Err(PrecompileError::other(
1489                    "input too short for resource weight",
1490                ));
1491            }
1492            let resource: u8 = U256::from_be_slice(&data[r_start..r_start + 32])
1493                .try_into()
1494                .unwrap_or(0);
1495            let weight: u64 = U256::from_be_slice(&data[r_start + 32..r_start + 64])
1496                .try_into()
1497                .unwrap_or(0);
1498
1499            if (resource as u64) < NUM_RESOURCE_KINDS {
1500                weights[resource as usize] = weight;
1501                if weight > max_weight {
1502                    max_weight = weight;
1503                }
1504            }
1505        }
1506
1507        // Write constraint to storage.
1508        let elem_key = vector_element_key(vector_key, i as u64);
1509        sstore_field(
1510            input,
1511            constraint_field_slot(elem_key, CONSTRAINT_TARGET),
1512            U256::from(target),
1513        )?;
1514        sstore_field(
1515            input,
1516            constraint_field_slot(elem_key, CONSTRAINT_WINDOW),
1517            U256::from(window),
1518        )?;
1519        sstore_field(
1520            input,
1521            constraint_field_slot(elem_key, CONSTRAINT_BACKLOG),
1522            U256::from(backlog),
1523        )?;
1524        sstore_field(
1525            input,
1526            constraint_field_slot(elem_key, MGC_MAX_WEIGHT),
1527            U256::from(max_weight),
1528        )?;
1529
1530        // Write resource weights.
1531        for (r, &weight) in weights.iter().enumerate().take(NUM_RESOURCE_KINDS as usize) {
1532            sstore_field(
1533                input,
1534                constraint_field_slot(elem_key, MGC_WEIGHTS_BASE + r as u64),
1535                U256::from(weight),
1536            )?;
1537        }
1538
1539        // Increment vector length.
1540        sstore_field(input, len_slot, U256::from(i as u64 + 1))?;
1541
1542        // Validate exponents after each constraint.
1543        validate_multi_gas_exponents(input, vector_key, i as u64 + 1)?;
1544    }
1545
1546    let gas_used = (count * 16 + 2) * SSTORE_GAS + (count * 12 + 2) * SLOAD_GAS + COPY_GAS;
1547    Ok(PrecompileOutput::new(
1548        gas_used.min(gas_limit),
1549        Vec::new().into(),
1550    ))
1551}
1552
1553/// Validate that no pricing exponent exceeds the maximum.
1554fn validate_multi_gas_exponents(
1555    input: &mut PrecompileInput<'_>,
1556    vector_key: B256,
1557    count: u64,
1558) -> Result<(), PrecompileError> {
1559    let mut exponents = [0u64; 8];
1560
1561    for i in 0..count {
1562        let elem_key = vector_element_key(vector_key, i);
1563        let target: u64 = sload_field(input, constraint_field_slot(elem_key, CONSTRAINT_TARGET))?
1564            .try_into()
1565            .unwrap_or(0);
1566        let backlog: u64 = sload_field(input, constraint_field_slot(elem_key, CONSTRAINT_BACKLOG))?
1567            .try_into()
1568            .unwrap_or(0);
1569
1570        if backlog == 0 {
1571            continue;
1572        }
1573
1574        let window: u64 = sload_field(input, constraint_field_slot(elem_key, CONSTRAINT_WINDOW))?
1575            .try_into()
1576            .unwrap_or(0);
1577        let max_weight: u64 = sload_field(input, constraint_field_slot(elem_key, MGC_MAX_WEIGHT))?
1578            .try_into()
1579            .unwrap_or(0);
1580
1581        if max_weight == 0 || target == 0 || window == 0 {
1582            continue;
1583        }
1584
1585        // divisor = window * target * max_weight (in bips).
1586        let divisor = (window as u128)
1587            .saturating_mul(target as u128)
1588            .saturating_mul(max_weight as u128);
1589        let divisor_bips = divisor.saturating_mul(10000);
1590
1591        for (r, exponent) in exponents
1592            .iter_mut()
1593            .enumerate()
1594            .take(NUM_RESOURCE_KINDS as usize)
1595        {
1596            let weight: u64 = sload_field(
1597                input,
1598                constraint_field_slot(elem_key, MGC_WEIGHTS_BASE + r as u64),
1599            )?
1600            .try_into()
1601            .unwrap_or(0);
1602
1603            if weight == 0 {
1604                continue;
1605            }
1606
1607            let dividend = (backlog as u128).saturating_mul(weight as u128) * 10000;
1608            let exp = if divisor_bips > 0 {
1609                (dividend / divisor_bips) as u64
1610            } else {
1611                0
1612            };
1613            *exponent = exponent.saturating_add(exp);
1614        }
1615    }
1616
1617    for &exp in &exponents {
1618        if exp > MAX_PRICING_EXPONENT_BIPS {
1619            return Err(PrecompileError::other("pricing exponent exceeds maximum"));
1620        }
1621    }
1622
1623    Ok(())
1624}
1625
1626// ── SetChainConfig ───────────────────────────────────────────────────
1627
1628/// ABI: `setChainConfig(bytes)`
1629///
1630/// Stores the serialized chain config in StorageBackedBytes format.
1631fn handle_set_chain_config(input: &mut PrecompileInput<'_>) -> PrecompileResult {
1632    let data = input.data;
1633    if data.len() < 68 {
1634        return Err(PrecompileError::other("input too short"));
1635    }
1636    let gas_limit = input.gas;
1637
1638    // ABI: offset(32) + length(32) + data.
1639    let bytes_len: usize = U256::from_be_slice(&data[36..68])
1640        .try_into()
1641        .map_err(|_| PrecompileError::other("bytes length overflow"))?;
1642
1643    if data.len() < 68 + bytes_len {
1644        return Err(PrecompileError::other(
1645            "input too short for chain config bytes",
1646        ));
1647    }
1648    let config_bytes = &data[68..68 + bytes_len];
1649
1650    // Chain config sub-storage key.
1651    let cc_key = derive_subspace_key(ROOT_STORAGE_KEY, CHAIN_CONFIG_SUBSPACE);
1652
1653    // Clear existing bytes (StorageBackedBytes: slot 0 = length, slots 1..N = data).
1654    let old_len: u64 = sload_field(input, map_slot(cc_key.as_slice(), 0))?
1655        .try_into()
1656        .unwrap_or(0);
1657    let old_slots = old_len.div_ceil(32);
1658    for s in 1..=old_slots {
1659        sstore_field(input, map_slot(cc_key.as_slice(), s), U256::ZERO)?;
1660    }
1661
1662    // Write new length.
1663    sstore_field(
1664        input,
1665        map_slot(cc_key.as_slice(), 0),
1666        U256::from(bytes_len as u64),
1667    )?;
1668
1669    // Write data in 32-byte chunks.
1670    let mut remaining = config_bytes;
1671    let mut offset = 1u64;
1672    while remaining.len() >= 32 {
1673        let mut slot = [0u8; 32];
1674        slot.copy_from_slice(&remaining[..32]);
1675        sstore_field(
1676            input,
1677            map_slot(cc_key.as_slice(), offset),
1678            U256::from_be_bytes(slot),
1679        )?;
1680        remaining = &remaining[32..];
1681        offset += 1;
1682    }
1683    if !remaining.is_empty() {
1684        let mut slot = [0u8; 32];
1685        slot[..remaining.len()].copy_from_slice(remaining);
1686        sstore_field(
1687            input,
1688            map_slot(cc_key.as_slice(), offset),
1689            U256::from_be_bytes(slot),
1690        )?;
1691    }
1692
1693    let new_slots = (bytes_len as u64).div_ceil(32);
1694    let total_stores = old_slots + 1 + new_slots; // clear + length + data
1695    let gas_used = total_stores * SSTORE_GAS + SLOAD_GAS + COPY_GAS;
1696    Ok(PrecompileOutput::new(
1697        gas_used.min(gas_limit),
1698        Vec::new().into(),
1699    ))
1700}
1701
1702/// Set the calldata price increase feature flag.
1703/// Reads a bool from calldata and sets bit 0 of the features bitmask.
1704fn handle_set_calldata_price_increase(input: &mut PrecompileInput<'_>) -> PrecompileResult {
1705    let data = input.data;
1706    if data.len() < 36 {
1707        return Err(PrecompileError::other("input too short"));
1708    }
1709    let gas_limit = input.gas;
1710    let enabled = U256::from_be_slice(&data[4..36]) != U256::ZERO;
1711
1712    load_arbos(input)?;
1713
1714    let features_key = derive_subspace_key(ROOT_STORAGE_KEY, FEATURES_SUBSPACE);
1715    let features_slot = map_slot(features_key.as_slice(), 0);
1716    let current = sload_field(input, features_slot)?;
1717
1718    let updated = if enabled {
1719        current | U256::from(1)
1720    } else {
1721        current & !(U256::from(1))
1722    };
1723    sstore_field(input, features_slot, updated)?;
1724
1725    let gas_used = SLOAD_GAS + SSTORE_GAS + COPY_GAS;
1726    Ok(PrecompileOutput::new(
1727        gas_used.min(gas_limit),
1728        Vec::new().into(),
1729    ))
1730}