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	evm::{
20		api::{GenericTransaction, TransactionSigned},
21		fees::InfoT,
22		CreateCallMode,
23	},
24	AccountIdOf, AddressMapper, BalanceOf, CallOf, Config, Pallet, Zero, LOG_TARGET,
25};
26use codec::{Decode, DecodeWithMemTracking, Encode};
27use frame_support::{
28	dispatch::{DispatchInfo, GetDispatchInfo},
29	traits::{
30		fungible::Balanced,
31		tokens::{Fortitude, Precision, Preservation},
32		InherentBuilder, IsSubType, SignedTransactionBuilder,
33	},
34};
35use pallet_transaction_payment::Config as TxConfig;
36use scale_info::{StaticTypeInfo, TypeInfo};
37use sp_core::U256;
38use sp_runtime::{
39	generic::{self, CheckedExtrinsic, ExtrinsicFormat},
40	traits::{
41		Checkable, ExtrinsicCall, ExtrinsicLike, ExtrinsicMetadata, LazyExtrinsic,
42		TransactionExtension,
43	},
44	transaction_validity::{InvalidTransaction, TransactionValidityError},
45	Debug, OpaqueExtrinsic, Weight,
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<Address, CallOf<E::Config>, Signature, E::Extension>,
61);
62
63impl<Address, Signature, E: EthExtra> TypeInfo for UncheckedExtrinsic<Address, Signature, E>
64where
65	Address: StaticTypeInfo,
66	Signature: StaticTypeInfo,
67	E::Extension: StaticTypeInfo,
68{
69	type Identity =
70		generic::UncheckedExtrinsic<Address, CallOf<E::Config>, Signature, E::Extension>;
71	fn type_info() -> scale_info::Type {
72		generic::UncheckedExtrinsic::<Address, CallOf<E::Config>, Signature, E::Extension>::type_info()
73	}
74}
75
76impl<Address, Signature, E: EthExtra>
77	From<generic::UncheckedExtrinsic<Address, CallOf<E::Config>, Signature, E::Extension>>
78	for UncheckedExtrinsic<Address, Signature, E>
79{
80	fn from(
81		utx: generic::UncheckedExtrinsic<Address, CallOf<E::Config>, Signature, E::Extension>,
82	) -> Self {
83		Self(utx)
84	}
85}
86
87impl<Address: TypeInfo, Signature: TypeInfo, E: EthExtra> ExtrinsicLike
88	for UncheckedExtrinsic<Address, Signature, E>
89{
90	fn is_bare(&self) -> bool {
91		ExtrinsicLike::is_bare(&self.0)
92	}
93}
94
95impl<Address, Signature, E: EthExtra> ExtrinsicMetadata
96	for UncheckedExtrinsic<Address, Signature, E>
97{
98	const VERSIONS: &'static [u8] = generic::UncheckedExtrinsic::<
99		Address,
100		CallOf<E::Config>,
101		Signature,
102		E::Extension,
103	>::VERSIONS;
104	type TransactionExtensions = E::Extension;
105}
106
107impl<Address: TypeInfo, Signature: TypeInfo, E: EthExtra> ExtrinsicCall
108	for UncheckedExtrinsic<Address, Signature, E>
109{
110	type Call = CallOf<E::Config>;
111
112	fn call(&self) -> &Self::Call {
113		self.0.call()
114	}
115
116	fn into_call(self) -> Self::Call {
117		self.0.into_call()
118	}
119}
120
121impl<LookupSource, Signature, E, Lookup> Checkable<Lookup>
122	for UncheckedExtrinsic<LookupSource, Signature, E>
123where
124	E: EthExtra,
125	Self: Encode,
126	<E::Config as frame_system::Config>::Nonce: TryFrom<U256>,
127	CallOf<E::Config>: SetWeightLimit,
128	// required by Checkable for `generic::UncheckedExtrinsic`
129	generic::UncheckedExtrinsic<LookupSource, CallOf<E::Config>, Signature, E::Extension>:
130		Checkable<
131			Lookup,
132			Checked = CheckedExtrinsic<AccountIdOf<E::Config>, CallOf<E::Config>, E::Extension>,
133		>,
134{
135	type Checked = CheckedExtrinsic<AccountIdOf<E::Config>, CallOf<E::Config>, E::Extension>;
136
137	fn check(self, lookup: &Lookup) -> Result<Self::Checked, TransactionValidityError> {
138		if !self.0.is_signed() {
139			if let Some(crate::Call::eth_transact { payload }) = self.0.function.is_sub_type() {
140				let checked = E::try_into_checked_extrinsic(payload, self.encoded_size())?;
141				return Ok(checked)
142			};
143		}
144		self.0.check(lookup)
145	}
146
147	#[cfg(feature = "try-runtime")]
148	fn unchecked_into_checked_i_know_what_i_am_doing(
149		self,
150		lookup: &Lookup,
151	) -> Result<Self::Checked, TransactionValidityError> {
152		self.0.unchecked_into_checked_i_know_what_i_am_doing(lookup)
153	}
154}
155
156impl<Address, Signature, E: EthExtra> GetDispatchInfo
157	for UncheckedExtrinsic<Address, Signature, E>
158{
159	fn get_dispatch_info(&self) -> DispatchInfo {
160		self.0.get_dispatch_info()
161	}
162}
163
164impl<Address: Encode, Signature: Encode, E: EthExtra> serde::Serialize
165	for UncheckedExtrinsic<Address, Signature, E>
166{
167	fn serialize<S>(&self, seq: S) -> Result<S::Ok, S::Error>
168	where
169		S: ::serde::Serializer,
170	{
171		self.0.serialize(seq)
172	}
173}
174
175impl<'a, Address: Decode, Signature: Decode, E: EthExtra> serde::Deserialize<'a>
176	for UncheckedExtrinsic<Address, Signature, E>
177{
178	fn deserialize<D>(de: D) -> Result<Self, D::Error>
179	where
180		D: serde::Deserializer<'a>,
181	{
182		let r = sp_core::bytes::deserialize(de)?;
183		Decode::decode(&mut &r[..])
184			.map_err(|e| serde::de::Error::custom(alloc::format!("Decode error: {}", e)))
185	}
186}
187
188impl<Address, Signature, E: EthExtra> SignedTransactionBuilder
189	for UncheckedExtrinsic<Address, Signature, E>
190where
191	Address: TypeInfo,
192	Signature: TypeInfo,
193	E::Extension: TypeInfo,
194{
195	type Address = Address;
196	type Signature = Signature;
197	type Extension = E::Extension;
198
199	fn new_signed_transaction(
200		call: Self::Call,
201		signed: Address,
202		signature: Signature,
203		tx_ext: E::Extension,
204	) -> Self {
205		generic::UncheckedExtrinsic::new_signed(call, signed, signature, tx_ext).into()
206	}
207}
208
209impl<Address, Signature, E: EthExtra> InherentBuilder for UncheckedExtrinsic<Address, Signature, E>
210where
211	Address: TypeInfo,
212	Signature: TypeInfo,
213	E::Extension: TypeInfo,
214{
215	fn new_inherent(call: Self::Call) -> Self {
216		generic::UncheckedExtrinsic::new_bare(call).into()
217	}
218}
219
220impl<Address, Signature, E: EthExtra> From<UncheckedExtrinsic<Address, Signature, E>>
221	for OpaqueExtrinsic
222where
223	Address: Encode,
224	Signature: Encode,
225	E::Extension: Encode,
226{
227	fn from(extrinsic: UncheckedExtrinsic<Address, Signature, E>) -> Self {
228		extrinsic.0.into()
229	}
230}
231
232impl<Address, Signature, E: EthExtra> LazyExtrinsic for UncheckedExtrinsic<Address, Signature, E>
233where
234	generic::UncheckedExtrinsic<Address, CallOf<E::Config>, Signature, E::Extension>: LazyExtrinsic,
235{
236	fn decode_unprefixed(data: &[u8]) -> Result<Self, codec::Error> {
237		Ok(Self(LazyExtrinsic::decode_unprefixed(data)?))
238	}
239}
240
241/// EthExtra convert an unsigned [`crate::Call::eth_transact`] into a [`CheckedExtrinsic`].
242pub trait EthExtra {
243	/// The Runtime configuration.
244	type Config: Config + TxConfig;
245
246	/// The Runtime's transaction extension.
247	/// It should include at least:
248	/// - [`frame_system::CheckNonce`] to ensure that the nonce from the Ethereum transaction is
249	///   correct.
250	type Extension: TransactionExtension<CallOf<Self::Config>>;
251
252	/// Get the transaction extension to apply to an unsigned [`crate::Call::eth_transact`]
253	/// extrinsic.
254	///
255	/// # Parameters
256	/// - `nonce`: The nonce extracted from the Ethereum transaction.
257	/// - `tip`: The transaction tip calculated from the Ethereum transaction.
258	fn get_eth_extension(
259		nonce: <Self::Config as frame_system::Config>::Nonce,
260		tip: BalanceOf<Self::Config>,
261	) -> Self::Extension;
262
263	/// Convert the unsigned [`crate::Call::eth_transact`] into a [`CheckedExtrinsic`].
264	/// and ensure that the fees from the Ethereum transaction correspond to the fees computed from
265	/// the encoded_len and the injected weight_limit.
266	///
267	/// # Parameters
268	/// - `payload`: The RLP-encoded Ethereum transaction.
269	/// - `encoded_len`: The encoded length of the extrinsic.
270	fn try_into_checked_extrinsic(
271		payload: &[u8],
272		encoded_len: usize,
273	) -> Result<
274		CheckedExtrinsic<AccountIdOf<Self::Config>, CallOf<Self::Config>, Self::Extension>,
275		InvalidTransaction,
276	>
277	where
278		<Self::Config as frame_system::Config>::Nonce: TryFrom<U256>,
279		CallOf<Self::Config>: SetWeightLimit,
280	{
281		let tx = TransactionSigned::decode(&payload).map_err(|err| {
282			log::debug!(target: LOG_TARGET, "Failed to decode transaction: {err:?}");
283			InvalidTransaction::Call
284		})?;
285
286		// Check transaction type and reject unsupported transaction types
287		match &tx {
288			crate::evm::api::TransactionSigned::Transaction1559Signed(_) |
289			crate::evm::api::TransactionSigned::Transaction2930Signed(_) |
290			crate::evm::api::TransactionSigned::TransactionLegacySigned(_) => {
291				// Supported transaction types, continue processing
292			},
293			crate::evm::api::TransactionSigned::Transaction7702Signed(_) => {
294				log::debug!(target: LOG_TARGET, "EIP-7702 transactions are not supported");
295				return Err(InvalidTransaction::Call);
296			},
297			crate::evm::api::TransactionSigned::Transaction4844Signed(_) => {
298				log::debug!(target: LOG_TARGET, "EIP-4844 transactions are not supported");
299				return Err(InvalidTransaction::Call);
300			},
301		}
302
303		let signer_addr = tx.recover_eth_address().map_err(|err| {
304			log::debug!(target: LOG_TARGET, "Failed to recover signer: {err:?}");
305			InvalidTransaction::BadProof
306		})?;
307
308		let signer = <Self::Config as Config>::AddressMapper::to_fallback_account_id(&signer_addr);
309		let base_fee = <Pallet<Self::Config>>::evm_base_fee();
310		let tx = GenericTransaction::from_signed(tx, base_fee, None);
311		let nonce = tx.nonce.unwrap_or_default().try_into().map_err(|_| {
312			log::debug!(target: LOG_TARGET, "Failed to convert nonce");
313			InvalidTransaction::Call
314		})?;
315
316		log::debug!(target: LOG_TARGET, "Decoded Ethereum transaction with signer: {signer_addr:?} nonce: {nonce:?}");
317		let call_info = tx.into_call::<Self::Config>(CreateCallMode::ExtrinsicExecution(
318			encoded_len as u32,
319			payload.to_vec(),
320		))?;
321		let storage_credit = <Self::Config as Config>::Currency::withdraw(
322			&signer,
323			call_info.storage_deposit,
324			Precision::Exact,
325			Preservation::Preserve,
326			Fortitude::Polite,
327		).map_err(|_| {
328			log::debug!(target: LOG_TARGET, "Not enough balance to hold additional storage deposit of {:?}", call_info.storage_deposit);
329			InvalidTransaction::Payment
330		})?;
331		<Self::Config as Config>::FeeInfo::deposit_txfee(storage_credit);
332
333		crate::tracing::if_tracing(|tracer| {
334			tracer.watch_address(&Pallet::<Self::Config>::block_author());
335			tracer.watch_address(&signer_addr);
336		});
337
338		log::debug!(target: LOG_TARGET, "\
339			Created checked Ethereum transaction with: \
340			from={signer_addr:?} \
341			eth_gas={} \
342			encoded_len={encoded_len} \
343			tx_fee={:?} \
344			storage_deposit={:?} \
345			weight_limit={} \
346			nonce={nonce:?}\
347			",
348			call_info.eth_gas_limit,
349			call_info.tx_fee,
350			call_info.storage_deposit,
351			call_info.weight_limit,
352		);
353
354		// We can't calculate a tip because it needs to be based on the actual gas used which we
355		// cannot know pre-dispatch. Hence we never supply a tip here or it would be way too high.
356		Ok(CheckedExtrinsic {
357			format: ExtrinsicFormat::Signed(
358				signer.into(),
359				Self::get_eth_extension(nonce, Zero::zero()),
360			),
361			function: call_info.call,
362		})
363	}
364}
365
366#[cfg(test)]
367mod test {
368	use super::*;
369	use crate::{
370		evm::*,
371		test_utils::*,
372		tests::{
373			Address, ExtBuilder, RuntimeCall, RuntimeOrigin, SignedExtra, Test, UncheckedExtrinsic,
374		},
375		EthTransactInfo, Weight, RUNTIME_PALLETS_ADDR,
376	};
377	use frame_support::{error::LookupError, traits::fungible::Mutate};
378	use pallet_revive_fixtures::compile_module;
379	use sp_runtime::traits::{self, Checkable, DispatchTransaction};
380
381	type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
382
383	struct TestContext;
384
385	impl traits::Lookup for TestContext {
386		type Source = Address;
387		type Target = AccountIdOf<Test>;
388		fn lookup(&self, s: Self::Source) -> Result<Self::Target, LookupError> {
389			match s {
390				Self::Source::Id(id) => Ok(id),
391				_ => Err(LookupError),
392			}
393		}
394	}
395
396	/// A builder for creating an unchecked extrinsic, and test that the check function works.
397	#[derive(Clone)]
398	struct UncheckedExtrinsicBuilder {
399		tx: GenericTransaction,
400		before_validate: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
401		dry_run: Option<EthTransactInfo<BalanceOf<Test>>>,
402	}
403
404	impl UncheckedExtrinsicBuilder {
405		/// Create a new builder with default values.
406		fn new() -> Self {
407			Self {
408				tx: GenericTransaction {
409					from: Some(Account::default().address()),
410					chain_id: Some(<Test as Config>::ChainId::get().into()),
411					..Default::default()
412				},
413				before_validate: None,
414				dry_run: None,
415			}
416		}
417
418		fn data(mut self, data: Vec<u8>) -> Self {
419			self.tx.input = Bytes(data).into();
420			self
421		}
422
423		fn fund_account(account: &Account) {
424			let _ = <Test as Config>::Currency::set_balance(
425				&account.substrate_account(),
426				100_000_000_000_000,
427			);
428		}
429
430		fn estimate_gas(&mut self) {
431			let account = Account::default();
432			Self::fund_account(&account);
433
434			let dry_run =
435				crate::Pallet::<Test>::dry_run_eth_transact(self.tx.clone(), Default::default());
436			self.tx.gas_price = Some(<Pallet<Test>>::evm_base_fee());
437
438			match dry_run {
439				Ok(dry_run) => {
440					self.tx.gas = Some(dry_run.eth_gas);
441					self.dry_run = Some(dry_run);
442				},
443				Err(err) => {
444					log::debug!(target: LOG_TARGET, "Failed to estimate gas: {:?}", err);
445				},
446			}
447		}
448
449		/// Create a new builder with a call to the given address.
450		fn call_with(dest: H160) -> Self {
451			let mut builder = Self::new();
452			builder.tx.to = Some(dest);
453			builder
454		}
455
456		/// Create a new builder with an instantiate call.
457		fn instantiate_with(code: Vec<u8>, data: Vec<u8>) -> Self {
458			let mut builder = Self::new();
459			builder.tx.input = Bytes(code.into_iter().chain(data.into_iter()).collect()).into();
460			builder
461		}
462
463		/// Set before_validate function.
464		fn before_validate(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
465			self.before_validate = Some(std::sync::Arc::new(f));
466			self
467		}
468
469		fn check(
470			self,
471		) -> Result<
472			(u32, RuntimeCall, SignedExtra, GenericTransaction, Weight, TransactionSigned),
473			TransactionValidityError,
474		> {
475			self.mutate_estimate_and_check(Box::new(|_| ()))
476		}
477
478		/// Call `check` on the unchecked extrinsic, and `pre_dispatch` on the signed extension.
479		fn mutate_estimate_and_check(
480			mut self,
481			f: Box<dyn FnOnce(&mut GenericTransaction) -> ()>,
482		) -> Result<
483			(u32, RuntimeCall, SignedExtra, GenericTransaction, Weight, TransactionSigned),
484			TransactionValidityError,
485		> {
486			ExtBuilder::default().build().execute_with(|| self.estimate_gas());
487			ExtBuilder::default().build().execute_with(|| {
488				f(&mut self.tx);
489				let UncheckedExtrinsicBuilder { tx, before_validate, .. } = self.clone();
490
491				// Fund the account.
492				let account = Account::default();
493				Self::fund_account(&account);
494
495				let signed_transaction =
496					account.sign_transaction(tx.clone().try_into_unsigned().unwrap());
497				let call = RuntimeCall::Contracts(crate::Call::eth_transact {
498					payload: signed_transaction.signed_payload().clone(),
499				});
500
501				let uxt: UncheckedExtrinsic = generic::UncheckedExtrinsic::new_bare(call).into();
502				let encoded_len = uxt.encoded_size();
503				let result: CheckedExtrinsic<_, _, _> = uxt.check(&TestContext {})?;
504				let (account_id, extra): (AccountId32, SignedExtra) = match result.format {
505					ExtrinsicFormat::Signed(signer, extra) => (signer, extra),
506					_ => unreachable!(),
507				};
508
509				before_validate.map(|f| f());
510				extra.clone().validate_and_prepare(
511					RuntimeOrigin::signed(account_id),
512					&result.function,
513					&result.function.get_dispatch_info(),
514					encoded_len,
515					0,
516				)?;
517
518				Ok((
519					encoded_len as u32,
520					result.function,
521					extra,
522					tx,
523					self.dry_run.unwrap().weight_required,
524					signed_transaction,
525				))
526			})
527		}
528	}
529
530	#[test]
531	fn check_eth_transact_call_works() {
532		let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
533		let (expected_encoded_len, call, _, tx, weight_required, signed_transaction) =
534			builder.check().unwrap();
535		let expected_effective_gas_price =
536			ExtBuilder::default().build().execute_with(|| Pallet::<Test>::evm_base_fee());
537
538		match call {
539			RuntimeCall::Contracts(crate::Call::eth_call::<Test> {
540				dest,
541				value,
542				weight_limit,
543				data,
544				transaction_encoded,
545				effective_gas_price,
546				encoded_len,
547				..
548			}) if dest == tx.to.unwrap() &&
549				value == tx.value.unwrap_or_default().as_u64().into() &&
550				data == tx.input.to_vec() &&
551				transaction_encoded == signed_transaction.signed_payload() &&
552				effective_gas_price == expected_effective_gas_price =>
553			{
554				assert_eq!(encoded_len, expected_encoded_len);
555				assert!(
556					weight_limit.all_gte(weight_required),
557					"Assert failed: weight_limit={weight_limit:?} >= weight_required={weight_required:?}"
558				);
559			},
560			_ => panic!("Call does not match."),
561		}
562	}
563
564	#[test]
565	fn check_eth_transact_instantiate_works() {
566		let (expected_code, _) = compile_module("dummy").unwrap();
567		let expected_data = vec![];
568		let builder = UncheckedExtrinsicBuilder::instantiate_with(
569			expected_code.clone(),
570			expected_data.clone(),
571		);
572		let (expected_encoded_len, call, _, tx, weight_required, signed_transaction) =
573			builder.check().unwrap();
574		let expected_effective_gas_price =
575			ExtBuilder::default().build().execute_with(|| Pallet::<Test>::evm_base_fee());
576		let expected_value = tx.value.unwrap_or_default().as_u64().into();
577
578		match call {
579			RuntimeCall::Contracts(crate::Call::eth_instantiate_with_code::<Test> {
580				value,
581				weight_limit,
582				code,
583				data,
584				transaction_encoded,
585				effective_gas_price,
586				encoded_len,
587				..
588			}) if value == expected_value &&
589				code == expected_code &&
590				data == expected_data &&
591				transaction_encoded == signed_transaction.signed_payload() &&
592				effective_gas_price == expected_effective_gas_price =>
593			{
594				assert_eq!(encoded_len, expected_encoded_len);
595				assert!(
596					weight_limit.all_gte(weight_required),
597					"Assert failed: weight_limit={weight_limit:?} >= weight_required={weight_required:?}"
598				);
599			},
600			_ => panic!("Call does not match."),
601		}
602	}
603
604	#[test]
605	fn check_eth_transact_nonce_works() {
606		let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
607
608		assert_eq!(
609			builder.mutate_estimate_and_check(Box::new(|tx| tx.nonce = Some(1u32.into()))),
610			Err(TransactionValidityError::Invalid(InvalidTransaction::Future))
611		);
612
613		let builder =
614			UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20])).before_validate(|| {
615				<crate::System<Test>>::inc_account_nonce(Account::default().substrate_account());
616			});
617
618		assert_eq!(
619			builder.check(),
620			Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))
621		);
622	}
623
624	#[test]
625	fn check_eth_transact_chain_id_works() {
626		let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
627
628		assert_eq!(
629			builder.mutate_estimate_and_check(Box::new(|tx| tx.chain_id = Some(42.into()))),
630			Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
631		);
632	}
633
634	#[test]
635	fn check_instantiate_data() {
636		let code: Vec<u8> = polkavm_common::program::BLOB_MAGIC
637			.into_iter()
638			.chain(b"invalid code".iter().cloned())
639			.collect();
640		let data = vec![1];
641
642		let builder = UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone());
643
644		// Fail because the tx input fail to get the blob length
645		assert_eq!(
646			builder.check(),
647			Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
648		);
649	}
650
651	#[test]
652	fn check_transaction_fees() {
653		let scenarios: Vec<(_, Box<dyn FnOnce(&mut GenericTransaction)>, _)> = vec![
654			(
655				"Eth fees too low",
656				Box::new(|tx| {
657					tx.gas_price = Some(100u64.into());
658				}),
659				InvalidTransaction::Payment,
660			),
661			(
662				"Gas fees too low",
663				Box::new(|tx| {
664					tx.gas = Some(tx.gas.unwrap() / 2);
665				}),
666				InvalidTransaction::Payment,
667			),
668		];
669
670		for (msg, update_tx, err) in scenarios {
671			let res = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]))
672				.mutate_estimate_and_check(update_tx);
673
674			assert_eq!(res, Err(TransactionValidityError::Invalid(err)), "{}", msg);
675		}
676	}
677
678	#[test]
679	fn check_transaction_tip() {
680		let (code, _) = compile_module("dummy").unwrap();
681		// create some dummy data to increase the gas fee
682		let data = vec![42u8; crate::limits::CALLDATA_BYTES as usize];
683		let (_, _, extra, _tx, _gas_required, _) =
684			UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone())
685				.mutate_estimate_and_check(Box::new(|tx| {
686					tx.gas_price = Some(tx.gas_price.unwrap() * 103 / 100);
687					log::debug!(target: LOG_TARGET, "Gas price: {:?}", tx.gas_price);
688				}))
689				.unwrap();
690
691		assert_eq!(U256::from(extra.1.tip()), 0u32.into());
692	}
693
694	#[test]
695	fn check_runtime_pallets_addr_works() {
696		let remark: CallOf<Test> =
697			frame_system::Call::remark { remark: b"Hello, world!".to_vec() }.into();
698
699		let builder =
700			UncheckedExtrinsicBuilder::call_with(RUNTIME_PALLETS_ADDR).data(remark.encode());
701		let (_, call, _, _, _, _) = builder.check().unwrap();
702
703		match call {
704			RuntimeCall::Contracts(crate::Call::eth_substrate_call {
705				call: inner_call, ..
706			}) => {
707				assert_eq!(*inner_call, remark);
708			},
709			_ => panic!("Expected the RuntimeCall::Contracts variant, got: {:?}", call),
710		}
711	}
712}