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