Skip to content

Commit 8297d46

Browse files
committed
Merge branch 'pg/fix-gas-encoding' into pg/hack-indexer
2 parents 5d08af9 + 11872c9 commit 8297d46

File tree

2 files changed

+64
-72
lines changed

2 files changed

+64
-72
lines changed

prdoc/pr_6689.prdoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ doc:
33
- audience: Runtime Dev
44
description: |-
55
Update the current approach to attach the `ref_time`, `pov` and `deposit` parameters to an Ethereum transaction.
6-
Previously we will pass these 3 parameters along with the signed payload, and check that the fees resulting from `gas x gas_price` match the actual fees paid by the user for the extrinsic.
6+
Previously, these three parameters were passed along with the signed payload, and the fees resulting from gas × gas_price were checked to ensure they matched the actual fees paid by the user for the extrinsic
77

88
This approach unfortunately can be attacked. A malicious actor could force such a transaction to fail by injecting low values for some of these extra parameters as they are not part of the signed payload.
99

10-
The new approach encodes these 3 extra parameters in the lower digits of the transaction gas, approximating the the log2 of the actual values to encode each components on 2 digits
10+
The new approach encodes these 3 extra parameters in the lower digits of the transaction gas, using the log2 of the actual values to encode each components on 2 digits
1111
crates:
1212
- name: pallet-revive-eth-rpc
1313
bump: minor

substrate/frame/revive/src/evm/runtime.rs

Lines changed: 62 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ type CallOf<T> = <T as frame_system::Config>::RuntimeCall;
5454
/// - Not too high, ensuring the gas value is large enough (at least 7 digits) to encode the
5555
/// ref_time, proof_size, and deposit into the less significant (6 lower) digits of the gas value.
5656
/// - Not too low, enabling users to adjust the gas price to define a tip.
57-
pub const GAS_PRICE: u32 = 100_000u32;
57+
pub const GAS_PRICE: u32 = 1_000u32;
5858

