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