arb_precompiles/
arbowner.rs

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