1use crate::{
19 AccountIdOf, AddressMapper, BalanceOf, CallOf, Config, LOG_TARGET, Pallet, Zero,
20 evm::{
21 CreateCallMode,
22 api::{GenericTransaction, TransactionSigned},
23 fees::InfoT,
24 },
25};
26use codec::{Decode, DecodeWithMemTracking, Encode};
27use frame_support::{
28 dispatch::{DispatchInfo, GetDispatchInfo},
29 traits::{
30 InherentBuilder, IsSubType, SignedTransactionBuilder,
31 fungible::Balanced,
32 tokens::{Fortitude, Precision, Preservation},
33 },
34};
35use pallet_transaction_payment::Config as TxConfig;
36use scale_info::{StaticTypeInfo, TypeInfo};
37use sp_core::U256;
38use sp_runtime::{
39 Debug, OpaqueExtrinsic, Weight,
40 generic::{self, CheckedExtrinsic, ExtrinsicFormat},
41 traits::{
42 Checkable, ExtrinsicCall, ExtrinsicLike, ExtrinsicMetadata, LazyExtrinsic, Pipeline,
43 TransactionExtension,
44 },
45 transaction_validity::{InvalidTransaction, TransactionValidityError},
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<
61 Address,
62 CallOf<E::Config>,
63 Signature,
64 E::ExtensionV0,
65 E::ExtensionOtherVersions,
66 >,
67);
68
69impl<Address, Signature, E: EthExtra> TypeInfo for UncheckedExtrinsic<Address, Signature, E>
70where
71 Address: StaticTypeInfo,
72 Signature: StaticTypeInfo,
73 E::ExtensionV0: StaticTypeInfo,
74{
75 type Identity = generic::UncheckedExtrinsic<
76 Address,
77 CallOf<E::Config>,
78 Signature,
79 E::ExtensionV0,
80 E::ExtensionOtherVersions,
81 >;
82 fn type_info() -> scale_info::Type {
83 generic::UncheckedExtrinsic::<
84 Address,
85 CallOf<E::Config>,
86 Signature,
87 E::ExtensionV0,
88 E::ExtensionOtherVersions,
89 >::type_info()
90 }
91}
92
93impl<Address, Signature, E: EthExtra>
94 From<
95 generic::UncheckedExtrinsic<
96 Address,
97 CallOf<E::Config>,
98 Signature,
99 E::ExtensionV0,
100 E::ExtensionOtherVersions,
101 >,
102 > for UncheckedExtrinsic<Address, Signature, E>
103{
104 fn from(
105 utx: generic::UncheckedExtrinsic<
106 Address,
107 CallOf<E::Config>,
108 Signature,
109 E::ExtensionV0,
110 E::ExtensionOtherVersions,
111 >,
112 ) -> Self {
113 Self(utx)
114 }
115}
116
117impl<Address: TypeInfo, Signature: TypeInfo, E: EthExtra> ExtrinsicLike
118 for UncheckedExtrinsic<Address, Signature, E>
119{
120 fn is_bare(&self) -> bool {
121 ExtrinsicLike::is_bare(&self.0)
122 }
123}
124
125impl<Address, Signature, E: EthExtra> ExtrinsicMetadata
126 for UncheckedExtrinsic<Address, Signature, E>
127{
128 const VERSIONS: &'static [u8] = generic::UncheckedExtrinsic::<
129 Address,
130 CallOf<E::Config>,
131 Signature,
132 E::ExtensionV0,
133 E::ExtensionOtherVersions,
134 >::VERSIONS;
135 type TransactionExtensionPipelines = <generic::UncheckedExtrinsic<
136 Address,
137 CallOf<E::Config>,
138 Signature,
139 E::ExtensionV0,
140 E::ExtensionOtherVersions,
141 > as ExtrinsicMetadata>::TransactionExtensionPipelines;
142}
143
144impl<Address: TypeInfo, Signature: TypeInfo, E: EthExtra> ExtrinsicCall
145 for UncheckedExtrinsic<Address, Signature, E>
146{
147 type Call = CallOf<E::Config>;
148
149 fn call(&self) -> &Self::Call {
150 self.0.call()
151 }
152
153 fn into_call(self) -> Self::Call {
154 self.0.into_call()
155 }
156}
157
158impl<LookupSource, Signature, E, Lookup> Checkable<Lookup>
159 for UncheckedExtrinsic<LookupSource, Signature, E>
160where
161 E: EthExtra,
162 Self: Encode,
163 <E::Config as frame_system::Config>::Nonce: TryFrom<U256>,
164 CallOf<E::Config>: SetWeightLimit,
165 generic::UncheckedExtrinsic<
167 LookupSource,
168 CallOf<E::Config>,
169 Signature,
170 E::ExtensionV0,
171 E::ExtensionOtherVersions,
172 >: Checkable<
173 Lookup,
174 Checked = CheckedExtrinsic<
175 AccountIdOf<E::Config>,
176 CallOf<E::Config>,
177 E::ExtensionV0,
178 E::ExtensionOtherVersions,
179 >,
180 >,
181{
182 type Checked = CheckedExtrinsic<
183 AccountIdOf<E::Config>,
184 CallOf<E::Config>,
185 E::ExtensionV0,
186 E::ExtensionOtherVersions,
187 >;
188
189 fn check(self, lookup: &Lookup) -> Result<Self::Checked, TransactionValidityError> {
190 if !self.0.is_signed() {
191 if let Some(crate::Call::eth_transact { payload }) = self.0.function.is_sub_type() {
192 log::trace!(
193 target: LOG_TARGET,
194 "eth_transact substrate tx hash: 0x{}",
195 sp_core::hexdisplay::HexDisplay::from(&sp_crypto_hashing::blake2_256(&self.encode())),
196 );
197 let checked = E::try_into_checked_extrinsic(payload, self.encoded_size())?;
198 return Ok(checked);
199 };
200 }
201 self.0.check(lookup)
202 }
203
204 #[cfg(feature = "try-runtime")]
205 fn unchecked_into_checked_i_know_what_i_am_doing(
206 self,
207 lookup: &Lookup,
208 ) -> Result<Self::Checked, TransactionValidityError> {
209 self.0.unchecked_into_checked_i_know_what_i_am_doing(lookup)
210 }
211}
212
213impl<Address, Signature, E: EthExtra> GetDispatchInfo
214 for UncheckedExtrinsic<Address, Signature, E>
215{
216 fn get_dispatch_info(&self) -> DispatchInfo {
217 self.0.get_dispatch_info()
218 }
219}
220
221impl<Address: Encode, Signature: Encode, E: EthExtra> serde::Serialize
222 for UncheckedExtrinsic<Address, Signature, E>
223{
224 fn serialize<S>(&self, seq: S) -> Result<S::Ok, S::Error>
225 where
226 S: ::serde::Serializer,
227 {
228 self.0.serialize(seq)
229 }
230}
231
232impl<'a, Address: DecodeWithMemTracking, Signature: DecodeWithMemTracking, E: EthExtra>
233 serde::Deserialize<'a> for UncheckedExtrinsic<Address, Signature, E>
234{
235 fn deserialize<D>(de: D) -> Result<Self, D::Error>
236 where
237 D: serde::Deserializer<'a>,
238 {
239 let r = sp_core::bytes::deserialize(de)?;
240 Decode::decode(&mut &r[..])
241 .map_err(|e| serde::de::Error::custom(alloc::format!("Decode error: {}", e)))
242 }
243}
244
245impl<Address, Signature, E: EthExtra> SignedTransactionBuilder
246 for UncheckedExtrinsic<Address, Signature, E>
247where
248 Address: TypeInfo,
249 Signature: TypeInfo,
250 E::ExtensionV0: TypeInfo,
251{
252 type Address = Address;
253 type Signature = Signature;
254 type Extension = E::ExtensionV0;
255
256 fn new_signed_transaction(
257 call: Self::Call,
258 signed: Address,
259 signature: Signature,
260 tx_ext: E::ExtensionV0,
261 ) -> Self {
262 generic::UncheckedExtrinsic::new_signed(call, signed, signature, tx_ext).into()
263 }
264}
265
266impl<Address, Signature, E: EthExtra> InherentBuilder for UncheckedExtrinsic<Address, Signature, E>
267where
268 Address: TypeInfo,
269 Signature: TypeInfo,
270 E::ExtensionV0: TypeInfo,
271{
272 fn new_inherent(call: Self::Call) -> Self {
273 generic::UncheckedExtrinsic::new_bare(call).into()
274 }
275}
276
277impl<Address, Signature, E: EthExtra> From<UncheckedExtrinsic<Address, Signature, E>>
278 for OpaqueExtrinsic
279where
280 Address: Encode,
281 Signature: Encode,
282 E::ExtensionV0: Encode,
283{
284 fn from(extrinsic: UncheckedExtrinsic<Address, Signature, E>) -> Self {
285 extrinsic.0.into()
286 }
287}
288
289impl<Address, Signature, E: EthExtra> LazyExtrinsic for UncheckedExtrinsic<Address, Signature, E>
290where
291 generic::UncheckedExtrinsic<
292 Address,
293 CallOf<E::Config>,
294 Signature,
295 E::ExtensionV0,
296 E::ExtensionOtherVersions,
297 >: LazyExtrinsic,
298{
299 fn decode_unprefixed(data: &[u8]) -> Result<Self, codec::Error> {
300 Ok(Self(LazyExtrinsic::decode_unprefixed(data)?))
301 }
302}
303
304pub trait EthExtra {
306 type Config: Config + TxConfig;
308
309 type ExtensionV0: TransactionExtension<CallOf<Self::Config>>;
314
315 type ExtensionOtherVersions: Pipeline<CallOf<Self::Config>>;
319
320 fn get_eth_extension(
327 nonce: <Self::Config as frame_system::Config>::Nonce,
328 tip: BalanceOf<Self::Config>,
329 ) -> Self::ExtensionV0;
330
331 fn try_into_checked_extrinsic(
339 payload: &[u8],
340 encoded_len: usize,
341 ) -> Result<
342 CheckedExtrinsic<
343 AccountIdOf<Self::Config>,
344 CallOf<Self::Config>,
345 Self::ExtensionV0,
346 Self::ExtensionOtherVersions,
347 >,
348 InvalidTransaction,
349 >
350 where
351 <Self::Config as frame_system::Config>::Nonce: TryFrom<U256>,
352 CallOf<Self::Config>: SetWeightLimit,
353 {
354 let tx = TransactionSigned::decode(&payload).map_err(|err| {
355 log::debug!(target: LOG_TARGET, "Failed to decode transaction: {err:?}");
356 InvalidTransaction::Call
357 })?;
358
359 match &tx {
361 crate::evm::api::TransactionSigned::Transaction1559Signed(_) |
362 crate::evm::api::TransactionSigned::Transaction2930Signed(_) |
363 crate::evm::api::TransactionSigned::TransactionLegacySigned(_) |
364 crate::evm::api::TransactionSigned::Transaction7702Signed(_) => {
365 },
367 crate::evm::api::TransactionSigned::Transaction4844Signed(_) => {
368 log::debug!(target: LOG_TARGET, "EIP-4844 transactions are not supported");
369 return Err(InvalidTransaction::Call);
370 },
371 }
372
373 let signer_addr = tx.recover_eth_address().map_err(|err| {
374 log::debug!(target: LOG_TARGET, "Failed to recover signer: {err:?}");
375 InvalidTransaction::BadProof
376 })?;
377
378 let signer = <Self::Config as Config>::AddressMapper::to_fallback_account_id(&signer_addr);
379 let base_fee = <Pallet<Self::Config>>::evm_base_fee();
380 let tx = GenericTransaction::from_signed(tx, base_fee, None);
381 let nonce = tx.nonce.unwrap_or_default().try_into().map_err(|_| {
382 log::debug!(target: LOG_TARGET, "Failed to convert nonce");
383 InvalidTransaction::Call
384 })?;
385
386 log::debug!(target: LOG_TARGET, "Decoded Ethereum transaction with signer: {signer_addr:?} nonce: {nonce:?}");
387 log::trace!(target: LOG_TARGET, "Decoded Ethereum transaction was: {tx:?}");
388 let call_info = tx.into_call::<Self::Config>(CreateCallMode::ExtrinsicExecution(
389 encoded_len as u32,
390 payload.to_vec(),
391 ))?;
392 let storage_credit = <Self::Config as Config>::Currency::withdraw(
393 &signer,
394 call_info.storage_deposit,
395 Precision::Exact,
396 Preservation::Preserve,
397 Fortitude::Polite,
398 ).map_err(|_| {
399 log::debug!(target: LOG_TARGET, "Not enough balance to hold additional storage deposit of {:?}", call_info.storage_deposit);
400 InvalidTransaction::Payment
401 })?;
402 <Self::Config as Config>::FeeInfo::deposit_txfee(storage_credit);
403
404 crate::tracing::if_tracing(|tracer| {
405 tracer.watch_address(&Pallet::<Self::Config>::block_author());
406 tracer.watch_address(&signer_addr);
407 });
408
409 log::debug!(target: LOG_TARGET, "\
410 Created checked Ethereum transaction with: \
411 from={signer_addr:?} \
412 eth_gas={} \
413 encoded_len={encoded_len} \
414 tx_fee={:?} \
415 storage_deposit={:?} \
416 weight_limit={} \
417 nonce={nonce:?}\
418 ",
419 call_info.eth_gas_limit,
420 call_info.tx_fee,
421 call_info.storage_deposit,
422 call_info.weight_limit,
423 );
424
425 Ok(CheckedExtrinsic {
428 format: ExtrinsicFormat::Signed(
429 signer.into(),
430 Self::get_eth_extension(nonce, Zero::zero()),
431 ),
432 function: call_info.call,
433 })
434 }
435}
436
437#[cfg(test)]
438mod test {
439 use super::*;
440 use crate::{
441 EthTransactInfo, RUNTIME_PALLETS_ADDR, Weight,
442 evm::*,
443 test_utils::*,
444 tests::{
445 Address, ExtBuilder, RuntimeCall, RuntimeOrigin, SignedExtra, Test, TestSigner,
446 UncheckedExtrinsic,
447 },
448 };
449 use frame_support::traits::fungible::Mutate;
450 use pallet_revive_fixtures::compile_module;
451 use sp_runtime::traits::{self, Checkable, DispatchTransaction, LookupError};
452
453 type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
454
455 struct TestContext;
456
457 impl traits::Lookup for TestContext {
458 type Source = Address;
459 type Target = AccountIdOf<Test>;
460 fn lookup(&self, s: Self::Source) -> Result<Self::Target, LookupError> {
461 match s {
462 Self::Source::Id(id) => Ok(id),
463 _ => Err(LookupError),
464 }
465 }
466 }
467
468 #[derive(Clone)]
470 struct UncheckedExtrinsicBuilder {
471 tx: GenericTransaction,
472 before_validate: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
473 dry_run: Option<EthTransactInfo<BalanceOf<Test>>>,
474 }
475
476 impl UncheckedExtrinsicBuilder {
477 fn new() -> Self {
479 Self {
480 tx: GenericTransaction {
481 from: Some(Account::default().address()),
482 chain_id: Some(<Test as Config>::ChainId::get().into()),
483 ..Default::default()
484 },
485 before_validate: None,
486 dry_run: None,
487 }
488 }
489
490 fn data(mut self, data: Vec<u8>) -> Self {
491 self.tx.input = Bytes(data).into();
492 self
493 }
494
495 fn fund_account(account: &Account) {
496 let _ = <Test as Config>::Currency::set_balance(
497 &account.substrate_account(),
498 100_000_000_000_000,
499 );
500 }
501
502 fn estimate_gas(&mut self) {
503 let account = Account::default();
504 Self::fund_account(&account);
505
506 let dry_run =
507 crate::Pallet::<Test>::dry_run_eth_transact(self.tx.clone(), None, true, None);
508 let base_fee = <Pallet<Test>>::evm_base_fee();
509 self.tx.gas_price = Some(base_fee);
510 self.tx.max_fee_per_gas = Some(base_fee);
511
512 match dry_run {
513 Ok(dry_run) => {
514 self.tx.gas = Some(dry_run.eth_gas);
515 self.dry_run = Some(dry_run);
516 },
517 Err(err) => {
518 log::debug!(target: LOG_TARGET, "Failed to estimate gas: {:?}", err);
519 },
520 }
521 }
522
523 fn call_with(dest: H160) -> Self {
525 let mut builder = Self::new();
526 builder.tx.to = Some(dest);
527 builder
528 }
529
530 fn call_with_authorization(
532 dest: H160,
533 authorization_list: Vec<AuthorizationListEntry>,
534 ) -> Self {
535 let mut builder = Self::new();
536 builder.tx.to = Some(dest);
537 builder.tx.r#type = Some(TYPE_EIP7702.into());
538 builder.tx.authorization_list = authorization_list;
539 builder
540 }
541
542 fn instantiate_with(code: Vec<u8>, data: Vec<u8>) -> Self {
544 let mut builder = Self::new();
545 builder.tx.input = Bytes(code.into_iter().chain(data.into_iter()).collect()).into();
546 builder
547 }
548
549 fn before_validate(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
551 self.before_validate = Some(std::sync::Arc::new(f));
552 self
553 }
554
555 fn check(
556 self,
557 ) -> Result<
558 (u32, RuntimeCall, SignedExtra, GenericTransaction, Weight, TransactionSigned),
559 TransactionValidityError,
560 > {
561 self.mutate_estimate_and_check(Box::new(|_| ()))
562 }
563
564 fn mutate_estimate_and_check(
566 mut self,
567 f: Box<dyn FnOnce(&mut GenericTransaction) -> ()>,
568 ) -> Result<
569 (u32, RuntimeCall, SignedExtra, GenericTransaction, Weight, TransactionSigned),
570 TransactionValidityError,
571 > {
572 ExtBuilder::default().build().execute_with(|| self.estimate_gas());
573 ExtBuilder::default().build().execute_with(|| {
574 f(&mut self.tx);
575 let UncheckedExtrinsicBuilder { tx, before_validate, .. } = self.clone();
576
577 let account = Account::default();
579 Self::fund_account(&account);
580
581 let signed_transaction =
582 account.sign_transaction(tx.clone().try_into_unsigned().unwrap());
583 let call = RuntimeCall::Contracts(crate::Call::eth_transact {
584 payload: signed_transaction.signed_payload().clone(),
585 });
586
587 let uxt: UncheckedExtrinsic = generic::UncheckedExtrinsic::new_bare(call).into();
588 let encoded_len = uxt.encoded_size();
589 let result: CheckedExtrinsic<_, _, _> = uxt.check(&TestContext {})?;
590 let (account_id, extra): (AccountId32, SignedExtra) = match result.format {
591 ExtrinsicFormat::Signed(signer, extra) => (signer, extra),
592 _ => unreachable!(),
593 };
594
595 before_validate.map(|f| f());
596 extra.clone().validate_and_prepare(
597 RuntimeOrigin::signed(account_id),
598 &result.function,
599 &result.function.get_dispatch_info(),
600 encoded_len,
601 0,
602 )?;
603
604 Ok((
605 encoded_len as u32,
606 result.function,
607 extra,
608 tx,
609 self.dry_run.unwrap().weight_required,
610 signed_transaction,
611 ))
612 })
613 }
614 }
615
616 #[test]
617 fn check_eth_transact_call_works() {
618 let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
619 let (expected_encoded_len, call, _, tx, weight_required, signed_transaction) =
620 builder.check().unwrap();
621 let expected_effective_gas_price =
622 ExtBuilder::default().build().execute_with(|| Pallet::<Test>::evm_base_fee());
623
624 match call {
625 RuntimeCall::Contracts(crate::Call::eth_call::<Test> {
626 dest,
627 value,
628 weight_limit,
629 data,
630 transaction_encoded,
631 effective_gas_price,
632 encoded_len,
633 ..
634 }) if dest == tx.to.unwrap() &&
635 value == tx.value.unwrap_or_default().as_u64().into() &&
636 data == tx.input.to_vec() &&
637 transaction_encoded == signed_transaction.signed_payload() &&
638 effective_gas_price == expected_effective_gas_price =>
639 {
640 assert_eq!(encoded_len, expected_encoded_len);
641 assert!(
642 weight_limit.all_gte(weight_required),
643 "Assert failed: weight_limit={weight_limit:?} >= weight_required={weight_required:?}"
644 );
645 },
646 _ => panic!("Call does not match."),
647 }
648 }
649
650 #[test]
651 fn check_eth_transact_instantiate_works() {
652 let (expected_code, _) = compile_module("dummy").unwrap();
653 let expected_data = vec![];
654 let builder = UncheckedExtrinsicBuilder::instantiate_with(
655 expected_code.clone(),
656 expected_data.clone(),
657 );
658 let (expected_encoded_len, call, _, tx, weight_required, signed_transaction) =
659 builder.check().unwrap();
660 let expected_effective_gas_price =
661 ExtBuilder::default().build().execute_with(|| Pallet::<Test>::evm_base_fee());
662 let expected_value = tx.value.unwrap_or_default().as_u64().into();
663
664 match call {
665 RuntimeCall::Contracts(crate::Call::eth_instantiate_with_code::<Test> {
666 value,
667 weight_limit,
668 code,
669 data,
670 transaction_encoded,
671 effective_gas_price,
672 encoded_len,
673 ..
674 }) if value == expected_value &&
675 code == expected_code &&
676 data == expected_data &&
677 transaction_encoded == signed_transaction.signed_payload() &&
678 effective_gas_price == expected_effective_gas_price =>
679 {
680 assert_eq!(encoded_len, expected_encoded_len);
681 assert!(
682 weight_limit.all_gte(weight_required),
683 "Assert failed: weight_limit={weight_limit:?} >= weight_required={weight_required:?}"
684 );
685 },
686 _ => panic!("Call does not match."),
687 }
688 }
689
690 #[test]
691 fn check_eth_transact_nonce_works() {
692 let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
693
694 assert_eq!(
695 builder.mutate_estimate_and_check(Box::new(|tx| tx.nonce = Some(1u32.into()))),
696 Err(TransactionValidityError::Invalid(InvalidTransaction::Future))
697 );
698
699 let builder =
700 UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20])).before_validate(|| {
701 <crate::System<Test>>::inc_account_nonce(Account::default().substrate_account());
702 });
703
704 assert_eq!(
705 builder.check(),
706 Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))
707 );
708 }
709
710 #[test]
711 fn check_eth_transact_chain_id_works() {
712 let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
713
714 assert_eq!(
715 builder.mutate_estimate_and_check(Box::new(|tx| tx.chain_id = Some(42.into()))),
716 Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
717 );
718 }
719
720 #[test]
721 fn check_instantiate_data() {
722 let code: Vec<u8> = polkavm_common::program::BLOB_MAGIC
723 .into_iter()
724 .chain(b"invalid code".iter().cloned())
725 .collect();
726 let data = vec![1];
727
728 let builder = UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone());
729
730 assert_eq!(
732 builder.check(),
733 Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
734 );
735 }
736
737 #[test]
738 fn check_transaction_fees() {
739 let scenarios: Vec<(_, Box<dyn FnOnce(&mut GenericTransaction)>, _)> = vec![
740 (
741 "Eth fees too low",
742 Box::new(|tx| {
743 tx.gas_price = Some(100u64.into());
744 }),
745 InvalidTransaction::Payment,
746 ),
747 (
748 "Gas fees too low",
749 Box::new(|tx| {
750 tx.gas = Some(tx.gas.unwrap() / 2);
751 }),
752 InvalidTransaction::Payment,
753 ),
754 ];
755
756 for (msg, update_tx, err) in scenarios {
757 let res = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]))
758 .mutate_estimate_and_check(update_tx);
759
760 assert_eq!(res, Err(TransactionValidityError::Invalid(err)), "{}", msg);
761 }
762 }
763
764 #[test]
765 fn eth_pre_dispatch_weight_matches_check_weight_booking() {
766 let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
767 let (encoded_len, call, _, _, _, signed_transaction) = builder.check().unwrap();
768
769 ExtBuilder::default().build().execute_with(|| {
770 let reported =
771 Pallet::<Test>::eth_pre_dispatch_weight(signed_transaction.signed_payload())
772 .unwrap();
773 let info = <Test as Config>::FeeInfo::dispatch_info(&call);
774 let expected = frame_system::calculate_consumed_extrinsic_weight::<CallOf<Test>>(
775 &<Test as frame_system::Config>::BlockWeights::get(),
776 &info,
777 encoded_len as usize,
778 );
779
780 assert_eq!(reported, expected);
781 });
782 }
783
784 #[test]
785 fn check_transaction_tip() {
786 let (code, _) = compile_module("dummy").unwrap();
787 let data = vec![42u8; crate::limits::CALLDATA_BYTES as usize];
789 let (_, _, extra, _tx, _gas_required, _) =
790 UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone())
791 .mutate_estimate_and_check(Box::new(|tx| {
792 tx.gas_price = Some(tx.gas_price.unwrap() * 103 / 100);
793 log::debug!(target: LOG_TARGET, "Gas price: {:?}", tx.gas_price);
794 }))
795 .unwrap();
796
797 assert_eq!(U256::from(extra.1.tip()), 0u32.into());
798 }
799
800 #[test]
801 fn check_runtime_pallets_addr_works() {
802 let remark: CallOf<Test> =
803 frame_system::Call::remark { remark: b"Hello, world!".to_vec() }.into();
804
805 let builder =
806 UncheckedExtrinsicBuilder::call_with(RUNTIME_PALLETS_ADDR).data(remark.encode());
807 let (_, call, _, _, _, _) = builder.check().unwrap();
808
809 match call {
810 RuntimeCall::Contracts(crate::Call::eth_substrate_call {
811 call: inner_call, ..
812 }) => {
813 assert_eq!(*inner_call, remark);
814 },
815 _ => panic!("Expected the RuntimeCall::Contracts variant, got: {:?}", call),
816 }
817 }
818
819 #[test]
829 fn contract_deployment_with_nick_method_works() {
830 let raw_transaction_bytes = alloy_core::hex!(
832 "0xf9016c8085174876e8008303c4d88080b90154608060405234801561001057600080fd5b50610134806100206000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80634af63f0214602d575b600080fd5b60cf60048036036040811015604157600080fd5b810190602081018135640100000000811115605b57600080fd5b820183602082011115606c57600080fd5b80359060200191846001830284011164010000000083111715608d57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550509135925060eb915050565b604080516001600160a01b039092168252519081900360200190f35b6000818351602085016000f5939250505056fea26469706673582212206b44f8a82cb6b156bfcc3dc6aadd6df4eefd204bc928a4397fd15dacf6d5320564736f6c634300060200331b83247000822470"
833 );
834
835 let mut signed_transaction = TransactionSigned::decode(raw_transaction_bytes.as_slice())
836 .expect("Invalid raw transaction bytes");
837 if let TransactionSigned::TransactionLegacySigned(ref mut legacy_transaction) =
838 signed_transaction
839 {
840 legacy_transaction.transaction_legacy_unsigned.gas =
841 U256::from_dec_str("3750815700000").unwrap();
842 }
843 let generic_transaction = GenericTransaction::from_signed(
844 signed_transaction.clone(),
845 ExtBuilder::default().build().execute_with(|| Pallet::<Test>::evm_base_fee()),
846 None,
847 );
848
849 let unchecked_extrinsic_builder = UncheckedExtrinsicBuilder {
850 tx: generic_transaction,
851 before_validate: None,
852 dry_run: None,
853 };
854
855 let eth_transact_result = unchecked_extrinsic_builder.check();
857
858 let (
860 _encoded_len,
861 _function,
862 _extra,
863 generic_transaction,
864 _gas_required,
865 _signed_transaction,
866 ) = eth_transact_result.expect("eth_transact failed");
867 assert!(
868 generic_transaction.chain_id.is_none(),
869 "Chain Id in the generic transaction is not None"
870 );
871 }
872
873 #[test]
874 fn check_eth_transact_7702_call_works() {
875 let chain_id = U256::from(<Test as Config>::ChainId::get());
876 let signer = TestSigner::new(&[0xCC; 32]);
877 let auth = signer.sign_authorization(chain_id, H160::from([1u8; 20]), U256::zero());
878
879 let builder =
880 UncheckedExtrinsicBuilder::call_with_authorization(H160::from([1u8; 20]), vec![auth]);
881 let (expected_encoded_len, call, _, tx, weight_required, _) = builder.check().unwrap();
882
883 match call {
884 RuntimeCall::Contracts(crate::Call::eth_call::<Test> {
885 dest,
886 weight_limit,
887 encoded_len,
888 authorization_list,
889 ..
890 }) if dest == tx.to.unwrap() => {
891 assert_eq!(encoded_len, expected_encoded_len);
892 assert_eq!(authorization_list.len(), 1);
893 assert!(
894 weight_limit.all_gte(weight_required),
895 "weight_limit={weight_limit:?} >= weight_required={weight_required:?}"
896 );
897 },
898 _ => panic!("Call does not match."),
899 }
900 }
901
902 #[test]
903 fn check_eth_transact_7702_insufficient_gas() {
904 use crate::evm::fees::InfoT;
905
906 let chain_id = U256::from(<Test as Config>::ChainId::get());
907 let dest = H160::from([1u8; 20]);
908 let auths: Vec<_> = (0..3u8)
909 .map(|i| {
910 let mut seed = [0u8; 32];
911 seed[0] = 0xCC + i;
912 TestSigner::new(&seed).sign_authorization(chain_id, dest, U256::zero())
913 })
914 .collect();
915 let num_auths = auths.len() as u128;
916 let gas_scale = <Test as Config>::GasScale::get() as u128;
917 let auth_cost = Pallet::<Test>::worst_case_delegation_deposit().saturating_mul(num_auths);
918
919 UncheckedExtrinsicBuilder::call_with_authorization(dest, auths.clone())
921 .check()
922 .expect("estimated gas should pass validation");
923
924 UncheckedExtrinsicBuilder::call_with_authorization(dest, auths.clone())
926 .mutate_estimate_and_check(Box::new(move |tx| {
927 let mut call_info = tx
928 .clone()
929 .into_call::<Test>(CreateCallMode::DryRun)
930 .expect("dry run should succeed");
931 let base_info = <Test as Config>::FeeInfo::base_dispatch_info(&mut call_info.call);
932 let overhead = <Test as Config>::FeeInfo::fixed_fee(call_info.encoded_len as u32) +
933 <Test as Config>::FeeInfo::weight_to_fee(&base_info.total_weight());
934 let sufficient_gas = 1 + (auth_cost + overhead) / gas_scale;
935 tx.gas = Some(U256::from(sufficient_gas));
936 }))
937 .expect("gas covering auth deposits + overhead should pass");
938
939 let res = UncheckedExtrinsicBuilder::call_with_authorization(dest, auths)
941 .mutate_estimate_and_check(Box::new(move |tx| {
942 let insufficient_gas = auth_cost / gas_scale;
943 tx.gas = Some(U256::from(insufficient_gas));
944 }));
945
946 assert_eq!(res, Err(TransactionValidityError::Invalid(InvalidTransaction::Payment)));
947 }
948
949 #[test]
952 fn check_eth_transact_7702_rejects_oversized_nonce() {
953 let chain_id = U256::from(<Test as Config>::ChainId::get());
954 let dest = H160::from([1u8; 20]);
955 let signer = TestSigner::new(&[0xCC; 32]);
956 let auth = signer.sign_authorization(chain_id, dest, U256::zero());
957
958 let oversized_nonce = U256::one() << 64;
960 let bad_auth = crate::evm::AuthorizationListEntry { nonce: oversized_nonce, ..auth };
961
962 assert_eq!(
963 UncheckedExtrinsicBuilder::call_with_authorization(dest, vec![bad_auth]).check(),
964 Err(TransactionValidityError::Invalid(InvalidTransaction::Call)),
965 );
966 }
967
968 #[test]
971 fn check_eth_transact_7702_rejects_oversized_y_parity() {
972 let chain_id = U256::from(<Test as Config>::ChainId::get());
973 let dest = H160::from([1u8; 20]);
974 let signer = TestSigner::new(&[0xCC; 32]);
975 let auth = signer.sign_authorization(chain_id, dest, U256::zero());
976
977 let bad_auth = crate::evm::AuthorizationListEntry { y_parity: U256::from(256u32), ..auth };
979
980 assert_eq!(
981 UncheckedExtrinsicBuilder::call_with_authorization(dest, vec![bad_auth]).check(),
982 Err(TransactionValidityError::Invalid(InvalidTransaction::Call)),
983 );
984 }
985
986 #[test]
991 fn check_eth_transact_7702_rejects_empty_auth_list() {
992 let dest = H160::from([1u8; 20]);
993 assert_eq!(
994 UncheckedExtrinsicBuilder::call_with_authorization(dest, vec![]).check(),
995 Err(TransactionValidityError::Invalid(InvalidTransaction::Call)),
996 );
997 }
998
999 #[test]
1004 fn check_eth_transact_7702_rejects_runtime_pallets_addr() {
1005 let chain_id = U256::from(<Test as Config>::ChainId::get());
1006 let signer = TestSigner::new(&[0xCC; 32]);
1007 let auth = signer.sign_authorization(chain_id, H160::from([1u8; 20]), U256::zero());
1008
1009 let remark: CallOf<Test> =
1010 frame_system::Call::remark { remark: b"Hello, world!".to_vec() }.into();
1011
1012 assert_eq!(
1013 UncheckedExtrinsicBuilder::call_with_authorization(RUNTIME_PALLETS_ADDR, vec![auth])
1014 .data(remark.encode())
1015 .check(),
1016 Err(TransactionValidityError::Invalid(InvalidTransaction::Call)),
1017 );
1018 }
1019}