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