arb_precompiles/
arbgasinfo.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{Address, U256};
3use alloy_sol_types::SolInterface;
4use revm::{
5    context_interface::block::Block,
6    precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult},
7};
8
9use crate::{
10    interfaces::IArbGasInfo,
11    storage_slot::{
12        derive_subspace_key, gas_constraints_vec_key, map_slot, multi_gas_base_fees_subspace,
13        multi_gas_constraints_vec_key, subspace_slot, vector_element_field, vector_element_key,
14        vector_length_slot, ARBOS_STATE_ADDRESS, L1_PRICING_SUBSPACE, L2_PRICING_SUBSPACE,
15        ROOT_STORAGE_KEY,
16    },
17};
18
19/// ArbGasInfo precompile address (0x6c).
20pub const ARBGASINFO_ADDRESS: Address = Address::new([
21    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
22    0x00, 0x00, 0x00, 0x6c,
23]);
24
25const SLOAD_GAS: u64 = 800;
26const COPY_GAS: u64 = 3;
27
28// L1 pricing field offsets (within L1 pricing subspace).
29const L1_PAY_REWARDS_TO: u64 = 0;
30const L1_INERTIA: u64 = 2;
31const L1_PER_UNIT_REWARD: u64 = 3;
32const L1_PRICE_PER_UNIT: u64 = 7;
33const L1_LAST_SURPLUS: u64 = 8;
34const L1_PER_BATCH_GAS_COST: u64 = 9;
35const L1_AMORTIZED_COST_CAP_BIPS: u64 = 10;
36const L1_EQUILIBRATION_UNITS: u64 = 1;
37const L1_LAST_UPDATE_TIME: u64 = 4;
38const L1_FUNDS_DUE_FOR_REWARDS: u64 = 5;
39const L1_UNITS_SINCE: u64 = 6;
40const L1_FEES_AVAILABLE: u64 = 11;
41
42// L2 pricing field offsets (within L2 pricing subspace).
43const L2_SPEED_LIMIT: u64 = 0;
44const L2_PER_BLOCK_GAS_LIMIT: u64 = 1;
45const L2_BASE_FEE: u64 = 2;
46const L2_MIN_BASE_FEE: u64 = 3;
47const L2_GAS_BACKLOG: u64 = 4;
48const L2_PRICING_INERTIA: u64 = 5;
49const L2_BACKLOG_TOLERANCE: u64 = 6;
50const L2_PER_TX_GAS_LIMIT: u64 = 7;
51
52const TX_DATA_NON_ZERO_GAS: u64 = 16;
53const ASSUMED_SIMPLE_TX_SIZE: u64 = 140;
54const STORAGE_WRITE_COST: u64 = 20_000;
55
56/// Batch poster table subspace key within L1 pricing.
57const BATCH_POSTER_TABLE_KEY: &[u8] = &[0];
58/// TotalFundsDue offset within batch poster table subspace.
59const TOTAL_FUNDS_DUE_OFFSET: u64 = 0;
60
61/// L1 pricer funds pool address (0xa4b05...fffffffffffffffffffffffffffffffffff).
62const L1_PRICER_FUNDS_POOL_ADDRESS: Address = Address::new([
63    0xa4, 0xb0, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
64    0xff, 0xff, 0xff, 0xff,
65]);
66
67pub fn create_arbgasinfo_precompile() -> DynPrecompile {
68    DynPrecompile::new_stateful(PrecompileId::custom("arbgasinfo"), handler)
69}
70
71fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
72    let gas_limit = input.gas;
73    crate::init_precompile_gas(input.data.len());
74
75    let call = match IArbGasInfo::ArbGasInfoCalls::abi_decode(input.data) {
76        Ok(c) => c,
77        Err(_) => return crate::burn_all_revert(gas_limit),
78    };
79
80    use IArbGasInfo::ArbGasInfoCalls as Calls;
81    let result = match call {
82        Calls::getL1BaseFeeEstimate(_) | Calls::getL1GasPriceEstimate(_) => {
83            read_l1_field(&mut input, L1_PRICE_PER_UNIT)
84        }
85        Calls::getMinimumGasPrice(_) => read_l2_field(&mut input, L2_MIN_BASE_FEE),
86        Calls::getPricesInWei(_) | Calls::getPricesInWeiWithAggregator(_) => {
87            handle_prices_in_wei(&mut input)
88        }
89        Calls::getGasAccountingParams(_) => handle_gas_accounting_params(&mut input),
90        Calls::getCurrentTxL1GasFees(_) => {
91            let fee = U256::from(crate::get_current_tx_poster_fee());
92            Ok(PrecompileOutput::new(
93                (SLOAD_GAS + COPY_GAS).min(gas_limit),
94                fee.to_be_bytes::<32>().to_vec().into(),
95            ))
96        }
97        Calls::getPricesInArbGas(_) | Calls::getPricesInArbGasWithAggregator(_) => {
98            handle_prices_in_arbgas(&mut input)
99        }
100        Calls::getL1BaseFeeEstimateInertia(_) => read_l1_field(&mut input, L1_INERTIA),
101        Calls::getGasBacklog(_) => read_l2_field(&mut input, L2_GAS_BACKLOG),
102        Calls::getPricingInertia(_) => read_l2_field(&mut input, L2_PRICING_INERTIA),
103        Calls::getGasBacklogTolerance(_) => read_l2_field(&mut input, L2_BACKLOG_TOLERANCE),
104        Calls::getL1PricingSurplus(_) => handle_l1_pricing_surplus(&mut input),
105        Calls::getPerBatchGasCharge(_) => read_l1_field(&mut input, L1_PER_BATCH_GAS_COST),
106        Calls::getAmortizedCostCapBips(_) => read_l1_field(&mut input, L1_AMORTIZED_COST_CAP_BIPS),
107        Calls::getL1FeesAvailable(_) => {
108            if let Some(r) = crate::check_method_version(gas_limit, 10, 0) {
109                return r;
110            }
111            read_l1_field(&mut input, L1_FEES_AVAILABLE)
112        }
113        Calls::getL1RewardRate(_) => {
114            if let Some(r) = crate::check_method_version(gas_limit, 11, 0) {
115                return r;
116            }
117            read_l1_field(&mut input, L1_PER_UNIT_REWARD)
118        }
119        Calls::getL1RewardRecipient(_) => {
120            if let Some(r) = crate::check_method_version(gas_limit, 11, 0) {
121                return r;
122            }
123            read_l1_field(&mut input, L1_PAY_REWARDS_TO)
124        }
125        Calls::getL1PricingEquilibrationUnits(_) => {
126            if let Some(r) = crate::check_method_version(gas_limit, 20, 0) {
127                return r;
128            }
129            read_l1_field(&mut input, L1_EQUILIBRATION_UNITS)
130        }
131        Calls::getLastL1PricingUpdateTime(_) => {
132            if let Some(r) = crate::check_method_version(gas_limit, 20, 0) {
133                return r;
134            }
135            read_l1_field(&mut input, L1_LAST_UPDATE_TIME)
136        }
137        Calls::getL1PricingFundsDueForRewards(_) => {
138            if let Some(r) = crate::check_method_version(gas_limit, 20, 0) {
139                return r;
140            }
141            read_l1_field(&mut input, L1_FUNDS_DUE_FOR_REWARDS)
142        }
143        Calls::getL1PricingUnitsSinceUpdate(_) => {
144            if let Some(r) = crate::check_method_version(gas_limit, 20, 0) {
145                return r;
146            }
147            read_l1_field(&mut input, L1_UNITS_SINCE)
148        }
149        Calls::getLastL1PricingSurplus(_) => {
150            if let Some(r) = crate::check_method_version(gas_limit, 20, 0) {
151                return r;
152            }
153            read_l1_field(&mut input, L1_LAST_SURPLUS)
154        }
155        Calls::getMaxBlockGasLimit(_) => {
156            if let Some(r) = crate::check_method_version(gas_limit, 50, 0) {
157                return r;
158            }
159            read_l2_field(&mut input, L2_PER_BLOCK_GAS_LIMIT)
160        }
161        Calls::getMaxTxGasLimit(_) => {
162            if let Some(r) = crate::check_method_version(gas_limit, 50, 0) {
163                return r;
164            }
165            read_l2_field(&mut input, L2_PER_TX_GAS_LIMIT)
166        }
167        Calls::getGasPricingConstraints(_) => {
168            if let Some(r) = crate::check_method_version(gas_limit, 50, 0) {
169                return r;
170            }
171            handle_gas_pricing_constraints(&mut input)
172        }
173        Calls::getMultiGasPricingConstraints(_) => {
174            if let Some(r) = crate::check_method_version(gas_limit, 60, 0) {
175                return r;
176            }
177            handle_multi_gas_pricing_constraints(&mut input)
178        }
179        Calls::getMultiGasBaseFee(_) => {
180            if let Some(r) = crate::check_method_version(gas_limit, 60, 0) {
181                return r;
182            }
183            handle_multi_gas_base_fee(&mut input)
184        }
185    };
186    crate::gas_check(gas_limit, result)
187}
188
189// ── helpers ──────────────────────────────────────────────────────────
190
191fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
192    input
193        .internals_mut()
194        .load_account(ARBOS_STATE_ADDRESS)
195        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
196    Ok(())
197}
198
199fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
200    let val = input
201        .internals_mut()
202        .sload(ARBOS_STATE_ADDRESS, slot)
203        .map_err(|_| PrecompileError::other("sload failed"))?;
204    crate::charge_precompile_gas(SLOAD_GAS);
205    Ok(val.data)
206}
207
208fn read_l1_field(input: &mut PrecompileInput<'_>, offset: u64) -> PrecompileResult {
209    let gas_limit = input.gas;
210    load_arbos(input)?;
211    let field_slot = subspace_slot(L1_PRICING_SUBSPACE, offset);
212    let value = sload_field(input, field_slot)?;
213    Ok(PrecompileOutput::new(
214        (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
215        value.to_be_bytes::<32>().to_vec().into(),
216    ))
217}
218
219fn read_l2_field(input: &mut PrecompileInput<'_>, offset: u64) -> PrecompileResult {
220    let gas_limit = input.gas;
221    load_arbos(input)?;
222    let field_slot = subspace_slot(L2_PRICING_SUBSPACE, offset);
223    let value = sload_field(input, field_slot)?;
224    Ok(PrecompileOutput::new(
225        (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
226        value.to_be_bytes::<32>().to_vec().into(),
227    ))
228}
229
230/// Compute L1 pricing surplus.
231/// v10+: `L1FeesAvailable - (TotalFundsDue + FundsDueForRewards)` (signed).
232/// pre-v10: `Balance(L1PricerFundsPool) - (TotalFundsDue + FundsDueForRewards)`.
233fn handle_l1_pricing_surplus(input: &mut PrecompileInput<'_>) -> PrecompileResult {
234    let gas_limit = input.gas;
235    let arbos_version = crate::get_arbos_version();
236
237    load_arbos(input)?;
238
239    // Read TotalFundsDue from batch poster table subspace.
240    let l1_sub_key = derive_subspace_key(ROOT_STORAGE_KEY, L1_PRICING_SUBSPACE);
241    let bpt_key = derive_subspace_key(l1_sub_key.as_slice(), BATCH_POSTER_TABLE_KEY);
242    let total_funds_due_slot = map_slot(bpt_key.as_slice(), TOTAL_FUNDS_DUE_OFFSET);
243    let total_funds_due = sload_field(input, total_funds_due_slot)?;
244
245    // Read FundsDueForRewards from L1 pricing subspace.
246    let fdr_slot = subspace_slot(L1_PRICING_SUBSPACE, L1_FUNDS_DUE_FOR_REWARDS);
247    let funds_due_for_rewards = sload_field(input, fdr_slot)?;
248
249    let need_funds = total_funds_due.saturating_add(funds_due_for_rewards);
250
251    let have_funds = if arbos_version >= 10 {
252        // v10+: read from stored L1FeesAvailable.
253        let slot = subspace_slot(L1_PRICING_SUBSPACE, L1_FEES_AVAILABLE);
254        sload_field(input, slot)?
255    } else {
256        // pre-v10: read actual balance of L1PricerFundsPool.
257        let account = input
258            .internals_mut()
259            .load_account(L1_PRICER_FUNDS_POOL_ADDRESS)
260            .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
261        account.data.info.balance
262    };
263
264    // Signed result: surplus can be negative.
265    let surplus = if have_funds >= need_funds {
266        have_funds - need_funds
267    } else {
268        // Two's complement encoding for negative value.
269        let deficit = need_funds - have_funds;
270        U256::ZERO.wrapping_sub(deficit)
271    };
272
273    let gas_cost = (4 * SLOAD_GAS + COPY_GAS).min(gas_limit);
274    Ok(PrecompileOutput::new(
275        gas_cost,
276        surplus.to_be_bytes::<32>().to_vec().into(),
277    ))
278}
279
280fn handle_prices_in_wei(input: &mut PrecompileInput<'_>) -> PrecompileResult {
281    let data_len = input.data.len();
282    let gas_limit = input.gas;
283
284    // Reth zeros BlockEnv basefee for eth_call without a gas price;
285    // fall back to the L2PricingState slot (written at StartBlock) so
286    // eth_call returns the current block's basefee.
287    let block_basefee = U256::from(input.internals().block_env().basefee());
288    load_arbos(input)?;
289
290    let l1_price = sload_field(input, subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT))?;
291    let l2_min = sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_MIN_BASE_FEE))?;
292    let l2_gas_price = if block_basefee.is_zero() {
293        sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_BASE_FEE))?
294    } else {
295        block_basefee
296    };
297
298    let wei_for_l1_calldata = l1_price.saturating_mul(U256::from(TX_DATA_NON_ZERO_GAS));
299    let per_l2_tx = wei_for_l1_calldata.saturating_mul(U256::from(ASSUMED_SIMPLE_TX_SIZE));
300    let per_arbgas_base = l2_gas_price.min(l2_min);
301    let per_arbgas_congestion = l2_gas_price.saturating_sub(per_arbgas_base);
302    let per_arbgas_total = l2_gas_price;
303    let wei_for_l2_storage = l2_gas_price.saturating_mul(U256::from(STORAGE_WRITE_COST));
304
305    let mut out = Vec::with_capacity(192);
306    out.extend_from_slice(&per_l2_tx.to_be_bytes::<32>());
307    out.extend_from_slice(&wei_for_l1_calldata.to_be_bytes::<32>());
308    out.extend_from_slice(&wei_for_l2_storage.to_be_bytes::<32>());
309    out.extend_from_slice(&per_arbgas_base.to_be_bytes::<32>());
310    out.extend_from_slice(&per_arbgas_congestion.to_be_bytes::<32>());
311    out.extend_from_slice(&per_arbgas_total.to_be_bytes::<32>());
312
313    // OpenArbosState SLOAD + 2 body SLOADs (L1_PRICE_PER_UNIT, L2_MIN_BASE_FEE)
314    // + copy gas for args and 6-word return tuple. Total 2418 gas.
315    let arg_words = (data_len as u64).saturating_sub(4).div_ceil(32);
316    let gas_cost = (3 * SLOAD_GAS + (arg_words + 6) * COPY_GAS).min(gas_limit);
317    Ok(PrecompileOutput::new(gas_cost, out.into()))
318}
319
320fn handle_gas_accounting_params(input: &mut PrecompileInput<'_>) -> PrecompileResult {
321    let gas_limit = input.gas;
322    load_arbos(input)?;
323
324    let speed_limit = sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_SPEED_LIMIT))?;
325    let gas_limit_val = sload_field(
326        input,
327        subspace_slot(L2_PRICING_SUBSPACE, L2_PER_BLOCK_GAS_LIMIT),
328    )?;
329
330    let mut out = Vec::with_capacity(96);
331    out.extend_from_slice(&speed_limit.to_be_bytes::<32>());
332    out.extend_from_slice(&gas_limit_val.to_be_bytes::<32>());
333    out.extend_from_slice(&gas_limit_val.to_be_bytes::<32>());
334
335    Ok(PrecompileOutput::new(
336        (3 * SLOAD_GAS + 3 * COPY_GAS).min(gas_limit),
337        out.into(),
338    ))
339}
340
341fn handle_prices_in_arbgas(input: &mut PrecompileInput<'_>) -> PrecompileResult {
342    let data_len = input.data.len();
343    let gas_limit = input.gas;
344
345    let block_basefee = U256::from(input.internals().block_env().basefee());
346    load_arbos(input)?;
347
348    let l1_price = sload_field(input, subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT))?;
349    let l2_gas_price = if block_basefee.is_zero() {
350        sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_BASE_FEE))?
351    } else {
352        block_basefee
353    };
354
355    let wei_for_l1_calldata = l1_price.saturating_mul(U256::from(TX_DATA_NON_ZERO_GAS));
356    let wei_per_l2_tx = wei_for_l1_calldata.saturating_mul(U256::from(ASSUMED_SIMPLE_TX_SIZE));
357
358    let (gas_for_l1_calldata, gas_per_l2_tx) = if l2_gas_price > U256::ZERO {
359        (
360            wei_for_l1_calldata / l2_gas_price,
361            wei_per_l2_tx / l2_gas_price,
362        )
363    } else {
364        (U256::ZERO, U256::ZERO)
365    };
366
367    let mut out = Vec::with_capacity(96);
368    out.extend_from_slice(&gas_per_l2_tx.to_be_bytes::<32>());
369    out.extend_from_slice(&gas_for_l1_calldata.to_be_bytes::<32>());
370    out.extend_from_slice(&U256::from(STORAGE_WRITE_COST).to_be_bytes::<32>());
371
372    // OpenArbosState SLOAD + 1 body SLOAD (L1_PRICE_PER_UNIT) + copy gas.
373    // l2GasPrice comes from evm.Context.BaseFee (free).
374    let arg_words = (data_len as u64).saturating_sub(4).div_ceil(32);
375    let gas_cost = (2 * SLOAD_GAS + (arg_words + 3) * COPY_GAS).min(gas_limit);
376    Ok(PrecompileOutput::new(gas_cost, out.into()))
377}
378
379// ── Constraint getters (ArbOS v50+) ─────────────────────────────────
380
381/// Constraint field offsets (matching gas_constraint.rs / multi_gas_constraint.rs).
382const CONSTRAINT_TARGET: u64 = 0;
383const CONSTRAINT_ADJ_WINDOW: u64 = 1;
384const CONSTRAINT_BACKLOG: u64 = 2;
385const MULTI_CONSTRAINT_WEIGHTED_BASE: u64 = 4;
386
387const NUM_RESOURCE_KIND: u64 = 8;
388/// Offset within MultiGasFees for current-block fees.
389const CURRENT_BLOCK_FEES_OFFSET: u64 = NUM_RESOURCE_KIND;
390
391/// Returns `[][3]uint64` — (target, adjustmentWindow, backlog) per constraint.
392fn handle_gas_pricing_constraints(input: &mut PrecompileInput<'_>) -> PrecompileResult {
393    let gas_limit = input.gas;
394    load_arbos(input)?;
395
396    let vec_key = gas_constraints_vec_key();
397    let count = sload_field(input, vector_length_slot(&vec_key))?.saturating_to::<u64>();
398    let mut sloads: u64 = 2; // 1 for OpenArbosState + 1 for vec length
399
400    // ABI: offset to dynamic array, then length, then N×3 uint64 values.
401    let mut out = Vec::with_capacity(64 + count as usize * 96);
402    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
403    out.extend_from_slice(&U256::from(count).to_be_bytes::<32>());
404
405    for i in 0..count {
406        let target = sload_field(input, vector_element_field(&vec_key, i, CONSTRAINT_TARGET))?;
407        let window = sload_field(
408            input,
409            vector_element_field(&vec_key, i, CONSTRAINT_ADJ_WINDOW),
410        )?;
411        let backlog = sload_field(input, vector_element_field(&vec_key, i, CONSTRAINT_BACKLOG))?;
412
413        out.extend_from_slice(&target.to_be_bytes::<32>());
414        out.extend_from_slice(&window.to_be_bytes::<32>());
415        out.extend_from_slice(&backlog.to_be_bytes::<32>());
416        sloads += 3;
417    }
418
419    let result_words = (out.len() as u64).div_ceil(32);
420    Ok(PrecompileOutput::new(
421        (sloads * SLOAD_GAS + result_words * COPY_GAS).min(gas_limit),
422        out.into(),
423    ))
424}
425
426/// Returns `[]MultiGasConstraint` ABI-encoded.
427///
428/// MultiGasConstraint = (WeightedResource[] resources, uint32 adjustmentWindowSecs,
429///                        uint64 targetPerSec, uint64 backlog)
430/// WeightedResource   = (uint8 resource, uint64 weight)
431fn handle_multi_gas_pricing_constraints(input: &mut PrecompileInput<'_>) -> PrecompileResult {
432    let gas_limit = input.gas;
433    load_arbos(input)?;
434
435    let vec_key = multi_gas_constraints_vec_key();
436    let count = sload_field(input, vector_length_slot(&vec_key))?.saturating_to::<u64>();
437    let mut sloads: u64 = 2; // 1 for OpenArbosState + 1 for vec length
438
439    // Collect per-constraint data before encoding, since we need to know sizes for offsets.
440    struct ConstraintData {
441        target: U256,
442        window: U256,
443        backlog: U256,
444        resources: Vec<(u8, U256)>,
445    }
446    let mut constraints = Vec::with_capacity(count as usize);
447
448    for i in 0..count {
449        let target = sload_field(input, vector_element_field(&vec_key, i, CONSTRAINT_TARGET))?;
450        let window = sload_field(
451            input,
452            vector_element_field(&vec_key, i, CONSTRAINT_ADJ_WINDOW),
453        )?;
454        let backlog = sload_field(input, vector_element_field(&vec_key, i, CONSTRAINT_BACKLOG))?;
455        sloads += 3;
456
457        let elem_key = vector_element_key(&vec_key, i);
458        let mut resources = Vec::new();
459        for kind in 0..NUM_RESOURCE_KIND {
460            let w = sload_field(
461                input,
462                map_slot(elem_key.as_slice(), MULTI_CONSTRAINT_WEIGHTED_BASE + kind),
463            )?;
464            sloads += 1;
465            if w > U256::ZERO {
466                resources.push((kind as u8, w));
467            }
468        }
469        constraints.push(ConstraintData {
470            target,
471            window,
472            backlog,
473            resources,
474        });
475    }
476
477    // ABI-encode: dynamic array of dynamic tuples.
478    let n = constraints.len();
479    let mut out = Vec::new();
480
481    // Top-level: offset to outer array.
482    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
483    // Array length.
484    out.extend_from_slice(&U256::from(n).to_be_bytes::<32>());
485
486    // Element tuple size = 4 × 32 (head) + 32 (resources length) + resources.len() × 64.
487    let elem_sizes: Vec<usize> = constraints
488        .iter()
489        .map(|c| 4 * 32 + 32 + c.resources.len() * 64)
490        .collect();
491
492    // Write offsets (relative to start of offsets area).
493    let mut running_offset = n * 32;
494    for size in &elem_sizes {
495        out.extend_from_slice(&U256::from(running_offset).to_be_bytes::<32>());
496        running_offset += size;
497    }
498
499    // Write each element.
500    for c in &constraints {
501        let m = c.resources.len();
502        // Tuple head: offset to Resources data = 4 × 32 = 128.
503        out.extend_from_slice(&U256::from(4u64 * 32).to_be_bytes::<32>());
504        // AdjustmentWindowSecs (uint32).
505        out.extend_from_slice(&c.window.to_be_bytes::<32>());
506        // TargetPerSec (uint64).
507        out.extend_from_slice(&c.target.to_be_bytes::<32>());
508        // Backlog (uint64).
509        out.extend_from_slice(&c.backlog.to_be_bytes::<32>());
510        // Resources array length.
511        out.extend_from_slice(&U256::from(m).to_be_bytes::<32>());
512        // Each WeightedResource (uint8 resource, uint64 weight).
513        for &(kind, ref weight) in &c.resources {
514            out.extend_from_slice(&U256::from(kind).to_be_bytes::<32>());
515            out.extend_from_slice(&weight.to_be_bytes::<32>());
516        }
517    }
518
519    let result_words = (out.len() as u64).div_ceil(32);
520    Ok(PrecompileOutput::new(
521        (sloads * SLOAD_GAS + result_words * COPY_GAS).min(gas_limit),
522        out.into(),
523    ))
524}
525
526/// Returns `uint256[]` — current-block base fee per resource kind.
527fn handle_multi_gas_base_fee(input: &mut PrecompileInput<'_>) -> PrecompileResult {
528    let gas_limit = input.gas;
529    load_arbos(input)?;
530
531    let fees_key = multi_gas_base_fees_subspace();
532
533    let mut out = Vec::with_capacity(64 + NUM_RESOURCE_KIND as usize * 32);
534    // ABI: offset, then length, then values.
535    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
536    out.extend_from_slice(&U256::from(NUM_RESOURCE_KIND).to_be_bytes::<32>());
537
538    for kind in 0..NUM_RESOURCE_KIND {
539        let slot = map_slot(fees_key.as_slice(), CURRENT_BLOCK_FEES_OFFSET + kind);
540        let fee = sload_field(input, slot)?;
541        out.extend_from_slice(&fee.to_be_bytes::<32>());
542    }
543
544    let result_words = (out.len() as u64).div_ceil(32);
545    Ok(PrecompileOutput::new(
546        ((1 + NUM_RESOURCE_KIND) * SLOAD_GAS + result_words * COPY_GAS).min(gas_limit),
547        out.into(),
548    ))
549}