Skip to main content

revm_handler/
validation.rs

1use context_interface::{
2    cfg::GasParams,
3    result::{InvalidHeader, InvalidTransaction},
4    transaction::{Transaction, TransactionType},
5    Block, Cfg, ContextTr,
6};
7use core::cmp;
8use interpreter::InitialAndFloorGas;
9use primitives::{eip4844, hardfork::SpecId, B256};
10
11/// Validates the execution environment including block and transaction parameters.
12pub fn validate_env<CTX: ContextTr, ERROR: From<InvalidHeader> + From<InvalidTransaction>>(
13    context: CTX,
14) -> Result<(), ERROR> {
15    let spec = context.cfg().spec().into();
16    // `prevrandao` is required for the merge
17    if spec.is_enabled_in(SpecId::MERGE) && context.block().prevrandao().is_none() {
18        return Err(InvalidHeader::PrevrandaoNotSet.into());
19    }
20    // `excess_blob_gas` is required for Cancun
21    if spec.is_enabled_in(SpecId::CANCUN) && context.block().blob_excess_gas_and_price().is_none() {
22        return Err(InvalidHeader::ExcessBlobGasNotSet.into());
23    }
24    validate_tx_env::<CTX>(context, spec).map_err(Into::into)
25}
26
27/// Validate legacy transaction gas price against basefee.
28#[inline]
29pub const fn validate_legacy_gas_price(
30    gas_price: u128,
31    base_fee: Option<u128>,
32) -> Result<(), InvalidTransaction> {
33    // Gas price must be at least the basefee.
34    if let Some(base_fee) = base_fee {
35        if gas_price < base_fee {
36            return Err(InvalidTransaction::GasPriceLessThanBasefee);
37        }
38    }
39    Ok(())
40}
41
42/// Validate transaction that has EIP-1559 priority fee
43pub fn validate_priority_fee_tx(
44    max_fee: u128,
45    max_priority_fee: u128,
46    base_fee: Option<u128>,
47    disable_priority_fee_check: bool,
48) -> Result<(), InvalidTransaction> {
49    if !disable_priority_fee_check && max_priority_fee > max_fee {
50        // Or gas_max_fee for eip1559
51        return Err(InvalidTransaction::PriorityFeeGreaterThanMaxFee);
52    }
53
54    // Check minimal cost against basefee
55    if let Some(base_fee) = base_fee {
56        let effective_gas_price = cmp::min(max_fee, base_fee.saturating_add(max_priority_fee));
57        if effective_gas_price < base_fee {
58            return Err(InvalidTransaction::GasPriceLessThanBasefee);
59        }
60    }
61
62    Ok(())
63}
64
65/// Validate priority fee for transactions that support EIP-1559 (Eip1559, Eip4844, Eip7702).
66#[inline]
67fn validate_priority_fee_for_tx<TX: Transaction>(
68    tx: TX,
69    base_fee: Option<u128>,
70    disable_priority_fee_check: bool,
71) -> Result<(), InvalidTransaction> {
72    validate_priority_fee_tx(
73        tx.max_fee_per_gas(),
74        tx.max_priority_fee_per_gas().unwrap_or_default(),
75        base_fee,
76        disable_priority_fee_check,
77    )
78}
79
80/// Validate EIP-4844 transaction.
81pub fn validate_eip4844_tx(
82    blobs: &[B256],
83    max_blob_fee: u128,
84    block_blob_gas_price: u128,
85    max_blobs: Option<u64>,
86) -> Result<(), InvalidTransaction> {
87    // Ensure that the user was willing to at least pay the current blob gasprice
88    if block_blob_gas_price > max_blob_fee {
89        return Err(InvalidTransaction::BlobGasPriceGreaterThanMax {
90            block_blob_gas_price,
91            tx_max_fee_per_blob_gas: max_blob_fee,
92        });
93    }
94
95    // There must be at least one blob
96    if blobs.is_empty() {
97        return Err(InvalidTransaction::EmptyBlobs);
98    }
99
100    // All versioned blob hashes must start with VERSIONED_HASH_VERSION_KZG
101    for blob in blobs {
102        if blob[0] != eip4844::VERSIONED_HASH_VERSION_KZG {
103            return Err(InvalidTransaction::BlobVersionNotSupported);
104        }
105    }
106
107    // Ensure the total blob gas spent is at most equal to the limit
108    // assert blob_gas_used <= MAX_BLOB_GAS_PER_BLOCK
109    if let Some(max_blobs) = max_blobs {
110        if blobs.len() > max_blobs as usize {
111            return Err(InvalidTransaction::TooManyBlobs {
112                have: blobs.len(),
113                max: max_blobs as usize,
114            });
115        }
116    }
117    Ok(())
118}
119
120/// Validate transaction against block and configuration for mainnet.
121pub fn validate_tx_env<CTX: ContextTr>(
122    context: CTX,
123    spec_id: SpecId,
124) -> Result<(), InvalidTransaction> {
125    // Check if the transaction's chain id is correct
126    let tx = context.tx();
127    let tx_type = tx.tx_type();
128
129    let base_fee = if context.cfg().is_base_fee_check_disabled() {
130        None
131    } else {
132        Some(context.block().basefee() as u128)
133    };
134
135    let tx_type = TransactionType::from(tx_type);
136
137    // Check chain_id if config is enabled.
138    // EIP-155: Simple replay attack protection
139    if context.cfg().tx_chain_id_check() {
140        if let Some(chain_id) = tx.chain_id() {
141            if chain_id != context.cfg().chain_id() {
142                return Err(InvalidTransaction::InvalidChainId);
143            }
144        } else if !tx_type.is_legacy() && !tx_type.is_custom() {
145            // Legacy transaction are the only one that can omit chain_id.
146            return Err(InvalidTransaction::MissingChainId);
147        }
148    }
149
150    // tx gas cap is not enforced if state gas is enabled.
151    if !context.cfg().is_amsterdam_eip8037_enabled() {
152        // EIP-7825: Transaction Gas Limit Cap
153        let cap = context.cfg().tx_gas_limit_cap();
154        if tx.gas_limit() > cap {
155            return Err(InvalidTransaction::TxGasLimitGreaterThanCap {
156                gas_limit: tx.gas_limit(),
157                cap,
158            });
159        }
160    }
161
162    let disable_priority_fee_check = context.cfg().is_priority_fee_check_disabled();
163
164    match tx_type {
165        TransactionType::Legacy => {
166            validate_legacy_gas_price(tx.gas_price(), base_fee)?;
167        }
168        TransactionType::Eip2930 => {
169            // Enabled in BERLIN hardfork
170            if !spec_id.is_enabled_in(SpecId::BERLIN) {
171                return Err(InvalidTransaction::Eip2930NotSupported);
172            }
173            validate_legacy_gas_price(tx.gas_price(), base_fee)?;
174        }
175        TransactionType::Eip1559 => {
176            if !spec_id.is_enabled_in(SpecId::LONDON) {
177                return Err(InvalidTransaction::Eip1559NotSupported);
178            }
179            validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
180        }
181        TransactionType::Eip4844 => {
182            if !spec_id.is_enabled_in(SpecId::CANCUN) {
183                return Err(InvalidTransaction::Eip4844NotSupported);
184            }
185
186            validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
187
188            validate_eip4844_tx(
189                tx.blob_versioned_hashes(),
190                tx.max_fee_per_blob_gas(),
191                context.block().blob_gasprice().unwrap_or_default(),
192                context.cfg().max_blobs_per_tx(),
193            )?;
194        }
195        TransactionType::Eip7702 => {
196            // Check if EIP-7702 transaction is enabled.
197            if !spec_id.is_enabled_in(SpecId::PRAGUE) {
198                return Err(InvalidTransaction::Eip7702NotSupported);
199            }
200
201            validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
202
203            let auth_list_len = tx.authorization_list_len();
204            // The transaction is considered invalid if the length of authorization_list is zero.
205            if auth_list_len == 0 {
206                return Err(InvalidTransaction::EmptyAuthorizationList);
207            }
208        }
209        TransactionType::Custom => {
210            // Custom transaction type check is not done here.
211        }
212    };
213
214    // Check if gas_limit is more than block_gas_limit
215    // TODO(eip8037) should we enforce to `min(tx.gas_limit(), 16M) < block.gas_limit`?
216    // This would enforce that regular gas is constrained.
217    if !context.cfg().is_block_gas_limit_disabled() && tx.gas_limit() > context.block().gas_limit()
218    {
219        return Err(InvalidTransaction::CallerGasLimitMoreThanBlock);
220    }
221
222    // EIP-3860: Limit and meter initcode. Still valid with EIP-7907 and increase of initcode size.
223    if spec_id.is_enabled_in(SpecId::SHANGHAI)
224        && tx.kind().is_create()
225        && tx.input().len() > context.cfg().max_initcode_size()
226    {
227        return Err(InvalidTransaction::CreateInitCodeSizeLimit);
228    }
229
230    // Check that the transaction's nonce is not at the maximum value.
231    // Incrementing the nonce would overflow. Can't happen in the real world.
232    if tx.nonce() == u64::MAX {
233        return Err(InvalidTransaction::NonceOverflowInTransaction);
234    }
235
236    Ok(())
237}
238
239/// Validate initial transaction gas using the default [`GasParams`] for the given [`SpecId`].
240///
241/// For custom gas parameters (e.g. configured on the context), use
242/// [`validate_initial_tx_gas_with_gas_params`].
243#[allow(clippy::too_many_arguments)]
244pub fn validate_initial_tx_gas(
245    tx: impl Transaction,
246    spec: SpecId,
247    is_eip7623_disabled: bool,
248    is_amsterdam_eip8037_enabled: bool,
249    tx_gas_limit_cap: u64,
250    eip2780: Option<context_interface::cfg::gas_params::Eip2780TxInfo>,
251) -> Result<InitialAndFloorGas, InvalidTransaction> {
252    validate_initial_tx_gas_with_gas_params(
253        tx,
254        spec,
255        &GasParams::new_spec(spec),
256        is_eip7623_disabled,
257        is_amsterdam_eip8037_enabled,
258        tx_gas_limit_cap,
259        eip2780,
260    )
261}
262
263/// Validate initial transaction gas using the provided [`GasParams`].
264#[allow(clippy::too_many_arguments)]
265pub fn validate_initial_tx_gas_with_gas_params(
266    tx: impl Transaction,
267    spec: SpecId,
268    gas_params: &GasParams,
269    is_eip7623_disabled: bool,
270    is_amsterdam_eip8037_enabled: bool,
271    tx_gas_limit_cap: u64,
272    eip2780: Option<context_interface::cfg::gas_params::Eip2780TxInfo>,
273) -> Result<InitialAndFloorGas, InvalidTransaction> {
274    let mut gas = gas_params.initial_tx_gas_for_tx(&tx, eip2780);
275
276    if is_eip7623_disabled {
277        gas.set_floor_gas(0);
278    }
279
280    if !is_amsterdam_eip8037_enabled {
281        gas.set_initial_state_gas(0);
282    }
283
284    // Additional check to see if limit is big enough to cover initial gas.
285    if gas.initial_total_gas() > tx.gas_limit() {
286        return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
287            gas_limit: tx.gas_limit(),
288            initial_gas: gas.initial_total_gas(),
289        });
290    }
291
292    // EIP-7623: Increase calldata cost
293    // floor gas should be less than gas limit.
294    if spec.is_enabled_in(SpecId::PRAGUE) && gas.floor_gas() > tx.gas_limit() {
295        return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
296            gas_floor: gas.floor_gas(),
297            gas_limit: tx.gas_limit(),
298        });
299    };
300
301    // EIP-8037: Regular gas is capped at TX_MAX_GAS_LIMIT.
302    // Validate that both intrinsic regular gas and floor gas fit within the cap.
303    // State gas is excluded — it uses its own reservoir.
304    if is_amsterdam_eip8037_enabled && tx.gas_limit() > tx_gas_limit_cap {
305        let min_regular_gas = gas.initial_regular_gas().max(gas.floor_gas());
306        if min_regular_gas > tx_gas_limit_cap {
307            return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
308                gas_floor: min_regular_gas,
309                gas_limit: tx_gas_limit_cap,
310            });
311        }
312    }
313
314    Ok(gas)
315}
316
317#[cfg(test)]
318mod tests {
319    use crate::{api::ExecuteEvm, ExecuteCommitEvm, MainBuilder, MainContext};
320    use bytecode::opcode;
321    use context::{
322        result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction, Output},
323        Context, ContextTr, TxEnv,
324    };
325    use database::{CacheDB, EmptyDB};
326    use primitives::{address, eip3860, eip7954, hardfork::SpecId, Bytes, TxKind, B256};
327    use state::{AccountInfo, Bytecode};
328
329    fn deploy_contract(
330        bytecode: Bytes,
331        spec_id: Option<SpecId>,
332    ) -> Result<ExecutionResult, EVMError<core::convert::Infallible>> {
333        let ctx = Context::mainnet()
334            .modify_cfg_chained(|c| {
335                if let Some(spec_id) = spec_id {
336                    c.set_spec_and_mainnet_gas_params(spec_id);
337                }
338            })
339            .modify_block_chained(|block| block.gas_limit = 100_000_000)
340            .with_db(CacheDB::<EmptyDB>::default());
341
342        let mut evm = ctx.build_mainnet();
343        evm.transact_commit(
344            TxEnv::builder()
345                .kind(TxKind::Create)
346                .data(bytecode.clone())
347                .build()
348                .unwrap(),
349        )
350    }
351
352    #[test]
353    fn test_eip3860_initcode_size_limit_failure() {
354        let large_bytecode = vec![opcode::STOP; eip3860::MAX_INITCODE_SIZE + 1];
355        let bytecode: Bytes = large_bytecode.into();
356        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
357        assert!(matches!(
358            result,
359            Err(EVMError::Transaction(
360                InvalidTransaction::CreateInitCodeSizeLimit
361            ))
362        ));
363    }
364
365    #[test]
366    fn test_eip3860_initcode_size_limit_success_prague() {
367        let large_bytecode = vec![opcode::STOP; eip3860::MAX_INITCODE_SIZE];
368        let bytecode: Bytes = large_bytecode.into();
369        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
370        assert!(matches!(result, Ok(ExecutionResult::Success { .. })));
371    }
372
373    #[test]
374    fn test_eip7954_initcode_size_limit_failure_amsterdam() {
375        let large_bytecode = vec![opcode::STOP; eip7954::MAX_INITCODE_SIZE + 1];
376        let bytecode: Bytes = large_bytecode.into();
377        let result = deploy_contract(bytecode, Some(SpecId::AMSTERDAM));
378        assert!(matches!(
379            result,
380            Err(EVMError::Transaction(
381                InvalidTransaction::CreateInitCodeSizeLimit
382            ))
383        ));
384    }
385
386    #[test]
387    fn test_eip7954_initcode_size_limit_success_amsterdam() {
388        let large_bytecode = vec![opcode::STOP; eip7954::MAX_INITCODE_SIZE];
389        let bytecode: Bytes = large_bytecode.into();
390        let result = deploy_contract(bytecode, Some(SpecId::AMSTERDAM));
391        assert!(matches!(result, Ok(ExecutionResult::Success { .. })));
392    }
393
394    #[test]
395    fn test_eip7954_initcode_between_old_and_new_limit() {
396        // Size between old limit (0xC000) and new limit (0x20000):
397        // should fail pre-Amsterdam, succeed at Amsterdam
398        let size = eip3860::MAX_INITCODE_SIZE + 1; // 0xC001
399        let large_bytecode = vec![opcode::STOP; size];
400
401        // Pre-Amsterdam (Prague): should fail
402        let bytecode: Bytes = large_bytecode.clone().into();
403        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
404        assert!(matches!(
405            result,
406            Err(EVMError::Transaction(
407                InvalidTransaction::CreateInitCodeSizeLimit
408            ))
409        ));
410
411        // Amsterdam: should succeed
412        let bytecode: Bytes = large_bytecode.into();
413        let result = deploy_contract(bytecode, Some(SpecId::AMSTERDAM));
414        assert!(matches!(result, Ok(ExecutionResult::Success { .. })));
415    }
416
417    #[test]
418    fn test_eip7954_code_size_limit_failure() {
419        // EIP-7954: MAX_CODE_SIZE = 0x10000
420        // use the simplest method to return a contract code size greater than 0x10000
421        // PUSH3 0x10001 (greater than 0x10000) - return size
422        // PUSH1 0x00 - memory position 0
423        // RETURN - return uninitialized memory, will be filled with 0
424        let init_code = vec![
425            0x62, 0x01, 0x00, 0x01, // PUSH3 0x10001 (greater than 0x10000)
426            0x60, 0x00, // PUSH1 0
427            0xf3, // RETURN
428        ];
429        let bytecode: Bytes = init_code.into();
430        let result = deploy_contract(bytecode, Some(SpecId::AMSTERDAM));
431        assert!(
432            matches!(
433                result,
434                Ok(ExecutionResult::Halt {
435                    reason: HaltReason::CreateContractSizeLimit,
436                    ..
437                },)
438            ),
439            "{result:?}"
440        );
441    }
442
443    #[test]
444    fn test_eip170_code_size_limit_failure() {
445        // use the simplest method to return a contract code size greater than 0x6000
446        // PUSH3 0x6001 (greater than 0x6000) - return size
447        // PUSH1 0x00 - memory position 0
448        // RETURN - return uninitialized memory, will be filled with 0
449        let init_code = vec![
450            0x62, 0x00, 0x60, 0x01, // PUSH3 0x6001 (greater than 0x6000)
451            0x60, 0x00, // PUSH1 0
452            0xf3, // RETURN
453        ];
454        let bytecode: Bytes = init_code.into();
455        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
456        assert!(
457            matches!(
458                result,
459                Ok(ExecutionResult::Halt {
460                    reason: HaltReason::CreateContractSizeLimit,
461                    ..
462                },)
463            ),
464            "{result:?}"
465        );
466    }
467
468    #[test]
469    fn test_eip170_code_size_limit_success() {
470        // use the  simplest method to return a contract code size equal to 0x6000
471        // PUSH3 0x6000 - return size
472        // PUSH1 0x00 - memory position 0
473        // RETURN - return uninitialized memory, will be filled with 0
474        let init_code = vec![
475            0x62, 0x00, 0x60, 0x00, // PUSH3 0x6000
476            0x60, 0x00, // PUSH1 0
477            0xf3, // RETURN
478        ];
479        let bytecode: Bytes = init_code.into();
480        let result = deploy_contract(bytecode, None);
481        assert!(matches!(result, Ok(ExecutionResult::Success { .. },)));
482    }
483
484    #[test]
485    fn test_eip170_create_opcode_size_limit_failure() {
486        // 1. create a "factory" contract, which will use the CREATE opcode to create another large contract
487        // 2. because the sub contract exceeds the EIP-170 limit, the CREATE operation should fail
488
489        // the bytecode of the factory contract:
490        // PUSH1 0x01      - the value for MSTORE
491        // PUSH1 0x00      - the memory position
492        // MSTORE          - store a non-zero value at the beginning of memory
493
494        // PUSH3 0x6001    - the return size (exceeds 0x6000)
495        // PUSH1 0x00      - the memory offset
496        // PUSH1 0x00      - the amount of ETH sent
497        // CREATE          - create contract instruction (create contract from current memory)
498
499        // PUSH1 0x00      - the return value storage position
500        // MSTORE          - store the address returned by CREATE to the memory position 0
501        // PUSH1 0x20      - the return size (32 bytes)
502        // PUSH1 0x00      - the return offset
503        // RETURN          - return the result
504
505        let factory_code = vec![
506            // 1. store a non-zero value at the beginning of memory
507            0x60, 0x01, // PUSH1 0x01
508            0x60, 0x00, // PUSH1 0x00
509            0x52, // MSTORE
510            // 2. prepare to create a large contract
511            0x62, 0x00, 0x60, 0x01, // PUSH3 0x6001 (exceeds 0x6000)
512            0x60, 0x00, // PUSH1 0x00 (the memory offset)
513            0x60, 0x00, // PUSH1 0x00 (the amount of ETH sent)
514            0xf0, // CREATE
515            // 3. store the address returned by CREATE to the memory position 0
516            0x60, 0x00, // PUSH1 0x00
517            0x52, // MSTORE (store the address returned by CREATE to the memory position 0)
518            // 4. return the result
519            0x60, 0x20, // PUSH1 0x20 (32 bytes)
520            0x60, 0x00, // PUSH1 0x00
521            0xf3, // RETURN
522        ];
523
524        // deploy factory contract
525        let factory_bytecode: Bytes = factory_code.into();
526        let factory_result = deploy_contract(factory_bytecode, Some(SpecId::PRAGUE))
527            .expect("factory contract deployment failed");
528
529        // get factory contract address
530        let factory_address = match &factory_result {
531            ExecutionResult::Success {
532                output: Output::Create(_, Some(addr)),
533                ..
534            } => *addr,
535            _ => panic!("factory contract deployment failed: {factory_result:?}"),
536        };
537
538        // call factory contract to create sub contract
539        let tx_caller = address!("0x0000000000000000000000000000000000100000");
540        let call_result = Context::mainnet()
541            .with_db(CacheDB::<EmptyDB>::default())
542            .build_mainnet()
543            .transact_commit(
544                TxEnv::builder()
545                    .caller(tx_caller)
546                    .kind(TxKind::Call(factory_address))
547                    .data(Bytes::new())
548                    .build()
549                    .unwrap(),
550            )
551            .expect("call factory contract failed");
552
553        match &call_result {
554            ExecutionResult::Success { output, .. } => match output {
555                Output::Call(bytes) => {
556                    if !bytes.is_empty() {
557                        assert!(
558                            bytes.iter().all(|&b| b == 0),
559                            "When CREATE operation failed, it should return all zero address"
560                        );
561                    }
562                }
563                _ => panic!("unexpected output type"),
564            },
565            _ => panic!("execution result is not Success"),
566        }
567    }
568
569    #[test]
570    fn test_eip170_create_opcode_size_limit_success() {
571        // 1. create a "factory" contract, which will use the CREATE opcode to create another contract
572        // 2. the sub contract generated by the factory contract does not exceed the EIP-170 limit, so it should be created successfully
573
574        // the bytecode of the factory contract:
575        // PUSH1 0x01      - the value for MSTORE
576        // PUSH1 0x00      - the memory position
577        // MSTORE          - store a non-zero value at the beginning of memory
578
579        // PUSH3 0x6000    - the return size (0x6000)
580        // PUSH1 0x00      - the memory offset
581        // PUSH1 0x00      - the amount of ETH sent
582        // CREATE          - create contract instruction (create contract from current memory)
583
584        // PUSH1 0x00      - the return value storage position
585        // MSTORE          - store the address returned by CREATE to the memory position 0
586        // PUSH1 0x20      - the return size (32 bytes)
587        // PUSH1 0x00      - the return offset
588        // RETURN          - return the result
589
590        let factory_code = vec![
591            // 1. store a non-zero value at the beginning of memory
592            0x60, 0x01, // PUSH1 0x01
593            0x60, 0x00, // PUSH1 0x00
594            0x52, // MSTORE
595            // 2. prepare to create a contract
596            0x62, 0x00, 0x60, 0x00, // PUSH3 0x6000 (0x6000)
597            0x60, 0x00, // PUSH1 0x00 (the memory offset)
598            0x60, 0x00, // PUSH1 0x00 (the amount of ETH sent)
599            0xf0, // CREATE
600            // 3. store the address returned by CREATE to the memory position 0
601            0x60, 0x00, // PUSH1 0x00
602            0x52, // MSTORE (store the address returned by CREATE to the memory position 0)
603            // 4. return the result
604            0x60, 0x20, // PUSH1 0x20 (32 bytes)
605            0x60, 0x00, // PUSH1 0x00
606            0xf3, // RETURN
607        ];
608
609        // deploy factory contract
610        let factory_bytecode: Bytes = factory_code.into();
611        let factory_result = deploy_contract(factory_bytecode, Some(SpecId::PRAGUE))
612            .expect("factory contract deployment failed");
613        // get factory contract address
614        let factory_address = match &factory_result {
615            ExecutionResult::Success {
616                output: Output::Create(_, Some(addr)),
617                ..
618            } => *addr,
619            _ => panic!("factory contract deployment failed: {factory_result:?}"),
620        };
621
622        // call factory contract to create sub contract
623        let tx_caller = address!("0x0000000000000000000000000000000000100000");
624        let call_result = Context::mainnet()
625            .with_db(CacheDB::<EmptyDB>::default())
626            .build_mainnet()
627            .transact_commit(
628                TxEnv::builder()
629                    .caller(tx_caller)
630                    .kind(TxKind::Call(factory_address))
631                    .data(Bytes::new())
632                    .build()
633                    .unwrap(),
634            )
635            .expect("call factory contract failed");
636
637        match &call_result {
638            ExecutionResult::Success { output, .. } => {
639                match output {
640                    Output::Call(bytes) => {
641                        // check if CREATE operation is successful (return non-zero address)
642                        if !bytes.is_empty() {
643                            assert!(bytes.iter().any(|&b| b != 0), "create sub contract failed");
644                        }
645                    }
646                    _ => panic!("unexpected output type"),
647                }
648            }
649            _ => panic!("execution result is not Success"),
650        }
651    }
652
653    #[test]
654    fn test_transact_many_with_transaction_index_error() {
655        use context::result::TransactionIndexedError;
656
657        let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
658        let mut evm = ctx.build_mainnet();
659
660        // Create a transaction that will fail (invalid gas limit)
661        let invalid_tx = TxEnv::builder()
662            .gas_limit(0) // This will cause a validation error
663            .build()
664            .unwrap();
665
666        // Create a valid transaction
667        let valid_tx = TxEnv::builder().gas_limit(100000).build().unwrap();
668
669        // Test that the first transaction fails with index 0
670        let result = evm.transact_many([invalid_tx.clone()].into_iter());
671        assert!(matches!(
672            result,
673            Err(TransactionIndexedError {
674                transaction_index: 0,
675                ..
676            })
677        ));
678
679        // Test that the second transaction fails with index 1
680        let result = evm.transact_many([valid_tx, invalid_tx].into_iter());
681        assert!(matches!(
682            result,
683            Err(TransactionIndexedError {
684                transaction_index: 1,
685                ..
686            })
687        ));
688    }
689
690    #[test]
691    fn test_transact_many_success() {
692        use primitives::{address, U256};
693
694        let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
695        let mut evm = ctx.build_mainnet();
696
697        // Add balance to the caller account
698        let caller = address!("0x0000000000000000000000000000000000000001");
699        evm.db_mut().insert_account_info(
700            caller,
701            AccountInfo::new(
702                U256::from(1000000000000000000u64),
703                0,
704                B256::ZERO,
705                Bytecode::new(),
706            ),
707        );
708
709        // Create valid transactions with proper data
710        let tx1 = TxEnv::builder()
711            .caller(caller)
712            .gas_limit(100000)
713            .gas_price(20_000_000_000u128)
714            .nonce(0)
715            .build()
716            .unwrap();
717
718        let tx2 = TxEnv::builder()
719            .caller(caller)
720            .gas_limit(100000)
721            .gas_price(20_000_000_000u128)
722            .nonce(1)
723            .build()
724            .unwrap();
725
726        // Test that all transactions succeed
727        let result = evm.transact_many([tx1, tx2].into_iter());
728        if let Err(e) = &result {
729            println!("Error: {e:?}");
730        }
731        let outputs = result.expect("All transactions should succeed");
732        assert_eq!(outputs.len(), 2);
733    }
734
735    #[test]
736    fn test_transact_many_finalize_with_error() {
737        use context::result::TransactionIndexedError;
738
739        let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
740        let mut evm = ctx.build_mainnet();
741
742        // Create transactions where the second one fails
743        let valid_tx = TxEnv::builder().gas_limit(100000).build().unwrap();
744
745        let invalid_tx = TxEnv::builder()
746            .gas_limit(0) // This will cause a validation error
747            .build()
748            .unwrap();
749
750        // Test that transact_many_finalize returns the error with correct index
751        let result = evm.transact_many_finalize([valid_tx, invalid_tx].into_iter());
752        assert!(matches!(
753            result,
754            Err(TransactionIndexedError {
755                transaction_index: 1,
756                ..
757            })
758        ));
759    }
760
761    #[test]
762    fn test_transact_many_commit_with_error() {
763        use context::result::TransactionIndexedError;
764
765        let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
766        let mut evm = ctx.build_mainnet();
767
768        // Create transactions where the first one fails
769        let invalid_tx = TxEnv::builder()
770            .gas_limit(0) // This will cause a validation error
771            .build()
772            .unwrap();
773
774        let valid_tx = TxEnv::builder().gas_limit(100000).build().unwrap();
775
776        // Test that transact_many_commit returns the error with correct index
777        let result = evm.transact_many_commit([invalid_tx, valid_tx].into_iter());
778        assert!(matches!(
779            result,
780            Err(TransactionIndexedError {
781                transaction_index: 0,
782                ..
783            })
784        ));
785    }
786}