Skip to main content

foundry_evm_core/
utils.rs

1use crate::{EvmEnv, FoundryBlock, hardfork::FoundryHardfork};
2use alloy_chains::Chain;
3use alloy_consensus::{BlockHeader, private::alloy_eips::eip7840::BlobParams};
4use alloy_hardforks::EthereumHardfork;
5use alloy_json_abi::{Function, JsonAbi};
6use alloy_primitives::{ChainId, Selector, U256};
7use alloy_provider::{Network, network::BlockResponse};
8use foundry_config::NamedChain::{
9    self, Avalanche, AvalancheFuji, BinanceSmartChain, BinanceSmartChainTestnet, Polygon,
10    PolygonAmoy,
11};
12use foundry_evm_networks::NetworkConfigs;
13use revm::primitives::hardfork::SpecId;
14pub use revm::state::EvmState as StateChangeset;
15
16/// Hints to the compiler that this is a cold path, i.e. unlikely to be taken.
17#[cold]
18#[inline(always)]
19pub const fn cold_path() {
20    // TODO: remove `#[cold]` and call `std::hint::cold_path` once stable.
21}
22
23/// Constructs a generic [`FoundryBlock`] from a block header.
24pub fn block_env_from_header<BLOCK: FoundryBlock + Default>(header: &impl BlockHeader) -> BLOCK {
25    let mut block = BLOCK::default();
26    block.set_number(U256::from(header.number()));
27    block.set_slot_num(header.slot_number().unwrap_or_default());
28    block.set_beneficiary(header.beneficiary());
29    block.set_timestamp(U256::from(header.timestamp()));
30    block.set_difficulty(header.difficulty());
31    block.set_prevrandao(header.mix_hash());
32    block.set_basefee(header.base_fee_per_gas().unwrap_or_default());
33    block.set_gas_limit(header.gas_limit());
34    block
35}
36
37/// Applies replay normalization for the provided source chain.
38///
39/// This keeps fork-specific transaction validation independent from an execution `CHAINID`
40/// override.
41pub fn apply_chain_specific_tx_replay_env_changes_for_chain<SPEC, BLOCK>(
42    evm_env: &mut EvmEnv<SPEC, BLOCK>,
43    source_chain_id: ChainId,
44) {
45    if NamedChain::try_from(source_chain_id).is_ok_and(|chain| chain.is_arbitrum()) {
46        // Arbitrum does not enforce the EIP-1559 priority fee ordering constraint.
47        evm_env.cfg_env.disable_priority_fee_check = true;
48    }
49}
50
51/// Applies block normalization for the provided source chain.
52///
53/// - checks for prevrandao mixhash after merge
54/// - applies chain specifics: on Arbitrum `block.number` is the L1 block
55///
56/// This keeps fork-specific header handling independent from an execution `CHAINID` override.
57pub fn apply_chain_and_block_specific_env_changes_for_chain<
58    N: Network,
59    SPEC: Into<SpecId> + Copy,
60    BLOCK: FoundryBlock,
61>(
62    evm_env: &mut EvmEnv<SPEC, BLOCK>,
63    block: &N::BlockResponse,
64    source_chain_id: ChainId,
65    configs: NetworkConfigs,
66) {
67    // The blob fee market is priced from the header's excess blob gas and the source chain's
68    // blob schedule at the block timestamp. Headers without the field (pre-Cancun blocks and
69    // chains without EIP-4844) keep the default blob environment.
70    if let Some(excess_blob_gas) = block.header().excess_blob_gas() {
71        evm_env.block_env.set_blob_excess_gas_and_price(
72            excess_blob_gas,
73            get_blob_base_fee_update_fraction(source_chain_id, block.header().timestamp()),
74        );
75    }
76
77    if let Ok(chain) = NamedChain::try_from(source_chain_id) {
78        match chain {
79            BinanceSmartChain
80            | BinanceSmartChainTestnet
81            | Polygon
82            | PolygonAmoy
83            | Avalanche
84            | AvalancheFuji => {
85                // https://github.com/foundry-rs/foundry/issues/9942
86                // As far as observed from the source code of bnb-chain/bsc, the `difficulty` field
87                // is still in use and returned by the corresponding opcode but `prevrandao`
88                // (`mixHash`) is always zero, even though bsc adopts the newer EVM
89                // specification. This will confuse revm and causes emulation
90                // failure. Polygon and Avalanche behave the same way.
91                evm_env.block_env.set_prevrandao(Some(evm_env.block_env.difficulty().into()));
92                return;
93            }
94            c if c.is_arbitrum() => {
95                // on arbitrum `block.number` is the L1 block which is included in the
96                // `l1BlockNumber` field
97                if let Some(l1_block_number) = block
98                    .other_fields()
99                    .and_then(|other| other.get("l1BlockNumber").cloned())
100                    .and_then(|l1_block_number| {
101                        serde_json::from_value::<U256>(l1_block_number).ok()
102                    })
103                {
104                    evm_env.block_env.set_number(l1_block_number);
105                }
106
107                // `mixHash` carries L1 metadata rather than randomness here, while the
108                // `PREVRANDAO` opcode returns `difficulty` like it does on the chains above.
109                evm_env.block_env.set_prevrandao(Some(evm_env.block_env.difficulty().into()));
110            }
111            _ => {}
112        }
113    }
114
115    if configs.bypass_prevrandao(source_chain_id) && evm_env.block_env.prevrandao().is_none() {
116        // Chains without randomness keep the historical DIFFICULTY opcode semantics.
117        // <https://github.com/foundry-rs/foundry/issues/4232>
118        evm_env.block_env.set_prevrandao(Some(evm_env.block_env.difficulty().into()));
119    }
120
121    // if difficulty is `0` we assume it's past merge
122    if block.header().difficulty().is_zero() {
123        evm_env.block_env.set_difficulty(evm_env.block_env.prevrandao().unwrap_or_default().into());
124    }
125}
126
127/// Derives the active [`BlobParams`] based on the given timestamp.
128///
129/// This falls back to regular ethereum blob params if no hardforks for the given chain id are
130/// detected.
131pub fn get_blob_params(chain_id: ChainId, timestamp: u64) -> BlobParams {
132    let hardfork = EthereumHardfork::from_chain_and_timestamp(Chain::from_id(chain_id), timestamp)
133        .unwrap_or_default();
134
135    match hardfork {
136        EthereumHardfork::Prague => BlobParams::prague(),
137        EthereumHardfork::Osaka => BlobParams::osaka(),
138        EthereumHardfork::Bpo1 => BlobParams::bpo1(),
139        EthereumHardfork::Bpo2 => BlobParams::bpo2(),
140
141        // future hardforks/unknown settings: update once decided
142        EthereumHardfork::Bpo3 => BlobParams::bpo2(),
143        EthereumHardfork::Bpo4 => BlobParams::bpo2(),
144        EthereumHardfork::Bpo5 => BlobParams::bpo2(),
145        EthereumHardfork::Amsterdam => BlobParams::bpo2(),
146
147        // fallback
148        _ => BlobParams::cancun(),
149    }
150}
151
152/// Derive the blob base fee update fraction based on the chain and timestamp by checking the
153/// hardfork.
154pub fn get_blob_base_fee_update_fraction(chain_id: ChainId, timestamp: u64) -> u64 {
155    get_blob_params(chain_id, timestamp).update_fraction as u64
156}
157
158/// Returns the blob params based on the spec id.
159pub fn get_blob_params_by_spec_id(spec: SpecId) -> BlobParams {
160    if spec >= SpecId::AMSTERDAM {
161        BlobParams::bpo2()
162    } else if spec >= SpecId::OSAKA {
163        BlobParams::osaka()
164    } else if spec >= SpecId::PRAGUE {
165        BlobParams::prague()
166    } else {
167        BlobParams::cancun()
168    }
169}
170
171/// Returns the blob parameters selected by an explicit Foundry hardfork.
172pub fn get_blob_params_by_hardfork(hardfork: FoundryHardfork) -> BlobParams {
173    match hardfork {
174        FoundryHardfork::Ethereum(EthereumHardfork::Prague) => BlobParams::prague(),
175        FoundryHardfork::Ethereum(EthereumHardfork::Osaka) => BlobParams::osaka(),
176        FoundryHardfork::Ethereum(EthereumHardfork::Bpo1) => BlobParams::bpo1(),
177        FoundryHardfork::Ethereum(EthereumHardfork::Bpo2) => BlobParams::bpo2(),
178        FoundryHardfork::Ethereum(
179            EthereumHardfork::Bpo3
180            | EthereumHardfork::Bpo4
181            | EthereumHardfork::Bpo5
182            | EthereumHardfork::Amsterdam,
183        ) => BlobParams::bpo2(),
184        _ => get_blob_params_by_spec_id(hardfork.into()),
185    }
186}
187
188/// Returns the blob base fee update fraction based on the spec id.
189pub fn get_blob_base_fee_update_fraction_by_spec_id(spec: SpecId) -> u64 {
190    get_blob_params_by_spec_id(spec).update_fraction as u64
191}
192
193/// Given an ABI and selector, it tries to find the respective function.
194pub fn get_function<'a>(
195    contract_name: &str,
196    selector: Selector,
197    abi: &'a JsonAbi,
198) -> eyre::Result<&'a Function> {
199    abi.functions()
200        .find(|func| func.selector() == selector)
201        .ok_or_else(|| eyre::eyre!("{contract_name} does not have the selector {selector}"))
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use alloy_network::{AnyHeader, AnyNetwork, AnyRpcBlock, AnyRpcHeader};
208    use alloy_primitives::B256;
209    use alloy_rpc_types::{Block, BlockTransactions};
210    use revm::context::{BlockEnv, CfgEnv};
211
212    #[test]
213    fn block_env_preserves_slot_number() {
214        for slot_number in [None, Some(0), Some(42), Some(u64::MAX)] {
215            let header = AnyHeader { slot_number, ..Default::default() };
216            let block = block_env_from_header::<BlockEnv>(&header);
217            assert_eq!(block.slot_num, slot_number.unwrap_or_default());
218        }
219    }
220
221    #[test]
222    fn block_normalization_uses_source_chain() {
223        let header = AnyHeader { number: 500, ..Default::default() };
224        let mut block = AnyRpcBlock::new(
225            Block::new(
226                AnyRpcHeader::from_sealed(header.seal(B256::ZERO)),
227                BlockTransactions::Full(Vec::new()),
228            )
229            .into(),
230        );
231        block.other.insert("l1BlockNumber".to_string(), serde_json::json!("0x64"));
232
233        let mut cfg_env = CfgEnv::<SpecId>::default();
234        cfg_env.chain_id = NamedChain::Mainnet as u64;
235        let mut evm_env = EvmEnv {
236            cfg_env,
237            block_env: BlockEnv { number: U256::from(500), ..Default::default() },
238        };
239
240        apply_chain_and_block_specific_env_changes_for_chain::<AnyNetwork, _, _>(
241            &mut evm_env,
242            &block,
243            NamedChain::Arbitrum as u64,
244            NetworkConfigs::default(),
245        );
246
247        assert_eq!(evm_env.cfg_env.chain_id, NamedChain::Mainnet as u64);
248        assert_eq!(evm_env.block_env.number, U256::from(100));
249    }
250
251    #[test]
252    fn block_normalization_sets_blob_excess_gas_from_header() {
253        // Mainnet block 22_000_000 (Cancun): 22_151_168 excess blob gas prices blobs at 761 wei.
254        let header = AnyHeader {
255            timestamp: 1_741_410_875,
256            excess_blob_gas: Some(22_151_168),
257            ..Default::default()
258        };
259        let block = AnyRpcBlock::new(
260            Block::new(
261                AnyRpcHeader::from_sealed(header.seal(B256::ZERO)),
262                BlockTransactions::Full(Vec::new()),
263            )
264            .into(),
265        );
266        let mut evm_env = EvmEnv::new(CfgEnv::<SpecId>::default(), BlockEnv::default());
267        // The execution chain id can be overridden; the blob schedule follows the source chain.
268        evm_env.cfg_env.chain_id = 1337;
269
270        apply_chain_and_block_specific_env_changes_for_chain::<AnyNetwork, _, _>(
271            &mut evm_env,
272            &block,
273            NamedChain::Mainnet as u64,
274            NetworkConfigs::default(),
275        );
276
277        let blob = evm_env.block_env.blob_excess_gas_and_price.unwrap();
278        assert_eq!(blob.excess_blob_gas, 22_151_168);
279        assert_eq!(blob.blob_gasprice, 761);
280    }
281
282    #[test]
283    fn block_normalization_keeps_default_blob_env_without_header_field() {
284        let header = AnyHeader { excess_blob_gas: None, ..Default::default() };
285        let block = AnyRpcBlock::new(
286            Block::new(
287                AnyRpcHeader::from_sealed(header.seal(B256::ZERO)),
288                BlockTransactions::Full(Vec::new()),
289            )
290            .into(),
291        );
292        let mut evm_env = EvmEnv::new(CfgEnv::<SpecId>::default(), BlockEnv::default());
293
294        apply_chain_and_block_specific_env_changes_for_chain::<AnyNetwork, _, _>(
295            &mut evm_env,
296            &block,
297            NamedChain::Mainnet as u64,
298            NetworkConfigs::default(),
299        );
300
301        assert_eq!(
302            evm_env.block_env.blob_excess_gas_and_price,
303            BlockEnv::default().blob_excess_gas_and_price
304        );
305    }
306
307    #[test]
308    fn block_normalization_handles_missing_prevrandao_deterministically() {
309        for (chain, configs, bypass) in [
310            (NamedChain::Moonbeam as u64, NetworkConfigs::default(), true),
311            (NamedChain::Gnosis as u64, NetworkConfigs::default(), true),
312            (NamedChain::Rsk as u64, NetworkConfigs::default(), true),
313            (98_765_432, NetworkConfigs::default(), false),
314            (
315                98_765_432,
316                serde_json::from_str(r#"{"bypass_prevrandao":true,"celo":false}"#).unwrap(),
317                true,
318            ),
319        ] {
320            for difficulty in [U256::ZERO, U256::from(42)] {
321                let header = AnyHeader { difficulty, mix_hash: None, ..Default::default() };
322                let block = AnyRpcBlock::new(
323                    Block::new(
324                        AnyRpcHeader::from_sealed(header.seal(B256::ZERO)),
325                        BlockTransactions::Full(Vec::new()),
326                    )
327                    .into(),
328                );
329                let mut previous = None;
330                for _ in 0..2 {
331                    let mut evm_env = EvmEnv::new(
332                        CfgEnv::<SpecId>::default(),
333                        block_env_from_header::<BlockEnv>(block.header()),
334                    );
335                    apply_chain_and_block_specific_env_changes_for_chain::<AnyNetwork, _, _>(
336                        &mut evm_env,
337                        &block,
338                        chain,
339                        configs,
340                    );
341                    assert_eq!(evm_env.block_env.prevrandao, bypass.then(|| difficulty.into()));
342                    assert_eq!(evm_env.block_env.difficulty, difficulty);
343                    if let Some(previous) = &previous {
344                        assert_eq!(&evm_env.block_env, previous);
345                    }
346                    previous = Some(evm_env.block_env);
347                }
348            }
349        }
350    }
351
352    #[test]
353    fn block_normalization_preserves_header_prevrandao() {
354        let randao = B256::repeat_byte(0xab);
355        let header = AnyHeader { mix_hash: Some(randao), ..Default::default() };
356        let block = AnyRpcBlock::new(
357            Block::new(
358                AnyRpcHeader::from_sealed(header.seal(B256::ZERO)),
359                BlockTransactions::Full(Vec::new()),
360            )
361            .into(),
362        );
363        let mut evm_env = EvmEnv::new(
364            CfgEnv::<SpecId>::default(),
365            block_env_from_header::<BlockEnv>(block.header()),
366        );
367        apply_chain_and_block_specific_env_changes_for_chain::<AnyNetwork, _, _>(
368            &mut evm_env,
369            &block,
370            NamedChain::Gnosis as u64,
371            NetworkConfigs::default(),
372        );
373        assert_eq!(evm_env.block_env.prevrandao, Some(randao));
374    }
375
376    #[test]
377    fn block_normalization_preserves_mainnet_merge_boundary() {
378        let randao = B256::repeat_byte(0xab);
379        for number in [15_537_350, 15_537_351, 15_537_393, 15_537_394] {
380            let difficulty = if number < 15_537_394 { U256::from(42) } else { U256::ZERO };
381            let header =
382                AnyHeader { number, difficulty, mix_hash: Some(randao), ..Default::default() };
383            let block = AnyRpcBlock::new(
384                Block::new(
385                    AnyRpcHeader::from_sealed(header.seal(B256::ZERO)),
386                    BlockTransactions::Full(Vec::new()),
387                )
388                .into(),
389            );
390            let mut evm_env = EvmEnv::new(
391                CfgEnv::<SpecId>::default(),
392                block_env_from_header::<BlockEnv>(block.header()),
393            );
394            apply_chain_and_block_specific_env_changes_for_chain::<AnyNetwork, _, _>(
395                &mut evm_env,
396                &block,
397                NamedChain::Mainnet as u64,
398                NetworkConfigs::default(),
399            );
400            let expected = if difficulty.is_zero() { randao.into() } else { difficulty };
401            assert_eq!(evm_env.block_env.difficulty, expected, "block {number}");
402            assert_eq!(evm_env.block_env.prevrandao, Some(randao));
403        }
404    }
405
406    #[test]
407    fn block_normalization_uses_difficulty_as_prevrandao() {
408        // These chains keep using `difficulty` and return it from `PREVRANDAO`, so a header
409        // `mixHash` of zero (or, on Arbitrum, packed L1 metadata) must not reach the block env.
410        for (chain, mix_hash) in [
411            (NamedChain::BinanceSmartChain, B256::ZERO),
412            (NamedChain::Polygon, B256::ZERO),
413            (NamedChain::PolygonAmoy, B256::ZERO),
414            (NamedChain::Avalanche, B256::ZERO),
415            (NamedChain::AvalancheFuji, B256::ZERO),
416            (NamedChain::Arbitrum, B256::repeat_byte(0xab)),
417            (NamedChain::ArbitrumNova, B256::repeat_byte(0xab)),
418            (NamedChain::ArbitrumSepolia, B256::repeat_byte(0xab)),
419        ] {
420            let header =
421                AnyHeader { difficulty: U256::ONE, mix_hash: Some(mix_hash), ..Default::default() };
422            let block = AnyRpcBlock::new(
423                Block::new(
424                    AnyRpcHeader::from_sealed(header.seal(B256::ZERO)),
425                    BlockTransactions::Full(Vec::new()),
426                )
427                .into(),
428            );
429            let mut evm_env = EvmEnv::new(
430                CfgEnv::<SpecId>::default(),
431                BlockEnv {
432                    difficulty: U256::ONE,
433                    prevrandao: Some(mix_hash),
434                    ..Default::default()
435                },
436            );
437
438            apply_chain_and_block_specific_env_changes_for_chain::<AnyNetwork, _, _>(
439                &mut evm_env,
440                &block,
441                chain as u64,
442                NetworkConfigs::default(),
443            );
444
445            assert_eq!(
446                evm_env.block_env.prevrandao,
447                Some(B256::with_last_byte(1)),
448                "{chain:?} should expose `difficulty` as `PREVRANDAO`"
449            );
450        }
451    }
452
453    #[test]
454    fn tx_replay_env_changes_disable_priority_fee_check_only_for_arbitrum() {
455        let mut evm_env = EvmEnv::new(
456            revm::context::CfgEnv::<SpecId>::default(),
457            revm::context::BlockEnv::default(),
458        );
459        apply_chain_specific_tx_replay_env_changes_for_chain(
460            &mut evm_env,
461            NamedChain::Arbitrum as u64,
462        );
463        assert!(evm_env.cfg_env.disable_priority_fee_check);
464
465        evm_env.cfg_env.disable_priority_fee_check = false;
466
467        apply_chain_specific_tx_replay_env_changes_for_chain(
468            &mut evm_env,
469            NamedChain::Mainnet as u64,
470        );
471        assert!(!evm_env.cfg_env.disable_priority_fee_check);
472    }
473
474    #[test]
475    fn tx_replay_env_changes_use_source_chain() {
476        let mut evm_env = EvmEnv::new(
477            revm::context::CfgEnv::<SpecId>::default(),
478            revm::context::BlockEnv::default(),
479        );
480        evm_env.cfg_env.chain_id = NamedChain::Mainnet as u64;
481
482        apply_chain_specific_tx_replay_env_changes_for_chain(
483            &mut evm_env,
484            NamedChain::Arbitrum as u64,
485        );
486
487        assert_eq!(evm_env.cfg_env.chain_id, NamedChain::Mainnet as u64);
488        assert!(evm_env.cfg_env.disable_priority_fee_check);
489    }
490
491    #[test]
492    fn blob_params_by_spec_id_tracks_latest_known_blob_schedule() {
493        assert_eq!(get_blob_params_by_spec_id(SpecId::CANCUN), BlobParams::cancun());
494        assert_eq!(get_blob_params_by_spec_id(SpecId::PRAGUE), BlobParams::prague());
495        assert_eq!(get_blob_params_by_spec_id(SpecId::OSAKA), BlobParams::osaka());
496        assert_eq!(get_blob_params_by_spec_id(SpecId::AMSTERDAM), BlobParams::bpo2());
497        assert_eq!(
498            get_blob_base_fee_update_fraction_by_spec_id(SpecId::AMSTERDAM),
499            BlobParams::bpo2().update_fraction as u64
500        );
501    }
502
503    #[test]
504    fn blob_params_by_explicit_hardfork() {
505        for (hardfork, expected) in [
506            (EthereumHardfork::Cancun, BlobParams::cancun()),
507            (EthereumHardfork::Prague, BlobParams::prague()),
508            (EthereumHardfork::Osaka, BlobParams::osaka()),
509            (EthereumHardfork::Bpo1, BlobParams::bpo1()),
510            (EthereumHardfork::Bpo2, BlobParams::bpo2()),
511            (EthereumHardfork::Amsterdam, BlobParams::bpo2()),
512        ] {
513            assert_eq!(get_blob_params_by_hardfork(hardfork.into()), expected);
514        }
515    }
516}