// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Runtime types for integrating `pezpallet-revive` with the EVM.
use crate::{
evm::{
api::{GenericTransaction, TransactionSigned},
create_call,
fees::InfoT,
},
AccountIdOf, AddressMapper, BalanceOf, CallOf, Config, Pezpallet, Zero, LOG_TARGET,
};
use codec::{Decode, DecodeWithMemTracking, Encode};
use pezframe_support::{
dispatch::{DispatchInfo, GetDispatchInfo},
traits::{
fungible::Balanced,
tokens::{Fortitude, Precision, Preservation},
InherentBuilder, IsSubType, SignedTransactionBuilder,
},
};
use pezpallet_transaction_payment::Config as TxConfig;
use pezsp_core::U256;
use pezsp_runtime::{
generic::{self, CheckedExtrinsic, ExtrinsicFormat},
traits::{
Checkable, ExtrinsicCall, ExtrinsicLike, ExtrinsicMetadata, LazyExtrinsic,
TransactionExtension,
},
transaction_validity::{InvalidTransaction, TransactionValidityError},
OpaqueExtrinsic, RuntimeDebug, Weight,
};
use scale_info::{StaticTypeInfo, TypeInfo};
/// Used to set the weight limit argument of a `eth_call` or `eth_instantiate_with_code` call.
pub trait SetWeightLimit {
/// Set the weight limit of this call.
///
/// Returns the replaced weight.
fn set_weight_limit(&mut self, weight_limit: Weight) -> Weight;
}
/// Wraps [`generic::UncheckedExtrinsic`] to support checking unsigned
/// [`crate::Call::eth_transact`] extrinsic.
#[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, RuntimeDebug)]
pub struct UncheckedExtrinsic
(
pub generic::UncheckedExtrinsic, Signature, E::Extension>,
);
impl TypeInfo for UncheckedExtrinsic
where
Address: StaticTypeInfo,
Signature: StaticTypeInfo,
E::Extension: StaticTypeInfo,
{
type Identity =
generic::UncheckedExtrinsic, Signature, E::Extension>;
fn type_info() -> scale_info::Type {
generic::UncheckedExtrinsic::, Signature, E::Extension>::type_info()
}
}
impl
From, Signature, E::Extension>>
for UncheckedExtrinsic
{
fn from(
utx: generic::UncheckedExtrinsic, Signature, E::Extension>,
) -> Self {
Self(utx)
}
}
impl ExtrinsicLike
for UncheckedExtrinsic
{
fn is_bare(&self) -> bool {
ExtrinsicLike::is_bare(&self.0)
}
}
impl ExtrinsicMetadata
for UncheckedExtrinsic
{
const VERSIONS: &'static [u8] = generic::UncheckedExtrinsic::<
Address,
CallOf,
Signature,
E::Extension,
>::VERSIONS;
type TransactionExtensions = E::Extension;
}
impl ExtrinsicCall
for UncheckedExtrinsic
{
type Call = CallOf;
fn call(&self) -> &Self::Call {
self.0.call()
}
fn into_call(self) -> Self::Call {
self.0.into_call()
}
}
impl Checkable
for UncheckedExtrinsic
where
E: EthExtra,
Self: Encode,
::Nonce: TryFrom,
CallOf: SetWeightLimit,
// required by Checkable for `generic::UncheckedExtrinsic`
generic::UncheckedExtrinsic, Signature, E::Extension>:
Checkable<
Lookup,
Checked = CheckedExtrinsic, CallOf, E::Extension>,
>,
{
type Checked = CheckedExtrinsic, CallOf, E::Extension>;
fn check(self, lookup: &Lookup) -> Result {
if !self.0.is_signed() {
if let Some(crate::Call::eth_transact { payload }) = self.0.function.is_sub_type() {
let checked = E::try_into_checked_extrinsic(payload, self.encoded_size())?;
return Ok(checked);
};
}
self.0.check(lookup)
}
#[cfg(feature = "try-runtime")]
fn unchecked_into_checked_i_know_what_i_am_doing(
self,
lookup: &Lookup,
) -> Result {
self.0.unchecked_into_checked_i_know_what_i_am_doing(lookup)
}
}
impl GetDispatchInfo
for UncheckedExtrinsic
{
fn get_dispatch_info(&self) -> DispatchInfo {
self.0.get_dispatch_info()
}
}
impl serde::Serialize
for UncheckedExtrinsic
{
fn serialize(&self, seq: S) -> Result
where
S: ::serde::Serializer,
{
self.0.serialize(seq)
}
}
impl<'a, Address: Decode, Signature: Decode, E: EthExtra> serde::Deserialize<'a>
for UncheckedExtrinsic
{
fn deserialize(de: D) -> Result
where
D: serde::Deserializer<'a>,
{
let r = pezsp_core::bytes::deserialize(de)?;
Decode::decode(&mut &r[..])
.map_err(|e| serde::de::Error::custom(alloc::format!("Decode error: {}", e)))
}
}
impl SignedTransactionBuilder
for UncheckedExtrinsic
where
Address: TypeInfo,
Signature: TypeInfo,
E::Extension: TypeInfo,
{
type Address = Address;
type Signature = Signature;
type Extension = E::Extension;
fn new_signed_transaction(
call: Self::Call,
signed: Address,
signature: Signature,
tx_ext: E::Extension,
) -> Self {
generic::UncheckedExtrinsic::new_signed(call, signed, signature, tx_ext).into()
}
}
impl InherentBuilder for UncheckedExtrinsic
where
Address: TypeInfo,
Signature: TypeInfo,
E::Extension: TypeInfo,
{
fn new_inherent(call: Self::Call) -> Self {
generic::UncheckedExtrinsic::new_bare(call).into()
}
}
impl From>
for OpaqueExtrinsic
where
Address: Encode,
Signature: Encode,
E::Extension: Encode,
{
fn from(extrinsic: UncheckedExtrinsic) -> Self {
extrinsic.0.into()
}
}
impl LazyExtrinsic for UncheckedExtrinsic
where
generic::UncheckedExtrinsic, Signature, E::Extension>: LazyExtrinsic,
{
fn decode_unprefixed(data: &[u8]) -> Result {
Ok(Self(LazyExtrinsic::decode_unprefixed(data)?))
}
}
/// EthExtra convert an unsigned [`crate::Call::eth_transact`] into a [`CheckedExtrinsic`].
pub trait EthExtra {
/// The Runtime configuration.
type Config: Config + TxConfig;
/// The Runtime's transaction extension.
/// It should include at least:
/// - [`pezframe_system::CheckNonce`] to ensure that the nonce from the Ethereum transaction is
/// correct.
type Extension: TransactionExtension>;
/// Get the transaction extension to apply to an unsigned [`crate::Call::eth_transact`]
/// extrinsic.
///
/// # Parameters
/// - `nonce`: The nonce extracted from the Ethereum transaction.
/// - `tip`: The transaction tip calculated from the Ethereum transaction.
fn get_eth_extension(
nonce: ::Nonce,
tip: BalanceOf,
) -> Self::Extension;
/// Convert the unsigned [`crate::Call::eth_transact`] into a [`CheckedExtrinsic`].
/// and ensure that the fees from the Ethereum transaction correspond to the fees computed from
/// the encoded_len, the injected gas_limit and storage_deposit_limit.
///
/// # Parameters
/// - `payload`: The RLP-encoded Ethereum transaction.
/// - `encoded_len`: The encoded length of the extrinsic.
fn try_into_checked_extrinsic(
payload: &[u8],
encoded_len: usize,
) -> Result<
CheckedExtrinsic, CallOf, Self::Extension>,
InvalidTransaction,
>
where
::Nonce: TryFrom,
CallOf: SetWeightLimit,
{
let tx = TransactionSigned::decode(&payload).map_err(|err| {
log::debug!(target: LOG_TARGET, "Failed to decode transaction: {err:?}");
InvalidTransaction::Call
})?;
// Check transaction type and reject unsupported transaction types
match &tx {
crate::evm::api::TransactionSigned::Transaction1559Signed(_)
| crate::evm::api::TransactionSigned::Transaction2930Signed(_)
| crate::evm::api::TransactionSigned::TransactionLegacySigned(_) => {
// Supported transaction types, continue processing
},
crate::evm::api::TransactionSigned::Transaction7702Signed(_) => {
log::debug!(target: LOG_TARGET, "EIP-7702 transactions are not supported");
return Err(InvalidTransaction::Call);
},
crate::evm::api::TransactionSigned::Transaction4844Signed(_) => {
log::debug!(target: LOG_TARGET, "EIP-4844 transactions are not supported");
return Err(InvalidTransaction::Call);
},
}
let signer_addr = tx.recover_eth_address().map_err(|err| {
log::debug!(target: LOG_TARGET, "Failed to recover signer: {err:?}");
InvalidTransaction::BadProof
})?;
let signer = ::AddressMapper::to_fallback_account_id(&signer_addr);
let base_fee = >::evm_base_fee();
let tx = GenericTransaction::from_signed(tx, base_fee, None);
let nonce = tx.nonce.unwrap_or_default().try_into().map_err(|_| {
log::debug!(target: LOG_TARGET, "Failed to convert nonce");
InvalidTransaction::Call
})?;
log::trace!(target: LOG_TARGET, "Decoded Ethereum transaction with signer: {signer_addr:?} nonce: {nonce:?}");
let call_info =
create_call::(tx, Some((encoded_len as u32, payload.to_vec())), true)?;
let storage_credit = ::Currency::withdraw(
&signer,
call_info.storage_deposit,
Precision::Exact,
Preservation::Preserve,
Fortitude::Polite,
).map_err(|_| {
log::debug!(target: LOG_TARGET, "Not enough balance to hold additional storage deposit of {:?}", call_info.storage_deposit);
InvalidTransaction::Payment
})?;
::FeeInfo::deposit_txfee(storage_credit);
crate::tracing::if_tracing(|tracer| {
tracer.watch_address(&Pezpallet::::block_author());
tracer.watch_address(&signer_addr);
});
log::debug!(target: LOG_TARGET, "\
Created checked Ethereum transaction with: \
from={signer_addr:?} \
eth_gas={} \
encoded_len={encoded_len} \
tx_fee={:?} \
storage_deposit={:?} \
weight_limit={} \
nonce={nonce:?}\
",
call_info.gas,
call_info.tx_fee,
call_info.storage_deposit,
call_info.weight_limit,
);
// We can't calculate a tip because it needs to be based on the actual gas used which we
// cannot know pre-dispatch. Hence we never supply a tip here or it would be way too high.
Ok(CheckedExtrinsic {
format: ExtrinsicFormat::Signed(
signer.into(),
Self::get_eth_extension(nonce, Zero::zero()),
),
function: call_info.call,
})
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{
evm::*,
test_utils::*,
tests::{
Address, ExtBuilder, RuntimeCall, RuntimeOrigin, SignedExtra, Test, UncheckedExtrinsic,
},
EthTransactInfo, Weight, RUNTIME_PALLETS_ADDR,
};
use pezframe_support::{error::LookupError, traits::fungible::Mutate};
use pezpallet_revive_fixtures::compile_module;
use pezsp_runtime::traits::{self, Checkable, DispatchTransaction};
type AccountIdOf = ::AccountId;
struct TestContext;
impl traits::Lookup for TestContext {
type Source = Address;
type Target = AccountIdOf;
fn lookup(&self, s: Self::Source) -> Result {
match s {
Self::Source::Id(id) => Ok(id),
_ => Err(LookupError),
}
}
}
/// A builder for creating an unchecked extrinsic, and test that the check function works.
#[derive(Clone)]
struct UncheckedExtrinsicBuilder {
tx: GenericTransaction,
before_validate: Option>,
dry_run: Option>>,
}
impl UncheckedExtrinsicBuilder {
/// Create a new builder with default values.
fn new() -> Self {
Self {
tx: GenericTransaction {
from: Some(Account::default().address()),
chain_id: Some(::ChainId::get().into()),
..Default::default()
},
before_validate: None,
dry_run: None,
}
}
fn data(mut self, data: Vec) -> Self {
self.tx.input = Bytes(data).into();
self
}
fn fund_account(account: &Account) {
let _ = ::Currency::set_balance(
&account.bizinikiwi_account(),
100_000_000_000_000,
);
}
fn estimate_gas(&mut self) {
let account = Account::default();
Self::fund_account(&account);
let dry_run =
crate::Pezpallet::::dry_run_eth_transact(self.tx.clone(), Default::default());
self.tx.gas_price = Some(>::evm_base_fee());
match dry_run {
Ok(dry_run) => {
self.tx.gas = Some(dry_run.eth_gas);
self.dry_run = Some(dry_run);
},
Err(err) => {
log::debug!(target: LOG_TARGET, "Failed to estimate gas: {:?}", err);
},
}
}
/// Create a new builder with a call to the given address.
fn call_with(dest: H160) -> Self {
let mut builder = Self::new();
builder.tx.to = Some(dest);
builder
}
/// Create a new builder with an instantiate call.
fn instantiate_with(code: Vec, data: Vec) -> Self {
let mut builder = Self::new();
builder.tx.input = Bytes(code.into_iter().chain(data.into_iter()).collect()).into();
builder
}
/// Set before_validate function.
fn before_validate(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
self.before_validate = Some(std::sync::Arc::new(f));
self
}
fn check(
self,
) -> Result<
(u32, RuntimeCall, SignedExtra, GenericTransaction, Weight, TransactionSigned),
TransactionValidityError,
> {
self.mutate_estimate_and_check(Box::new(|_| ()))
}
/// Call `check` on the unchecked extrinsic, and `pre_dispatch` on the signed extension.
fn mutate_estimate_and_check(
mut self,
f: Box ()>,
) -> Result<
(u32, RuntimeCall, SignedExtra, GenericTransaction, Weight, TransactionSigned),
TransactionValidityError,
> {
ExtBuilder::default().build().execute_with(|| self.estimate_gas());
ExtBuilder::default().build().execute_with(|| {
f(&mut self.tx);
let UncheckedExtrinsicBuilder { tx, before_validate, .. } = self.clone();
// Fund the account.
let account = Account::default();
Self::fund_account(&account);
let signed_transaction =
account.sign_transaction(tx.clone().try_into_unsigned().unwrap());
let call = RuntimeCall::Contracts(crate::Call::eth_transact {
payload: signed_transaction.signed_payload().clone(),
});
let uxt: UncheckedExtrinsic = generic::UncheckedExtrinsic::new_bare(call).into();
let encoded_len = uxt.encoded_size();
let result: CheckedExtrinsic<_, _, _> = uxt.check(&TestContext {})?;
let (account_id, extra): (AccountId32, SignedExtra) = match result.format {
ExtrinsicFormat::Signed(signer, extra) => (signer, extra),
_ => unreachable!(),
};
before_validate.map(|f| f());
extra.clone().validate_and_prepare(
RuntimeOrigin::signed(account_id),
&result.function,
&result.function.get_dispatch_info(),
encoded_len,
0,
)?;
Ok((
encoded_len as u32,
result.function,
extra,
tx,
self.dry_run.unwrap().gas_required,
signed_transaction,
))
})
}
}
#[test]
fn check_eth_transact_call_works() {
let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
let (expected_encoded_len, call, _, tx, gas_required, signed_transaction) =
builder.check().unwrap();
let expected_effective_gas_price =
ExtBuilder::default().build().execute_with(|| Pezpallet::::evm_base_fee());
match call {
RuntimeCall::Contracts(crate::Call::eth_call:: {
dest,
value,
data,
gas_limit,
transaction_encoded,
effective_gas_price,
encoded_len,
}) if dest == tx.to.unwrap()
&& value == tx.value.unwrap_or_default().as_u64().into()
&& data == tx.input.to_vec()
&& transaction_encoded == signed_transaction.signed_payload()
&& effective_gas_price == expected_effective_gas_price =>
{
assert_eq!(encoded_len, expected_encoded_len);
assert!(
gas_limit.all_gte(gas_required),
"Assert failed: gas_limit={gas_limit:?} >= gas_required={gas_required:?}"
);
},
_ => panic!("Call does not match."),
}
}
#[test]
fn check_eth_transact_instantiate_works() {
let (expected_code, _) = compile_module("dummy").unwrap();
let expected_data = vec![];
let builder = UncheckedExtrinsicBuilder::instantiate_with(
expected_code.clone(),
expected_data.clone(),
);
let (expected_encoded_len, call, _, tx, gas_required, signed_transaction) =
builder.check().unwrap();
let expected_effective_gas_price =
ExtBuilder::default().build().execute_with(|| Pezpallet::::evm_base_fee());
let expected_value = tx.value.unwrap_or_default().as_u64().into();
match call {
RuntimeCall::Contracts(crate::Call::eth_instantiate_with_code:: {
value,
code,
data,
gas_limit,
transaction_encoded,
effective_gas_price,
encoded_len,
}) if value == expected_value
&& code == expected_code
&& data == expected_data
&& transaction_encoded == signed_transaction.signed_payload()
&& effective_gas_price == expected_effective_gas_price =>
{
assert_eq!(encoded_len, expected_encoded_len);
assert!(
gas_limit.all_gte(gas_required),
"Assert failed: gas_limit={gas_limit:?} >= gas_required={gas_required:?}"
);
},
_ => panic!("Call does not match."),
}
}
#[test]
fn check_eth_transact_nonce_works() {
let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
assert_eq!(
builder.mutate_estimate_and_check(Box::new(|tx| tx.nonce = Some(1u32.into()))),
Err(TransactionValidityError::Invalid(InvalidTransaction::Future))
);
let builder =
UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20])).before_validate(|| {
>::inc_account_nonce(Account::default().bizinikiwi_account());
});
assert_eq!(
builder.check(),
Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))
);
}
#[test]
fn check_eth_transact_chain_id_works() {
let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]));
assert_eq!(
builder.mutate_estimate_and_check(Box::new(|tx| tx.chain_id = Some(42.into()))),
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
}
#[test]
fn check_instantiate_data() {
let code: Vec = polkavm_common::program::BLOB_MAGIC
.into_iter()
.chain(b"invalid code".iter().cloned())
.collect();
let data = vec![1];
let builder = UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone());
// Fail because the tx input fail to get the blob length
assert_eq!(
builder.check(),
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
}
#[test]
fn check_transaction_fees() {
let scenarios: Vec<(_, Box, _)> = vec![
(
"Eth fees too low",
Box::new(|tx| {
tx.gas_price = Some(100u64.into());
}),
InvalidTransaction::Payment,
),
(
"Gas fees too low",
Box::new(|tx| {
tx.gas = Some(tx.gas.unwrap() / 2);
}),
InvalidTransaction::Payment,
),
];
for (msg, update_tx, err) in scenarios {
let res = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20]))
.mutate_estimate_and_check(update_tx);
assert_eq!(res, Err(TransactionValidityError::Invalid(err)), "{}", msg);
}
}
#[test]
fn check_transaction_tip() {
let (code, _) = compile_module("dummy").unwrap();
// create some dummy data to increase the gas fee
let data = vec![42u8; crate::limits::CALLDATA_BYTES as usize];
let (_, _, extra, _tx, _gas_required, _) =
UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone())
.mutate_estimate_and_check(Box::new(|tx| {
tx.gas_price = Some(tx.gas_price.unwrap() * 103 / 100);
log::debug!(target: LOG_TARGET, "Gas price: {:?}", tx.gas_price);
}))
.unwrap();
assert_eq!(U256::from(extra.1.tip()), 0u32.into());
}
#[test]
fn check_runtime_pallets_addr_works() {
let remark: CallOf =
pezframe_system::Call::remark { remark: b"Hello, world!".to_vec() }.into();
let builder =
UncheckedExtrinsicBuilder::call_with(RUNTIME_PALLETS_ADDR).data(remark.encode());
let (_, call, _, _, _, _) = builder.check().unwrap();
match call {
RuntimeCall::Contracts(crate::Call::eth_bizinikiwi_call {
call: inner_call, ..
}) => {
assert_eq!(*inner_call, remark);
},
_ => panic!("Expected the RuntimeCall::Contracts variant, got: {:?}", call),
}
}
}