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::