1use 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
48pub trait SetWeightLimit {
50 fn set_weight_limit(&mut self, weight_limit: Weight) -> Weight;
54}
55
56#[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 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
241pub trait EthExtra {
243 type Config: Config + TxConfig;
245
246 type Extension: TransactionExtension<CallOf<Self::Config>>;
251
252 fn get_eth_extension(
259 nonce: <Self::Config as frame_system::Config>::Nonce,
260 tip: BalanceOf<Self::Config>,
261 ) -> Self::Extension;
262
263 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 match &tx {
288 crate::evm::api::TransactionSigned::Transaction1559Signed(_) |
289 crate::evm::api::TransactionSigned::Transaction2930Signed(_) |
290 crate::evm::api::TransactionSigned::TransactionLegacySigned(_) => {
291 },
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 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 #[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 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 fn call_with(dest: H160) -> Self {
451 let mut builder = Self::new();
452 builder.tx.to = Some(dest);
453 builder
454 }
455
456 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 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 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 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 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 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}