5959
/// Convert a `Balance` into a gas value, using the fixed `GAS_PRICE`.
6060
/// The gas is calculated as `balance / GAS_PRICE`, rounded up to the nearest integer.
@@ -324,6 +324,11 @@ pub trait EthExtra {
324324
let GenericTransaction { nonce, chain_id, to, value, input, gas, gas_price, .. } =
325325
GenericTransaction::from_signed(tx, None);
326326

327+
let Some(gas) = gas else {
328+
log::debug!(target: LOG_TARGET, "No gas provided");
329+
return Err(InvalidTransaction::Call);
330+
};
331+
327332
if chain_id.unwrap_or_default() != <Self::Config as Config>::ChainId::get().into() {
328333
log::debug!(target: LOG_TARGET, "Invalid chain_id {chain_id:?}");
329334
return Err(InvalidTransaction::Call);
@@ -337,13 +342,11 @@ pub trait EthExtra {
337342

338343
let data = input.unwrap_or_default().0;
339344

340-
let (gas_limit, storage_deposit_limit) = <Self::Config as Config>::EthGasEncoder::decode(
341-
gas.unwrap_or_default(),
342-
)
343-
.ok_or_else(|| {
344-
log::debug!(target: LOG_TARGET, "Failed to decode gas: {gas:?}");
345-
InvalidTransaction::Call
346-
})?;
345+
let (gas_limit, storage_deposit_limit) =
346+
<Self::Config as Config>::EthGasEncoder::decode(gas).ok_or_else(|| {
347+
log::debug!(target: LOG_TARGET, "Failed to decode gas: {gas:?}");
348+
InvalidTransaction::Call
349+
})?;
347350

348351
let call = if let Some(dest) = to {
349352
crate::Call::call::<Self::Config> {
@@ -380,13 +383,13 @@ pub trait EthExtra {
380383
// Fees calculated with the fixed `GAS_PRICE`
381384
// When we dry-run the transaction, we set the gas to `Fee / GAS_PRICE`
382385
let eth_fee_no_tip = U256::from(GAS_PRICE)
383-
.saturating_mul(gas.unwrap_or_default())
386+
.saturating_mul(gas)
384387
.try_into()
385388
.map_err(|_| InvalidTransaction::Call)?;
386389

387390
// Fees with the actual gas_price from the transaction.
388391
let eth_fee: BalanceOf<Self::Config> = U256::from(gas_price.unwrap_or_default())
389-
.saturating_mul(gas.unwrap_or_default())
392+
.saturating_mul(gas)
390393
.try_into()
391394
.map_err(|_| InvalidTransaction::Call)?;
392395

@@ -410,11 +413,6 @@ pub trait EthExtra {
410413
return Err(InvalidTransaction::Payment.into())
411414
}
412415

413-
if eth_fee_no_tip > actual_fee.saturating_mul(2u32.into()) {
414-
log::debug!(target: LOG_TARGET, "actual fees: {actual_fee:?} too high, base eth fees: {eth_fee_no_tip:?}");
415-
return Err(InvalidTransaction::Call.into())
416-
}
417-
418416
let tip = eth_fee.saturating_sub(eth_fee_no_tip);
419417
log::debug!(target: LOG_TARGET, "Created checked Ethereum transaction with nonce: {nonce:?} and tip: {tip:?}");
420418
Ok(CheckedExtrinsic {
@@ -473,8 +471,6 @@ mod test {
473471
#[derive(Clone)]
474472
struct UncheckedExtrinsicBuilder {
475473
tx: GenericTransaction,
476-
gas_limit: Weight,
477-
storage_deposit_limit: BalanceOf<Test>,
478474
before_validate: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
479475
}
480476

@@ -488,8 +484,6 @@ mod test {
488484
gas_price: Some(U256::from(GAS_PRICE)),
489485
..Default::default()
490486
},
491-
gas_limit: Weight::zero(),
492-
storage_deposit_limit: 0,
493487
before_validate: None,
494488
}
495489
}
@@ -506,11 +500,6 @@ mod test {
506500
Ok(dry_run) => {
507501
log::debug!(target: LOG_TARGET, "Estimated gas: {:?}", dry_run.eth_gas);
508502
self.tx.gas = Some(dry_run.eth_gas);
509-
let (gas_limit, deposit) =
510-
<<Test as Config>::EthGasEncoder as GasEncoder<_>>::decode(dry_run.eth_gas)
511-
.unwrap();
512-
self.gas_limit = gas_limit;
513-
self.storage_deposit_limit = deposit;
514503
},
515504
Err(err) => {
516505
log::debug!(target: LOG_TARGET, "Failed to estimate gas: {:?}", err);
@@ -522,31 +511,35 @@ mod test {
522511
fn call_with(dest: H160) -> Self {
523512
let mut builder = Self::new();
524513
builder.tx.to = Some(dest);
525-
ExtBuilder::default().build().execute_with(|| builder.estimate_gas());
526514
builder
527515
}
528516

529517
/// Create a new builder with an instantiate call.
530518
fn instantiate_with(code: Vec<u8>, data: Vec<u8>) -> Self {
531519
let mut builder = Self::new();
532520
builder.tx.input = Some(Bytes(code.into_iter().chain(data.into_iter()).collect()));
533-
ExtBuilder::default().build().execute_with(|| builder.estimate_gas());
534521
builder
535522
}
536523

537-
/// Update the transaction with the given function.
538-
fn update(mut self, f: impl FnOnce(&mut GenericTransaction) -> ()) -> Self {
539-
f(&mut self.tx);
540-
self
541-
}
542524
/// Set before_validate function.
543525
fn before_validate(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
544526
self.before_validate = Some(std::sync::Arc::new(f));
545527
self
546528
}
547529

530+
fn check(
531+
self,
532+
) -> Result<(RuntimeCall, SignedExtra, GenericTransaction), TransactionValidityError> {
533+
self.mutate_estimate_and_check(Box::new(|_| ()))
534+
}
535+
548536
/// Call `check` on the unchecked extrinsic, and `pre_dispatch` on the signed extension.
549-
fn check(&self) -> Result<(RuntimeCall, SignedExtra), TransactionValidityError> {
537+
fn mutate_estimate_and_check(
538+
mut self,
539+
f: Box<dyn FnOnce(&mut GenericTransaction) -> ()>,
540+
) -> Result<(RuntimeCall, SignedExtra, GenericTransaction), TransactionValidityError> {
541+
ExtBuilder::default().build().execute_with(|| self.estimate_gas());
542+
f(&mut self.tx);
550543
ExtBuilder::default().build().execute_with(|| {
551544
let UncheckedExtrinsicBuilder { tx, before_validate, .. } = self.clone();
552545

@@ -557,8 +550,9 @@ mod test {
557550
100_000_000_000_000,
558551
);
559552

560-
let payload =
561-
account.sign_transaction(tx.try_into_unsigned().unwrap()).signed_payload();
553+
let payload = account
554+
.sign_transaction(tx.clone().try_into_unsigned().unwrap())
555+
.signed_payload();
562556
let call = RuntimeCall::Contracts(crate::Call::eth_transact { payload });
563557

564558
let encoded_len = call.encoded_size();
@@ -578,22 +572,26 @@ mod test {
578572
0,
579573
)?;
580574

581-
Ok((result.function, extra))
575+
Ok((result.function, extra, tx))
582576
})
583577
}
584578
}
585579

586580
#[test]
587581
fn check_eth_transact_call_works() {
588582
let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
583+
let (call, _, tx) = builder.check().unwrap();
584+
let (gas_limit, storage_deposit_limit) =
585+
<<Test as Config>::EthGasEncoder as GasEncoder<_>>::decode(tx.gas.unwrap()).unwrap();
586+
589587
assert_eq!(
590-
builder.check().unwrap().0,
588+
call,
591589
crate::Call::call::<Test> {
592-
dest: builder.tx.to.unwrap(),
593-
value: builder.tx.value.unwrap_or_default().as_u64(),
594-
gas_limit: builder.gas_limit,
595-
storage_deposit_limit: builder.storage_deposit_limit,
596-
data: builder.tx.input.unwrap_or_default().0
590+
dest: tx.to.unwrap(),
591+
value: tx.value.unwrap_or_default().as_u64(),
592+
data: tx.input.unwrap_or_default().0,
593+
gas_limit,
594+
storage_deposit_limit
597595
}
598596
.into()
599597
);
@@ -604,28 +602,30 @@ mod test {
604602
let (code, _) = compile_module("dummy").unwrap();
605603
let data = vec![];
606604
let builder = UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone());
605+
let (call, _, tx) = builder.check().unwrap();
606+
let (gas_limit, storage_deposit_limit) =
607+
<<Test as Config>::EthGasEncoder as GasEncoder<_>>::decode(tx.gas.unwrap()).unwrap();
607608

608609
assert_eq!(
609-
builder.check().unwrap().0,
610+
call,
610611
crate::Call::instantiate_with_code::<Test> {
611-
value: builder.tx.value.unwrap_or_default().as_u64(),
612-
gas_limit: builder.gas_limit,
613-
storage_deposit_limit: builder.storage_deposit_limit,
612+
value: tx.value.unwrap_or_default().as_u64(),
614613
code,
615614
data,
616-
salt: None
615+
salt: None,
616+
gas_limit,
617+
storage_deposit_limit
617618
}
618619
.into()
619620
);
620621
}
621622

622623
#[test]
623624
fn check_eth_transact_nonce_works() {
624-
let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]))
625-
.update(|tx| tx.nonce = Some(1u32.into()));
625+
let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
626626

627627
assert_eq!(
628-
builder.check(),
628+
builder.mutate_estimate_and_check(Box::new(|tx| tx.nonce = Some(1u32.into()))),
629629
Err(TransactionValidityError::Invalid(InvalidTransaction::Future))
630630
);
631631

@@ -642,11 +642,10 @@ mod test {
642642

643643
#[test]
644644
fn check_eth_transact_chain_id_works() {
645-
let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]))
646-
.update(|tx| tx.chain_id = Some(42.into()));
645+
let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
647646

648647
assert_eq!(
649-
builder.check(),
648+
builder.mutate_estimate_and_check(Box::new(|tx| tx.chain_id = Some(42.into()))),
650649
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
651650
);
652651
}
@@ -659,7 +658,7 @@ mod test {
659658

660659
// Fail because the tx input fail to get the blob length
661660
assert_eq!(
662-
builder.clone().update(|tx| tx.input = Some(Bytes(vec![1, 2, 3]))).check(),
661+
builder.mutate_estimate_and_check(Box::new(|tx| tx.input = Some(Bytes(vec![1, 2, 3])))),
663662
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
664663
);
665664
}
@@ -674,13 +673,6 @@ mod test {
674673
}),
675674
InvalidTransaction::Payment,
676675
),
677-
(
678-
"Gas fees too high",
679-
Box::new(|tx| {
680-
tx.gas = Some(tx.gas.unwrap() * 2);
681-
}),
682-
InvalidTransaction::Call,
683-
),
684676
(
685677
"Gas fees too low",
686678
Box::new(|tx| {
@@ -691,27 +683,27 @@ mod test {
691683
];
692684

693685
for (msg, update_tx, err) in scenarios {
694-
let builder =
695-
UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20])).update(update_tx);
686+
let res = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]))
687+
.mutate_estimate_and_check(update_tx);
696688

697-
assert_eq!(builder.check(), Err(TransactionValidityError::Invalid(err)), "{}", msg);
689+
assert_eq!(res, Err(TransactionValidityError::Invalid(err)), "{}", msg);
698690
}
699691
}
700692

701693
#[test]
702694
fn check_transaction_tip() {
703695
let (code, _) = compile_module("dummy").unwrap();
704696
let data = vec![];
705-
let builder = UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone())
706-
.update(|tx| {
707-
tx.gas_price = Some(tx.gas_price.unwrap() * 103 / 100);
708-
log::debug!(target: LOG_TARGET, "Gas price: {:?}", tx.gas_price);
709-
});
697+
let (_, extra, tx) =
698+
UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone())
699+
.mutate_estimate_and_check(Box::new(|tx| {
700+
tx.gas_price = Some(tx.gas_price.unwrap() * 103 / 100);
701+
log::debug!(target: LOG_TARGET, "Gas price: {:?}", tx.gas_price);
702+
}))
703+
.unwrap();
710704

711-
let tx = &builder.tx;
712705
let expected_tip =
713706
tx.gas_price.unwrap() * tx.gas.unwrap() - U256::from(GAS_PRICE) * tx.gas.unwrap();
714-
let (_, extra) = builder.check().unwrap();
715707
assert_eq!(U256::from(extra.1.tip()), expected_tip);
716708
}
717709
}

0 commit comments

Comments
 (0)