arb_precompiles/
lib.rs

1//! Arbitrum precompile contracts.
2//!
3//! Implements the system contracts at addresses `0x64`+ that provide
4//! on-chain access to ArbOS state, gas pricing, retryable tickets,
5//! Stylus WASM management, and node interface queries.
6
7mod error;
8mod interfaces;
9
10mod arbaddresstable;
11mod arbaggregator;
12mod arbbls;
13mod arbdebug;
14mod arbfilteredtxmanager;
15mod arbfunctiontable;
16mod arbgasinfo;
17mod arbinfo;
18mod arbnativetokenmanager;
19mod arbosacts;
20mod arbostest;
21mod arbowner;
22mod arbownerpublic;
23mod arbretryabletx;
24mod arbstatistics;
25pub mod arbsys;
26mod arbwasm;
27mod arbwasmcache;
28mod nodeinterface;
29mod nodeinterface_debug;
30
31use std::sync::Arc;
32
33use alloy_evm::{
34    EvmInternals,
35    precompiles::{DynPrecompile, PrecompileInput, PrecompilesMap},
36};
37use arb_context::ArbPrecompileCtx;
38pub use arbaddresstable::{ARBADDRESSTABLE_ADDRESS, create_arbaddresstable_precompile};
39pub use arbaggregator::{ARBAGGREGATOR_ADDRESS, create_arbaggregator_precompile};
40pub use arbbls::{ARBBLS_ADDRESS, create_arbbls_precompile};
41pub use arbdebug::{ARBDEBUG_ADDRESS, create_arbdebug_precompile};
42pub use arbfilteredtxmanager::{
43    ARBFILTEREDTXMANAGER_ADDRESS, create_arbfilteredtxmanager_precompile,
44};
45pub use arbfunctiontable::{ARBFUNCTIONTABLE_ADDRESS, create_arbfunctiontable_precompile};
46pub use arbgasinfo::{ARBGASINFO_ADDRESS, create_arbgasinfo_precompile};
47pub use arbinfo::{ARBINFO_ADDRESS, create_arbinfo_precompile};
48pub use arbnativetokenmanager::{
49    ARBNATIVETOKENMANAGER_ADDRESS, create_arbnativetokenmanager_precompile,
50};
51pub use arbosacts::{ARBOSACTS_ADDRESS, create_arbosacts_precompile};
52pub use arbostest::{ARBOSTEST_ADDRESS, create_arbostest_precompile};
53pub use arbowner::{ARBOWNER_ADDRESS, create_arbowner_precompile};
54pub use arbownerpublic::{ARBOWNERPUBLIC_ADDRESS, create_arbownerpublic_precompile};
55pub use arbretryabletx::{
56    ARBRETRYABLETX_ADDRESS, create_arbretryabletx_precompile, redeem_scheduled_topic,
57    ticket_created_topic,
58};
59pub use arbstatistics::{ARBSTATISTICS_ADDRESS, create_arbstatistics_precompile};
60pub use arbsys::{ARBSYS_ADDRESS, create_arbsys_precompile};
61pub use arbwasm::{ARBWASM_ADDRESS, create_arbwasm_precompile};
62pub use arbwasmcache::{ARBWASMCACHE_ADDRESS, create_arbwasmcache_precompile};
63pub use error::ArbPrecompileError;
64pub use nodeinterface::{
65    NODE_INTERFACE_ADDRESS, build_fake_tx_bytes, compute_l1_gas_for_estimate,
66    create_nodeinterface_precompile, decode_estimate_args,
67};
68pub use nodeinterface_debug::{
69    NODE_INTERFACE_DEBUG_ADDRESS, create_nodeinterface_debug_precompile,
70};
71use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
72
73/// RIP-7212 P256VERIFY precompile address (ArbOS v30+).
74pub const P256VERIFY_ADDRESS: alloy_primitives::Address =
75    alloy_primitives::address!("0000000000000000000000000000000000000100");
76
77/// modexp precompile address (0x05).
78const MODEXP_ADDRESS: alloy_primitives::Address =
79    alloy_primitives::address!("0000000000000000000000000000000000000005");
80
81/// BLS12-381 precompile addresses (EIP-2537), enabled from ArbOS v50.
82const BLS12_381_ADDRESSES: [alloy_primitives::Address; 7] = [
83    alloy_primitives::address!("000000000000000000000000000000000000000b"),
84    alloy_primitives::address!("000000000000000000000000000000000000000c"),
85    alloy_primitives::address!("000000000000000000000000000000000000000d"),
86    alloy_primitives::address!("000000000000000000000000000000000000000e"),
87    alloy_primitives::address!("000000000000000000000000000000000000000f"),
88    alloy_primitives::address!("0000000000000000000000000000000000000010"),
89    alloy_primitives::address!("0000000000000000000000000000000000000011"),
90];
91
92fn create_p256verify_precompile() -> DynPrecompile {
93    DynPrecompile::new(PrecompileId::P256Verify, |input: PrecompileInput<'_>| {
94        revm::precompile::secp256r1::p256_verify(input.data, input.gas)
95    })
96}
97
98fn create_p256verify_osaka_precompile() -> DynPrecompile {
99    DynPrecompile::new(PrecompileId::P256Verify, |input: PrecompileInput<'_>| {
100        revm::precompile::secp256r1::p256_verify_osaka(input.data, input.gas)
101    })
102}
103
104fn create_modexp_osaka_precompile() -> DynPrecompile {
105    DynPrecompile::new(PrecompileId::ModExp, |input: PrecompileInput<'_>| {
106        revm::precompile::modexp::osaka_run(input.data, input.gas)
107    })
108}
109
110pub fn charge_precompile_gas(gas_used: &mut u64, gas: u64) {
111    *gas_used = gas_used.saturating_add(gas);
112}
113
114/// Runs `f`, reverting the access-list warming its loads record so a touched
115/// address stays cold for the caller's later EIP-2929 access. For precompile
116/// reads of account code/balance, which must not warm the address.
117pub(crate) fn without_access_list_effect<R>(
118    internals: &mut EvmInternals<'_>,
119    f: impl FnOnce(&mut EvmInternals<'_>) -> R,
120) -> R {
121    let checkpoint = internals.checkpoint();
122    let result = f(internals);
123    internals.checkpoint_revert(checkpoint);
124    result
125}
126
127/// Charge precompile gas for an ArbOS state read, recording it as
128/// `StorageAccessRead` for the v60 multi-dimensional pricing backlog (mirrors
129/// the reference, which dimensions every state `Get` as a storage read). The
130/// single-gas total is unchanged; only the resource breakdown is recorded.
131pub fn charge_storage_read(gas_used: &mut u64, ctx: &arb_context::ArbPrecompileCtx, gas: u64) {
132    charge_precompile_gas(gas_used, gas);
133    ctx.add_precompile_multi_gas(
134        arb_primitives::multigas::ResourceKind::StorageAccessRead,
135        gas,
136    );
137}
138
139/// Charge precompile gas for an ArbOS state write, recording it as
140/// `StorageAccessWrite`. See [`charge_storage_read`].
141pub fn charge_storage_write(gas_used: &mut u64, ctx: &arb_context::ArbPrecompileCtx, gas: u64) {
142    charge_precompile_gas(gas_used, gas);
143    ctx.add_precompile_multi_gas(
144        arb_primitives::multigas::ResourceKind::StorageAccessWrite,
145        gas,
146    );
147}
148
149/// Charge precompile gas for calldata processing (the framework `argsCost` and
150/// any per-call copy fees on caller-supplied data), recording it as
151/// `L2Calldata`. The single-gas total is unchanged; only the resource
152/// breakdown is recorded.
153pub fn charge_l2_calldata(gas_used: &mut u64, ctx: &arb_context::ArbPrecompileCtx, gas: u64) {
154    charge_precompile_gas(gas_used, gas);
155    ctx.add_precompile_multi_gas(arb_primitives::multigas::ResourceKind::L2Calldata, gas);
156}
157
158/// Charge precompile gas for emitting a log, recording it as `HistoryGrowth`.
159pub fn charge_history_growth(gas_used: &mut u64, ctx: &arb_context::ArbPrecompileCtx, gas: u64) {
160    charge_precompile_gas(gas_used, gas);
161    ctx.add_precompile_multi_gas(arb_primitives::multigas::ResourceKind::HistoryGrowth, gas);
162}
163
164/// Charge precompile gas attributed to pure computation: constant per-method
165/// work, the framework `resultCost` for encoding return data, and any other
166/// non-resource-bound costs. Mirrors the reference framework's
167/// `Burn(Computation, ...)` calls.
168pub fn charge_computation(gas_used: &mut u64, ctx: &arb_context::ArbPrecompileCtx, gas: u64) {
169    charge_precompile_gas(gas_used, gas);
170    ctx.add_precompile_multi_gas(arb_primitives::multigas::ResourceKind::Computation, gas);
171}
172
173/// `WarmStorageReadCostEIP2929` — the cost of the warm StylusParams slot read.
174const PARAMS_WARM_READ_GAS: u64 = 100;
175
176/// Charge the warm StylusParams read. The params slot is read frequently and
177/// billed to `Computation`, not as a storage read; centralized so every reader
178/// attributes it to the same resource.
179pub fn charge_params_read(gas_used: &mut u64, ctx: &arb_context::ArbPrecompileCtx) {
180    charge_computation(gas_used, ctx, PARAMS_WARM_READ_GAS);
181}
182
183/// Initialize gas tracking for a precompile call: charge `argsCost` as
184/// `L2Calldata` and the `OpenArbosState` read (1 SLOAD = 800) as
185/// `StorageAccessRead`, mirroring the reference framework's per-call
186/// dimensioned framework gas.
187pub fn init_precompile_gas(
188    gas_used: &mut u64,
189    ctx: &arb_context::ArbPrecompileCtx,
190    input_len: usize,
191) {
192    let args_cost = 3u64 * (input_len as u64).saturating_sub(4).div_ceil(32);
193    charge_l2_calldata(gas_used, ctx, args_cost);
194    charge_storage_read(gas_used, ctx, 800);
195}
196
197/// Initialize gas tracking for a `pure` precompile method: like
198/// [`init_precompile_gas`] but skips the `OpenArbosState` SLOAD, matching the
199/// reference framework's pure-method path which does not open ArbOS state.
200pub fn init_precompile_gas_pure(
201    gas_used: &mut u64,
202    ctx: &arb_context::ArbPrecompileCtx,
203    input_len: usize,
204) {
205    let args_cost = 3u64 * (input_len as u64).saturating_sub(4).div_ceil(32);
206    charge_l2_calldata(gas_used, ctx, args_cost);
207}
208
209fn check_precompile_version(ctx: &ArbPrecompileCtx, min_version: u64) -> Option<PrecompileResult> {
210    if ctx.block.arbos_version < min_version {
211        Some(Ok(PrecompileOutput::new(0, Default::default())))
212    } else {
213        None
214    }
215}
216
217/// Pre-dispatch error: consumes all supplied gas and reverts.
218fn burn_all_revert(gas_limit: u64) -> PrecompileResult {
219    Ok(PrecompileOutput::new_reverted(
220        gas_limit,
221        Default::default(),
222    ))
223}
224
225/// Revert with an ABI-encoded Solidity error, charging the result copy as
226/// computation. Consumes all gas if the result cannot be afforded.
227pub(crate) fn revert_sol_error(
228    gas_used: &mut u64,
229    ctx: &arb_context::ArbPrecompileCtx,
230    payload: Vec<u8>,
231    input_gas: u64,
232) -> PrecompileResult {
233    charge_computation(gas_used, ctx, 3 * (payload.len() as u64).div_ceil(32));
234    if *gas_used > input_gas {
235        return Err(ArbPrecompileError::OutOfGas.into());
236    }
237    Ok(PrecompileOutput::new_reverted(*gas_used, payload.into()))
238}
239
240/// Reject call value sent to a non-payable method, reverting and consuming all
241/// forwarded gas. `payable` lists the selectors that may receive value. Call
242/// only once the precompile is active for the current ArbOS version.
243pub fn reject_nonpayable_value(
244    value: alloy_primitives::U256,
245    data: &[u8],
246    gas_limit: u64,
247    payable: &[[u8; 4]],
248) -> Option<PrecompileResult> {
249    if value.is_zero() {
250        return None;
251    }
252    if payable.contains(&input_selector(data)) {
253        return None;
254    }
255    Some(burn_all_revert(gas_limit))
256}
257
258fn input_selector(data: &[u8]) -> [u8; 4] {
259    data.get(..4)
260        .and_then(|s| s.try_into().ok())
261        .unwrap_or([0u8; 4])
262}
263
264/// Reject a state-modifying method invoked under STATICCALL, reverting and
265/// consuming all forwarded gas. `write` lists the state-modifying selectors.
266pub fn reject_static_write(
267    is_static: bool,
268    data: &[u8],
269    gas_limit: u64,
270    write: &[[u8; 4]],
271) -> Option<PrecompileResult> {
272    if is_static && write.contains(&input_selector(data)) {
273        return Some(burn_all_revert(gas_limit));
274    }
275    None
276}
277
278/// Like [`reject_static_write`] but for a mostly-writing precompile: under
279/// read-only, reject every method except the listed read-only (view/pure)
280/// selectors.
281pub fn reject_static_unless_read(
282    is_static: bool,
283    data: &[u8],
284    gas_limit: u64,
285    reads: &[[u8; 4]],
286) -> Option<PrecompileResult> {
287    if is_static && !reads.contains(&input_selector(data)) {
288        return Some(burn_all_revert(gas_limit));
289    }
290    None
291}
292
293/// Reject a non-`pure` method invoked via DELEGATECALL, reverting and consuming
294/// all forwarded gas. `is_delegate` is true when acting as an address other than
295/// the precompile; `pure` lists the stateless selectors.
296pub fn reject_delegate_nonpure(
297    is_delegate: bool,
298    data: &[u8],
299    gas_limit: u64,
300    pure: &[[u8; 4]],
301) -> Option<PrecompileResult> {
302    if is_delegate && !pure.contains(&input_selector(data)) {
303        return Some(burn_all_revert(gas_limit));
304    }
305    None
306}
307
308/// Emit a pre-encoded Solidity custom-error payload (selector + ABI args)
309/// as a revert. Adds the copy cost for the payload to the accumulated gas,
310/// attributed to `Computation` to mirror the reference framework's
311/// `resultCost` burn.
312pub fn sol_error_revert(
313    gas_used: &mut u64,
314    ctx: &ArbPrecompileCtx,
315    payload: Vec<u8>,
316    gas_limit: u64,
317) -> PrecompileResult {
318    let result_cost = 3u64 * (payload.len() as u64).div_ceil(32); // CopyGas * words
319    charge_computation(gas_used, ctx, result_cost);
320    Ok(PrecompileOutput::new_reverted(
321        (*gas_used).min(gas_limit),
322        payload.into(),
323    ))
324}
325
326fn gas_check(
327    ctx: &ArbPrecompileCtx,
328    gas_limit: u64,
329    gas_used: u64,
330    result: PrecompileResult,
331) -> PrecompileResult {
332    if gas_used > gas_limit {
333        return Err(PrecompileError::OutOfGas);
334    }
335    match result {
336        Err(PrecompileError::Other(_)) if ctx.block.arbos_version >= 11 => Ok(
337            PrecompileOutput::new_reverted(gas_used.min(gas_limit), Default::default()),
338        ),
339        other => other,
340    }
341}
342
343/// Returns a revert that consumes the full `gas_limit` if the current ArbOS
344/// version is outside `[min_version, max_version]`. `max_version == 0` is
345/// unbounded.
346fn check_method_version(
347    ctx: &ArbPrecompileCtx,
348    gas_limit: u64,
349    min_version: u64,
350    max_version: u64,
351) -> Option<PrecompileResult> {
352    let v = ctx.block.arbos_version;
353    if v < min_version || (max_version > 0 && v > max_version) {
354        Some(burn_all_revert(gas_limit))
355    } else {
356        None
357    }
358}
359
360const KZG_POINT_EVALUATION_ADDRESS: alloy_primitives::Address =
361    alloy_primitives::address!("000000000000000000000000000000000000000a");
362
363/// Registers Arbitrum precompiles into `map` and applies the per-ArbOS-version
364/// adjustments to the standard Ethereum precompile set.
365///
366/// `ctx` is captured into every handler closure so that handlers read the
367/// per-block / per-tx context as a typed function parameter rather than via
368/// a thread-local.
369pub fn register_arb_precompiles(map: &mut PrecompilesMap, ctx: Arc<ArbPrecompileCtx>) {
370    let arbos_version = ctx.block.arbos_version;
371    map.extend_precompiles([
372        (ARBSYS_ADDRESS, create_arbsys_precompile(ctx.clone())),
373        (
374            ARBGASINFO_ADDRESS,
375            create_arbgasinfo_precompile(ctx.clone()),
376        ),
377        (ARBINFO_ADDRESS, create_arbinfo_precompile(ctx.clone())),
378        (
379            ARBSTATISTICS_ADDRESS,
380            create_arbstatistics_precompile(ctx.clone()),
381        ),
382        (
383            ARBFUNCTIONTABLE_ADDRESS,
384            create_arbfunctiontable_precompile(ctx.clone()),
385        ),
386        (ARBOSACTS_ADDRESS, create_arbosacts_precompile(ctx.clone())),
387        (ARBOSTEST_ADDRESS, create_arbostest_precompile(ctx.clone())),
388        (
389            ARBOWNERPUBLIC_ADDRESS,
390            create_arbownerpublic_precompile(ctx.clone()),
391        ),
392        (
393            ARBADDRESSTABLE_ADDRESS,
394            create_arbaddresstable_precompile(ctx.clone()),
395        ),
396        (
397            ARBAGGREGATOR_ADDRESS,
398            create_arbaggregator_precompile(ctx.clone()),
399        ),
400        (
401            ARBRETRYABLETX_ADDRESS,
402            create_arbretryabletx_precompile(ctx.clone()),
403        ),
404        (ARBOWNER_ADDRESS, create_arbowner_precompile(ctx.clone())),
405        (ARBBLS_ADDRESS, create_arbbls_precompile()),
406        (ARBDEBUG_ADDRESS, create_arbdebug_precompile(ctx.clone())),
407        (ARBWASM_ADDRESS, create_arbwasm_precompile(ctx.clone())),
408        (
409            ARBWASMCACHE_ADDRESS,
410            create_arbwasmcache_precompile(ctx.clone()),
411        ),
412        (
413            ARBFILTEREDTXMANAGER_ADDRESS,
414            create_arbfilteredtxmanager_precompile(ctx.clone()),
415        ),
416        (
417            ARBNATIVETOKENMANAGER_ADDRESS,
418            create_arbnativetokenmanager_precompile(ctx.clone()),
419        ),
420    ]);
421
422    if arbos_version >= arb_chainspec::arbos_version::ARBOS_VERSION_50 {
423        // P256VERIFY adopts the EIP-7951 Osaka schedule (6900 gas) at v50+.
424        map.extend_precompiles([(P256VERIFY_ADDRESS, create_p256verify_osaka_precompile())]);
425    } else if arbos_version >= arb_chainspec::arbos_version::ARBOS_VERSION_30 {
426        // RIP-7212 P256VERIFY at 3450 gas (ArbOS 30..49).
427        map.extend_precompiles([(P256VERIFY_ADDRESS, create_p256verify_precompile())]);
428    } else {
429        map.apply_precompile(&KZG_POINT_EVALUATION_ADDRESS, |_| None);
430        map.apply_precompile(&P256VERIFY_ADDRESS, |_| None);
431    }
432
433    if arbos_version >= arb_chainspec::arbos_version::ARBOS_VERSION_50 {
434        // ArbOS 50+ switches modexp to the EIP-7823 + EIP-7883 gas schedule.
435        map.extend_precompiles([(MODEXP_ADDRESS, create_modexp_osaka_precompile())]);
436    } else {
437        // BLS12-381 precompiles are not available before ArbOS 50.
438        for addr in &BLS12_381_ADDRESSES {
439            map.apply_precompile(addr, |_| None);
440        }
441    }
442}
443
444#[cfg(test)]
445mod recent_wasms_tests {
446    use alloy_primitives::B256;
447    use arb_context::BlockCtx;
448
449    #[test]
450    fn reset_clears_entries_and_sets_capacity() {
451        let block = BlockCtx::default();
452        let h1 = B256::repeat_byte(0xa1);
453        let h2 = B256::repeat_byte(0xa2);
454        block.reset_recent_wasms(8);
455        assert!(!block.insert_recent_wasm(h1));
456        assert!(!block.insert_recent_wasm(h2));
457        assert!(block.insert_recent_wasm(h1));
458        block.reset_recent_wasms(8);
459        assert!(
460            !block.insert_recent_wasm(h1),
461            "reset must wipe prior entries"
462        );
463    }
464
465    #[test]
466    fn capacity_evicts_oldest() {
467        let block = BlockCtx::default();
468        let h1 = B256::repeat_byte(0x01);
469        let h2 = B256::repeat_byte(0x02);
470        let h3 = B256::repeat_byte(0x03);
471        block.reset_recent_wasms(2);
472        assert!(!block.insert_recent_wasm(h1));
473        assert!(!block.insert_recent_wasm(h2));
474        assert!(!block.insert_recent_wasm(h3));
475        assert!(
476            !block.insert_recent_wasm(h1),
477            "h1 should be evicted after h3 push"
478        );
479    }
480
481    #[test]
482    fn zero_capacity_is_no_op_cache() {
483        let block = BlockCtx::default();
484        let h = B256::repeat_byte(0xff);
485        block.reset_recent_wasms(0);
486        assert!(!block.insert_recent_wasm(h));
487        block.reset_recent_wasms(0);
488        assert!(!block.insert_recent_wasm(h));
489    }
490}
491
492#[cfg(test)]
493mod p256_gas_tests {
494    //! P256VERIFY gas: 3450 for ArbOS 30..49, 6900 for v50+ (EIP-7951 Osaka).
495    use revm::precompile::secp256r1::{p256_verify, p256_verify_osaka};
496
497    // Valid p256 signature input from the upstream RIP-7212 test vectors.
498    const VALID_INPUT_HEX: &str = "4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e";
499
500    fn input_bytes() -> Vec<u8> {
501        (0..VALID_INPUT_HEX.len() / 2)
502            .map(|i| u8::from_str_radix(&VALID_INPUT_HEX[i * 2..i * 2 + 2], 16).unwrap())
503            .collect()
504    }
505
506    #[test]
507    fn rip7212_charges_3450() {
508        let input = input_bytes();
509        let out = p256_verify(&input, 10_000).expect("ok");
510        assert_eq!(out.gas_used, 3450);
511    }
512
513    #[test]
514    fn osaka_charges_6900() {
515        let input = input_bytes();
516        let out = p256_verify_osaka(&input, 10_000).expect("ok");
517        assert_eq!(out.gas_used, 6900);
518    }
519}