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