diff --git a/Cargo.lock b/Cargo.lock index cb075119f4..9aa74cd3fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5517,6 +5517,7 @@ dependencies = [ "pallet-ethereum-checked", "pallet-evm", "pallet-evm-precompile-assets-erc20", + "pallet-evm-precompile-batch", "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", "pallet-evm-precompile-dapps-staking", @@ -7295,6 +7296,34 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-evm-precompile-batch" +version = "0.1.0" +dependencies = [ + "derive_more", + "evm", + "fp-evm", + "frame-support", + "frame-system", + "hex-literal", + "log", + "num_enum 0.5.11", + "pallet-balances", + "pallet-evm", + "pallet-timestamp", + "parity-scale-codec", + "paste", + "precompile-utils", + "scale-info", + "serde", + "sha3", + "slices", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-evm-precompile-blake2" version = "2.0.0-dev" @@ -9815,9 +9844,11 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "precompile-utils" -version = "0.4.3" +version = "0.5.0" dependencies = [ "assert_matches", + "derive_more", + "environmental", "evm", "fp-evm", "frame-support", @@ -9829,6 +9860,8 @@ dependencies = [ "pallet-evm", "parity-scale-codec", "precompile-utils-macro", + "scale-info", + "serde", "sha3", "similar-asserts", "sp-core", @@ -12359,6 +12392,7 @@ dependencies = [ "pallet-evm", "pallet-evm-chain-id", "pallet-evm-precompile-assets-erc20", + "pallet-evm-precompile-batch", "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", "pallet-evm-precompile-dapps-staking", diff --git a/Cargo.toml b/Cargo.toml index 566fadbe41..6e3948a47f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ num-traits = { version = "0.2", default-features = false } rand = { version = "0.8.5", default-features = false } bounded-collections = { version = "0.1.5", default-features = false } hex = { version = "0.4.3", default-features = false } +paste = "1.0.6" # (native) array-bytes = "6.0.0" @@ -285,6 +286,7 @@ pallet-evm-precompile-substrate-ecdsa = { path = "./precompiles/substrate-ecdsa" pallet-evm-precompile-xcm = { path = "./precompiles/xcm", default-features = false } pallet-evm-precompile-xvm = { path = "./precompiles/xvm", default-features = false } pallet-evm-precompile-dapps-staking = { path = "./precompiles/dapps-staking", default-features = false } +pallet-evm-precompile-batch = { path = "./precompiles/batch", default-features = false } pallet-chain-extension-dapps-staking = { path = "./chain-extensions/dapps-staking", default-features = false } pallet-chain-extension-xvm = { path = "./chain-extensions/xvm", default-features = false } diff --git a/precompiles/batch/Batch.sol b/precompiles/batch/Batch.sol new file mode 100644 index 0000000000..b2884de6d3 --- /dev/null +++ b/precompiles/batch/Batch.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// Interface to the precompiled contract on Shibuya/Shiden/Astar +/// Predeployed at the address 0x0000000000000000000000000000000000005006 +/// For better understanding check the source code: +/// repo: https://github.com/AstarNetwork/astar + +/// @title Batch precompile +/// @dev Allows to perform multiple calls through one call to the precompile. +/// Can be used by EOA to do multiple calls in a single transaction. +interface Batch { + /// @dev Batch multiple calls into a single transaction. + /// All calls are performed from the address calling this precompile. + /// + /// In case of one subcall reverting following subcalls will still be attempted. + /// + /// @param to List of addresses to call. + /// @param value List of values for each subcall. If array is shorter than `to` then additional + /// calls will be performed with a value of 0. + /// @param callData Call data for each `to` address. If array is shorter than `to` then + /// additional calls will be performed with an empty call data. + /// @param gasLimit Gas limit for each `to` address. Use 0 to forward all the remaining gas. + /// If array is shorter than `to` then the remaining gas available will be used. + function batchSome( + address[] memory to, + uint256[] memory value, + bytes[] memory callData, + uint64[] memory gasLimit + ) external; + + /// @dev Batch multiple calls into a single transaction. + /// All calls are performed from the address calling this precompile. + /// + /// In case of one subcall reverting, no more subcalls will be executed but + /// the batch transaction will succeed. Use "batchAll" to revert on any subcall revert. + /// + /// @param to List of addresses to call. + /// @param value List of values for each subcall. If array is shorter than `to` then additional + /// calls will be performed with a value of 0. + /// @param callData Call data for each `to` address. If array is shorter than `to` then + /// additional calls will be performed with an empty call data. + /// @param gasLimit Gas limit for each `to` address. Use 0 to forward all the remaining gas. + /// If array is shorter than `to` then the remaining gas available will be used. + function batchSomeUntilFailure( + address[] memory to, + uint256[] memory value, + bytes[] memory callData, + uint64[] memory gasLimit + ) external; + + /// @dev Batch multiple calls into a single transaction. + /// All calls are performed from the address calling this precompile. + /// + /// In case of one subcall reverting, the entire batch will revert. + /// + /// @param to List of addresses to call. + /// @param value List of values for each subcall. If array is shorter than `to` then additional + /// calls will be performed with a value of 0. + /// @param callData Call data for each `to` address. If array is shorter than `to` then + /// additional calls will be performed with an empty call data. + /// @param gasLimit Gas limit for each `to` address. Use 0 to forward all the remaining gas. + /// If array is shorter than `to` then the remaining gas available will be used. + function batchAll( + address[] memory to, + uint256[] memory value, + bytes[] memory callData, + uint64[] memory gasLimit + ) external; + + /// Emitted when a subcall succeeds. + event SubcallSucceeded(uint256 index); + + /// Emitted when a subcall fails. + event SubcallFailed(uint256 index); + +} diff --git a/precompiles/batch/Cargo.toml b/precompiles/batch/Cargo.toml new file mode 100644 index 0000000000..56a366b43e --- /dev/null +++ b/precompiles/batch/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "pallet-evm-precompile-batch" +description = "A Precompile to batch multiple calls." +version = "0.1.0" +authors = ["StakeTechnologies", "PureStake"] +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +log = { workspace = true } +num_enum = { workspace = true } +paste = { workspace = true } +slices = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true } + +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +parity-scale-codec = { workspace = true, features = ["max-encoded-len"] } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-std = { workspace = true } + +# Frontier +evm = { workspace = true, features = ["with-codec"] } +fp-evm = { workspace = true } +pallet-evm = { workspace = true } + +[dev-dependencies] +derive_more = { workspace = true } +hex-literal = { workspace = true } +serde = { workspace = true } +sha3 = { workspace = true } + +pallet-balances = { workspace = true, features = ["std"] } +pallet-timestamp = { workspace = true, features = ["std"] } +parity-scale-codec = { workspace = true, features = ["max-encoded-len", "std"] } +precompile-utils = { workspace = true, features = ["std", "testing"] } +scale-info = { workspace = true, features = ["derive", "std"] } +sp-runtime = { workspace = true, features = ["std"] } + +[features] +default = ["std"] +std = [ + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "pallet-evm/std", + "parity-scale-codec/std", + "precompile-utils/std", + "sp-core/std", + "sp-io/std", + "sp-std/std", +] diff --git a/precompiles/batch/src/lib.rs b/precompiles/batch/src/lib.rs new file mode 100644 index 0000000000..029c2e09a1 --- /dev/null +++ b/precompiles/batch/src/lib.rs @@ -0,0 +1,337 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +// Copyright 2019-2022 PureStake Inc. +// Copyright 2022 Stake Technologies +// This file is part of pallet-evm-precompile-batch package, originally developed by Purestake Inc. +// pallet-evm-precompile-batch package used in Astar Network in terms of GPLv3. +// +// pallet-evm-precompile-batch is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// pallet-evm-precompile-batch is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with pallet-evm-precompile-batch. If not, see . +#![cfg_attr(not(feature = "std"), no_std)] + +use ::evm::{ExitError, ExitReason}; +use fp_evm::{Context, Log, PrecompileFailure, PrecompileHandle, Transfer}; +use frame_support::{ + dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}, + traits::ConstU32, +}; +use pallet_evm::{Precompile, PrecompileOutput}; +use precompile_utils::{bytes::BoundedBytes, data::BoundedVec, *}; +use sp_core::{H160, U256}; +use sp_std::{iter::repeat, marker::PhantomData, vec, vec::Vec}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Mode { + BatchSome, // = "batchSome(address[],uint256[],bytes[],uint64[])", + BatchSomeUntilFailure, // = "batchSomeUntilFailure(address[],uint256[],bytes[],uint64[])", + BatchAll, // = "batchAll(address[],uint256[],bytes[],uint64[])", +} + +pub const LOG_SUBCALL_SUCCEEDED: [u8; 32] = keccak256!("SubcallSucceeded(uint256)"); +pub const LOG_SUBCALL_FAILED: [u8; 32] = keccak256!("SubcallFailed(uint256)"); +pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16); +pub const ARRAY_LIMIT: u32 = 2u32.pow(9); + +type GetCallDataLimit = ConstU32; +type GetArrayLimit = ConstU32; + +fn log_subcall_succeeded(address: impl Into, index: usize) -> Log { + LogsBuilder::new(address.into()).log1( + LOG_SUBCALL_SUCCEEDED, + data::encode_event_data(U256::from(index)), + ) +} + +fn log_subcall_failed(address: impl Into, index: usize) -> Log { + LogsBuilder::new(address.into()).log1( + LOG_SUBCALL_FAILED, + data::encode_event_data(U256::from(index)), + ) +} + +#[precompile_utils::generate_function_selector] +#[derive(Debug, PartialEq)] +pub enum Action { + BatchSome = "batchSome(address[],uint256[],bytes[],uint64[])", + BatchSomeUntilFailure = "batchSomeUntilFailure(address[],uint256[],bytes[],uint64[])", + BatchAll = "batchAll(address[],uint256[],bytes[],uint64[])", +} + +/// Batch precompile. +#[derive(Debug, Clone)] +pub struct BatchPrecompile(PhantomData); + +impl Precompile for BatchPrecompile +where + Runtime: pallet_evm::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, +{ + fn execute(handle: &mut impl PrecompileHandle) -> EvmResult { + let selector = handle.read_selector()?; + + handle.check_function_modifier(FunctionModifier::NonPayable)?; + match selector { + Action::BatchSome => Self::batch_some(handle), + Action::BatchAll => Self::batch_all(handle), + Action::BatchSomeUntilFailure => Self::batch_some_until_failure(handle), + } + } +} +// No funds are transfered to the precompile address. +// Transfers will directly be made on the behalf of the user by the precompile. +// #[precompile_utils::precompile] +impl BatchPrecompile +where + Runtime: pallet_evm::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, +{ + fn batch_some(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(4)?; + let to = input.read::>()?; + let value = input.read::>()?; + let call_data = + input.read::, GetArrayLimit>>()?; + let gas_limit = input.read::>()?; + log::trace!(target: "batch-precompile", "batch_some\n to address(s) {:?}, value(s) {:?} call_data(s) {:?}, gas_limit(s) {:?}", to, value,call_data, gas_limit); + Self::inner_batch(Mode::BatchSome, handle, to, value, call_data, gas_limit) + } + + fn batch_some_until_failure(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(4)?; + let to = input.read::>()?; + let value = input.read::>()?; + let call_data = + input.read::, GetArrayLimit>>()?; + let gas_limit = input.read::>()?; + log::trace!(target: "batch-precompile", "batch_some_until_failure\n to address(s) {:?}, value(s) {:?} call_data(s) {:?}, gas_limit(s) {:?}", to, value,call_data, gas_limit); + Self::inner_batch( + Mode::BatchSomeUntilFailure, + handle, + to, + value, + call_data, + gas_limit, + ) + } + + fn batch_all(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(4)?; + let to = input.read::>()?; + let value = input.read::>()?; + let call_data = + input.read::, GetArrayLimit>>()?; + let gas_limit = input.read::>()?; + log::trace!(target: "batch-precompile", "batch_all\n to address(s) {:?}, value(s) {:?} call_data(s) {:?}, gas_limit(s) {:?}", to, value,call_data, gas_limit); + Self::inner_batch(Mode::BatchAll, handle, to, value, call_data, gas_limit) + } + + fn inner_batch( + mode: Mode, + handle: &mut impl PrecompileHandle, + to: BoundedVec, + value: BoundedVec, + call_data: BoundedVec, GetArrayLimit>, + gas_limit: BoundedVec, + ) -> EvmResult { + let addresses = Vec::from(to).into_iter().enumerate(); + let values = Vec::from(value) + .into_iter() + .map(|x| Some(x)) + .chain(repeat(None)); + let calls_data = Vec::from(call_data) + .into_iter() + .map(|x| Some(x.into())) + .chain(repeat(None)); + let gas_limits = Vec::from(gas_limit).into_iter().map(|x| + // x = 0 => forward all remaining gas + if x == 0 { + None + } else { + Some(x) + } + ).chain(repeat(None)); + + // Cost of batch log. (doesn't change when index changes) + let log_cost = log_subcall_failed(handle.code_address(), 0) + .compute_cost() + .map_err(|_| revert("Failed to compute log cost"))?; + + for ((i, address), (value, (call_data, gas_limit))) in + addresses.zip(values.zip(calls_data.zip(gas_limits))) + { + let address = address.0; + let value = value.unwrap_or(U256::zero()); + let call_data = call_data.unwrap_or(vec![]); + + let sub_context = Context { + caller: handle.context().caller, + address: address.clone(), + apparent_value: value, + }; + + let transfer = if value.is_zero() { + None + } else { + Some(Transfer { + source: handle.context().caller, + target: address.clone(), + value, + }) + }; + + // We reserve enough gas to emit a final log and perform the subcall itself. + // If not enough gas we stop there according to Mode strategy. + let remaining_gas = handle.remaining_gas(); + + let forwarded_gas = match (remaining_gas.checked_sub(log_cost), mode) { + (Some(remaining), _) => remaining, + (None, Mode::BatchAll) => { + return Err(PrecompileFailure::Error { + exit_status: ExitError::OutOfGas, + }) + } + (None, _) => { + return Ok(succeed(EvmDataWriter::new().write(true).build())); + } + }; + + // Cost of the call itself that the batch precompile must pay. + let call_cost = call_cost(value, ::config()); + + let forwarded_gas = match forwarded_gas.checked_sub(call_cost) { + Some(remaining) => remaining, + None => { + let log = log_subcall_failed(handle.code_address(), i); + handle.record_log_costs(&[&log])?; + log.record(handle)?; + + match mode { + Mode::BatchAll => { + return Err(PrecompileFailure::Error { + exit_status: ExitError::OutOfGas, + }) + } + Mode::BatchSomeUntilFailure => { + return Ok(succeed(EvmDataWriter::new().write(true).build())) + } + Mode::BatchSome => continue, + } + } + }; + + // If there is a provided gas limit we ensure there is enough gas remaining. + let forwarded_gas = match gas_limit { + None => forwarded_gas, // provide all gas if no gas limit, + Some(limit) => { + if limit > forwarded_gas { + let log = log_subcall_failed(handle.code_address(), i); + handle.record_log_costs(&[&log])?; + log.record(handle)?; + + match mode { + Mode::BatchAll => { + return Err(PrecompileFailure::Error { + exit_status: ExitError::OutOfGas, + }) + } + Mode::BatchSomeUntilFailure => { + return Ok(succeed(EvmDataWriter::new().write(true).build())) + } + Mode::BatchSome => continue, + } + } + limit + } + }; + + let (reason, output) = handle.call( + address, + transfer, + call_data, + Some(forwarded_gas), + false, + &sub_context, + ); + + // Logs + // We reserved enough gas so this should not OOG. + match reason { + ExitReason::Revert(_) | ExitReason::Error(_) => { + let log = log_subcall_failed(handle.code_address(), i); + handle.record_log_costs(&[&log])?; + log.record(handle)? + } + ExitReason::Succeed(_) => { + let log = log_subcall_succeeded(handle.code_address(), i); + handle.record_log_costs(&[&log])?; + log.record(handle)? + } + _ => (), + } + + // How to proceed + match (mode, reason) { + // _: Fatal is always fatal + (_, ExitReason::Fatal(exit_status)) => { + return Err(PrecompileFailure::Fatal { exit_status }) + } + + // BatchAll : Reverts and errors are immediatly forwarded. + (Mode::BatchAll, ExitReason::Revert(exit_status)) => { + return Err(PrecompileFailure::Revert { + exit_status, + output, + }) + } + (Mode::BatchAll, ExitReason::Error(exit_status)) => { + return Err(PrecompileFailure::Error { exit_status }) + } + + // BatchSomeUntilFailure : Reverts and errors prevent subsequent subcalls to + // be executed but the precompile still succeed. + (Mode::BatchSomeUntilFailure, ExitReason::Revert(_) | ExitReason::Error(_)) => { + return Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + // Success or ignored revert/error. + (_, _) => (), + } + } + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } +} diff --git a/precompiles/batch/src/mock.rs b/precompiles/batch/src/mock.rs new file mode 100644 index 0000000000..4ac61de881 --- /dev/null +++ b/precompiles/batch/src/mock.rs @@ -0,0 +1,225 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +// Copyright 2019-2022 PureStake Inc. +// Copyright 2022 Stake Technologies +// This file is part of pallet-evm-precompile-batch package, originally developed by Purestake Inc. +// pallet-evm-precompile-batch package used in Astar Network in terms of GPLv3. +// +// pallet-evm-precompile-batch is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// pallet-evm-precompile-batch is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with pallet-evm-precompile-batch. If not, see . + +//! Test utilities +use super::*; + +use fp_evm::IsPrecompileResult; +use frame_support::traits::{ConstU64, Everything}; +use frame_support::{construct_runtime, parameter_types, weights::Weight}; +use pallet_evm::{EnsureAddressNever, EnsureAddressRoot, PrecompileResult, PrecompileSet}; +use precompile_utils::{mock_account, testing::MockAccount}; +use sp_core::H256; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + Perbill, +}; + +pub type AccountId = MockAccount; +pub type Balance = u128; +pub type BlockNumber = u32; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Evm: pallet_evm::{Pallet, Call, Storage, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + } +); + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub const MaximumBlockWeight: Weight = Weight::from_parts(1024, 1); + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const SS58Prefix: u8 = 42; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type BlockNumber = BlockNumber; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = sp_runtime::generic::Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} +parameter_types! { + pub const ExistentialDeposit: u128 = 1; +} +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = [u8; 4]; + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); +} + +pub fn precompile_address() -> H160 { + H160::from_low_u64_be(0x5002) +} + +#[derive(Debug, Clone, Copy)] +pub struct BatchPrecompileMock(PhantomData); + +impl PrecompileSet for BatchPrecompileMock +where + R: pallet_evm::Config, + BatchPrecompile: Precompile, +{ + fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { + match handle.code_address() { + a if a == precompile_address() => Some(BatchPrecompile::::execute(handle)), + _ => None, + } + } + + fn is_precompile(&self, address: sp_core::H160, _gas: u64) -> IsPrecompileResult { + IsPrecompileResult::Answer { + is_precompile: address == precompile_address(), + extra_cost: 0, + } + } +} + +mock_account!(Revert, |_| MockAccount::from_u64(2)); + +parameter_types! { + pub BlockGasLimit: U256 = U256::max_value(); + pub PrecompilesValue: BatchPrecompileMock = BatchPrecompileMock(Default::default()); + pub const WeightPerGas: Weight = Weight::from_parts(1, 0); +} + +impl pallet_evm::Config for Runtime { + type FeeCalculator = (); + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AccountId; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesType = BatchPrecompileMock; + type PrecompilesValue = PrecompilesValue; + type Timestamp = Timestamp; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = BlockGasLimit; + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type WeightInfo = (); + type GasLimitPovSizeRatio = ConstU64<4>; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { balances: vec![] } + } +} + +impl ExtBuilder { + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + balances: self.balances, + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + System::set_block_number(1); + pallet_evm::Pallet::::create_account( + Revert.into(), + hex_literal::hex!("1460006000fd").to_vec(), + ); + }); + ext + } +} diff --git a/precompiles/batch/src/tests.rs b/precompiles/batch/src/tests.rs new file mode 100644 index 0000000000..55241fe08a --- /dev/null +++ b/precompiles/batch/src/tests.rs @@ -0,0 +1,638 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +// Copyright 2019-2022 PureStake Inc. +// Copyright 2022 Stake Technologies +// This file is part of pallet-evm-precompile-batch package, originally developed by Purestake Inc. +// pallet-evm-precompile-batch package used in Astar Network in terms of GPLv3. +// +// pallet-evm-precompile-batch is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// pallet-evm-precompile-batch is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with pallet-evm-precompile-batch. If not, see . + +use crate::mock::{precompile_address, BatchPrecompileMock, ExtBuilder, PrecompilesValue, Runtime}; +use crate::{log_subcall_failed, log_subcall_succeeded, Mode, *}; +use fp_evm::ExitError; +use precompile_utils::{call_cost, testing::*, LogsBuilder}; +use sp_core::{H256, U256}; + +fn precompiles() -> BatchPrecompileMock { + PrecompilesValue::get() +} + +fn costs() -> (u64, u64) { + let return_log_cost = log_subcall_failed(precompile_address(), 0) + .compute_cost() + .unwrap(); + let call_cost = + return_log_cost + call_cost(U256::one(), ::config()); + (return_log_cost, call_cost) +} + +#[test] +fn batch_some_empty() { + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test( + Alice, + precompile_address(), + EvmDataWriter::new_with_selector(Action::BatchSome) + .write::>(vec![]) + .write::>(vec![]) + .write::>(vec![]) + .write::>(vec![]) + .build(), + ) + .execute_returns(EvmDataWriter::new().write(true).build()); + }) +} + +#[test] +fn batch_some_until_failure_empty() { + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test( + Alice, + precompile_address(), + EvmDataWriter::new_with_selector(Action::BatchSomeUntilFailure) + .write::>(vec![]) + .write::>(vec![]) + .write::>(vec![]) + .write::>(vec![]) + .build(), + ) + .execute_returns(EvmDataWriter::new().write(true).build()); + }) +} + +#[test] +fn batch_all_empty() { + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test( + Alice, + precompile_address(), + EvmDataWriter::new_with_selector(Action::BatchAll) + .write::>(vec![]) + .write::>(vec![]) + .write::>(vec![]) + .write::>(vec![]) + .build(), + ) + .execute_returns(EvmDataWriter::new().write(true).build()); + }) +} + +fn check_mode(mode: Mode) -> Action { + match mode { + Mode::BatchAll => Action::BatchAll, + Mode::BatchSome => Action::BatchSome, + Mode::BatchSomeUntilFailure => Action::BatchSomeUntilFailure, + } +} + +fn batch_returns( + precompiles: &BatchPrecompileMock, + mode: Mode, +) -> PrecompilesTester> { + let mut counter = 0; + let one = b"one"; + let two = b"two"; + let (_, total_call_cost) = costs(); + + precompiles + .prepare_test( + Alice, + precompile_address(), + EvmDataWriter::new_with_selector(check_mode(mode)) + .write(vec![Address(Bob.into()), Address(Charlie.into())]) + .write(vec![U256::from(1u8), U256::from(2u8)]) + .write(vec![Bytes::from(&one[..]), Bytes::from(&two[..])]) + .write::>(vec![]) + .build(), + ) + .with_target_gas(Some(100_000)) + .with_subcall_handle(move |subcall| { + let Subcall { + address, + transfer, + input, + target_gas, + is_static, + context, + } = subcall; + + // Called from the precompile caller. + assert_eq!(context.caller, Alice.into()); + assert_eq!(is_static, false); + + match address { + a if a == Bob.into() => { + assert_eq!(counter, 0, "this is the first call"); + counter += 1; + + assert_eq!( + target_gas, + Some(100_000 - total_call_cost), + "batch forward all gas" + ); + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, Alice.into()); + assert_eq!(transfer.target, Bob.into()); + assert_eq!(transfer.value, 1u8.into()); + + assert_eq!(context.address, Bob.into()); + assert_eq!(context.apparent_value, 1u8.into()); + + assert_eq!(&input, b"one"); + + SubcallOutput { + cost: 13, + logs: vec![ + LogsBuilder::new(Bob.into()).log1(H256::repeat_byte(0x11), vec![]) + ], + ..SubcallOutput::succeed() + } + } + a if a == Charlie.into() => { + assert_eq!(counter, 1, "this is the second call"); + counter += 1; + + assert_eq!( + target_gas, + Some(100_000 - 13 - total_call_cost * 2), + "batch forward all gas" + ); + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, Alice.into()); + assert_eq!(transfer.target, Charlie.into()); + assert_eq!(transfer.value, 2u8.into()); + + assert_eq!(context.address, Charlie.into()); + assert_eq!(context.apparent_value, 2u8.into()); + + assert_eq!(&input, b"two"); + + SubcallOutput { + cost: 17, + logs: vec![ + LogsBuilder::new(Charlie.into()).log1(H256::repeat_byte(0x22), vec![]) + ], + ..SubcallOutput::succeed() + } + } + _ => panic!("unexpected subcall"), + } + }) + .expect_cost(13 + 17 + total_call_cost * 2) +} + +#[test] +fn batch_some_returns() { + ExtBuilder::default().build().execute_with(|| { + batch_returns(&precompiles(), Mode::BatchSome) + .expect_log(LogsBuilder::new(Bob.into()).log1(H256::repeat_byte(0x11), vec![])) + .expect_log(log_subcall_succeeded(precompile_address(), 0)) + .expect_log(LogsBuilder::new(Charlie.into()).log1(H256::repeat_byte(0x22), vec![])) + .expect_log(log_subcall_succeeded(precompile_address(), 1)) + .execute_returns(EvmDataWriter::new().write(true).build()) + }) +} + +#[test] +fn batch_some_until_failure_returns() { + ExtBuilder::default().build().execute_with(|| { + batch_returns(&precompiles(), Mode::BatchSomeUntilFailure) + .expect_log(LogsBuilder::new(Bob.into()).log1(H256::repeat_byte(0x11), vec![])) + .expect_log(log_subcall_succeeded(precompile_address(), 0)) + .expect_log(LogsBuilder::new(Charlie.into()).log1(H256::repeat_byte(0x22), vec![])) + .expect_log(log_subcall_succeeded(precompile_address(), 1)) + .execute_returns(EvmDataWriter::new().write(true).build()) + }) +} + +#[test] +fn batch_all_returns() { + ExtBuilder::default().build().execute_with(|| { + batch_returns(&precompiles(), Mode::BatchAll) + .expect_log(LogsBuilder::new(Bob.into()).log1(H256::repeat_byte(0x11), vec![])) + .expect_log(log_subcall_succeeded(precompile_address(), 0)) + .expect_log(LogsBuilder::new(Charlie.into()).log1(H256::repeat_byte(0x22), vec![])) + .expect_log(log_subcall_succeeded(precompile_address(), 1)) + .execute_returns(EvmDataWriter::new().write(true).build()) + }) +} + +fn batch_out_of_gas( + precompiles: &BatchPrecompileMock, + mode: Mode, +) -> PrecompilesTester> { + let one = b"one"; + let (_, total_call_cost) = costs(); + precompiles + .prepare_test( + Alice, + precompile_address(), + EvmDataWriter::new_with_selector(check_mode(mode)) + .write(vec![Address(Bob.into())]) + .write(vec![U256::from(1u8)]) + .write(vec![Bytes::from(&one[..])]) + .write::>(vec![]) + .build(), + ) + .with_target_gas(Some(50_000)) + .with_subcall_handle(move |subcall| { + let Subcall { + address, + transfer, + input, + target_gas, + is_static, + context, + } = subcall; + + // Called from the precompile caller. + assert_eq!(context.caller, Alice.into()); + assert_eq!(is_static, false); + + match address { + a if a == Bob.into() => { + assert_eq!( + target_gas, + Some(50_000 - total_call_cost), + "batch forward all gas" + ); + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, Alice.into()); + assert_eq!(transfer.target, Bob.into()); + assert_eq!(transfer.value, 1u8.into()); + + assert_eq!(context.address, Bob.into()); + assert_eq!(context.apparent_value, 1u8.into()); + + assert_eq!(&input, b"one"); + + SubcallOutput { + cost: 11_000, + ..SubcallOutput::out_of_gas() + } + } + _ => panic!("unexpected subcall"), + } + }) +} + +#[test] +fn batch_some_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_out_of_gas(&precompiles(), Mode::BatchSome) + .expect_log(log_subcall_failed(precompile_address(), 0)) + .execute_returns(EvmDataWriter::new().write(true).build()) + }) +} + +#[test] +fn batch_some_until_failure_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_out_of_gas(&precompiles(), Mode::BatchSomeUntilFailure) + .expect_log(log_subcall_failed(precompile_address(), 0)) + .execute_returns(EvmDataWriter::new().write(true).build()) + }) +} + +#[test] +fn batch_all_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_out_of_gas(&precompiles(), Mode::BatchAll).execute_error(ExitError::OutOfGas) + }) +} + +fn batch_incomplete( + precompiles: &BatchPrecompileMock, + mode: Mode, +) -> PrecompilesTester> { + let mut counter = 0; + let one = b"one"; + + let (_, total_call_cost) = costs(); + + precompiles + .prepare_test( + Alice, + precompile_address(), + EvmDataWriter::new_with_selector(check_mode(mode)) + .write(vec![ + Address(Bob.into()), + Address(Charlie.into()), + Address(Alice.into()), + ]) + .write(vec![U256::from(1u8), U256::from(2u8), U256::from(3u8)]) + .write(vec![Bytes::from(&one[..])]) + .write::>(vec![]) + .build(), + ) + .with_target_gas(Some(300_000)) + .with_subcall_handle(move |subcall| { + let Subcall { + address, + transfer, + input, + target_gas, + is_static, + context, + } = subcall; + + // Called from the precompile caller. + assert_eq!(context.caller, Alice.into()); + assert_eq!(is_static, false); + + match address { + a if a == Bob.into() => { + assert_eq!(counter, 0, "this is the first call"); + counter += 1; + + assert_eq!( + target_gas, + Some(300_000 - total_call_cost), + "batch forward all gas" + ); + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, Alice.into()); + assert_eq!(transfer.target, Bob.into()); + assert_eq!(transfer.value, 1u8.into()); + + assert_eq!(context.address, Bob.into()); + assert_eq!(context.apparent_value, 1u8.into()); + + assert_eq!(&input, b"one"); + + SubcallOutput { + cost: 13, + logs: vec![ + LogsBuilder::new(Bob.into()).log1(H256::repeat_byte(0x11), vec![]) + ], + ..SubcallOutput::succeed() + } + } + a if a == Charlie.into() => { + assert_eq!(counter, 1, "this is the second call"); + counter += 1; + + assert_eq!( + target_gas, + Some(300_000 - 13 - total_call_cost * 2), + "batch forward all gas" + ); + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, Alice.into()); + assert_eq!(transfer.target, Charlie.into()); + assert_eq!(transfer.value, 2u8.into()); + + assert_eq!(context.address, Charlie.into()); + assert_eq!(context.apparent_value, 2u8.into()); + + assert_eq!(&input, b""); + + SubcallOutput { + output: String::from("Revert message").as_bytes().to_vec(), + cost: 17, + ..SubcallOutput::revert() + } + } + a if a == Alice.into() => { + assert_eq!(counter, 2, "this is the third call"); + counter += 1; + + assert_eq!( + target_gas, + Some(300_000 - 13 - 17 - total_call_cost * 3), + "batch forward all gas" + ); + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, Alice.into()); + assert_eq!(transfer.target, Alice.into()); + assert_eq!(transfer.value, 3u8.into()); + + assert_eq!(context.address, Alice.into()); + assert_eq!(context.apparent_value, 3u8.into()); + + assert_eq!(&input, b""); + + SubcallOutput { + cost: 19, + logs: vec![ + LogsBuilder::new(Alice.into()).log1(H256::repeat_byte(0x33), vec![]) + ], + ..SubcallOutput::succeed() + } + } + _ => panic!("unexpected subcall"), + } + }) +} + +#[test] +fn batch_some_incomplete() { + ExtBuilder::default().build().execute_with(|| { + let (_, total_call_cost) = costs(); + + batch_incomplete(&precompiles(), Mode::BatchSome) + .expect_log(LogsBuilder::new(Bob.into()).log1(H256::repeat_byte(0x11), vec![])) + .expect_log(log_subcall_succeeded(precompile_address(), 0)) + .expect_log(log_subcall_failed(precompile_address(), 1)) + .expect_log(LogsBuilder::new(Alice.into()).log1(H256::repeat_byte(0x33), vec![])) + .expect_log(log_subcall_succeeded(precompile_address(), 2)) + .expect_cost(13 + 17 + 19 + total_call_cost * 3) + .execute_returns(EvmDataWriter::new().write(true).build()) + }) +} + +#[test] +fn batch_some_until_failure_incomplete() { + ExtBuilder::default().build().execute_with(|| { + let (_, total_call_cost) = costs(); + + batch_incomplete(&precompiles(), Mode::BatchSomeUntilFailure) + .expect_log(LogsBuilder::new(Bob.into()).log1(H256::repeat_byte(0x11), vec![])) + .expect_log(log_subcall_succeeded(precompile_address(), 0)) + .expect_log(log_subcall_failed(precompile_address(), 1)) + .expect_cost(13 + 17 + total_call_cost * 2) + .execute_returns(EvmDataWriter::new().write(true).build()) + }) +} + +#[test] +fn batch_all_incomplete() { + ExtBuilder::default().build().execute_with(|| { + batch_incomplete(&precompiles(), Mode::BatchAll) + .execute_reverts(|output| output == b"Revert message") + }) +} + +fn batch_log_out_of_gas( + precompiles: &BatchPrecompileMock, + mode: Mode, +) -> PrecompilesTester> { + let (log_cost, _) = costs(); + let one = b"one"; + + precompiles + .prepare_test( + Alice, + precompile_address(), + EvmDataWriter::new_with_selector(check_mode(mode)) + .write(vec![Address(Bob.into())]) + .write(vec![U256::from(1u8)]) + .write(vec![Bytes::from(&one[..])]) + .write::>(vec![]) + .build(), + ) + .with_target_gas(Some(log_cost - 1)) + .with_subcall_handle(move |_subcall| panic!("there shouldn't be any subcalls")) +} + +#[test] +fn batch_all_log_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_log_out_of_gas(&precompiles(), Mode::BatchAll).execute_error(ExitError::OutOfGas); + }) +} + +#[test] +fn batch_some_log_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_log_out_of_gas(&precompiles(), Mode::BatchSome) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + }) +} + +#[test] +fn batch_some_until_failure_log_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_log_out_of_gas(&precompiles(), Mode::BatchSomeUntilFailure) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + }) +} + +fn batch_call_out_of_gas( + precompiles: &BatchPrecompileMock, + mode: Mode, +) -> PrecompilesTester> { + let (_, total_call_cost) = costs(); + let one = b"one"; + + precompiles + .prepare_test( + Alice, + precompile_address(), + EvmDataWriter::new_with_selector(check_mode(mode)) + .write(vec![Address(Bob.into())]) + .write(vec![U256::from(1u8)]) + .write(vec![Bytes::from(&one[..])]) + .write::>(vec![]) + .build(), + ) + .with_target_gas(Some(total_call_cost - 1)) + .with_subcall_handle(move |_subcall| panic!("there shouldn't be any subcalls")) +} + +#[test] +fn batch_all_call_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_call_out_of_gas(&precompiles(), Mode::BatchAll).execute_error(ExitError::OutOfGas); + }) +} + +#[test] +fn batch_some_call_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_call_out_of_gas(&precompiles(), Mode::BatchSome) + .expect_log(log_subcall_failed(precompile_address(), 0)) + .execute_returns(EvmDataWriter::new().write(true).build()); + }) +} + +#[test] +fn batch_some_until_failure_call_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_call_out_of_gas(&precompiles(), Mode::BatchSomeUntilFailure) + .expect_log(log_subcall_failed(precompile_address(), 0)) + .execute_returns(EvmDataWriter::new().write(true).build()); + }) +} + +fn batch_gas_limit( + precompiles: &BatchPrecompileMock, + mode: Mode, +) -> PrecompilesTester> { + let (_, total_call_cost) = costs(); + let one = b"one"; + + precompiles + .prepare_test( + Alice, + precompile_address(), + EvmDataWriter::new_with_selector(check_mode(mode)) + .write(vec![Address(Bob.into())]) + .write(vec![U256::from(1u8)]) + .write(vec![Bytes::from(&one[..])]) + .write::>(vec![U256::from(50_000 - total_call_cost + 1)]) + .build(), + ) + .with_target_gas(Some(50_000)) + .with_subcall_handle(move |_subcall| panic!("there shouldn't be any subcalls")) +} + +#[test] +fn batch_all_gas_limit() { + ExtBuilder::default().build().execute_with(|| { + batch_gas_limit(&precompiles(), Mode::BatchAll).execute_error(ExitError::OutOfGas); + }) +} + +#[test] +fn batch_some_gas_limit() { + ExtBuilder::default().build().execute_with(|| { + let (return_log_cost, _) = costs(); + + batch_gas_limit(&precompiles(), Mode::BatchSome) + .expect_log(log_subcall_failed(precompile_address(), 0)) + .expect_cost(return_log_cost) + .execute_returns(EvmDataWriter::new().write(true).build()); + }) +} + +#[test] +fn batch_some_until_failure_gas_limit() { + ExtBuilder::default().build().execute_with(|| { + batch_gas_limit(&precompiles(), Mode::BatchSomeUntilFailure) + .expect_log(log_subcall_failed(precompile_address(), 0)) + .execute_returns(EvmDataWriter::new().write(true).build()); + }) +} diff --git a/precompiles/dapps-staking/Cargo.toml b/precompiles/dapps-staking/Cargo.toml index 232780acf6..aa5584170c 100644 --- a/precompiles/dapps-staking/Cargo.toml +++ b/precompiles/dapps-staking/Cargo.toml @@ -31,7 +31,7 @@ pallet-evm = { workspace = true } [dev-dependencies] derive_more = { workspace = true } -pallet-balances = { workspace = true } +pallet-balances = { workspace = true, features = ["std"] } pallet-timestamp = { workspace = true } precompile-utils = { workspace = true, features = ["testing"] } serde = { workspace = true } diff --git a/precompiles/utils/Cargo.toml b/precompiles/utils/Cargo.toml index 6be1724ce6..a97a1a81ff 100644 --- a/precompiles/utils/Cargo.toml +++ b/precompiles/utils/Cargo.toml @@ -2,17 +2,22 @@ name = "precompile-utils" authors = ["StakeTechnologies", "PureStake"] description = "Utils to write EVM precompiles." -version = "0.4.3" +version = "0.5.0" edition.workspace = true homepage.workspace = true repository.workspace = true [dependencies] # There's a problem with --all-features when this is moved under dev-deps -evm = { workspace = true, features = ["std"], optional = true } +derive_more = { workspace = true, optional = true } +environmental = { workspace = true } +evm = { workspace = true, features = ["with-codec"] } +hex-literal = { workspace = true, optional = true } impl-trait-for-tuples = { workspace = true } log = { workspace = true } num_enum = { workspace = true } +scale-info = { workspace = true, optional = true, features = ["derive"] } +serde = { workspace = true, optional = true } sha3 = { workspace = true } similar-asserts = { workspace = true, optional = true } @@ -53,5 +58,6 @@ std = [ "sp-std/std", "sp-runtime/std", "xcm/std", + "environmental/std", ] -testing = ["similar-asserts", "std"] +testing = ["similar-asserts", "std", "scale-info", "serde", "derive_more", "hex-literal"] diff --git a/precompiles/utils/src/bytes.rs b/precompiles/utils/src/bytes.rs new file mode 100644 index 0000000000..ba86df9297 --- /dev/null +++ b/precompiles/utils/src/bytes.rs @@ -0,0 +1,218 @@ +// This file is part of Astar. + +// Copyright 2019-2022 PureStake Inc. +// Copyright (C) 2022-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Utils package, originally developed by Purestake Inc. +// Utils package used in Astar Network in terms of GPLv3. +// +// Utils is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Utils is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Utils. If not, see . + +use super::*; +use alloc::borrow::ToOwned; +pub use alloc::string::String; +use sp_core::{ConstU32, Get}; + +type ConstU32Max = ConstU32<{ u32::MAX }>; + +pub type UnboundedBytes = BoundedBytesString; +pub type BoundedBytes = BoundedBytesString; + +pub type UnboundedString = BoundedBytesString; +pub type BoundedString = BoundedBytesString; + +trait Kind { + fn signature() -> String; +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BytesKind; + +impl Kind for BytesKind { + fn signature() -> String { + String::from("bytes") + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StringKind; + +impl Kind for StringKind { + fn signature() -> String { + String::from("string") + } +} + +/// The `bytes/string` type of Solidity. +/// It is different from `Vec` which will be serialized with padding for each `u8` element +/// of the array, while `Bytes` is tightly packed. +#[derive(Debug)] +pub struct BoundedBytesString { + data: Vec, + _phantom: PhantomData<(K, S)>, +} + +impl> Clone for BoundedBytesString { + fn clone(&self) -> Self { + Self { + data: self.data.clone(), + _phantom: PhantomData, + } + } +} + +impl PartialEq> for BoundedBytesString { + fn eq(&self, other: &BoundedBytesString) -> bool { + self.data.eq(&other.data) + } +} + +impl Eq for BoundedBytesString {} + +impl> BoundedBytesString { + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + pub fn as_str(&self) -> Result<&str, sp_std::str::Utf8Error> { + sp_std::str::from_utf8(&self.data) + } +} + +impl> EvmData for BoundedBytesString { + fn read(reader: &mut EvmDataReader) -> EvmResult { + let mut inner_reader = reader.read_pointer()?; + + // Read bytes/string size. + let array_size: usize = inner_reader + .read::() + .map_err(|_| revert("length, out of bounds"))? + .try_into() + .map_err(|_| revert("length, value too large"))?; + + if array_size > S::get() as usize { + return Err(revert("length, value too large").into()); + } + + let data = inner_reader.read_raw_bytes(array_size)?; + + let bytes = Self { + data: data.to_owned(), + _phantom: PhantomData, + }; + + Ok(bytes) + } + + fn write(writer: &mut EvmDataWriter, value: Self) { + let value: Vec<_> = value.into(); + let length = value.len(); + + // Pad the data. + // Leave it as is if a multiple of 32, otherwise pad to next + // multiple or 32. + let chunks = length / 32; + let padded_size = match length % 32 { + 0 => chunks * 32, + _ => (chunks + 1) * 32, + }; + + let mut value = value.to_vec(); + value.resize(padded_size, 0); + + writer.write_pointer( + EvmDataWriter::new() + .write(U256::from(length)) + .write(value) + .build(), + ); + } + + fn has_static_size() -> bool { + false + } +} + +// BytesString <=> Vec/&[u8] + +impl From> for Vec { + fn from(value: BoundedBytesString) -> Self { + value.data + } +} + +impl From> for BoundedBytesString { + fn from(value: Vec) -> Self { + Self { + data: value, + _phantom: PhantomData, + } + } +} + +impl From<&[u8]> for BoundedBytesString { + fn from(value: &[u8]) -> Self { + Self { + data: value.to_vec(), + _phantom: PhantomData, + } + } +} + +impl From<[u8; N]> for BoundedBytesString { + fn from(value: [u8; N]) -> Self { + Self { + data: value.to_vec(), + _phantom: PhantomData, + } + } +} + +impl From<&[u8; N]> for BoundedBytesString { + fn from(value: &[u8; N]) -> Self { + Self { + data: value.to_vec(), + _phantom: PhantomData, + } + } +} + +// BytesString <=> String/str + +impl TryFrom> for String { + type Error = alloc::string::FromUtf8Error; + + fn try_from(value: BoundedBytesString) -> Result { + alloc::string::String::from_utf8(value.data) + } +} + +impl From<&str> for BoundedBytesString { + fn from(value: &str) -> Self { + Self { + data: value.as_bytes().into(), + _phantom: PhantomData, + } + } +} + +impl From for BoundedBytesString { + fn from(value: String) -> Self { + Self { + data: value.as_bytes().into(), + _phantom: PhantomData, + } + } +} diff --git a/precompiles/utils/src/data.rs b/precompiles/utils/src/data.rs index 2359bcefd4..2504171b33 100644 --- a/precompiles/utils/src/data.rs +++ b/precompiles/utils/src/data.rs @@ -23,9 +23,9 @@ use crate::{revert, EvmResult}; use alloc::borrow::ToOwned; -use core::{any::type_name, ops::Range}; +use core::{any::type_name, marker::PhantomData, ops::Range}; use impl_trait_for_tuples::impl_for_tuples; -use sp_core::{H160, H256, U256}; +use sp_core::{Get, H160, H256, U256}; use sp_std::{convert::TryInto, vec, vec::Vec}; /// The `address` type of Solidity. @@ -325,8 +325,31 @@ pub trait EvmData: Sized { fn read(reader: &mut EvmDataReader) -> EvmResult; fn write(writer: &mut EvmDataWriter, value: Self); fn has_static_size() -> bool; + fn is_explicit_tuple() -> bool { + false + } +} +/// Encode the value into its Solidity ABI format. +/// If `T` is a tuple it is encoded as a Solidity tuple with dynamic-size offset. +fn encode(value: T) -> Vec { + EvmDataWriter::new().write(value).build() +} + +/// Encode the value into its Solidity ABI format. +/// If `T` is a tuple every element is encoded without a prefixed offset. +/// It matches the encoding of Solidity function arguments and return value, or event data. +pub fn encode_arguments(value: T) -> Vec { + let output = encode(value); + if T::is_explicit_tuple() && !T::has_static_size() { + output[32..].to_vec() + } else { + output + } } +pub use self::encode_arguments as encode_return_value; +pub use self::encode_arguments as encode_event_data; + #[impl_for_tuples(1, 18)] impl EvmData for Tuple { fn has_static_size() -> bool { @@ -604,3 +627,105 @@ impl EvmData for Bytes { false } } + +/// Wrapper around a Vec that provides a max length bound on read. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BoundedVec { + inner: Vec, + _phantom: PhantomData, +} + +impl> EvmData for BoundedVec { + fn read(reader: &mut EvmDataReader) -> EvmResult { + let mut inner_reader = reader.read_pointer()?; + + let array_size: usize = inner_reader + .read::() + .map_err(|_| revert("out of bounds: length of array"))? + .try_into() + .map_err(|_| revert("value too large : Array has more than max items allowed"))?; + + if array_size > S::get() as usize { + return Err(revert("value too large : Array has more than max items allowed").into()); + } + + let mut array = vec![]; + + let mut item_reader = EvmDataReader { + input: inner_reader + .input + .get(32..) + .ok_or_else(|| revert("read out of bounds: array content"))?, + cursor: 0, + }; + + for _ in 0..array_size { + array.push(item_reader.read()?); + } + + Ok(BoundedVec { + inner: array, + _phantom: PhantomData, + }) + } + + fn write(writer: &mut EvmDataWriter, value: Self) { + let value: Vec<_> = value.into(); + let mut inner_writer = EvmDataWriter::new().write(U256::from(value.len())); + + for inner in value { + // Any offset in items are relative to the start of the item instead of the + // start of the array. However if there is offseted data it must but appended after + // all items (offsets) are written. We thus need to rely on `compute_offsets` to do + // that, and must store a "shift" to correct the offsets. + let shift = inner_writer.data.len(); + let item_writer = EvmDataWriter::new().write(inner); + + inner_writer = inner_writer.write_raw_bytes(&item_writer.data); + for mut offset_datum in item_writer.offset_data { + offset_datum.offset_shift += 32; + offset_datum.offset_position += shift; + inner_writer.offset_data.push(offset_datum); + } + } + + writer.write_pointer(inner_writer.build()); + } + + fn has_static_size() -> bool { + false + } +} + +impl From> for BoundedVec { + fn from(value: Vec) -> Self { + BoundedVec { + inner: value, + _phantom: PhantomData, + } + } +} + +impl From<&[T]> for BoundedVec { + fn from(value: &[T]) -> Self { + BoundedVec { + inner: value.to_vec(), + _phantom: PhantomData, + } + } +} + +impl From<[T; N]> for BoundedVec { + fn from(value: [T; N]) -> Self { + BoundedVec { + inner: value.to_vec(), + _phantom: PhantomData, + } + } +} + +impl From> for Vec { + fn from(value: BoundedVec) -> Self { + value.inner + } +} diff --git a/precompiles/utils/src/lib.rs b/precompiles/utils/src/lib.rs index 1b67c7ea24..ac7cfce9e3 100644 --- a/precompiles/utils/src/lib.rs +++ b/precompiles/utils/src/lib.rs @@ -25,6 +25,7 @@ extern crate alloc; use crate::alloc::borrow::ToOwned; +pub use alloc::string::String; use fp_evm::{ Context, ExitError, ExitRevert, ExitSucceed, PrecompileFailure, PrecompileHandle, PrecompileOutput, @@ -38,7 +39,8 @@ use pallet_evm::{GasWeightMapping, Log}; use sp_core::{H160, H256, U256}; use sp_std::{marker::PhantomData, vec, vec::Vec}; -mod data; +pub mod bytes; +pub mod data; pub use data::{Address, Bytes, EvmData, EvmDataReader, EvmDataWriter}; pub use precompile_utils_macro::{generate_function_selector, keccak256}; @@ -338,6 +340,68 @@ pub fn log_costs(topics: usize, data_len: usize) -> EvmResult { }) } +// Compute the cost of doing a subcall. +// Some parameters cannot be known in advance, so we estimate the worst possible cost. +pub fn call_cost(value: U256, config: &evm::Config) -> u64 { + // Copied from EVM code since not public. + pub const G_CALLVALUE: u64 = 9000; + pub const G_NEWACCOUNT: u64 = 25000; + + fn address_access_cost(is_cold: bool, regular_value: u64, config: &evm::Config) -> u64 { + if config.increase_state_access_gas { + if is_cold { + config.gas_account_access_cold + } else { + config.gas_storage_read_warm + } + } else { + regular_value + } + } + + fn xfer_cost(is_call_or_callcode: bool, transfers_value: bool) -> u64 { + if is_call_or_callcode && transfers_value { + G_CALLVALUE + } else { + 0 + } + } + + fn new_cost( + is_call_or_staticcall: bool, + new_account: bool, + transfers_value: bool, + config: &evm::Config, + ) -> u64 { + let eip161 = !config.empty_considered_exists; + if is_call_or_staticcall { + if eip161 { + if transfers_value && new_account { + G_NEWACCOUNT + } else { + 0 + } + } else if new_account { + G_NEWACCOUNT + } else { + 0 + } + } else { + 0 + } + } + + let transfers_value = value != U256::default(); + let is_cold = true; + let is_call_or_callcode = true; + let is_call_or_staticcall = true; + let new_account = true; + + address_access_cost(is_cold, config.gas_call, config) + + xfer_cost(is_call_or_callcode, transfers_value) + + new_cost(is_call_or_staticcall, new_account, transfers_value, config) +} + impl PrecompileHandleExt for T { #[must_use] /// Record cost of a log manualy. @@ -409,7 +473,7 @@ pub fn succeed(output: impl AsRef<[u8]>) -> PrecompileOutput { #[must_use] /// Check that a function call is compatible with the context it is /// called into. -fn check_function_modifier( +pub fn check_function_modifier( context: &Context, is_static: bool, modifier: FunctionModifier, diff --git a/precompiles/utils/src/testing.rs b/precompiles/utils/src/testing.rs deleted file mode 100644 index 721681ec87..0000000000 --- a/precompiles/utils/src/testing.rs +++ /dev/null @@ -1,448 +0,0 @@ -// This file is part of Astar. - -// Copyright 2019-2022 PureStake Inc. -// Copyright (C) 2022-2023 Stake Technologies Pte.Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later -// -// This file is part of Utils package, originally developed by Purestake Inc. -// Utils package used in Astar Network in terms of GPLv3. -// -// Utils is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Utils is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Utils. If not, see . -use super::*; -use assert_matches::assert_matches; -use fp_evm::{ - ExitReason, ExitSucceed, PrecompileOutput, PrecompileResult, PrecompileSet, Transfer, -}; -use sp_std::boxed::Box; - -pub struct Subcall { - pub address: H160, - pub transfer: Option, - pub input: Vec, - pub target_gas: Option, - pub is_static: bool, - pub context: Context, -} - -pub struct SubcallOutput { - pub reason: ExitReason, - pub output: Vec, - pub cost: u64, - pub logs: Vec, -} - -pub trait SubcallTrait: FnMut(Subcall) -> SubcallOutput + 'static {} - -impl SubcallOutput + 'static> SubcallTrait for T {} - -pub type SubcallHandle = Box; - -/// Mock handle to write tests for precompiles. -pub struct MockHandle { - pub gas_limit: u64, - pub gas_used: u64, - pub logs: Vec, - pub subcall_handle: Option, - pub code_address: H160, - pub input: Vec, - pub context: Context, - pub is_static: bool, -} - -impl MockHandle { - pub fn new(code_address: H160, context: Context) -> Self { - Self { - gas_limit: u64::MAX, - gas_used: 0, - logs: vec![], - subcall_handle: None, - code_address, - input: Vec::new(), - context, - is_static: false, - } - } -} - -// Compute the cost of doing a subcall. -// Some parameters cannot be known in advance, so we estimate the worst possible cost. -pub fn call_cost(value: U256, config: &evm::Config) -> u64 { - // Copied from EVM code since not public. - pub const G_CALLVALUE: u64 = 9000; - pub const G_NEWACCOUNT: u64 = 25000; - - fn address_access_cost(is_cold: bool, regular_value: u64, config: &evm::Config) -> u64 { - if config.increase_state_access_gas { - if is_cold { - config.gas_account_access_cold - } else { - config.gas_storage_read_warm - } - } else { - regular_value - } - } - - fn xfer_cost(is_call_or_callcode: bool, transfers_value: bool) -> u64 { - if is_call_or_callcode && transfers_value { - G_CALLVALUE - } else { - 0 - } - } - - fn new_cost( - is_call_or_staticcall: bool, - new_account: bool, - transfers_value: bool, - config: &evm::Config, - ) -> u64 { - let eip161 = !config.empty_considered_exists; - if is_call_or_staticcall { - if eip161 { - if transfers_value && new_account { - G_NEWACCOUNT - } else { - 0 - } - } else if new_account { - G_NEWACCOUNT - } else { - 0 - } - } else { - 0 - } - } - - let transfers_value = value != U256::default(); - let is_cold = true; - let is_call_or_callcode = true; - let is_call_or_staticcall = true; - let new_account = true; - - address_access_cost(is_cold, config.gas_call, config) - + xfer_cost(is_call_or_callcode, transfers_value) - + new_cost(is_call_or_staticcall, new_account, transfers_value, config) -} - -impl PrecompileHandle for MockHandle { - /// Perform subcall in provided context. - /// Precompile specifies in which context the subcall is executed. - fn call( - &mut self, - address: H160, - transfer: Option, - input: Vec, - target_gas: Option, - is_static: bool, - context: &Context, - ) -> (ExitReason, Vec) { - if self - .record_cost(call_cost(context.apparent_value, &evm::Config::london())) - .is_err() - { - return (ExitReason::Error(ExitError::OutOfGas), vec![]); - } - - match &mut self.subcall_handle { - Some(handle) => { - let SubcallOutput { - reason, - output, - cost, - logs, - } = handle(Subcall { - address, - transfer, - input, - target_gas, - is_static, - context: context.clone(), - }); - - if self.record_cost(cost).is_err() { - return (ExitReason::Error(ExitError::OutOfGas), vec![]); - } - - for log in logs { - self.log(log.address, log.topics, log.data) - .expect("cannot fail"); - } - - (reason, output) - } - None => panic!("no subcall handle registered"), - } - } - - fn record_cost(&mut self, cost: u64) -> Result<(), ExitError> { - self.gas_used += cost; - - if self.gas_used > self.gas_limit { - Err(ExitError::OutOfGas) - } else { - Ok(()) - } - } - - fn remaining_gas(&self) -> u64 { - self.gas_limit - self.gas_used - } - - fn log(&mut self, address: H160, topics: Vec, data: Vec) -> Result<(), ExitError> { - self.logs.push(PrettyLog(Log { - address, - topics, - data, - })); - Ok(()) - } - - /// Retreive the code address (what is the address of the precompile being called). - fn code_address(&self) -> H160 { - self.code_address - } - - /// Retreive the input data the precompile is called with. - fn input(&self) -> &[u8] { - &self.input - } - - /// Retreive the context in which the precompile is executed. - fn context(&self) -> &Context { - &self.context - } - - /// Is the precompile call is done statically. - fn is_static(&self) -> bool { - self.is_static - } - - /// Retreive the gas limit of this call. - fn gas_limit(&self) -> Option { - Some(self.gas_limit) - } - - fn record_external_cost( - &mut self, - _ref_time: Option, - _proof_size: Option, - ) -> Result<(), ExitError> { - Ok(()) - } - - fn refund_external_cost(&mut self, _ref_time: Option, _proof_size: Option) {} -} - -pub struct PrecompilesTester<'p, P> { - precompiles: &'p P, - handle: MockHandle, - - target_gas: Option, - subcall_handle: Option, - - expected_cost: Option, - expected_logs: Option>, -} - -impl<'p, P: PrecompileSet> PrecompilesTester<'p, P> { - pub fn new( - precompiles: &'p P, - from: impl Into, - to: impl Into, - data: Vec, - ) -> Self { - let to = to.into(); - let mut handle = MockHandle::new( - to, - Context { - address: to, - caller: from.into(), - apparent_value: U256::zero(), - }, - ); - - handle.input = data; - - Self { - precompiles, - handle, - - target_gas: None, - subcall_handle: None, - - expected_cost: None, - expected_logs: None, - } - } - - pub fn with_value(mut self, value: impl Into) -> Self { - self.handle.context.apparent_value = value.into(); - self - } - - pub fn with_subcall_handle(mut self, subcall_handle: impl SubcallTrait) -> Self { - self.subcall_handle = Some(Box::new(subcall_handle)); - self - } - - pub fn with_target_gas(mut self, target_gas: Option) -> Self { - self.target_gas = target_gas; - self - } - - pub fn with_gas_limit(mut self, gas_limit: u64) -> Self { - self.handle.gas_limit = gas_limit; - self - } - - pub fn expect_cost(mut self, cost: u64) -> Self { - self.expected_cost = Some(cost); - self - } - - pub fn expect_no_logs(mut self) -> Self { - self.expected_logs = Some(vec![]); - self - } - - pub fn expect_log(mut self, log: Log) -> Self { - self.expected_logs = Some({ - let mut logs = self.expected_logs.unwrap_or_default(); - logs.push(PrettyLog(log)); - logs - }); - self - } - - fn assert_optionals(&self) { - if let Some(cost) = &self.expected_cost { - assert_eq!(&self.handle.gas_used, cost); - } - - if let Some(logs) = &self.expected_logs { - similar_asserts::assert_eq!(&self.handle.logs, logs); - } - } - - fn execute(&mut self) -> Option { - let handle = &mut self.handle; - handle.subcall_handle = self.subcall_handle.take(); - - if let Some(gas_limit) = self.target_gas { - handle.gas_limit = gas_limit; - } - - let res = self.precompiles.execute(handle); - - self.subcall_handle = handle.subcall_handle.take(); - - res - } - - /// Execute the precompile set and expect some precompile to have been executed, regardless of the - /// result. - pub fn execute_some(mut self) { - let res = self.execute(); - assert!(res.is_some()); - self.assert_optionals(); - } - - /// Execute the precompile set and expect no precompile to have been executed. - pub fn execute_none(mut self) { - let res = self.execute(); - assert!(res.is_none()); - self.assert_optionals(); - } - - /// Execute the precompile set and check it returns provided output. - pub fn execute_returns(mut self, output: Vec) { - let res = self.execute(); - assert_eq!( - res, - Some(Ok(PrecompileOutput { - exit_status: ExitSucceed::Returned, - output - })) - ); - self.assert_optionals(); - } - - /// Execute the precompile set and check if it reverts. - /// Take a closure allowing to perform custom matching on the output. - pub fn execute_reverts(mut self, check: impl Fn(&[u8]) -> bool) { - let res = self.execute(); - assert_matches!( - res, - Some(Err(PrecompileFailure::Revert { output, ..})) - if check(&output) - ); - self.assert_optionals(); - } - - /// Execute the precompile set and check it returns provided output. - pub fn execute_error(mut self, error: ExitError) { - let res = self.execute(); - assert_eq!( - res, - Some(Err(PrecompileFailure::Error { exit_status: error })) - ); - self.assert_optionals(); - } -} - -pub trait PrecompileTesterExt: PrecompileSet + Sized { - fn prepare_test( - &self, - from: impl Into, - to: impl Into, - data: Vec, - ) -> PrecompilesTester; -} - -impl PrecompileTesterExt for T { - fn prepare_test( - &self, - from: impl Into, - to: impl Into, - data: Vec, - ) -> PrecompilesTester { - PrecompilesTester::new(self, from, to, data) - } -} - -#[derive(Clone, PartialEq, Eq)] -pub struct PrettyLog(Log); - -impl core::fmt::Debug for PrettyLog { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { - let bytes = self - .0 - .data - .iter() - .map(|b| format!("{:02X}", b)) - .collect::>() - .join(""); - - let message = String::from_utf8(self.0.data.clone()).ok(); - - f.debug_struct("Log") - .field("address", &self.0.address) - .field("topics", &self.0.topics) - .field("data", &bytes) - .field("data_utf8", &message) - .finish() - } -} diff --git a/precompiles/utils/src/testing/account.rs b/precompiles/utils/src/testing/account.rs new file mode 100644 index 0000000000..66c5a13714 --- /dev/null +++ b/precompiles/utils/src/testing/account.rs @@ -0,0 +1,186 @@ +// This file is part of Astar. + +// Copyright 2019-2022 PureStake Inc. +// Copyright (C) 2022-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Utils package, originally developed by Purestake Inc. +// Utils package used in Astar Network in terms of GPLv3. +// +// Utils is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Utils is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Utils. If not, see . +use { + pallet_evm::AddressMapping, + scale_info::TypeInfo, + serde::{Deserialize, Serialize}, + sp_core::{Decode, Encode, MaxEncodedLen, H160, H256}, +}; + +#[derive( + Eq, + PartialEq, + Ord, + PartialOrd, + Clone, + Encode, + Decode, + Debug, + MaxEncodedLen, + TypeInfo, + Serialize, + Deserialize, + derive_more::Display, +)] +pub struct MockAccount(pub H160); + +impl MockAccount { + pub fn from_u64(v: u64) -> Self { + H160::from_low_u64_be(v).into() + } + + pub fn zero() -> Self { + H160::zero().into() + } + + pub fn has_prefix(&self, prefix: &[u8]) -> bool { + &self.0[0..4] == prefix + } + + pub fn has_prefix_u32(&self, prefix: u32) -> bool { + self.0[0..4] == prefix.to_be_bytes() + } + + pub fn without_prefix(&self) -> u128 { + u128::from_be_bytes(<[u8; 16]>::try_from(&self.0[4..20]).expect("slice have len 16")) + } +} + +impl From for H160 { + fn from(account: MockAccount) -> H160 { + account.0 + } +} + +impl From for [u8; 20] { + fn from(account: MockAccount) -> [u8; 20] { + let x: H160 = account.into(); + x.into() + } +} + +impl From for H256 { + fn from(x: MockAccount) -> H256 { + let x: H160 = x.into(); + x.into() + } +} + +impl From for MockAccount { + fn from(address: H160) -> MockAccount { + MockAccount(address) + } +} + +impl From<[u8; 20]> for MockAccount { + fn from(address: [u8; 20]) -> MockAccount { + let x: H160 = address.into(); + MockAccount(x) + } +} + +impl AddressMapping for MockAccount { + fn into_account_id(address: H160) -> MockAccount { + address.into() + } +} + +#[macro_export] +macro_rules! mock_account { + ($name:ident, $convert:expr) => { + pub struct $name; + mock_account!(# $name, $convert); + }; + ($name:ident ( $($field:ty),* ), $convert:expr) => { + pub struct $name($(pub $field),*); + mock_account!(# $name, $convert); + }; + (# $name:ident, $convert:expr) => { + impl From<$name> for MockAccount { + fn from(value: $name) -> MockAccount { + $convert(value) + } + } + + impl From<$name> for sp_core::H160 { + fn from(value: $name) -> sp_core::H160 { + MockAccount::from(value).into() + } + } + + impl From<$name> for sp_core::H256 { + fn from(value: $name) -> sp_core::H256 { + MockAccount::from(value).into() + } + } + }; +} + +mock_account!(Zero, |_| MockAccount::zero()); +mock_account!(Alice, |_| H160::repeat_byte(0xAA).into()); +mock_account!(Bob, |_| H160::repeat_byte(0xBB).into()); +mock_account!(Charlie, |_| H160::repeat_byte(0xCC).into()); +mock_account!(David, |_| H160::repeat_byte(0xDD).into()); + +mock_account!(Precompile1, |_| MockAccount::from_u64(1)); + +mock_account!(CryptoAlith, |_| H160::from(hex_literal::hex!( + "f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac" +)) +.into()); +mock_account!(CryptoBaltathar, |_| H160::from(hex_literal::hex!( + "3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0" +)) +.into()); +mock_account!(CryptoCarleth, |_| H160::from(hex_literal::hex!( + "798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc" +)) +.into()); + +mock_account!( + AddressInPrefixedSet(u32, u128), + |value: AddressInPrefixedSet| { + let prefix: u32 = value.0; + let index: u128 = value.1; + + let mut buffer = Vec::with_capacity(20); // 160 bits + + buffer.extend_from_slice(&prefix.to_be_bytes()); + buffer.extend_from_slice(&index.to_be_bytes()); + + assert_eq!(buffer.len(), 20, "address buffer should have len of 20"); + + H160::from_slice(&buffer).into() + } +); + +pub fn alith_secret_key() -> [u8; 32] { + hex_literal::hex!("5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133") +} + +pub fn baltathar_secret_key() -> [u8; 32] { + hex_literal::hex!("8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b") +} + +pub fn charleth_secret_key() -> [u8; 32] { + hex_literal::hex!("0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b") +} diff --git a/precompiles/utils/src/testing/execution.rs b/precompiles/utils/src/testing/execution.rs new file mode 100644 index 0000000000..c7a52a9261 --- /dev/null +++ b/precompiles/utils/src/testing/execution.rs @@ -0,0 +1,262 @@ +// This file is part of Astar. + +// Copyright 2019-2022 PureStake Inc. +// Copyright (C) 2022-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Utils package, originally developed by Purestake Inc. +// Utils package used in Astar Network in terms of GPLv3. +// +// Utils is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Utils is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Utils. If not, see . +// This file is part of Astar. + +// Copyright 2019-2022 PureStake Inc. +// Copyright (C) 2022-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Utils package, originally developed by Purestake Inc. +// Utils package used in Astar Network in terms of GPLv3. +// +// Utils is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Utils is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Utils. If not, see . + +use { + crate::testing::{decode_revert_message, MockHandle, PrettyLog, SubcallHandle, SubcallTrait}, + assert_matches::assert_matches, + fp_evm::{ + Context, ExitError, ExitSucceed, Log, PrecompileFailure, PrecompileOutput, + PrecompileResult, PrecompileSet, + }, + sp_core::{H160, U256}, + sp_std::boxed::Box, +}; + +#[must_use] +pub struct PrecompilesTester<'p, P> { + precompiles: &'p P, + handle: MockHandle, + + target_gas: Option, + subcall_handle: Option, + + expected_cost: Option, + expected_logs: Option>, +} + +impl<'p, P: PrecompileSet> PrecompilesTester<'p, P> { + pub fn new( + precompiles: &'p P, + from: impl Into, + to: impl Into, + data: Vec, + ) -> Self { + let to = to.into(); + let mut handle = MockHandle::new( + to.clone(), + Context { + address: to, + caller: from.into(), + apparent_value: U256::zero(), + }, + ); + + handle.input = data; + + Self { + precompiles, + handle, + + target_gas: None, + subcall_handle: None, + + expected_cost: None, + expected_logs: None, + } + } + + pub fn with_value(mut self, value: impl Into) -> Self { + self.handle.context.apparent_value = value.into(); + self + } + + pub fn with_subcall_handle(mut self, subcall_handle: impl SubcallTrait) -> Self { + self.subcall_handle = Some(Box::new(subcall_handle)); + self + } + + pub fn with_target_gas(mut self, target_gas: Option) -> Self { + self.target_gas = target_gas; + self + } + + pub fn with_gas_limit(mut self, gas_limit: u64) -> Self { + self.handle.gas_limit = gas_limit; + self + } + + pub fn expect_cost(mut self, cost: u64) -> Self { + self.expected_cost = Some(cost); + self + } + + pub fn expect_no_logs(mut self) -> Self { + self.expected_logs = Some(vec![]); + self + } + + pub fn expect_log(mut self, log: Log) -> Self { + self.expected_logs = Some({ + let mut logs = self.expected_logs.unwrap_or_else(Vec::new); + logs.push(PrettyLog(log)); + logs + }); + self + } + + fn assert_optionals(&self) { + if let Some(cost) = &self.expected_cost { + assert_eq!(&self.handle.gas_used, cost); + } + + if let Some(logs) = &self.expected_logs { + similar_asserts::assert_eq!(&self.handle.logs, logs); + } + } + + fn execute(&mut self) -> Option { + let handle = &mut self.handle; + handle.subcall_handle = self.subcall_handle.take(); + + if let Some(gas_limit) = self.target_gas { + handle.gas_limit = gas_limit; + } + + let res = self.precompiles.execute(handle); + + self.subcall_handle = handle.subcall_handle.take(); + + res + } + + /// Execute the precompile set and expect some precompile to have been executed, regardless of the + /// result. + pub fn execute_some(mut self) { + let res = self.execute(); + assert!(res.is_some()); + self.assert_optionals(); + } + + /// Execute the precompile set and expect no precompile to have been executed. + pub fn execute_none(mut self) { + let res = self.execute(); + assert!(res.is_some()); + self.assert_optionals(); + } + + /// Execute the precompile set and check it returns provided output. + pub fn execute_returns_raw(mut self, output: Vec) { + let res = self.execute(); + + match res { + Some(Err(PrecompileFailure::Revert { output, .. })) => { + let decoded = decode_revert_message(&output); + eprintln!( + "Revert message (bytes): {:?}", + sp_core::hexdisplay::HexDisplay::from(&decoded) + ); + eprintln!( + "Revert message (string): {:?}", + core::str::from_utf8(decoded).ok() + ); + panic!("Shouldn't have reverted"); + } + Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: execution_output, + })) => { + if execution_output != output { + eprintln!( + "Output (bytes): {:?}", + sp_core::hexdisplay::HexDisplay::from(&execution_output) + ); + eprintln!( + "Output (string): {:?}", + core::str::from_utf8(&execution_output).ok() + ); + panic!("Output doesn't match"); + } + } + other => panic!("Unexpected result: {:?}", other), + } + + self.assert_optionals(); + } + + /// Execute the precompile set and check it returns provided Solidity encoded output. + pub fn execute_returns(self, output: Vec) { + self.execute_returns_raw(output) + } + + /// Execute the precompile set and check if it reverts. + /// Take a closure allowing to perform custom matching on the output. + pub fn execute_reverts(mut self, check: impl Fn(&[u8]) -> bool) { + let res = self.execute(); + assert_matches!( + res, + Some(Err(PrecompileFailure::Revert { output, ..})) + if check(&output) + ); + self.assert_optionals(); + } + + /// Execute the precompile set and check it returns provided output. + pub fn execute_error(mut self, error: ExitError) { + let res = self.execute(); + assert_eq!( + res, + Some(Err(PrecompileFailure::Error { exit_status: error })) + ); + self.assert_optionals(); + } +} + +pub trait PrecompileTesterExt: PrecompileSet + Sized { + fn prepare_test( + &self, + from: impl Into, + to: impl Into, + data: impl Into>, + ) -> PrecompilesTester; +} + +impl PrecompileTesterExt for T { + fn prepare_test( + &self, + from: impl Into, + to: impl Into, + data: impl Into>, + ) -> PrecompilesTester { + PrecompilesTester::new(self, from, to, data.into()) + } +} diff --git a/precompiles/utils/src/testing/handle.rs b/precompiles/utils/src/testing/handle.rs new file mode 100644 index 0000000000..b88714e7eb --- /dev/null +++ b/precompiles/utils/src/testing/handle.rs @@ -0,0 +1,220 @@ +// This file is part of Astar. + +// Copyright 2019-2022 PureStake Inc. +// Copyright (C) 2022-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Utils package, originally developed by Purestake Inc. +// Utils package used in Astar Network in terms of GPLv3. +// +// Utils is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Utils is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Utils. If not, see . + +use { + crate::testing::PrettyLog, + evm::{ExitRevert, ExitSucceed}, + fp_evm::{Context, ExitError, ExitReason, Log, PrecompileHandle, Transfer}, + sp_core::{H160, H256}, + sp_std::boxed::Box, +}; + +#[derive(Debug, Clone)] +pub struct Subcall { + pub address: H160, + pub transfer: Option, + pub input: Vec, + pub target_gas: Option, + pub is_static: bool, + pub context: Context, +} + +#[derive(Debug, Clone)] +pub struct SubcallOutput { + pub reason: ExitReason, + pub output: Vec, + pub cost: u64, + pub logs: Vec, +} + +impl SubcallOutput { + pub fn revert() -> Self { + Self { + reason: ExitReason::Revert(ExitRevert::Reverted), + output: Vec::new(), + cost: 0, + logs: Vec::new(), + } + } + + pub fn succeed() -> Self { + Self { + reason: ExitReason::Succeed(ExitSucceed::Returned), + output: Vec::new(), + cost: 0, + logs: Vec::new(), + } + } + + pub fn out_of_gas() -> Self { + Self { + reason: ExitReason::Error(ExitError::OutOfGas), + output: Vec::new(), + cost: 0, + logs: Vec::new(), + } + } +} + +pub trait SubcallTrait: FnMut(Subcall) -> SubcallOutput + 'static {} + +impl SubcallOutput + 'static> SubcallTrait for T {} + +pub type SubcallHandle = Box; + +/// Mock handle to write tests for precompiles. +pub struct MockHandle { + pub gas_limit: u64, + pub gas_used: u64, + pub logs: Vec, + pub subcall_handle: Option, + pub code_address: H160, + pub input: Vec, + pub context: Context, + pub is_static: bool, +} + +impl MockHandle { + pub fn new(code_address: H160, context: Context) -> Self { + Self { + gas_limit: u64::MAX, + gas_used: 0, + logs: vec![], + subcall_handle: None, + code_address, + input: Vec::new(), + context, + is_static: false, + } + } +} + +impl PrecompileHandle for MockHandle { + /// Perform subcall in provided context. + /// Precompile specifies in which context the subcall is executed. + fn call( + &mut self, + address: H160, + transfer: Option, + input: Vec, + target_gas: Option, + is_static: bool, + context: &Context, + ) -> (ExitReason, Vec) { + if self + .record_cost(crate::call_cost( + context.apparent_value, + &evm::Config::london(), + )) + .is_err() + { + return (ExitReason::Error(ExitError::OutOfGas), vec![]); + } + + match &mut self.subcall_handle { + Some(handle) => { + let SubcallOutput { + reason, + output, + cost, + logs, + } = handle(Subcall { + address, + transfer, + input, + target_gas, + is_static, + context: context.clone(), + }); + + if self.record_cost(cost).is_err() { + return (ExitReason::Error(ExitError::OutOfGas), vec![]); + } + + for log in logs { + self.log(log.address, log.topics, log.data) + .expect("cannot fail"); + } + + (reason, output) + } + None => panic!("no subcall handle registered"), + } + } + + fn record_cost(&mut self, cost: u64) -> Result<(), ExitError> { + self.gas_used += cost; + + if self.gas_used > self.gas_limit { + Err(ExitError::OutOfGas) + } else { + Ok(()) + } + } + + fn record_external_cost( + &mut self, + _ref_time: Option, + _proof_size: Option, + ) -> Result<(), ExitError> { + Ok(()) + } + fn refund_external_cost(&mut self, _ref_time: Option, _proof_size: Option) {} + + fn remaining_gas(&self) -> u64 { + self.gas_limit - self.gas_used + } + + fn log(&mut self, address: H160, topics: Vec, data: Vec) -> Result<(), ExitError> { + self.logs.push(PrettyLog(Log { + address, + topics, + data, + })); + Ok(()) + } + + /// Retreive the code address (what is the address of the precompile being called). + fn code_address(&self) -> H160 { + self.code_address + } + + /// Retreive the input data the precompile is called with. + fn input(&self) -> &[u8] { + &self.input + } + + /// Retreive the context in which the precompile is executed. + fn context(&self) -> &Context { + &self.context + } + + /// Is the precompile call is done statically. + fn is_static(&self) -> bool { + self.is_static + } + + /// Retreive the gas limit of this call. + fn gas_limit(&self) -> Option { + Some(self.gas_limit) + } +} diff --git a/precompiles/utils/src/testing/mod.rs b/precompiles/utils/src/testing/mod.rs new file mode 100644 index 0000000000..be6c5a881c --- /dev/null +++ b/precompiles/utils/src/testing/mod.rs @@ -0,0 +1,98 @@ +// This file is part of Astar. + +// Copyright 2019-2022 PureStake Inc. +// Copyright (C) 2022-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Utils package, originally developed by Purestake Inc. +// Utils package used in Astar Network in terms of GPLv3. +// +// Utils is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Utils is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Utils. If not, see . + +pub mod account; +pub mod execution; +pub mod handle; + +pub use {account::*, execution::*, handle::*}; + +use fp_evm::Log; + +pub fn decode_revert_message(encoded: &[u8]) -> &[u8] { + let encoded_len = encoded.len(); + // selector 4 + offset 32 + string length 32 + if encoded_len > 68 { + let message_len = encoded[36..68].iter().sum::(); + if encoded_len >= 68 + message_len as usize { + return &encoded[68..68 + message_len as usize]; + } + } + b"decode_revert_message: error" +} + +#[derive(Clone, PartialEq, Eq)] +pub struct PrettyLog(Log); + +impl core::fmt::Debug for PrettyLog { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + let bytes = self + .0 + .data + .iter() + .map(|b| format!("{:02X}", b)) + .collect::>() + .join(""); + + let message = String::from_utf8(self.0.data.clone()).ok(); + + f.debug_struct("Log") + .field("address", &self.0.address) + .field("topics", &self.0.topics) + .field("data", &bytes) + .field("data_utf8", &message) + .finish() + } +} +/// Panics if an event is not found in the system log of events +#[macro_export] +macro_rules! assert_event_emitted { + ($event:expr) => { + match &$event { + e => { + assert!( + crate::mock::events().iter().find(|x| *x == e).is_some(), + "Event {:?} was not found in events: \n {:?}", + e, + crate::mock::events() + ); + } + } + }; +} + +// Panics if an event is found in the system log of events +#[macro_export] +macro_rules! assert_event_not_emitted { + ($event:expr) => { + match &$event { + e => { + assert!( + crate::mock::events().iter().find(|x| *x == e).is_none(), + "Event {:?} was found in events: \n {:?}", + e, + crate::mock::events() + ); + } + } + }; +} diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index b49f360404..77a300febf 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -28,6 +28,7 @@ pallet-democracy = { workspace = true } pallet-ethereum = { workspace = true } pallet-ethereum-checked = { workspace = true } pallet-evm = { workspace = true } +pallet-evm-precompile-batch = { workspace = true } pallet-evm-precompile-blake2 = { workspace = true } pallet-evm-precompile-bn128 = { workspace = true } pallet-evm-precompile-dispatch = { workspace = true } @@ -127,6 +128,7 @@ std = [ "pallet-evm-precompile-modexp/std", "pallet-evm-precompile-sha3fips/std", "pallet-evm-precompile-dapps-staking/std", + "pallet-evm-precompile-batch/std", "pallet-evm-precompile-sr25519/std", "pallet-evm-precompile-substrate-ecdsa/std", "pallet-evm-precompile-xvm/std", diff --git a/runtime/local/src/precompiles.rs b/runtime/local/src/precompiles.rs index 9e0bf989d9..8520a7522a 100644 --- a/runtime/local/src/precompiles.rs +++ b/runtime/local/src/precompiles.rs @@ -23,6 +23,7 @@ use pallet_evm::{ PrecompileResult, PrecompileSet, }; use pallet_evm_precompile_assets_erc20::{AddressToAssetId, Erc20AssetsPrecompileSet}; +use pallet_evm_precompile_batch::BatchPrecompile; use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; use pallet_evm_precompile_dapps_staking::DappsStakingWrapper; @@ -54,9 +55,11 @@ impl LocalNetworkPrecompiles { /// Return all addresses that contain precompiles. This can be used to populate dummy code /// under the precompile. pub fn used_addresses() -> impl Iterator { - sp_std::vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 1024, 1025, 1026, 1027, 20481, 20482, 20483, 20485] - .into_iter() - .map(hash) + sp_std::vec![ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 1024, 1025, 1026, 1027, 20481, 20482, 20483, 20485, 20846 + ] + .into_iter() + .map(hash) } } @@ -67,6 +70,7 @@ impl PrecompileSet for LocalNetworkPrecompiles where Erc20AssetsPrecompileSet: PrecompileSet, DappsStakingWrapper: Precompile, + BatchPrecompile: Precompile, XvmPrecompile>: Precompile, Dispatch: Precompile, R: pallet_evm::Config @@ -113,6 +117,9 @@ where a if a == hash(20485) => { Some(XvmPrecompile::>::execute(handle)) } + // Batch 0x5006 + a if a == hash(20486) => Some(BatchPrecompile::::execute(handle)), + // If the address matches asset prefix, the we route through the asset precompile set a if &a.to_fixed_bytes()[0..4] == ASSET_PRECOMPILE_ADDRESS_PREFIX => { Erc20AssetsPrecompileSet::::new().execute(handle) diff --git a/runtime/shibuya/Cargo.toml b/runtime/shibuya/Cargo.toml index f02da1a5fb..5658d2e833 100644 --- a/runtime/shibuya/Cargo.toml +++ b/runtime/shibuya/Cargo.toml @@ -105,6 +105,7 @@ pallet-custom-signatures = { workspace = true } pallet-dapps-staking = { workspace = true } pallet-ethereum-checked = { workspace = true } pallet-evm-precompile-assets-erc20 = { workspace = true } +pallet-evm-precompile-batch = { workspace = true } pallet-evm-precompile-dapps-staking = { workspace = true } pallet-evm-precompile-sr25519 = { workspace = true } pallet-evm-precompile-substrate-ecdsa = { workspace = true } @@ -180,6 +181,7 @@ std = [ "pallet-evm-precompile-modexp/std", "pallet-evm-precompile-sha3fips/std", "pallet-evm-precompile-dapps-staking/std", + "pallet-evm-precompile-batch/std", "pallet-evm-precompile-sr25519/std", "pallet-evm-precompile-substrate-ecdsa/std", "pallet-evm-precompile-assets-erc20/std", diff --git a/runtime/shibuya/src/precompiles.rs b/runtime/shibuya/src/precompiles.rs index d84839aa4b..3761e33ff4 100644 --- a/runtime/shibuya/src/precompiles.rs +++ b/runtime/shibuya/src/precompiles.rs @@ -23,6 +23,7 @@ use pallet_evm::{ PrecompileResult, PrecompileSet, }; use pallet_evm_precompile_assets_erc20::{AddressToAssetId, Erc20AssetsPrecompileSet}; +use pallet_evm_precompile_batch::BatchPrecompile; use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; use pallet_evm_precompile_dapps_staking::DappsStakingWrapper; @@ -58,7 +59,8 @@ impl ShibuyaNetworkPrecompiles { /// under the precompile. pub fn used_addresses() -> impl Iterator { sp_std::vec![ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 1024, 1025, 1026, 1027, 20481, 20482, 20483, 20484, 20485 + 1, 2, 3, 4, 5, 6, 7, 8, 9, 1024, 1025, 1026, 1027, 20481, 20482, 20483, 20484, 20485, + 20486 ] .into_iter() .map(hash) @@ -73,6 +75,7 @@ where Erc20AssetsPrecompileSet: PrecompileSet, DappsStakingWrapper: Precompile, XcmPrecompile: Precompile, + BatchPrecompile: Precompile, XvmPrecompile>: Precompile, Dispatch: Precompile, R: pallet_evm::Config @@ -123,6 +126,8 @@ where a if a == hash(20485) => { Some(XvmPrecompile::>::execute(handle)) } + // Batch 0x5006 + a if a == hash(20486) => Some(BatchPrecompile::::execute(handle)), // If the address matches asset prefix, the we route through the asset precompile set a if &a.to_fixed_bytes()[0..4] == ASSET_PRECOMPILE_ADDRESS_PREFIX => { Erc20AssetsPrecompileSet::::new().execute(handle)