1use 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
48pub(crate) const GAS_PRICE: u64 = 1_000u64;
60
61#[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 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
246pub trait EthExtra {
248 type Config: Config + pallet_transaction_payment::Config;
250
251 type Extension: TransactionExtension<CallOf<Self::Config>>;
256
257 fn get_eth_extension(
264 nonce: <Self::Config as frame_system::Config>::Nonce,
265 tip: BalanceOf<Self::Config>,
266 ) -> Self::Extension;
267
268 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 match &tx {
300 crate::evm::api::TransactionSigned::Transaction1559Signed(_) |
301 crate::evm::api::TransactionSigned::Transaction2930Signed(_) |
302 crate::evm::api::TransactionSigned::TransactionLegacySigned(_) => {
303 },
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 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 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 #[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 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 fn call_with(dest: H160) -> Self {
539 let mut builder = Self::new();
540 builder.tx.to = Some(dest);
541 builder
542 }
543
544 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 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 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 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 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}