1use crate::{
19 evm::{
20 api::{GenericTransaction, TransactionSigned},
21 GasEncoder,
22 },
23 AccountIdOf, AddressMapper, BalanceOf, Config, ConversionPrecision, MomentOf, Pallet,
24 LOG_TARGET,
25};
26use alloc::vec::Vec;
27use codec::{Decode, DecodeWithMemTracking, Encode};
28use frame_support::{
29 dispatch::{DispatchInfo, GetDispatchInfo},
30 traits::{ExtrinsicCall, InherentBuilder, SignedTransactionBuilder},
31};
32use pallet_transaction_payment::OnChargeTransaction;
33use scale_info::{StaticTypeInfo, TypeInfo};
34use sp_core::{Get, H256, U256};
35use sp_runtime::{
36 generic::{self, CheckedExtrinsic, ExtrinsicFormat},
37 traits::{
38 self, Checkable, Dispatchable, ExtrinsicLike, ExtrinsicMetadata, IdentifyAccount, Member,
39 TransactionExtension,
40 },
41 transaction_validity::{InvalidTransaction, TransactionValidityError},
42 OpaqueExtrinsic, RuntimeDebug,
43};
44
45type CallOf<T> = <T as frame_system::Config>::RuntimeCall;
46
47pub(crate) const GAS_PRICE: u64 = 1_000u64;
59
60#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, RuntimeDebug)]
63pub struct UncheckedExtrinsic<Address, Signature, E: EthExtra>(
64 pub generic::UncheckedExtrinsic<Address, CallOf<E::Config>, Signature, E::Extension>,
65);
66
67impl<Address, Signature, E: EthExtra> TypeInfo for UncheckedExtrinsic<Address, Signature, E>
68where
69 Address: StaticTypeInfo,
70 Signature: StaticTypeInfo,
71 E::Extension: StaticTypeInfo,
72{
73 type Identity =
74 generic::UncheckedExtrinsic<Address, CallOf<E::Config>, Signature, E::Extension>;
75 fn type_info() -> scale_info::Type {
76 generic::UncheckedExtrinsic::<Address, CallOf<E::Config>, Signature, E::Extension>::type_info()
77 }
78}
79
80impl<Address, Signature, E: EthExtra>
81 From<generic::UncheckedExtrinsic<Address, CallOf<E::Config>, Signature, E::Extension>>
82 for UncheckedExtrinsic<Address, Signature, E>
83{
84 fn from(
85 utx: generic::UncheckedExtrinsic<Address, CallOf<E::Config>, Signature, E::Extension>,
86 ) -> Self {
87 Self(utx)
88 }
89}
90
91impl<Address: TypeInfo, Signature: TypeInfo, E: EthExtra> ExtrinsicLike
92 for UncheckedExtrinsic<Address, Signature, E>
93{
94 fn is_bare(&self) -> bool {
95 ExtrinsicLike::is_bare(&self.0)
96 }
97}
98
99impl<Address, Signature, E: EthExtra> ExtrinsicMetadata
100 for UncheckedExtrinsic<Address, Signature, E>
101{
102 const VERSIONS: &'static [u8] = generic::UncheckedExtrinsic::<
103 Address,
104 CallOf<E::Config>,
105 Signature,
106 E::Extension,
107 >::VERSIONS;
108 type TransactionExtensions = E::Extension;
109}
110
111impl<Address: TypeInfo, Signature: TypeInfo, E: EthExtra> ExtrinsicCall
112 for UncheckedExtrinsic<Address, Signature, E>
113{
114 type Call = CallOf<E::Config>;
115
116 fn call(&self) -> &Self::Call {
117 self.0.call()
118 }
119}
120
121use sp_runtime::traits::MaybeDisplay;
122type OnChargeTransactionBalanceOf<T> = <<T as pallet_transaction_payment::Config>::OnChargeTransaction as OnChargeTransaction<T>>::Balance;
123
124impl<LookupSource, Signature, E, Lookup> Checkable<Lookup>
125 for UncheckedExtrinsic<LookupSource, Signature, E>
126where
127 E: EthExtra,
128 Self: Encode,
129 <E::Config as frame_system::Config>::Nonce: TryFrom<U256>,
130 <E::Config as frame_system::Config>::RuntimeCall: Dispatchable<Info = DispatchInfo>,
131 OnChargeTransactionBalanceOf<E::Config>: Into<BalanceOf<E::Config>>,
132 BalanceOf<E::Config>: Into<U256> + TryFrom<U256>,
133 MomentOf<E::Config>: Into<U256>,
134 CallOf<E::Config>: From<crate::Call<E::Config>> + TryInto<crate::Call<E::Config>>,
135 <E::Config as frame_system::Config>::Hash: frame_support::traits::IsType<H256>,
136
137 LookupSource: Member + MaybeDisplay,
139 CallOf<E::Config>: Encode + Member + Dispatchable,
140 Signature: Member + traits::Verify,
141 <Signature as traits::Verify>::Signer: IdentifyAccount<AccountId = AccountIdOf<E::Config>>,
142 E::Extension: Encode + TransactionExtension<CallOf<E::Config>>,
143 Lookup: traits::Lookup<Source = LookupSource, Target = AccountIdOf<E::Config>>,
144{
145 type Checked = CheckedExtrinsic<AccountIdOf<E::Config>, CallOf<E::Config>, E::Extension>;
146
147 fn check(self, lookup: &Lookup) -> Result<Self::Checked, TransactionValidityError> {
148 if !self.0.is_signed() {
149 if let Ok(call) = self.0.function.clone().try_into() {
150 if let crate::Call::eth_transact { payload } = call {
151 let checked = E::try_into_checked_extrinsic(payload, self.encoded_size())?;
152 return Ok(checked)
153 };
154 }
155 }
156 self.0.check(lookup)
157 }
158
159 #[cfg(feature = "try-runtime")]
160 fn unchecked_into_checked_i_know_what_i_am_doing(
161 self,
162 lookup: &Lookup,
163 ) -> Result<Self::Checked, TransactionValidityError> {
164 self.0.unchecked_into_checked_i_know_what_i_am_doing(lookup)
165 }
166}
167
168impl<Address, Signature, E: EthExtra> GetDispatchInfo for UncheckedExtrinsic<Address, Signature, E>
169where
170 CallOf<E::Config>: GetDispatchInfo + Dispatchable,
171{
172 fn get_dispatch_info(&self) -> DispatchInfo {
173 self.0.get_dispatch_info()
174 }
175}
176
177impl<Address: Encode, Signature: Encode, E: EthExtra> serde::Serialize
178 for UncheckedExtrinsic<Address, Signature, E>
179{
180 fn serialize<S>(&self, seq: S) -> Result<S::Ok, S::Error>
181 where
182 S: ::serde::Serializer,
183 {
184 self.0.serialize(seq)
185 }
186}
187
188impl<'a, Address: Decode, Signature: Decode, E: EthExtra> serde::Deserialize<'a>
189 for UncheckedExtrinsic<Address, Signature, E>
190{
191 fn deserialize<D>(de: D) -> Result<Self, D::Error>
192 where
193 D: serde::Deserializer<'a>,
194 {
195 let r = sp_core::bytes::deserialize(de)?;
196 Decode::decode(&mut &r[..])
197 .map_err(|e| serde::de::Error::custom(alloc::format!("Decode error: {}", e)))
198 }
199}
200
201impl<Address, Signature, E: EthExtra> SignedTransactionBuilder
202 for UncheckedExtrinsic<Address, Signature, E>
203where
204 Address: TypeInfo,
205 CallOf<E::Config>: TypeInfo,
206 Signature: TypeInfo,
207 E::Extension: TypeInfo,
208{
209 type Address = Address;
210 type Signature = Signature;
211 type Extension = E::Extension;
212
213 fn new_signed_transaction(
214 call: Self::Call,
215 signed: Address,
216 signature: Signature,
217 tx_ext: E::Extension,
218 ) -> Self {
219 generic::UncheckedExtrinsic::new_signed(call, signed, signature, tx_ext).into()
220 }
221}
222
223impl<Address, Signature, E: EthExtra> InherentBuilder for UncheckedExtrinsic<Address, Signature, E>
224where
225 Address: TypeInfo,
226 CallOf<E::Config>: TypeInfo,
227 Signature: TypeInfo,
228 E::Extension: TypeInfo,
229{
230 fn new_inherent(call: Self::Call) -> Self {
231 generic::UncheckedExtrinsic::new_bare(call).into()
232 }
233}
234
235impl<Address, Signature, E: EthExtra> From<UncheckedExtrinsic<Address, Signature, E>>
236 for OpaqueExtrinsic
237where
238 Address: Encode,
239 Signature: Encode,
240 CallOf<E::Config>: Encode,
241 E::Extension: Encode,
242{
243 fn from(extrinsic: UncheckedExtrinsic<Address, Signature, E>) -> Self {
244 Self::from_bytes(extrinsic.encode().as_slice()).expect(
245 "both OpaqueExtrinsic and UncheckedExtrinsic have encoding that is compatible with \
246 raw Vec<u8> encoding; qed",
247 )
248 }
249}
250
251pub trait EthExtra {
253 type Config: Config + pallet_transaction_payment::Config;
255
256 type Extension: TransactionExtension<CallOf<Self::Config>>;
261
262 fn get_eth_extension(
269 nonce: <Self::Config as frame_system::Config>::Nonce,
270 tip: BalanceOf<Self::Config>,
271 ) -> Self::Extension;
272
273 fn try_into_checked_extrinsic(
283 payload: Vec<u8>,
284 encoded_len: usize,
285 ) -> Result<
286 CheckedExtrinsic<AccountIdOf<Self::Config>, CallOf<Self::Config>, Self::Extension>,
287 InvalidTransaction,
288 >
289 where
290 <Self::Config as frame_system::Config>::Nonce: TryFrom<U256>,
291 BalanceOf<Self::Config>: Into<U256> + TryFrom<U256>,
292 MomentOf<Self::Config>: Into<U256>,
293 <Self::Config as frame_system::Config>::RuntimeCall: Dispatchable<Info = DispatchInfo>,
294 OnChargeTransactionBalanceOf<Self::Config>: Into<BalanceOf<Self::Config>>,
295 CallOf<Self::Config>: From<crate::Call<Self::Config>>,
296 <Self::Config as frame_system::Config>::Hash: frame_support::traits::IsType<H256>,
297 {
298 let tx = TransactionSigned::decode(&payload).map_err(|err| {
299 log::debug!(target: LOG_TARGET, "Failed to decode transaction: {err:?}");
300 InvalidTransaction::Call
301 })?;
302
303 let signer = 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);
309 let GenericTransaction { nonce, chain_id, to, value, input, gas, gas_price, .. } =
310 GenericTransaction::from_signed(tx, None);
311
312 let Some(gas) = gas else {
313 log::debug!(target: LOG_TARGET, "No gas provided");
314 return Err(InvalidTransaction::Call);
315 };
316
317 if chain_id.unwrap_or_default() != <Self::Config as Config>::ChainId::get().into() {
318 log::debug!(target: LOG_TARGET, "Invalid chain_id {chain_id:?}");
319 return Err(InvalidTransaction::Call);
320 }
321
322 let value = crate::Pallet::<Self::Config>::convert_evm_to_native(
323 value.unwrap_or_default(),
324 ConversionPrecision::Exact,
325 )
326 .map_err(|err| {
327 log::debug!(target: LOG_TARGET, "Failed to convert value to native: {err:?}");
328 InvalidTransaction::Call
329 })?;
330
331 let data = input.to_vec();
332
333 let (gas_limit, storage_deposit_limit) =
334 <Self::Config as Config>::EthGasEncoder::decode(gas).ok_or_else(|| {
335 log::debug!(target: LOG_TARGET, "Failed to decode gas: {gas:?}");
336 InvalidTransaction::Call
337 })?;
338
339 let call = if let Some(dest) = to {
340 crate::Call::call::<Self::Config> {
341 dest,
342 value,
343 gas_limit,
344 storage_deposit_limit,
345 data,
346 }
347 } else {
348 let blob = match polkavm::ProgramBlob::blob_length(&data) {
349 Some(blob_len) =>
350 blob_len.try_into().ok().and_then(|blob_len| (data.split_at_checked(blob_len))),
351 _ => None,
352 };
353
354 let Some((code, data)) = blob else {
355 log::debug!(target: LOG_TARGET, "Failed to extract polkavm code & data");
356 return Err(InvalidTransaction::Call);
357 };
358
359 crate::Call::instantiate_with_code::<Self::Config> {
360 value,
361 gas_limit,
362 storage_deposit_limit,
363 code: code.to_vec(),
364 data: data.to_vec(),
365 salt: None,
366 }
367 };
368
369 let mut info = call.get_dispatch_info();
370 let function: CallOf<Self::Config> = call.into();
371 let nonce = nonce.unwrap_or_default().try_into().map_err(|_| InvalidTransaction::Call)?;
372 let gas_price = gas_price.unwrap_or_default();
373
374 let eth_fee = Pallet::<Self::Config>::evm_gas_to_fee(gas, gas_price)
375 .map_err(|_| InvalidTransaction::Call)?;
376
377 info.extension_weight = Self::get_eth_extension(nonce, 0u32.into()).weight(&function);
379 let actual_fee: BalanceOf<Self::Config> =
380 pallet_transaction_payment::Pallet::<Self::Config>::compute_fee(
381 encoded_len as u32,
382 &info,
383 Default::default(),
384 )
385 .into();
386 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:?}");
387
388 if eth_fee < actual_fee {
391 log::debug!(target: LOG_TARGET, "eth fees {eth_fee:?} too low, actual fees: {actual_fee:?}");
392 return Err(InvalidTransaction::Payment.into())
393 }
394
395 let tip =
396 Pallet::<Self::Config>::evm_gas_to_fee(gas, gas_price.saturating_sub(GAS_PRICE.into()))
397 .unwrap_or_default()
398 .min(actual_fee);
399
400 log::debug!(target: LOG_TARGET, "Created checked Ethereum transaction with nonce: {nonce:?} and tip: {tip:?}");
401 Ok(CheckedExtrinsic {
402 format: ExtrinsicFormat::Signed(signer.into(), Self::get_eth_extension(nonce, tip)),
403 function,
404 })
405 }
406}
407
408#[cfg(test)]
409mod test {
410 use super::*;
411 use crate::{
412 evm::*,
413 test_utils::*,
414 tests::{ExtBuilder, RuntimeCall, RuntimeOrigin, Test},
415 Weight,
416 };
417 use frame_support::{error::LookupError, traits::fungible::Mutate};
418 use pallet_revive_fixtures::compile_module;
419 use sp_runtime::{
420 traits::{Checkable, DispatchTransaction},
421 MultiAddress, MultiSignature,
422 };
423 type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
424
425 #[derive(Clone, PartialEq, Eq, Debug)]
426 pub struct Extra;
427 type SignedExtra = (frame_system::CheckNonce<Test>, ChargeTransactionPayment<Test>);
428
429 use pallet_transaction_payment::ChargeTransactionPayment;
430 impl EthExtra for Extra {
431 type Config = Test;
432 type Extension = SignedExtra;
433
434 fn get_eth_extension(nonce: u32, tip: BalanceOf<Test>) -> Self::Extension {
435 (frame_system::CheckNonce::from(nonce), ChargeTransactionPayment::from(tip))
436 }
437 }
438
439 type Ex = UncheckedExtrinsic<MultiAddress<AccountId32, u32>, MultiSignature, Extra>;
440 struct TestContext;
441
442 impl traits::Lookup for TestContext {
443 type Source = MultiAddress<AccountId32, u32>;
444 type Target = AccountIdOf<Test>;
445 fn lookup(&self, s: Self::Source) -> Result<Self::Target, LookupError> {
446 match s {
447 MultiAddress::Id(id) => Ok(id),
448 _ => Err(LookupError),
449 }
450 }
451 }
452
453 #[derive(Clone)]
455 struct UncheckedExtrinsicBuilder {
456 tx: GenericTransaction,
457 before_validate: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
458 }
459
460 impl UncheckedExtrinsicBuilder {
461 fn new() -> Self {
463 Self {
464 tx: GenericTransaction {
465 from: Some(Account::default().address()),
466 chain_id: Some(<Test as Config>::ChainId::get().into()),
467 gas_price: Some(U256::from(GAS_PRICE)),
468 ..Default::default()
469 },
470 before_validate: None,
471 }
472 }
473
474 fn estimate_gas(&mut self) {
475 let dry_run = crate::Pallet::<Test>::bare_eth_transact(
476 self.tx.clone(),
477 Weight::MAX,
478 |call, mut info| {
479 let call = RuntimeCall::Contracts(call);
480 info.extension_weight = Extra::get_eth_extension(0, 0u32.into()).weight(&call);
481 let uxt: Ex = sp_runtime::generic::UncheckedExtrinsic::new_bare(call).into();
482 pallet_transaction_payment::Pallet::<Test>::compute_fee(
483 uxt.encoded_size() as u32,
484 &info,
485 Default::default(),
486 )
487 },
488 );
489
490 match dry_run {
491 Ok(dry_run) => {
492 log::debug!(target: LOG_TARGET, "Estimated gas: {:?}", dry_run.eth_gas);
493 self.tx.gas = Some(dry_run.eth_gas);
494 },
495 Err(err) => {
496 log::debug!(target: LOG_TARGET, "Failed to estimate gas: {:?}", err);
497 },
498 }
499 }
500
501 fn call_with(dest: H160) -> Self {
503 let mut builder = Self::new();
504 builder.tx.to = Some(dest);
505 builder
506 }
507
508 fn instantiate_with(code: Vec<u8>, data: Vec<u8>) -> Self {
510 let mut builder = Self::new();
511 builder.tx.input = Bytes(code.into_iter().chain(data.into_iter()).collect()).into();
512 builder
513 }
514
515 fn before_validate(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
517 self.before_validate = Some(std::sync::Arc::new(f));
518 self
519 }
520
521 fn check(
522 self,
523 ) -> Result<(RuntimeCall, SignedExtra, GenericTransaction), TransactionValidityError> {
524 self.mutate_estimate_and_check(Box::new(|_| ()))
525 }
526
527 fn mutate_estimate_and_check(
529 mut self,
530 f: Box<dyn FnOnce(&mut GenericTransaction) -> ()>,
531 ) -> Result<(RuntimeCall, SignedExtra, GenericTransaction), TransactionValidityError> {
532 ExtBuilder::default().build().execute_with(|| self.estimate_gas());
533 f(&mut self.tx);
534 ExtBuilder::default().build().execute_with(|| {
535 let UncheckedExtrinsicBuilder { tx, before_validate, .. } = self.clone();
536
537 let account = Account::default();
539 let _ = <Test as Config>::Currency::set_balance(
540 &account.substrate_account(),
541 100_000_000_000_000,
542 );
543
544 let payload = account
545 .sign_transaction(tx.clone().try_into_unsigned().unwrap())
546 .signed_payload();
547 let call = RuntimeCall::Contracts(crate::Call::eth_transact { payload });
548
549 let encoded_len = call.encoded_size();
550 let uxt: Ex = generic::UncheckedExtrinsic::new_bare(call).into();
551 let result: CheckedExtrinsic<_, _, _> = uxt.check(&TestContext {})?;
552 let (account_id, extra): (AccountId32, SignedExtra) = match result.format {
553 ExtrinsicFormat::Signed(signer, extra) => (signer, extra),
554 _ => unreachable!(),
555 };
556
557 before_validate.map(|f| f());
558 extra.clone().validate_and_prepare(
559 RuntimeOrigin::signed(account_id),
560 &result.function,
561 &result.function.get_dispatch_info(),
562 encoded_len,
563 0,
564 )?;
565
566 Ok((result.function, extra, tx))
567 })
568 }
569 }
570
571 #[test]
572 fn check_eth_transact_call_works() {
573 let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
574 let (call, _, tx) = builder.check().unwrap();
575 let (gas_limit, storage_deposit_limit) =
576 <<Test as Config>::EthGasEncoder as GasEncoder<_>>::decode(tx.gas.unwrap()).unwrap();
577
578 assert_eq!(
579 call,
580 crate::Call::call::<Test> {
581 dest: tx.to.unwrap(),
582 value: tx.value.unwrap_or_default().as_u64(),
583 data: tx.input.to_vec(),
584 gas_limit,
585 storage_deposit_limit
586 }
587 .into()
588 );
589 }
590
591 #[test]
592 fn check_eth_transact_instantiate_works() {
593 let (code, _) = compile_module("dummy").unwrap();
594 let data = vec![];
595 let builder = UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone());
596 let (call, _, tx) = builder.check().unwrap();
597 let (gas_limit, storage_deposit_limit) =
598 <<Test as Config>::EthGasEncoder as GasEncoder<_>>::decode(tx.gas.unwrap()).unwrap();
599
600 assert_eq!(
601 call,
602 crate::Call::instantiate_with_code::<Test> {
603 value: tx.value.unwrap_or_default().as_u64(),
604 code,
605 data,
606 salt: None,
607 gas_limit,
608 storage_deposit_limit
609 }
610 .into()
611 );
612 }
613
614 #[test]
615 fn check_eth_transact_nonce_works() {
616 let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
617
618 assert_eq!(
619 builder.mutate_estimate_and_check(Box::new(|tx| tx.nonce = Some(1u32.into()))),
620 Err(TransactionValidityError::Invalid(InvalidTransaction::Future))
621 );
622
623 let builder =
624 UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20])).before_validate(|| {
625 <crate::System<Test>>::inc_account_nonce(Account::default().substrate_account());
626 });
627
628 assert_eq!(
629 builder.check(),
630 Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))
631 );
632 }
633
634 #[test]
635 fn check_eth_transact_chain_id_works() {
636 let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
637
638 assert_eq!(
639 builder.mutate_estimate_and_check(Box::new(|tx| tx.chain_id = Some(42.into()))),
640 Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
641 );
642 }
643
644 #[test]
645 fn check_instantiate_data() {
646 let code = b"invalid code".to_vec();
647 let data = vec![1];
648 let builder = UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone());
649
650 assert_eq!(
652 builder.mutate_estimate_and_check(Box::new(|tx| tx.input = vec![1, 2, 3].into())),
653 Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
654 );
655 }
656
657 #[test]
658 fn check_transaction_fees() {
659 let scenarios: Vec<(_, Box<dyn FnOnce(&mut GenericTransaction)>, _)> = vec![
660 (
661 "Eth fees too low",
662 Box::new(|tx| {
663 tx.gas_price = Some(tx.gas_price.unwrap() / 2);
664 }),
665 InvalidTransaction::Payment,
666 ),
667 (
668 "Gas fees too low",
669 Box::new(|tx| {
670 tx.gas = Some(tx.gas.unwrap() / 2);
671 }),
672 InvalidTransaction::Payment,
673 ),
674 ];
675
676 for (msg, update_tx, err) in scenarios {
677 let res = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]))
678 .mutate_estimate_and_check(update_tx);
679
680 assert_eq!(res, Err(TransactionValidityError::Invalid(err)), "{}", msg);
681 }
682 }
683
684 #[test]
685 fn check_transaction_tip() {
686 let (code, _) = compile_module("dummy").unwrap();
687 let data = vec![];
688 let (_, extra, tx) =
689 UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone())
690 .mutate_estimate_and_check(Box::new(|tx| {
691 tx.gas_price = Some(tx.gas_price.unwrap() * 103 / 100);
692 log::debug!(target: LOG_TARGET, "Gas price: {:?}", tx.gas_price);
693 }))
694 .unwrap();
695 let diff = tx.gas_price.unwrap() - U256::from(GAS_PRICE);
696 let expected_tip = crate::Pallet::<Test>::evm_gas_to_fee(tx.gas.unwrap(), diff).unwrap();
697 assert_eq!(extra.1.tip(), expected_tip);
698 }
699}