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