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