referrerpolicy=no-referrer-when-downgrade

pallet_revive/evm/
runtime.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17//! Runtime types for integrating `pallet-revive` with the EVM.
18use crate::{
19	AccountIdOf, AddressMapper, BalanceOf, CallOf, Config, LOG_TARGET, Pallet, Zero,
20	evm::{
21		CreateCallMode,
22		api::{GenericTransaction, TransactionSigned},
23		fees::InfoT,
24	},
25};
26use codec::{Decode, DecodeWithMemTracking, Encode};
27use frame_support::{
28	dispatch::{DispatchInfo, GetDispatchInfo},
29	traits::{
30		InherentBuilder, IsSubType, SignedTransactionBuilder,
31		fungible::Balanced,
32		tokens::{Fortitude, Precision, Preservation},
33	},
34};
35use pallet_transaction_payment::Config as TxConfig;
36use scale_info::{StaticTypeInfo, TypeInfo};
37use sp_core::U256;
38use sp_runtime::{
39	Debug, OpaqueExtrinsic, Weight,
40	generic::{self, CheckedExtrinsic, ExtrinsicFormat},
41	traits::{
42		Checkable, ExtrinsicCall, ExtrinsicLike, ExtrinsicMetadata, LazyExtrinsic, Pipeline,
43		TransactionExtension,
44	},
45	transaction_validity::{InvalidTransaction, TransactionValidityError},
46};
47
48/// Used to set the weight limit argument of a `eth_call` or `eth_instantiate_with_code` call.
49pub trait SetWeightLimit {
50	/// Set the weight limit of this call.
51	///
52	/// Returns the replaced weight.
53	fn set_weight_limit(&mut self, weight_limit: Weight) -> Weight;
54}
55
56/// Wraps [`generic::UncheckedExtrinsic`] to support checking unsigned
57/// [`crate::Call::eth_transact`] extrinsic.
58#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug)]
59pub struct UncheckedExtrinsic<Address, Signature, E: EthExtra>(
60	pub  generic::UncheckedExtrinsic<
61		Address,
62		CallOf<E::Config>,
63		Signature,
64		E::ExtensionV0,
65		E::ExtensionOtherVersions,
66	>,
67);
68
69impl<Address, Signature, E: EthExtra> TypeInfo for UncheckedExtrinsic<Address, Signature, E>
70where
71	Address: StaticTypeInfo,
72	Signature: StaticTypeInfo,
73	E::ExtensionV0: StaticTypeInfo,
74{
75	type Identity = generic::UncheckedExtrinsic<
76		Address,
77		CallOf<E::Config>,
78		Signature,
79		E::ExtensionV0,
80		E::ExtensionOtherVersions,
81	>;
82	fn type_info() -> scale_info::Type {
83		generic::UncheckedExtrinsic::<
84			Address,
85			CallOf<E::Config>,
86			Signature,
87			E::ExtensionV0,
88			E::ExtensionOtherVersions,
89		>::type_info()
90	}
91}
92
93impl<Address, Signature, E: EthExtra>
94	From<
95		generic::UncheckedExtrinsic<
96			Address,
97			CallOf<E::Config>,
98			Signature,
99			E::ExtensionV0,
100			E::ExtensionOtherVersions,
101		>,
102	> for UncheckedExtrinsic<Address, Signature, E>
103{
104	fn from(
105		utx: generic::UncheckedExtrinsic<
106			Address,
107			CallOf<E::Config>,
108			Signature,
109			E::ExtensionV0,
110			E::ExtensionOtherVersions,
111		>,
112	) -> Self {
113		Self(utx)
114	}
115}
116
117impl<Address: TypeInfo, Signature: TypeInfo, E: EthExtra> ExtrinsicLike
118	for UncheckedExtrinsic<Address, Signature, E>
119{
120	fn is_bare(&self) -> bool {
121		ExtrinsicLike::is_bare(&self.0)
122	}
123}
124
125impl<Address, Signature, E: EthExtra> ExtrinsicMetadata
126	for UncheckedExtrinsic<Address, Signature, E>
127{
128	const VERSIONS: &'static [u8] = generic::UncheckedExtrinsic::<
129		Address,
130		CallOf<E::Config>,
131		Signature,
132		E::ExtensionV0,
133		E::ExtensionOtherVersions,
134	>::VERSIONS;
135	type TransactionExtensionPipelines = <generic::UncheckedExtrinsic<
136		Address,
137		CallOf<E::Config>,
138		Signature,
139		E::ExtensionV0,
140		E::ExtensionOtherVersions,
141	> as ExtrinsicMetadata>::TransactionExtensionPipelines;
142}
143
144impl<Address: TypeInfo, Signature: TypeInfo, E: EthExtra> ExtrinsicCall
145	for UncheckedExtrinsic<Address, Signature, E>
146{
147	type Call = CallOf<E::Config>;
148
149	fn call(&self) -> &Self::Call {
150		self.0.call()
151	}
152
153	fn into_call(self) -> Self::Call {
154		self.0.into_call()
155	}
156}
157
158impl<LookupSource, Signature, E, Lookup> Checkable<Lookup>
159	for UncheckedExtrinsic<LookupSource, Signature, E>
160where
161	E: EthExtra,
162	Self: Encode,
163	<E::Config as frame_system::Config>::Nonce: TryFrom<U256>,
164	CallOf<E::Config>: SetWeightLimit,
165	// required by Checkable for `generic::UncheckedExtrinsic`
166	generic::UncheckedExtrinsic<
167		LookupSource,
168		CallOf<E::Config>,
169		Signature,
170		E::ExtensionV0,
171		E::ExtensionOtherVersions,
172	>: Checkable<
173			Lookup,
174			Checked = CheckedExtrinsic<
175				AccountIdOf<E::Config>,
176				CallOf<E::Config>,
177				E::ExtensionV0,
178				E::ExtensionOtherVersions,
179			>,
180		>,
181{
182	type Checked = CheckedExtrinsic<
183		AccountIdOf<E::Config>,
184		CallOf<E::Config>,
185		E::ExtensionV0,
186		E::ExtensionOtherVersions,
187	>;
188
189	fn check(self, lookup: &Lookup) -> Result<Self::Checked, TransactionValidityError> {
190		if !self.0.is_signed() {
191			if let Some(crate::Call::eth_transact { payload }) = self.0.function.is_sub_type() {
192				log::trace!(
193					target: LOG_TARGET,
194					"eth_transact substrate tx hash: 0x{}",
195					sp_core::hexdisplay::HexDisplay::from(&sp_crypto_hashing::blake2_256(&self.encode())),
196				);
197				let checked = E::try_into_checked_extrinsic(payload, self.encoded_size())?;
198				return Ok(checked);
199			};
200		}
201		self.0.check(lookup)
202	}
203
204	#[cfg(feature = "try-runtime")]
205	fn unchecked_into_checked_i_know_what_i_am_doing(
206		self,
207		lookup: &Lookup,
208	) -> Result<Self::Checked, TransactionValidityError> {
209		self.0.unchecked_into_checked_i_know_what_i_am_doing(lookup)
210	}
211}
212
213impl<Address, Signature, E: EthExtra> GetDispatchInfo
214	for UncheckedExtrinsic<Address, Signature, E>
215{
216	fn get_dispatch_info(&self) -> DispatchInfo {
217		self.0.get_dispatch_info()
218	}
219}
220
221impl<Address: Encode, Signature: Encode, E: EthExtra> serde::Serialize
222	for UncheckedExtrinsic<Address, Signature, E>
223{
224	fn serialize<S>(&self, seq: S) -> Result<S::Ok, S::Error>
225	where
226		S: ::serde::Serializer,
227	{
228		self.0.serialize(seq)
229	}
230}
231
232impl<'a, Address: DecodeWithMemTracking, Signature: DecodeWithMemTracking, E: EthExtra>
233	serde::Deserialize<'a> for UncheckedExtrinsic<Address, Signature, E>
234{
235	fn deserialize<D>(de: D) -> Result<Self, D::Error>
236	where
237		D: serde::Deserializer<'a>,
238	{
239		let r = sp_core::bytes::deserialize(de)?;
240		Decode::decode(&mut &r[..])
241			.map_err(|e| serde::de::Error::custom(alloc::format!("Decode error: {}", e)))
242	}
243}
244
245impl<Address, Signature, E: EthExtra> SignedTransactionBuilder
246	for UncheckedExtrinsic<Address, Signature, E>
247where
248	Address: TypeInfo,
249	Signature: TypeInfo,
250	E::ExtensionV0: TypeInfo,
251{
252	type Address = Address;
253	type Signature = Signature;
254	type Extension = E::ExtensionV0;
255
256	fn new_signed_transaction(
257		call: Self::Call,
258		signed: Address,
259		signature: Signature,
260		tx_ext: E::ExtensionV0,
261	) -> Self {
262		generic::UncheckedExtrinsic::new_signed(call, signed, signature, tx_ext).into()
263	}
264}
265
266impl<Address, Signature, E: EthExtra> InherentBuilder for UncheckedExtrinsic<Address, Signature, E>
267where
268	Address: TypeInfo,
269	Signature: TypeInfo,
270	E::ExtensionV0: TypeInfo,
271{
272	fn new_inherent(call: Self::Call) -> Self {
273		generic::UncheckedExtrinsic::new_bare(call).into()
274	}
275}
276
277impl<Address, Signature, E: EthExtra> From<UncheckedExtrinsic<Address, Signature, E>>
278	for OpaqueExtrinsic
279where
280	Address: Encode,
281	Signature: Encode,
282	E::ExtensionV0: Encode,
283{
284	fn from(extrinsic: UncheckedExtrinsic<Address, Signature, E>) -> Self {
285		extrinsic.0.into()
286	}
287}
288
289impl<Address, Signature, E: EthExtra> LazyExtrinsic for UncheckedExtrinsic<Address, Signature, E>
290where
291	generic::UncheckedExtrinsic<
292		Address,
293		CallOf<E::Config>,
294		Signature,
295		E::ExtensionV0,
296		E::ExtensionOtherVersions,
297	>: LazyExtrinsic,
298{
299	fn decode_unprefixed(data: &[u8]) -> Result<Self, codec::Error> {
300		Ok(Self(LazyExtrinsic::decode_unprefixed(data)?))
301	}
302}
303
304/// EthExtra convert an unsigned [`crate::Call::eth_transact`] into a [`CheckedExtrinsic`].
305pub trait EthExtra {
306	/// The Runtime configuration.
307	type Config: Config + TxConfig;
308
309	/// The Runtime's transaction extension version 0.
310	/// It should include at least:
311	/// - [`frame_system::CheckNonce`] to ensure that the nonce from the Ethereum transaction is
312	///   correct.
313	type ExtensionV0: TransactionExtension<CallOf<Self::Config>>;
314
315	/// The Runtime's transaction extension versions other than 0.
316	///
317	/// Use [`sp_runtime::traits::InvalidVersion`] if no other versions should be supported.
318	type ExtensionOtherVersions: Pipeline<CallOf<Self::Config>>;
319
320	/// Get the transaction extension to apply to an unsigned [`crate::Call::eth_transact`]
321	/// extrinsic.
322	///
323	/// # Parameters
324	/// - `nonce`: The nonce extracted from the Ethereum transaction.
325	/// - `tip`: The transaction tip calculated from the Ethereum transaction.
326	fn get_eth_extension(
327		nonce: <Self::Config as frame_system::Config>::Nonce,
328		tip: BalanceOf<Self::Config>,
329	) -> Self::ExtensionV0;
330
331	/// Convert the unsigned [`crate::Call::eth_transact`] into a [`CheckedExtrinsic`].
332	/// and ensure that the fees from the Ethereum transaction correspond to the fees computed from
333	/// the encoded_len and the injected weight_limit.
334	///
335	/// # Parameters
336	/// - `payload`: The RLP-encoded Ethereum transaction.
337	/// - `encoded_len`: The encoded length of the extrinsic.
338	fn try_into_checked_extrinsic(
339		payload: &[u8],
340		encoded_len: usize,
341	) -> Result<
342		CheckedExtrinsic<
343			AccountIdOf<Self::Config>,
344			CallOf<Self::Config>,
345			Self::ExtensionV0,
346			Self::ExtensionOtherVersions,
347		>,
348		InvalidTransaction,
349	>
350	where
351		<Self::Config as frame_system::Config>::Nonce: TryFrom<U256>,
352		CallOf<Self::Config>: SetWeightLimit,
353	{
354		let tx = TransactionSigned::decode(&payload).map_err(|err| {
355			log::debug!(target: LOG_TARGET, "Failed to decode transaction: {err:?}");
356			InvalidTransaction::Call
357		})?;
358
359		// Check transaction type and reject unsupported transaction types
360		match &tx {
361			crate::evm::api::TransactionSigned::Transaction1559Signed(_) |
362			crate::evm::api::TransactionSigned::Transaction2930Signed(_) |
363			crate::evm::api::TransactionSigned::TransactionLegacySigned(_) |
364			crate::evm::api::TransactionSigned::Transaction7702Signed(_) => {
365				// Supported transaction types, continue processing
366			},
367			crate::evm::api::TransactionSigned::Transaction4844Signed(_) => {
368				log::debug!(target: LOG_TARGET, "EIP-4844 transactions are not supported");
369				return Err(InvalidTransaction::Call);
370			},
371		}
372
373		let signer_addr = tx.recover_eth_address().map_err(|err| {
374			log::debug!(target: LOG_TARGET, "Failed to recover signer: {err:?}");
375			InvalidTransaction::BadProof
376		})?;
377
378		let signer = <Self::Config as Config>::AddressMapper::to_fallback_account_id(&signer_addr);
379		let base_fee = <Pallet<Self::Config>>::evm_base_fee();
380		let tx = GenericTransaction::from_signed(tx, base_fee, None);
381		let nonce = tx.nonce.unwrap_or_default().try_into().map_err(|_| {
382			log::debug!(target: LOG_TARGET, "Failed to convert nonce");
383			InvalidTransaction::Call
384		})?;
385
386		log::debug!(target: LOG_TARGET, "Decoded Ethereum transaction with signer: {signer_addr:?} nonce: {nonce:?}");
387		log::trace!(target: LOG_TARGET, "Decoded Ethereum transaction was: {tx:?}");
388		let call_info = tx.into_call::<Self::Config>(CreateCallMode::ExtrinsicExecution(
389			encoded_len as u32,
390			payload.to_vec(),
391		))?;
392		let storage_credit = <Self::Config as Config>::Currency::withdraw(
393			&signer,
394			call_info.storage_deposit,
395			Precision::Exact,
396			Preservation::Preserve,
397			Fortitude::Polite,
398		).map_err(|_| {
399			log::debug!(target: LOG_TARGET, "Not enough balance to hold additional storage deposit of {:?}", call_info.storage_deposit);
400			InvalidTransaction::Payment
401		})?;
402		<Self::Config as Config>::FeeInfo::deposit_txfee(storage_credit);
403
404		crate::tracing::if_tracing(|tracer| {
405			tracer.watch_address(&Pallet::<Self::Config>::block_author());
406			tracer.watch_address(&signer_addr);
407		});
408
409		log::debug!(target: LOG_TARGET, "\
410			Created checked Ethereum transaction with: \
411			from={signer_addr:?} \
412			eth_gas={} \
413			encoded_len={encoded_len} \
414			tx_fee={:?} \
415			storage_deposit={:?} \
416			weight_limit={} \
417			nonce={nonce:?}\
418			",
419			call_info.eth_gas_limit,
420			call_info.tx_fee,
421			call_info.storage_deposit,
422			call_info.weight_limit,
423		);
424
425		// We can't calculate a tip because it needs to be based on the actual gas used which we
426		// cannot know pre-dispatch. Hence we never supply a tip here or it would be way too high.
427		Ok(CheckedExtrinsic {
428			format: ExtrinsicFormat::Signed(
429				signer.into(),
430				Self::get_eth_extension(nonce, Zero::zero()),
431			),
432			function: call_info.call,
433		})
434	}
435}
436
437#[cfg(test)]
438mod test {
439	use super::*;
440	use crate::{
441		EthTransactInfo, RUNTIME_PALLETS_ADDR, Weight,
442		evm::*,
443		test_utils::*,
444		tests::{
445			Address, ExtBuilder, RuntimeCall, RuntimeOrigin, SignedExtra, Test, TestSigner,
446			UncheckedExtrinsic,
447		},
448	};
449	use frame_support::traits::fungible::Mutate;
450	use pallet_revive_fixtures::compile_module;
451	use sp_runtime::traits::{self, Checkable, DispatchTransaction, LookupError};
452
453	type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
454
455	struct TestContext;
456
457	impl traits::Lookup for TestContext {
458		type Source = Address;
459		type Target = AccountIdOf<Test>;
460		fn lookup(&self, s: Self::Source) -> Result<Self::Target, LookupError> {
461			match s {
462				Self::Source::Id(id) => Ok(id),
463				_ => Err(LookupError),
464			}
465		}
466	}
467
468	/// A builder for creating an unchecked extrinsic, and test that the check function works.
469	#[derive(Clone)]
470	struct UncheckedExtrinsicBuilder {
471		tx: GenericTransaction,
472		before_validate: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
473		dry_run: Option<EthTransactInfo<BalanceOf<Test>>>,
474	}
475
476	impl UncheckedExtrinsicBuilder {
477		/// Create a new builder with default values.
478		fn new() -> Self {
479			Self {
480				tx: GenericTransaction {
481					from: Some(Account::default().address()),
482					chain_id: Some(<Test as Config>::ChainId::get().into()),
483					..Default::default()
484				},
485				before_validate: None,
486				dry_run: None,
487			}
488		}
489
490		fn data(mut self, data: Vec<u8>) -> Self {
491			self.tx.input = Bytes(data).into();
492			self
493		}
494
495		fn fund_account(account: &Account) {
496			let _ = <Test as Config>::Currency::set_balance(
497				&account.substrate_account(),
498				100_000_000_000_000,
499			);
500		}
501
502		fn estimate_gas(&mut self) {
503			let account = Account::default();
504			Self::fund_account(&account);
505
506			let dry_run =
507				crate::Pallet::<Test>::dry_run_eth_transact(self.tx.clone(), None, true, None);
508			let base_fee = <Pallet<Test>>::evm_base_fee();
509			self.tx.gas_price = Some(base_fee);
510			self.tx.max_fee_per_gas = Some(base_fee);
511
512			match dry_run {
513				Ok(dry_run) => {
514					self.tx.gas = Some(dry_run.eth_gas);
515					self.dry_run = Some(dry_run);
516				},
517				Err(err) => {
518					log::debug!(target: LOG_TARGET, "Failed to estimate gas: {:?}", err);
519				},
520			}
521		}
522
523		/// Create a new builder with a call to the given address.
524		fn call_with(dest: H160) -> Self {
525			let mut builder = Self::new();
526			builder.tx.to = Some(dest);
527			builder
528		}
529
530		/// Create a new builder with a call that includes an EIP-7702 authorization list.
531		fn call_with_authorization(
532			dest: H160,
533			authorization_list: Vec<AuthorizationListEntry>,
534		) -> Self {
535			let mut builder = Self::new();
536			builder.tx.to = Some(dest);
537			builder.tx.r#type = Some(TYPE_EIP7702.into());
538			builder.tx.authorization_list = authorization_list;
539			builder
540		}
541
542		/// Create a new builder with an instantiate call.
543		fn instantiate_with(code: Vec<u8>, data: Vec<u8>) -> Self {
544			let mut builder = Self::new();
545			builder.tx.input = Bytes(code.into_iter().chain(data.into_iter()).collect()).into();
546			builder
547		}
548
549		/// Set before_validate function.
550		fn before_validate(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
551			self.before_validate = Some(std::sync::Arc::new(f));
552			self
553		}
554
555		fn check(
556			self,
557		) -> Result<
558			(u32, RuntimeCall, SignedExtra, GenericTransaction, Weight, TransactionSigned),
559			TransactionValidityError,
560		> {
561			self.mutate_estimate_and_check(Box::new(|_| ()))
562		}
563
564		/// Call `check` on the unchecked extrinsic, and `pre_dispatch` on the signed extension.
565		fn mutate_estimate_and_check(
566			mut self,
567			f: Box<dyn FnOnce(&mut GenericTransaction) -> ()>,
568		) -> Result<
569			(u32, RuntimeCall, SignedExtra, GenericTransaction, Weight, TransactionSigned),
570			TransactionValidityError,
571		> {
572			ExtBuilder::default().build().execute_with(|| self.estimate_gas());
573			ExtBuilder::default().build().execute_with(|| {
574				f(&mut self.tx);
575				let UncheckedExtrinsicBuilder { tx, before_validate, .. } = self.clone();
576
577				// Fund the account.
578				let account = Account::default();
579				Self::fund_account(&account);
580
581				let signed_transaction =
582					account.sign_transaction(tx.clone().try_into_unsigned().unwrap());
583				let call = RuntimeCall::Contracts(crate::Call::eth_transact {
584					payload: signed_transaction.signed_payload().clone(),
585				});
586
587				let uxt: UncheckedExtrinsic = generic::UncheckedExtrinsic::new_bare(call).into();
588				let encoded_len = uxt.encoded_size();
589				let result: CheckedExtrinsic<_, _, _> = uxt.check(&TestContext {})?;
590				let (account_id, extra): (AccountId32, SignedExtra) = match result.format {
591					ExtrinsicFormat::Signed(signer, extra) => (signer, extra),
592					_ => unreachable!(),
593				};
594
595				before_validate.map(|f| f());
596				extra.clone().validate_and_prepare(
597					RuntimeOrigin::signed(account_id),
598					&result.function,
599					&result.function.get_dispatch_info(),
600					encoded_len,
601					0,
602				)?;
603
604				Ok((
605					encoded_len as u32,
606					result.function,
607					extra,
608					tx,
609					self.dry_run.unwrap().weight_required,
610					signed_transaction,
611				))
612			})
613		}
614	}
615
616	#[test]
617	fn check_eth_transact_call_works() {
618		let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
619		let (expected_encoded_len, call, _, tx, weight_required, signed_transaction) =
620			builder.check().unwrap();
621		let expected_effective_gas_price =
622			ExtBuilder::default().build().execute_with(|| Pallet::<Test>::evm_base_fee());
623
624		match call {
625			RuntimeCall::Contracts(crate::Call::eth_call::<Test> {
626				dest,
627				value,
628				weight_limit,
629				data,
630				transaction_encoded,
631				effective_gas_price,
632				encoded_len,
633				..
634			}) if dest == tx.to.unwrap() &&
635				value == tx.value.unwrap_or_default().as_u64().into() &&
636				data == tx.input.to_vec() &&
637				transaction_encoded == signed_transaction.signed_payload() &&
638				effective_gas_price == expected_effective_gas_price =>
639			{
640				assert_eq!(encoded_len, expected_encoded_len);
641				assert!(
642					weight_limit.all_gte(weight_required),
643					"Assert failed: weight_limit={weight_limit:?} >= weight_required={weight_required:?}"
644				);
645			},
646			_ => panic!("Call does not match."),
647		}
648	}
649
650	#[test]
651	fn check_eth_transact_instantiate_works() {
652		let (expected_code, _) = compile_module("dummy").unwrap();
653		let expected_data = vec![];
654		let builder = UncheckedExtrinsicBuilder::instantiate_with(
655			expected_code.clone(),
656			expected_data.clone(),
657		);
658		let (expected_encoded_len, call, _, tx, weight_required, signed_transaction) =
659			builder.check().unwrap();
660		let expected_effective_gas_price =
661			ExtBuilder::default().build().execute_with(|| Pallet::<Test>::evm_base_fee());
662		let expected_value = tx.value.unwrap_or_default().as_u64().into();
663
664		match call {
665			RuntimeCall::Contracts(crate::Call::eth_instantiate_with_code::<Test> {
666				value,
667				weight_limit,
668				code,
669				data,
670				transaction_encoded,
671				effective_gas_price,
672				encoded_len,
673				..
674			}) if value == expected_value &&
675				code == expected_code &&
676				data == expected_data &&
677				transaction_encoded == signed_transaction.signed_payload() &&
678				effective_gas_price == expected_effective_gas_price =>
679			{
680				assert_eq!(encoded_len, expected_encoded_len);
681				assert!(
682					weight_limit.all_gte(weight_required),
683					"Assert failed: weight_limit={weight_limit:?} >= weight_required={weight_required:?}"
684				);
685			},
686			_ => panic!("Call does not match."),
687		}
688	}
689
690	#[test]
691	fn check_eth_transact_nonce_works() {
692		let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
693
694		assert_eq!(
695			builder.mutate_estimate_and_check(Box::new(|tx| tx.nonce = Some(1u32.into()))),
696			Err(TransactionValidityError::Invalid(InvalidTransaction::Future))
697		);
698
699		let builder =
700			UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20])).before_validate(|| {
701				<crate::System<Test>>::inc_account_nonce(Account::default().substrate_account());
702			});
703
704		assert_eq!(
705			builder.check(),
706			Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))
707		);
708	}
709
710	#[test]
711	fn check_eth_transact_chain_id_works() {
712		let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
713
714		assert_eq!(
715			builder.mutate_estimate_and_check(Box::new(|tx| tx.chain_id = Some(42.into()))),
716			Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
717		);
718	}
719
720	#[test]
721	fn check_instantiate_data() {
722		let code: Vec<u8> = polkavm_common::program::BLOB_MAGIC
723			.into_iter()
724			.chain(b"invalid code".iter().cloned())
725			.collect();
726		let data = vec![1];
727
728		let builder = UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone());
729
730		// Fail because the tx input fail to get the blob length
731		assert_eq!(
732			builder.check(),
733			Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
734		);
735	}
736
737	#[test]
738	fn check_transaction_fees() {
739		let scenarios: Vec<(_, Box<dyn FnOnce(&mut GenericTransaction)>, _)> = vec![
740			(
741				"Eth fees too low",
742				Box::new(|tx| {
743					tx.gas_price = Some(100u64.into());
744				}),
745				InvalidTransaction::Payment,
746			),
747			(
748				"Gas fees too low",
749				Box::new(|tx| {
750					tx.gas = Some(tx.gas.unwrap() / 2);
751				}),
752				InvalidTransaction::Payment,
753			),
754		];
755
756		for (msg, update_tx, err) in scenarios {
757			let res = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]))
758				.mutate_estimate_and_check(update_tx);
759
760			assert_eq!(res, Err(TransactionValidityError::Invalid(err)), "{}", msg);
761		}
762	}
763
764	#[test]
765	fn eth_pre_dispatch_weight_matches_check_weight_booking() {
766		let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
767		let (encoded_len, call, _, _, _, signed_transaction) = builder.check().unwrap();
768
769		ExtBuilder::default().build().execute_with(|| {
770			let reported =
771				Pallet::<Test>::eth_pre_dispatch_weight(signed_transaction.signed_payload())
772					.unwrap();
773			let info = <Test as Config>::FeeInfo::dispatch_info(&call);
774			let expected = frame_system::calculate_consumed_extrinsic_weight::<CallOf<Test>>(
775				&<Test as frame_system::Config>::BlockWeights::get(),
776				&info,
777				encoded_len as usize,
778			);
779
780			assert_eq!(reported, expected);
781		});
782	}
783
784	#[test]
785	fn check_transaction_tip() {
786		let (code, _) = compile_module("dummy").unwrap();
787		// create some dummy data to increase the gas fee
788		let data = vec![42u8; crate::limits::CALLDATA_BYTES as usize];
789		let (_, _, extra, _tx, _gas_required, _) =
790			UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone())
791				.mutate_estimate_and_check(Box::new(|tx| {
792					tx.gas_price = Some(tx.gas_price.unwrap() * 103 / 100);
793					log::debug!(target: LOG_TARGET, "Gas price: {:?}", tx.gas_price);
794				}))
795				.unwrap();
796
797		assert_eq!(U256::from(extra.1.tip()), 0u32.into());
798	}
799
800	#[test]
801	fn check_runtime_pallets_addr_works() {
802		let remark: CallOf<Test> =
803			frame_system::Call::remark { remark: b"Hello, world!".to_vec() }.into();
804
805		let builder =
806			UncheckedExtrinsicBuilder::call_with(RUNTIME_PALLETS_ADDR).data(remark.encode());
807		let (_, call, _, _, _, _) = builder.check().unwrap();
808
809		match call {
810			RuntimeCall::Contracts(crate::Call::eth_substrate_call {
811				call: inner_call, ..
812			}) => {
813				assert_eq!(*inner_call, remark);
814			},
815			_ => panic!("Expected the RuntimeCall::Contracts variant, got: {:?}", call),
816		}
817	}
818
819	/// The raw bytes seen in this test is of a deployment transaction from [eip-2470] which publish
820	/// a contract at a predicable address on any chain that it's run on. We use these bytes to test
821	/// that if we were to run this transaction on pallet-revive that it would run and also produce
822	/// a contract at the address described in the EIP.
823	///
824	/// Note: the linked EIP is not an EIP for Nick's method, it's just an EIP that makes use of
825	/// Nick's method.
826	///
827	/// [eip-2470]: https://eips.ethereum.org/EIPS/eip-2470
828	#[test]
829	fn contract_deployment_with_nick_method_works() {
830		// Arrange
831		let raw_transaction_bytes = alloy_core::hex!(
832			"0xf9016c8085174876e8008303c4d88080b90154608060405234801561001057600080fd5b50610134806100206000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80634af63f0214602d575b600080fd5b60cf60048036036040811015604157600080fd5b810190602081018135640100000000811115605b57600080fd5b820183602082011115606c57600080fd5b80359060200191846001830284011164010000000083111715608d57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550509135925060eb915050565b604080516001600160a01b039092168252519081900360200190f35b6000818351602085016000f5939250505056fea26469706673582212206b44f8a82cb6b156bfcc3dc6aadd6df4eefd204bc928a4397fd15dacf6d5320564736f6c634300060200331b83247000822470"
833		);
834
835		let mut signed_transaction = TransactionSigned::decode(raw_transaction_bytes.as_slice())
836			.expect("Invalid raw transaction bytes");
837		if let TransactionSigned::TransactionLegacySigned(ref mut legacy_transaction) =
838			signed_transaction
839		{
840			legacy_transaction.transaction_legacy_unsigned.gas =
841				U256::from_dec_str("3750815700000").unwrap();
842		}
843		let generic_transaction = GenericTransaction::from_signed(
844			signed_transaction.clone(),
845			ExtBuilder::default().build().execute_with(|| Pallet::<Test>::evm_base_fee()),
846			None,
847		);
848
849		let unchecked_extrinsic_builder = UncheckedExtrinsicBuilder {
850			tx: generic_transaction,
851			before_validate: None,
852			dry_run: None,
853		};
854
855		// Act
856		let eth_transact_result = unchecked_extrinsic_builder.check();
857
858		// Assert
859		let (
860			_encoded_len,
861			_function,
862			_extra,
863			generic_transaction,
864			_gas_required,
865			_signed_transaction,
866		) = eth_transact_result.expect("eth_transact failed");
867		assert!(
868			generic_transaction.chain_id.is_none(),
869			"Chain Id in the generic transaction is not None"
870		);
871	}
872
873	#[test]
874	fn check_eth_transact_7702_call_works() {
875		let chain_id = U256::from(<Test as Config>::ChainId::get());
876		let signer = TestSigner::new(&[0xCC; 32]);
877		let auth = signer.sign_authorization(chain_id, H160::from([1u8; 20]), U256::zero());
878
879		let builder =
880			UncheckedExtrinsicBuilder::call_with_authorization(H160::from([1u8; 20]), vec![auth]);
881		let (expected_encoded_len, call, _, tx, weight_required, _) = builder.check().unwrap();
882
883		match call {
884			RuntimeCall::Contracts(crate::Call::eth_call::<Test> {
885				dest,
886				weight_limit,
887				encoded_len,
888				authorization_list,
889				..
890			}) if dest == tx.to.unwrap() => {
891				assert_eq!(encoded_len, expected_encoded_len);
892				assert_eq!(authorization_list.len(), 1);
893				assert!(
894					weight_limit.all_gte(weight_required),
895					"weight_limit={weight_limit:?} >= weight_required={weight_required:?}"
896				);
897			},
898			_ => panic!("Call does not match."),
899		}
900	}
901
902	#[test]
903	fn check_eth_transact_7702_insufficient_gas() {
904		use crate::evm::fees::InfoT;
905
906		let chain_id = U256::from(<Test as Config>::ChainId::get());
907		let dest = H160::from([1u8; 20]);
908		let auths: Vec<_> = (0..3u8)
909			.map(|i| {
910				let mut seed = [0u8; 32];
911				seed[0] = 0xCC + i;
912				TestSigner::new(&seed).sign_authorization(chain_id, dest, U256::zero())
913			})
914			.collect();
915		let num_auths = auths.len() as u128;
916		let gas_scale = <Test as Config>::GasScale::get() as u128;
917		let auth_cost = Pallet::<Test>::worst_case_delegation_deposit().saturating_mul(num_auths);
918
919		// With estimated gas the transaction is valid
920		UncheckedExtrinsicBuilder::call_with_authorization(dest, auths.clone())
921			.check()
922			.expect("estimated gas should pass validation");
923
924		// Gas covering auth deposits + base transaction overhead is sufficient.
925		UncheckedExtrinsicBuilder::call_with_authorization(dest, auths.clone())
926			.mutate_estimate_and_check(Box::new(move |tx| {
927				let mut call_info = tx
928					.clone()
929					.into_call::<Test>(CreateCallMode::DryRun)
930					.expect("dry run should succeed");
931				let base_info = <Test as Config>::FeeInfo::base_dispatch_info(&mut call_info.call);
932				let overhead = <Test as Config>::FeeInfo::fixed_fee(call_info.encoded_len as u32) +
933					<Test as Config>::FeeInfo::weight_to_fee(&base_info.total_weight());
934				let sufficient_gas = 1 + (auth_cost + overhead) / gas_scale;
935				tx.gas = Some(U256::from(sufficient_gas));
936			}))
937			.expect("gas covering auth deposits + overhead should pass");
938
939		// Gas covering only auth deposits (without overhead) is insufficient:
940		let res = UncheckedExtrinsicBuilder::call_with_authorization(dest, auths)
941			.mutate_estimate_and_check(Box::new(move |tx| {
942				let insufficient_gas = auth_cost / gas_scale;
943				tx.gas = Some(U256::from(insufficient_gas));
944			}));
945
946		assert_eq!(res, Err(TransactionValidityError::Invalid(InvalidTransaction::Payment)));
947	}
948
949	/// EIP-7702 spec: an authorization with `nonce >= 2**64` invalidates the *entire*
950	/// transaction at validation time (not a per-tuple skip).
951	#[test]
952	fn check_eth_transact_7702_rejects_oversized_nonce() {
953		let chain_id = U256::from(<Test as Config>::ChainId::get());
954		let dest = H160::from([1u8; 20]);
955		let signer = TestSigner::new(&[0xCC; 32]);
956		let auth = signer.sign_authorization(chain_id, dest, U256::zero());
957
958		// nonce = 2^64 — first value that doesn't fit in u64.
959		let oversized_nonce = U256::one() << 64;
960		let bad_auth = crate::evm::AuthorizationListEntry { nonce: oversized_nonce, ..auth };
961
962		assert_eq!(
963			UncheckedExtrinsicBuilder::call_with_authorization(dest, vec![bad_auth]).check(),
964			Err(TransactionValidityError::Invalid(InvalidTransaction::Call)),
965		);
966	}
967
968	/// EIP-7702 spec: an authorization with `y_parity >= 2**8` invalidates the *entire*
969	/// transaction at validation time.
970	#[test]
971	fn check_eth_transact_7702_rejects_oversized_y_parity() {
972		let chain_id = U256::from(<Test as Config>::ChainId::get());
973		let dest = H160::from([1u8; 20]);
974		let signer = TestSigner::new(&[0xCC; 32]);
975		let auth = signer.sign_authorization(chain_id, dest, U256::zero());
976
977		// y_parity = 256 — first value that doesn't fit in u8.
978		let bad_auth = crate::evm::AuthorizationListEntry { y_parity: U256::from(256u32), ..auth };
979
980		assert_eq!(
981			UncheckedExtrinsicBuilder::call_with_authorization(dest, vec![bad_auth]).check(),
982			Err(TransactionValidityError::Invalid(InvalidTransaction::Call)),
983		);
984	}
985
986	/// EIP-7702 spec: a type-0x04 transaction with an empty `authorization_list` is
987	/// invalid. This is the boundary case opposite to `invalid_authorization_is_skipped`:
988	/// empty list → entire transaction invalidated at validation; individual bad
989	/// signature → per-tuple skip (transaction itself still valid).
990	#[test]
991	fn check_eth_transact_7702_rejects_empty_auth_list() {
992		let dest = H160::from([1u8; 20]);
993		assert_eq!(
994			UncheckedExtrinsicBuilder::call_with_authorization(dest, vec![]).check(),
995			Err(TransactionValidityError::Invalid(InvalidTransaction::Call)),
996		);
997	}
998
999	/// An EIP-7702 transaction targeting `RUNTIME_PALLETS_ADDR` should be rejected
1000	/// at validation time: that dispatch path resolves to `eth_substrate_call`,
1001	/// which has no `authorization_list` field, so the auths would otherwise be
1002	/// silently dropped while the user is still charged the worst-case deposit.
1003	#[test]
1004	fn check_eth_transact_7702_rejects_runtime_pallets_addr() {
1005		let chain_id = U256::from(<Test as Config>::ChainId::get());
1006		let signer = TestSigner::new(&[0xCC; 32]);
1007		let auth = signer.sign_authorization(chain_id, H160::from([1u8; 20]), U256::zero());
1008
1009		let remark: CallOf<Test> =
1010			frame_system::Call::remark { remark: b"Hello, world!".to_vec() }.into();
1011
1012		assert_eq!(
1013			UncheckedExtrinsicBuilder::call_with_authorization(RUNTIME_PALLETS_ADDR, vec![auth])
1014				.data(remark.encode())
1015				.check(),
1016			Err(TransactionValidityError::Invalid(InvalidTransaction::Call)),
1017		);
1018	}
1019}