From eb6c5f7b15488a98c4400e4ed26c6de87022f22a Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Mon, 27 May 2024 10:09:03 +0200 Subject: [PATCH 1/7] Port proxy precompile from Moonbeam --- Cargo.lock | 27 ++ Cargo.toml | 1 + precompiles/proxy/Cargo.toml | 60 +++ precompiles/proxy/Proxy.sol | 86 ++++ precompiles/proxy/src/lib.rs | 454 ++++++++++++++++++ precompiles/proxy/src/mock.rs | 301 ++++++++++++ precompiles/proxy/src/tests.rs | 839 +++++++++++++++++++++++++++++++++ 7 files changed, 1768 insertions(+) create mode 100644 precompiles/proxy/Cargo.toml create mode 100644 precompiles/proxy/Proxy.sol create mode 100644 precompiles/proxy/src/lib.rs create mode 100644 precompiles/proxy/src/mock.rs create mode 100644 precompiles/proxy/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 28386488..3da04fa5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7218,6 +7218,33 @@ dependencies = [ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", ] +[[package]] +name = "pallet-evm-precompile-proxy" +version = "0.1.0" +dependencies = [ + "derive_more", + "evm", + "fp-evm", + "frame-support", + "frame-system", + "hex-literal", + "log", + "num_enum", + "pallet-balances", + "pallet-evm", + "pallet-proxy", + "pallet-timestamp", + "parity-scale-codec", + "precompile-utils", + "scale-info", + "serde", + "sha3", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.11.0)", +] + [[package]] name = "pallet-evm-precompile-xcm" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 30294a73..5999ddb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -216,6 +216,7 @@ evm = { version = "0.41.1", default-features = false } pallet-evm-precompile-balances-erc20 = { path = "precompiles/balances-erc20", default-features = false } pallet-evm-precompile-batch = { path = "precompiles/batch", default-features = false } pallet-evm-precompile-call-permit = { path = "precompiles/call-permit", default-features = false } +pallet-evm-precompile-proxy = { path = "precompiles/proxy", default-features = false } pallet-evm-precompile-xcm-utils = { path = "precompiles/xcm-utils", default-features = false } async-backing-primitives = { path = "primitives/async-backing", default-features = false } pallet-author-inherent = { path = "pallets/author-inherent", default-features = false } diff --git a/precompiles/proxy/Cargo.toml b/precompiles/proxy/Cargo.toml new file mode 100644 index 00000000..81d26d43 --- /dev/null +++ b/precompiles/proxy/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "pallet-evm-precompile-proxy" +authors = { workspace = true } +description = "A Precompile to make proxy calls encoding accessible to pallet-evm" +edition = "2021" +version = "0.1.0" + +[dependencies] +log = { workspace = true } +num_enum = { workspace = true } +# rustc-hex = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true } + +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-balances = { workspace = true } +pallet-proxy = { workspace = true } +parity-scale-codec = { workspace = true, features = [ "derive" ] } +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# Frontier +evm = { workspace = true, features = [ "with-codec" ] } +fp-evm = { workspace = true } +pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] } + +[dev-dependencies] +derive_more = { workspace = true } +hex-literal = { workspace = true } +serde = { workspace = true } +sha3 = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true, features = [ "std", "testing" ] } + +# Substrate +pallet-balances = { workspace = true, features = [ "std" ] } +pallet-timestamp = { workspace = true, features = [ "std" ] } +scale-info = { workspace = true, features = [ "derive", "std" ] } +sp-io = { workspace = true, features = [ "std" ] } + +[features] +default = [ "std" ] +std = [ + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "pallet-evm/std", + "pallet-proxy/std", + "parity-scale-codec/std", + "precompile-utils/std", + "sp-core/std", + "sp-runtime/std", + "sp-std/std", +] diff --git a/precompiles/proxy/Proxy.sol b/precompiles/proxy/Proxy.sol new file mode 100644 index 00000000..8a01c86c --- /dev/null +++ b/precompiles/proxy/Proxy.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title Pallet Proxy Interface +/// @title The interface through which solidity contracts will interact with the Proxy pallet +interface Proxy { + /// @dev Defines the proxy permission types. + /// The values start at `0` (most permissive) and are represented as `uint8` + enum ProxyType { + Any, + NonTransfer, + Governance, + Staking, + CancelProxy, + Balances, + AuthorMapping, + IdentityJudgement + } + + /// @dev Register a proxy account for the sender that is able to make calls on its behalf + /// @custom:selector 74a34dd3 + /// @param delegate The account that the caller would like to make a proxy + /// @param proxyType The permissions allowed for this proxy account + /// @param delay The announcement period required of the initial proxy, will generally be zero + function addProxy( + address delegate, + ProxyType proxyType, + uint32 delay + ) external; + + /// @dev Removes a proxy account from the sender + /// @custom:selector fef3f708 + /// @param delegate The account that the caller would like to remove as a proxy + /// @param proxyType The permissions currently enabled for the removed proxy account + /// @param delay The announcement period required of the initial proxy, will generally be zero + function removeProxy( + address delegate, + ProxyType proxyType, + uint32 delay + ) external; + + /// @dev Unregister all proxy accounts for the sender + /// @custom:selector 14a5b5fa + function removeProxies() external; + + /// @dev Dispatch the given subcall (`callTo`, `callData`) from an account that the sender + /// is authorised for through `addProxy` + /// @custom:selector 0d3cff86 + /// @param real The account that the proxy will make a call on behalf of + /// @param callTo Recipient of the call to be made by the `real` account + /// @param callData Data of the call to be made by the `real` account + function proxy( + address real, + address callTo, + bytes memory callData + ) external payable; + + /// @dev Dispatch the given subcall (`callTo`, `callData`) from an account that the sender + /// is authorised for through `addProxy` + /// @custom:selector 685b9d2f + /// @param real The account that the proxy will make a call on behalf of + /// @param forceProxyType Specify the exact proxy type to be used and checked for this call + /// @param callTo Recipient of the call to be made by the `real` account + /// @param callData Data of the call to be made by the `real` account + function proxyForceType( + address real, + ProxyType forceProxyType, + address callTo, + bytes memory callData + ) external payable; + + /// @dev Checks if the caller has an account proxied with a given proxy type + /// @custom:selector e26d38ed + /// @param real The real account that maybe has a proxy + /// @param delegate The account that the caller has maybe proxied + /// @param proxyType The permissions allowed for the proxy + /// @param delay The announcement period required of the initial proxy, will generally be zero + /// @return exists True if a proxy exists, False otherwise + function isProxy( + address real, + address delegate, + ProxyType proxyType, + uint32 delay + ) external view returns (bool exists); +} diff --git a/precompiles/proxy/src/lib.rs b/precompiles/proxy/src/lib.rs new file mode 100644 index 00000000..16fd1d73 --- /dev/null +++ b/precompiles/proxy/src/lib.rs @@ -0,0 +1,454 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam 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. + +// Moonbeam 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 Moonbeam. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +use evm::ExitReason; +use fp_evm::{Context, PrecompileFailure, PrecompileHandle, Transfer}; +use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo}; +use pallet_balances::Call as BalancesCall; +use pallet_evm::AddressMapping; +use pallet_proxy::Call as ProxyCall; +use pallet_proxy::Pallet as ProxyPallet; +use precompile_utils::precompile_set::{self, AddressType, SelectorFilter}; +use precompile_utils::prelude::*; +use sp_core::{Get, H160, U256}; +use sp_runtime::{ + codec::Decode, + traits::{ConstU32, Dispatchable, StaticLookup, Zero}, +}; +use sp_std::marker::PhantomData; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[derive(Debug)] +pub struct OnlyIsProxy(PhantomData); + +impl SelectorFilter for OnlyIsProxy +where + Runtime: + pallet_proxy::Config + pallet_evm::Config + frame_system::Config + pallet_balances::Config, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: + From>, + ::ProxyType: Decode + EvmProxyCallFilter, + ::RuntimeCall: + Dispatchable + GetDispatchInfo, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: + From>, + ::RuntimeCall: + From> + From>, + >::Balance: TryFrom + Into, +{ + fn is_allowed(_caller: H160, selector: Option) -> bool { + match selector { + None => false, + Some(selector) => { + ProxyPrecompileCall::::is_proxy_selectors().contains(&selector) + } + } + } + + fn description() -> String { + "Allowed for all callers only for selector 'is_proxy'".into() + } +} + +#[derive(Debug)] +pub struct OnlyIsProxyAndProxy(PhantomData); + +impl SelectorFilter for OnlyIsProxyAndProxy +where + Runtime: + pallet_proxy::Config + pallet_evm::Config + frame_system::Config + pallet_balances::Config, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: + From>, + ::ProxyType: Decode + EvmProxyCallFilter, + ::RuntimeCall: + Dispatchable + GetDispatchInfo, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: + From>, + ::RuntimeCall: + From> + From>, + >::Balance: TryFrom + Into, +{ + fn is_allowed(_caller: H160, selector: Option) -> bool { + match selector { + None => false, + Some(selector) => { + ProxyPrecompileCall::::is_proxy_selectors().contains(&selector) + || ProxyPrecompileCall::::proxy_selectors().contains(&selector) + || ProxyPrecompileCall::::proxy_force_type_selectors() + .contains(&selector) + } + } + } + + fn description() -> String { + "Allowed for all callers only for selectors 'is_proxy', 'proxy', 'proxy_force_type'".into() + } +} + +pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16); + +type GetCallDataLimit = ConstU32; + +pub struct EvmSubCall { + pub to: Address, + pub value: U256, + pub call_data: BoundedBytes>, +} + +/// A trait to filter if an evm subcall is allowed to be executed by a proxy account. +/// This trait should be implemented by the `ProxyType` type configured in pallet proxy. +pub trait EvmProxyCallFilter: Sized + Send + Sync { + /// If returns `false`, then the subcall will not be executed and the evm transaction will + /// revert with error message "CallFiltered". + fn is_evm_proxy_call_allowed( + &self, + _call: &EvmSubCall, + _recipient_has_code: bool, + _gas: u64, + ) -> EvmResult { + Ok(false) + } +} + +/// A precompile to wrap the functionality from pallet-proxy. +pub struct ProxyPrecompile(PhantomData); + +#[precompile_utils::precompile] +impl ProxyPrecompile +where + Runtime: + pallet_proxy::Config + pallet_evm::Config + frame_system::Config + pallet_balances::Config, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: + From>, + ::ProxyType: Decode + EvmProxyCallFilter, + ::RuntimeCall: + Dispatchable + GetDispatchInfo, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: + From>, + ::RuntimeCall: + From> + From>, + >::Balance: TryFrom + Into, +{ + /// Register a proxy account for the sender that is able to make calls on its behalf. + /// The dispatch origin for this call must be Signed. + /// + /// Parameters: + /// * delegate: The account that the caller would like to make a proxy. + /// * proxy_type: The permissions allowed for this proxy account. + /// * delay: The announcement period required of the initial proxy. Will generally be zero. + #[precompile::public("addProxy(address,uint8,uint32)")] + fn add_proxy( + handle: &mut impl PrecompileHandle, + delegate: Address, + proxy_type: u8, + delay: u32, + ) -> EvmResult { + let delegate = Runtime::AddressMapping::into_account_id(delegate.into()); + let proxy_type = Runtime::ProxyType::decode(&mut proxy_type.to_le_bytes().as_slice()) + .map_err(|_| { + RevertReason::custom("Failed decoding value to ProxyType").in_field("proxyType") + })?; + let delay = delay.into(); + + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + // Disallow re-adding proxy via precompile to prevent re-entrancy. + // See: https://github.com/PureStake/sr-moonbeam/issues/30 + // Note: It is also assumed that EVM calls are only allowed through `Origin::Root` and + // filtered via CallFilter + // Proxies: + // Twox64Concat(8) + AccountId(20) + BoundedVec(ProxyDefinition * MaxProxies) + Balance(16) + handle.record_db_read::( + 28 + (29 * (::MaxProxies::get() as usize)) + 8, + )?; + if ProxyPallet::::proxies(&origin) + .0 + .iter() + .any(|pd| pd.delegate == delegate) + { + return Err(revert("Cannot add more than one proxy")); + } + + let delegate: ::Source = + Runtime::Lookup::unlookup(delegate.clone()); + let call: ProxyCall = ProxyCall::::add_proxy { + delegate, + proxy_type, + delay, + } + .into(); + + >::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + /// Unregister a proxy account for the sender. + /// The dispatch origin for this call must be Signed. + /// + /// Parameters: + /// * delegate: The account that the caller would like to remove as a proxy. + /// * proxy_type: The permissions currently enabled for the removed proxy account. + /// * delay: The announcement period required of the initial proxy. Will generally be zero. + #[precompile::public("removeProxy(address,uint8,uint32)")] + fn remove_proxy( + handle: &mut impl PrecompileHandle, + delegate: Address, + proxy_type: u8, + delay: u32, + ) -> EvmResult { + let delegate = Runtime::AddressMapping::into_account_id(delegate.into()); + let proxy_type = Runtime::ProxyType::decode(&mut proxy_type.to_le_bytes().as_slice()) + .map_err(|_| { + RevertReason::custom("Failed decoding value to ProxyType").in_field("proxyType") + })?; + let delay = delay.into(); + + let delegate: ::Source = + Runtime::Lookup::unlookup(delegate.clone()); + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let call: ProxyCall = ProxyCall::::remove_proxy { + delegate, + proxy_type, + delay, + } + .into(); + + >::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + /// Unregister all proxy accounts for the sender. + /// The dispatch origin for this call must be Signed. + /// WARNING: This may be called on accounts created by anonymous, however if done, then the + /// unreserved fees will be inaccessible. All access to this account will be lost. + #[precompile::public("removeProxies()")] + fn remove_proxies(handle: &mut impl PrecompileHandle) -> EvmResult { + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let call: ProxyCall = ProxyCall::::remove_proxies {}.into(); + + >::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + /// Dispatch the given subcall (`call_to`, `call_data`) from an account that the sender is + /// authorised for through `add_proxy`. + /// + /// Parameters: + /// - `real`: The account that the proxy will make a call on behalf of. + /// - `call_to`: Recipient of the call to be made by the `real` account. + /// - `call_data`: Data of the call to be made by the `real` account. + #[precompile::public("proxy(address,address,bytes)")] + #[precompile::payable] + fn proxy( + handle: &mut impl PrecompileHandle, + real: Address, + call_to: Address, + call_data: BoundedBytes, + ) -> EvmResult { + let evm_subcall = EvmSubCall { + to: call_to, + value: handle.context().apparent_value, + call_data, + }; + + Self::inner_proxy(handle, real, None, evm_subcall) + } + + /// Dispatch the given subcall (`call_to`, `call_data`) from an account that the sender is + /// authorised for through `add_proxy`. + /// + /// Parameters: + /// - `real`: The account that the proxy will make a call on behalf of. + /// - `force_proxy_type`: Specify the exact proxy type to be used and checked for this call. + /// - `call_to`: Recipient of the call to be made by the `real` account. + /// - `call_data`: Data of the call to be made by the `real` account. + #[precompile::public("proxyForceType(address,uint8,address,bytes)")] + #[precompile::public("proxy_force_type(address,uint8,address,bytes)")] + #[precompile::payable] + fn proxy_force_type( + handle: &mut impl PrecompileHandle, + real: Address, + force_proxy_type: u8, + call_to: Address, + call_data: BoundedBytes, + ) -> EvmResult { + let proxy_type = Runtime::ProxyType::decode(&mut force_proxy_type.to_le_bytes().as_slice()) + .map_err(|_| { + RevertReason::custom("Failed decoding value to ProxyType") + .in_field("forceProxyType") + })?; + + let evm_subcall = EvmSubCall { + to: call_to, + value: handle.context().apparent_value, + call_data, + }; + + Self::inner_proxy(handle, real, Some(proxy_type), evm_subcall) + } + + /// Checks if the caller has an account proxied with a given proxy type + /// + /// Parameters: + /// * delegate: The account that the caller has maybe proxied + /// * proxyType: The permissions allowed for the proxy + /// * delay: The announcement period required of the initial proxy. Will generally be zero. + #[precompile::public("isProxy(address,address,uint8,uint32)")] + #[precompile::view] + fn is_proxy( + handle: &mut impl PrecompileHandle, + real: Address, + delegate: Address, + proxy_type: u8, + delay: u32, + ) -> EvmResult { + let delegate = Runtime::AddressMapping::into_account_id(delegate.into()); + let proxy_type = Runtime::ProxyType::decode(&mut proxy_type.to_le_bytes().as_slice()) + .map_err(|_| { + RevertReason::custom("Failed decoding value to ProxyType").in_field("proxyType") + })?; + let delay = delay.into(); + + let real = Runtime::AddressMapping::into_account_id(real.into()); + + // Proxies: + // Twox64Concat(8) + AccountId(20) + BoundedVec(ProxyDefinition * MaxProxies) + Balance(16) + handle.record_db_read::( + 28 + (29 * (::MaxProxies::get() as usize)) + 8, + )?; + let is_proxy = ProxyPallet::::proxies(real) + .0 + .iter() + .any(|pd| pd.delegate == delegate && pd.proxy_type == proxy_type && pd.delay == delay); + + Ok(is_proxy) + } + + fn inner_proxy( + handle: &mut impl PrecompileHandle, + real: Address, + force_proxy_type: Option<::ProxyType>, + evm_subcall: EvmSubCall, + ) -> EvmResult { + // Check that we only perform proxy calls on behalf of externally owned accounts + let AddressType::EOA = precompile_set::get_address_type::(handle, real.into())? + else { + return Err(revert("real address must be EOA")); + }; + + // Read proxy + let real_account_id = Runtime::AddressMapping::into_account_id(real.into()); + let who = Runtime::AddressMapping::into_account_id(handle.context().caller); + // Proxies: + // Twox64Concat(8) + AccountId(20) + BoundedVec(ProxyDefinition * MaxProxies) + Balance(16) + handle.record_db_read::( + 28 + (29 * (::MaxProxies::get() as usize)) + 8, + )?; + let def = + pallet_proxy::Pallet::::find_proxy(&real_account_id, &who, force_proxy_type) + .map_err(|_| RevertReason::custom("Not proxy"))?; + frame_support::ensure!(def.delay.is_zero(), revert("Unannounced")); + + // Read subcall recipient code + // AccountCodes: Blake2128(16) + H160(20) + Vec(5) + // decode_len reads the first 5 bytes to find the payload len under this key + handle.record_db_read::(41)?; + let recipient_has_code = + pallet_evm::AccountCodes::::decode_len(evm_subcall.to.0).unwrap_or(0) > 0; + + // Apply proxy type filter + frame_support::ensure!( + def.proxy_type.is_evm_proxy_call_allowed( + &evm_subcall, + recipient_has_code, + handle.remaining_gas() + )?, + revert("CallFiltered") + ); + + let EvmSubCall { + to, + value, + call_data, + } = evm_subcall; + let address = to.0; + + let sub_context = Context { + caller: real.0, + address: address.clone(), + apparent_value: value, + }; + + let transfer = if value.is_zero() { + None + } else { + let contract_address: Runtime::AccountId = + Runtime::AddressMapping::into_account_id(handle.context().address); + + // Send back funds received by the precompile. + RuntimeHelper::::try_dispatch( + handle, + Some(contract_address).into(), + pallet_balances::Call::::transfer_allow_death { + dest: Runtime::Lookup::unlookup(who), + value: { + let balance: >::Balance = + value.try_into().map_err(|_| PrecompileFailure::Revert { + exit_status: fp_evm::ExitRevert::Reverted, + output: sp_std::vec::Vec::new(), + })?; + balance + }, + }, + )?; + + Some(Transfer { + source: sub_context.caller, + target: address.clone(), + value, + }) + }; + + let (reason, output) = handle.call( + address, + transfer, + call_data.into(), + Some(handle.remaining_gas()), + false, + &sub_context, + ); + + // Return subcall result + match reason { + ExitReason::Fatal(exit_status) => Err(PrecompileFailure::Fatal { exit_status }), + ExitReason::Revert(exit_status) => Err(PrecompileFailure::Revert { + exit_status, + output, + }), + ExitReason::Error(exit_status) => Err(PrecompileFailure::Error { exit_status }), + ExitReason::Succeed(_) => Ok(()), + } + } +} diff --git a/precompiles/proxy/src/mock.rs b/precompiles/proxy/src/mock.rs new file mode 100644 index 00000000..a8b4038e --- /dev/null +++ b/precompiles/proxy/src/mock.rs @@ -0,0 +1,301 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam 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. + +// Moonbeam 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 Moonbeam. If not, see . + +//! Test utilities +use crate::{ProxyPrecompile, ProxyPrecompileCall}; +use frame_support::{ + construct_runtime, parameter_types, + traits::{Everything, InstanceFilter}, + weights::Weight, +}; +use pallet_evm::{EnsureAddressNever, EnsureAddressOrigin, SubstrateBlockHashMapping}; +use precompile_utils::{ + precompile_set::{ + AddressU64, CallableByContract, CallableByPrecompile, OnlyFrom, PrecompileAt, + PrecompileSetBuilder, RevertPrecompile, SubcallWithMaxNesting, + }, + testing::MockAccount, +}; +use scale_info::TypeInfo; +use sp_core::{ConstU32, H160, H256, U256}; +use sp_io; +use sp_runtime::traits::{BlakeTwo256, IdentityLookup}; +use sp_runtime::{ + codec::{Decode, Encode, MaxEncodedLen}, + BuildStorage, +}; + +pub type AccountId = MockAccount; +pub type Balance = u128; + +type Block = frame_system::mocking::MockBlockU32; + +construct_runtime!( + pub enum Runtime { + System: frame_system, + Balances: pallet_balances, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + Proxy: pallet_proxy, + } +); + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub const SS58Prefix: u8 = 42; +} +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeTask = RuntimeTask; + type Nonce = u64; + type Block = Block; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + 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 = 0; +} +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = (); + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeFreezeReason = (); +} + +pub type Precompiles = PrecompileSetBuilder< + R, + ( + PrecompileAt< + AddressU64<1>, + ProxyPrecompile, + ( + SubcallWithMaxNesting<1>, + CallableByContract>, + // Batch is the only precompile allowed to call Proxy. + CallableByPrecompile>>, + ), + >, + RevertPrecompile>, + ), +>; + +pub type PCall = ProxyPrecompileCall; + +pub struct EnsureAddressAlways; +impl EnsureAddressOrigin for EnsureAddressAlways { + type Success = (); + + fn try_address_origin( + _address: &H160, + _origin: OuterOrigin, + ) -> Result { + Ok(()) + } + + fn ensure_address_origin( + _address: &H160, + _origin: OuterOrigin, + ) -> Result { + Ok(()) + } +} + +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; +/// Block storage limit in bytes. Set to 40 KB. +const BLOCK_STORAGE_LIMIT: u64 = 40 * 1024; + +parameter_types! { + pub BlockGasLimit: U256 = U256::from(u64::MAX); + pub PrecompilesValue: Precompiles = Precompiles::new(); + pub const WeightPerGas: Weight = Weight::from_parts(1, 0); + pub GasLimitPovSizeRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(MAX_POV_SIZE) + }; + pub GasLimitStorageGrowthRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(BLOCK_STORAGE_LIMIT) + }; +} +impl pallet_evm::Config for Runtime { + type FeeCalculator = (); + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressAlways; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AccountId; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesType = Precompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = BlockGasLimit; + type BlockHashMapping = SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type SuicideQuickClearLimit = ConstU32<0>; + type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; + type Timestamp = Timestamp; + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +#[repr(u8)] +#[derive( + Debug, Eq, PartialEq, Ord, PartialOrd, Decode, MaxEncodedLen, Encode, Clone, Copy, TypeInfo, +)] +pub enum ProxyType { + Any = 0, + Something = 1, + Nothing = 2, +} + +impl std::default::Default for ProxyType { + fn default() -> Self { + ProxyType::Any + } +} + +impl crate::EvmProxyCallFilter for ProxyType { + fn is_evm_proxy_call_allowed( + &self, + _call: &crate::EvmSubCall, + _recipient_has_code: bool, + _gas: u64, + ) -> precompile_utils::EvmResult { + Ok(match self { + Self::Any => true, + Self::Something => true, + Self::Nothing => false, + }) + } +} + +impl InstanceFilter for ProxyType { + fn filter(&self, _: &RuntimeCall) -> bool { + true + } + + fn is_superset(&self, o: &Self) -> bool { + (*self as u8) > (*o as u8) + } +} + +parameter_types! { + pub const ProxyDepositBase: u64 = 100; + pub const ProxyDepositFactor: u64 = 1; + pub const MaxProxies: u32 = 5; + pub const MaxPending: u32 = 5; +} +impl pallet_proxy::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type ProxyType = ProxyType; + type ProxyDepositBase = ProxyDepositBase; + type ProxyDepositFactor = ProxyDepositFactor; + type MaxProxies = MaxProxies; + type WeightInfo = (); + type MaxPending = MaxPending; + type CallHasher = BlakeTwo256; + type AnnouncementDepositBase = (); + type AnnouncementDepositFactor = (); +} + +/// Build test externalities, prepopulated with data for testing democracy precompiles +pub(crate) struct ExtBuilder { + /// Endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { balances: vec![] } + } +} + +impl ExtBuilder { + /// Fund some accounts before starting the test + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + + /// Build the test externalities for use in tests + 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.clone(), + } + .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); + }); + ext + } +} + +pub(crate) fn events() -> Vec { + System::events() + .into_iter() + .map(|r| r.event) + .collect::>() +} diff --git a/precompiles/proxy/src/tests.rs b/precompiles/proxy/src/tests.rs new file mode 100644 index 00000000..dc0cf682 --- /dev/null +++ b/precompiles/proxy/src/tests.rs @@ -0,0 +1,839 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam 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. + +// Moonbeam 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 Moonbeam. If not, see . + +use crate::mock::{ + AccountId, ExtBuilder, PCall, PrecompilesValue, ProxyType, Runtime, RuntimeCall, RuntimeEvent, + RuntimeOrigin, +}; +use frame_support::assert_ok; +use pallet_evm::Call as EvmCall; +use pallet_proxy::{ + Call as ProxyCall, Event as ProxyEvent, Pallet as ProxyPallet, ProxyDefinition, +}; +use precompile_utils::precompile_set::AddressU64; +use precompile_utils::{assert_event_emitted, assert_event_not_emitted, prelude::*, testing::*}; +use sp_core::{Get, H160, H256, U256}; +use sp_runtime::traits::Dispatchable; +use std::cell::Cell; +use std::rc::Rc; +use std::str::from_utf8; + +#[test] +fn test_selector_less_than_four_bytes_reverts() { + ExtBuilder::default().build().execute_with(|| { + PrecompilesValue::get() + .prepare_test(Alice, Precompile1, vec![1u8, 2, 3]) + .execute_reverts(|output| output == b"Tried to read selector out of bounds"); + }); +} + +#[test] +fn test_unimplemented_selector_reverts() { + ExtBuilder::default().build().execute_with(|| { + PrecompilesValue::get() + .prepare_test(Alice, Precompile1, vec![1u8, 2, 3, 4]) + .execute_reverts(|output| output == b"Unknown selector"); + }); +} + +#[test] +fn selectors() { + assert!(PCall::add_proxy_selectors().contains(&0x74a34dd3)); + assert!(PCall::remove_proxy_selectors().contains(&0xfef3f708)); + assert!(PCall::remove_proxies_selectors().contains(&0x14a5b5fa)); + assert!(PCall::proxy_selectors().contains(&0x0d3cff86)); + assert!(PCall::proxy_force_type_selectors().contains(&0x4a36b2cd)); + assert!(PCall::is_proxy_selectors().contains(&0xe26d38ed)); +} + +#[test] +fn modifiers() { + ExtBuilder::default().build().execute_with(|| { + let mut tester = + PrecompilesModifierTester::new(PrecompilesValue::get(), Alice, Precompile1); + + tester.test_default_modifier(PCall::add_proxy_selectors()); + tester.test_default_modifier(PCall::remove_proxy_selectors()); + tester.test_default_modifier(PCall::remove_proxies_selectors()); + tester.test_payable_modifier(PCall::proxy_selectors()); + tester.test_payable_modifier(PCall::proxy_force_type_selectors()); + tester.test_view_modifier(PCall::is_proxy_selectors()); + }); +} + +#[test] +fn test_add_proxy_fails_if_invalid_value_for_proxy_type() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::add_proxy { + delegate: Address(Bob.into()), + proxy_type: 10, + delay: 0, + }, + ) + .execute_reverts(|o| o == b"proxyType: Failed decoding value to ProxyType"); + }) +} + +#[test] +fn test_add_proxy_fails_if_duplicate_proxy() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Bob.into(), + proxy_type: ProxyType::Something, + delay: 0, + }) + .dispatch(RuntimeOrigin::signed(Alice.into()))); + + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::add_proxy { + delegate: Address(Bob.into()), + proxy_type: ProxyType::Something as u8, + delay: 0, + }, + ) + .execute_reverts(|o| o == b"Cannot add more than one proxy"); + }) +} + +#[test] +fn test_add_proxy_fails_if_less_permissive_proxy() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Bob.into(), + proxy_type: ProxyType::Something, + delay: 0, + }) + .dispatch(RuntimeOrigin::signed(Alice.into()))); + + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::add_proxy { + delegate: Address(Bob.into()), + proxy_type: ProxyType::Nothing as u8, + delay: 0, + }, + ) + .execute_reverts(|o| o == b"Cannot add more than one proxy"); + }) +} + +#[test] +fn test_add_proxy_fails_if_more_permissive_proxy() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Bob.into(), + proxy_type: ProxyType::Something, + delay: 0, + }) + .dispatch(RuntimeOrigin::signed(Alice.into()))); + + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::add_proxy { + delegate: Address(Bob.into()), + proxy_type: ProxyType::Any as u8, + delay: 0, + }, + ) + .execute_reverts(|o| o == b"Cannot add more than one proxy"); + }) +} + +#[test] +fn test_add_proxy_succeeds() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::add_proxy { + delegate: Address(Bob.into()), + proxy_type: ProxyType::Something as u8, + delay: 1, + }, + ) + .execute_returns(()); + assert_event_emitted!(RuntimeEvent::Proxy(ProxyEvent::ProxyAdded { + delegator: Alice.into(), + delegatee: Bob.into(), + proxy_type: ProxyType::Something, + delay: 1, + })); + + let proxies = >::proxies(AccountId::from(Alice)).0; + assert_eq!( + proxies, + vec![ProxyDefinition { + delegate: Bob.into(), + proxy_type: ProxyType::Something, + delay: 1, + }], + ) + }) +} + +#[test] +fn test_remove_proxy_fails_if_invalid_value_for_proxy_type() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Bob.into(), + proxy_type: ProxyType::Something, + delay: 0, + }) + .dispatch(RuntimeOrigin::signed(Alice.into()))); + + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::remove_proxy { + delegate: Address(Bob.into()), + proxy_type: 10, + delay: 0, + }, + ) + .execute_reverts(|o| o == b"proxyType: Failed decoding value to ProxyType"); + }) +} + +#[test] +fn test_remove_proxy_fails_if_proxy_not_exist() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::remove_proxy { + delegate: Address(Bob.into()), + proxy_type: ProxyType::Something as u8, + delay: 0, + }, + ) + .execute_reverts(|output| from_utf8(&output).unwrap().contains("NotFound")); + }) +} + +#[test] +fn test_remove_proxy_succeeds() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Bob.into(), + proxy_type: ProxyType::Something, + delay: 0, + }) + .dispatch(RuntimeOrigin::signed(Alice.into()))); + + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::remove_proxy { + delegate: Address(Bob.into()), + proxy_type: ProxyType::Something as u8, + delay: 0, + }, + ) + .execute_returns(()); + assert_event_emitted!(RuntimeEvent::Proxy(ProxyEvent::ProxyRemoved { + delegator: Alice.into(), + delegatee: Bob.into(), + proxy_type: ProxyType::Something, + delay: 0, + })); + + let proxies = >::proxies(AccountId::from(Alice)).0; + assert_eq!(proxies, vec![]) + }) +} + +#[test] +fn test_remove_proxies_succeeds() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Bob.into(), + proxy_type: ProxyType::Something, + delay: 0, + }) + .dispatch(RuntimeOrigin::signed(Alice.into()))); + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Charlie.into(), + proxy_type: ProxyType::Any, + delay: 0, + }) + .dispatch(RuntimeOrigin::signed(Alice.into()))); + + PrecompilesValue::get() + .prepare_test(Alice, Precompile1, PCall::remove_proxies {}) + .execute_returns(()); + + let proxies = >::proxies(AccountId::from(Alice)).0; + assert_eq!(proxies, vec![]) + }) +} + +#[test] +fn test_remove_proxies_succeeds_when_no_proxy_exists() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + PrecompilesValue::get() + .prepare_test(Alice, Precompile1, PCall::remove_proxies {}) + .execute_returns(()); + + let proxies = >::proxies(AccountId::from(Alice)).0; + assert_eq!(proxies, vec![]) + }) +} + +#[test] +fn test_proxy_force_type_fails_if_invalid_value_for_force_proxy_type() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::proxy_force_type { + real: Address(Bob.into()), + force_proxy_type: 10, + call_to: Address(Alice.into()), + call_data: BoundedBytes::from([]), + }, + ) + .execute_reverts(|o| o == b"forceProxyType: Failed decoding value to ProxyType"); + }) +} + +#[test] +fn test_proxy_fails_if_not_proxy() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::proxy { + real: Address(Bob.into()), + call_to: Address(Alice.into()), + call_data: BoundedBytes::from([]), + }, + ) + .execute_reverts(|o| o == b"Not proxy"); + }) +} + +#[test] +fn test_proxy_fails_if_call_filtered() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + // add delayed proxy + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::add_proxy { + delegate: Address(Bob.into()), + proxy_type: 2, + delay: 0, + }, + ) + .execute_returns(()); + + // Trying to use delayed proxy without any announcement + PrecompilesValue::get() + .prepare_test( + Bob, + Precompile1, + PCall::proxy { + real: Address(Alice.into()), + call_to: Address(Bob.into()), + call_data: BoundedBytes::from([]), + }, + ) + .execute_reverts(|o| o == b"CallFiltered"); + }) +} + +#[test] +fn test_is_proxy_returns_false_if_not_proxy() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::is_proxy { + real: Address(Alice.into()), + delegate: Address(Bob.into()), + proxy_type: ProxyType::Something as u8, + delay: 0, + }, + ) + .execute_returns(false); + }) +} + +#[test] +fn test_is_proxy_returns_false_if_proxy_type_incorrect() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Bob.into(), + proxy_type: ProxyType::Something, + delay: 0, + }) + .dispatch(RuntimeOrigin::signed(Alice.into()))); + + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::is_proxy { + real: Address(Alice.into()), + delegate: Address(Bob.into()), + proxy_type: ProxyType::Any as u8, + delay: 0, + }, + ) + .execute_returns(false); + }) +} + +#[test] +fn test_is_proxy_returns_false_if_proxy_delay_incorrect() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Bob.into(), + proxy_type: ProxyType::Something, + delay: 1, + }) + .dispatch(RuntimeOrigin::signed(Alice.into()))); + + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::is_proxy { + real: Address(Alice.into()), + delegate: Address(Bob.into()), + proxy_type: ProxyType::Any as u8, + delay: 0, + }, + ) + .execute_returns(false); + }) +} + +#[test] +fn test_is_proxy_returns_true_if_proxy() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Bob.into(), + proxy_type: ProxyType::Something, + delay: 1, + }) + .dispatch(RuntimeOrigin::signed(Alice.into()))); + + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::is_proxy { + real: Address(Alice.into()), + delegate: Address(Bob.into()), + proxy_type: ProxyType::Something as u8, + delay: 1, + }, + ) + .execute_returns(true); + }) +} + +#[test] +fn test_nested_evm_bypass_proxy_should_allow_elevating_proxy_type() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 100000000), (Bob.into(), 100000000)]) + .build() + .execute_with(|| { + // make Bob a ProxyType::Something for Alice + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Bob.into(), + proxy_type: ProxyType::Something, + delay: 0, + }) + .dispatch(RuntimeOrigin::signed(Alice.into()))); + + // construct the call wrapping the add_proxy precompile to escalate to ProxyType::Any + let add_proxy_precompile = PCall::add_proxy { + delegate: Address(Bob.into()), + proxy_type: ProxyType::Any as u8, + delay: 0, + } + .into(); + + let evm_call = RuntimeCall::Evm(EvmCall::call { + source: Alice.into(), + target: Precompile1.into(), + input: add_proxy_precompile, + value: U256::zero(), + gas_limit: u64::max_value(), + max_fee_per_gas: 0.into(), + max_priority_fee_per_gas: Some(U256::zero()), + nonce: None, + access_list: Vec::new(), + }); + + // call the evm call in a proxy call + assert_ok!(>::proxy( + RuntimeOrigin::signed(Bob.into()), + Alice.into(), + None, + Box::new(evm_call) + )); + + // assert Bob was not assigned ProxyType::Any + assert_event_not_emitted!(RuntimeEvent::Proxy(ProxyEvent::ProxyAdded { + delegator: Alice.into(), + delegatee: Bob.into(), + proxy_type: ProxyType::Any, + delay: 0, + })); + }) +} + +#[test] +fn fails_if_called_by_smart_contract() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + // Set code to Alice address as it if was a smart contract. + pallet_evm::AccountCodes::::insert(H160::from(Alice), vec![10u8]); + + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::add_proxy { + delegate: Address(Bob.into()), + proxy_type: ProxyType::Something as u8, + delay: 1, + }, + ) + .execute_reverts(|output| output == b"Function not callable by smart contracts"); + }) +} + +#[test] +fn succeed_if_called_by_precompile() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + // Set dummy code to Alice address as it if was a precompile. + pallet_evm::AccountCodes::::insert( + H160::from(Alice), + vec![0x60, 0x00, 0x60, 0x00, 0xfd], + ); + + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::add_proxy { + delegate: Address(Bob.into()), + proxy_type: ProxyType::Something as u8, + delay: 1, + }, + ) + .execute_returns(()); + }) +} + +#[test] +fn succeed_if_is_proxy_called_by_smart_contract() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000), (Bob.into(), 1000)]) + .build() + .execute_with(|| { + // Set code to Alice address as it if was a smart contract. + pallet_evm::AccountCodes::::insert(H160::from(Alice), vec![10u8]); + + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::is_proxy { + real: Address(Alice.into()), + delegate: Address(Bob.into()), + proxy_type: ProxyType::Something as u8, + delay: 1, + }, + ) + .execute_returns(false); + }) +} + +#[test] +fn proxy_proxy_should_fail_if_called_by_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AddressU64::<1>::get().into(), 1000), + (Bob.into(), 1000), + ]) + .build() + .execute_with(|| { + PrecompilesValue::get() + .prepare_test( + AddressU64::<1>::get(), + Precompile1, + PCall::proxy { + real: Address(Alice.into()), + call_to: Address(Bob.into()), + call_data: BoundedBytes::from([]), + }, + ) + .execute_reverts(|output| output == b"Function not callable by precompiles"); + }) +} + +#[test] +fn proxy_proxy_should_succeed_if_called_by_allowed_precompile() { + // "Not proxy" means that the security filter has passed, so the call to proxy.proxy would work + // if we had done a proxy.add_proxy before. + ExtBuilder::default() + .with_balances(vec![ + (AddressU64::<1>::get().into(), 1000), + (Bob.into(), 1000), + ]) + .build() + .execute_with(|| { + PrecompilesValue::get() + .prepare_test( + // Address<2> allowed in mock.rs + AddressU64::<2>::get(), + Precompile1, + PCall::proxy { + real: Address(Alice.into()), + call_to: Address(Bob.into()), + call_data: BoundedBytes::from([]), + }, + ) + .execute_reverts(|output| output == b"Not proxy"); + }) +} + +#[test] +fn proxy_proxy_should_succeed_if_called_by_smart_contract() { + ExtBuilder::default() + .with_balances(vec![ + (AddressU64::<1>::get().into(), 1000), + (Bob.into(), 1000), + ]) + .build() + .execute_with(|| { + // Set code to Alice address as it if was a smart contract. + pallet_evm::AccountCodes::::insert(H160::from(Alice), vec![10u8]); + + // Bob allows Alice to make calls on his behalf + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Alice.into(), + proxy_type: ProxyType::Any, + delay: 0, + }) + .dispatch(RuntimeOrigin::signed(Bob.into()))); + + let inside = Rc::new(Cell::new(false)); + let inside2 = inside.clone(); + + // The smart contract calls proxy.proxy to call address Charlie as if it was Bob + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::proxy { + real: Address(Bob.into()), + call_to: Address(Charlie.into()), + call_data: BoundedBytes::from([1]), + }, + ) + .with_subcall_handle(move |subcall| { + let Subcall { + address, + transfer, + input, + target_gas: _, + is_static, + context, + } = subcall; + + assert_eq!(context.caller, Bob.into()); + assert_eq!(address, Charlie.into()); + assert_eq!(is_static, false); + + assert!(transfer.is_none()); + + assert_eq!(context.address, Charlie.into()); + assert_eq!(context.apparent_value, 0u8.into()); + + assert_eq!(&input, &[1]); + + inside2.set(true); + + SubcallOutput { + output: b"TEST".to_vec(), + cost: 13, + logs: vec![log1(Bob, H256::repeat_byte(0x11), vec![])], + ..SubcallOutput::succeed() + } + }) + .execute_returns(()); + + // Ensure that the subcall was actually called. + // proxy.proxy does not propagate the return value, so we cannot check for the return + // value "TEST" + assert!(inside.get(), "subcall not called"); + }) +} + +#[test] +fn proxy_proxy_should_fail_if_called_by_smart_contract_for_a_non_eoa_account() { + ExtBuilder::default() + .with_balances(vec![ + (AddressU64::<1>::get().into(), 1000), + (Bob.into(), 1000), + ]) + .build() + .execute_with(|| { + // Set code to Alice & Bob addresses as if they are smart contracts. + pallet_evm::AccountCodes::::insert(H160::from(Alice), vec![10u8]); + pallet_evm::AccountCodes::::insert(H160::from(Bob), vec![10u8]); + + // Bob allows Alice to make calls on his behalf + assert_ok!(RuntimeCall::Proxy(ProxyCall::add_proxy { + delegate: Alice.into(), + proxy_type: ProxyType::Any, + delay: 0, + }) + .dispatch(RuntimeOrigin::signed(Bob.into()))); + + let inside = Rc::new(Cell::new(false)); + let inside2 = inside.clone(); + + // The smart contract calls proxy.proxy to call address Charlie as if it was Bob + PrecompilesValue::get() + .prepare_test( + Alice, + Precompile1, + PCall::proxy { + real: Address(Bob.into()), + call_to: Address(Charlie.into()), + call_data: BoundedBytes::from([1]), + }, + ) + .with_subcall_handle(move |subcall| { + let Subcall { + address, + transfer, + input, + target_gas: _, + is_static, + context, + } = subcall; + + assert_eq!(context.caller, Bob.into()); + assert_eq!(address, Charlie.into()); + assert_eq!(is_static, false); + + assert!(transfer.is_none()); + + assert_eq!(context.address, Charlie.into()); + assert_eq!(context.apparent_value, 0u8.into()); + + assert_eq!(&input, &[1]); + + inside2.set(true); + + SubcallOutput { + output: b"TEST".to_vec(), + cost: 13, + logs: vec![log1(Bob, H256::repeat_byte(0x11), vec![])], + ..SubcallOutput::succeed() + } + }) + .execute_reverts(|output| output == b"real address must be EOA"); + }) +} + +#[test] +fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() { + check_precompile_implements_solidity_interfaces(&["Proxy.sol"], PCall::supports_selector) +} From d9d9fadae3f95a7d02434effa35db69a855a6e7a Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Mon, 27 May 2024 10:11:39 +0200 Subject: [PATCH 2/7] cleanup --- precompiles/proxy/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/precompiles/proxy/Cargo.toml b/precompiles/proxy/Cargo.toml index 81d26d43..32476559 100644 --- a/precompiles/proxy/Cargo.toml +++ b/precompiles/proxy/Cargo.toml @@ -8,7 +8,6 @@ version = "0.1.0" [dependencies] log = { workspace = true } num_enum = { workspace = true } -# rustc-hex = { workspace = true } # Moonbeam precompile-utils = { workspace = true } From e89ae6d03c40a1a126ac47b5bb912d9b38053636 Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Mon, 27 May 2024 10:13:18 +0200 Subject: [PATCH 3/7] cleanup --- precompiles/proxy/Cargo.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/precompiles/proxy/Cargo.toml b/precompiles/proxy/Cargo.toml index 32476559..bfd5f5d1 100644 --- a/precompiles/proxy/Cargo.toml +++ b/precompiles/proxy/Cargo.toml @@ -9,9 +9,6 @@ version = "0.1.0" log = { workspace = true } num_enum = { workspace = true } -# Moonbeam -precompile-utils = { workspace = true } - # Substrate frame-support = { workspace = true } frame-system = { workspace = true } @@ -26,6 +23,7 @@ sp-std = { workspace = true } evm = { workspace = true, features = [ "with-codec" ] } fp-evm = { workspace = true } pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] } +precompile-utils = { workspace = true } [dev-dependencies] derive_more = { workspace = true } @@ -33,7 +31,7 @@ hex-literal = { workspace = true } serde = { workspace = true } sha3 = { workspace = true } -# Moonbeam +# Frontier precompile-utils = { workspace = true, features = [ "std", "testing" ] } # Substrate From 3ad162c9c9ae97a0d8fe7bdf403b51852c6a1d4a Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Mon, 27 May 2024 10:14:34 +0200 Subject: [PATCH 4/7] copyright --- precompiles/proxy/Proxy.sol | 2 +- precompiles/proxy/src/lib.rs | 10 +++++----- precompiles/proxy/src/mock.rs | 10 +++++----- precompiles/proxy/src/tests.rs | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/precompiles/proxy/Proxy.sol b/precompiles/proxy/Proxy.sol index 8a01c86c..2426c79e 100644 --- a/precompiles/proxy/Proxy.sol +++ b/precompiles/proxy/Proxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >=0.8.3; -/// @author The Moonbeam Team +/// @author The Moonsong Labs Team /// @title Pallet Proxy Interface /// @title The interface through which solidity contracts will interact with the Proxy pallet interface Proxy { diff --git a/precompiles/proxy/src/lib.rs b/precompiles/proxy/src/lib.rs index 16fd1d73..a8b4d457 100644 --- a/precompiles/proxy/src/lib.rs +++ b/precompiles/proxy/src/lib.rs @@ -1,18 +1,18 @@ -// Copyright 2019-2022 PureStake Inc. -// This file is part of Moonbeam. +// Copyright Moonsong Labs +// This file is part of Moonkit. -// Moonbeam is free software: you can redistribute it and/or modify +// Moonkit 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. -// Moonbeam is distributed in the hope that it will be useful, +// Moonkit 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 Moonbeam. If not, see . +// along with Moonkit. If not, see . #![cfg_attr(not(feature = "std"), no_std)] diff --git a/precompiles/proxy/src/mock.rs b/precompiles/proxy/src/mock.rs index a8b4038e..c2d7b8e2 100644 --- a/precompiles/proxy/src/mock.rs +++ b/precompiles/proxy/src/mock.rs @@ -1,18 +1,18 @@ -// Copyright 2019-2022 PureStake Inc. -// This file is part of Moonbeam. +// Copyright Moonsong Labs +// This file is part of Moonkit. -// Moonbeam is free software: you can redistribute it and/or modify +// Moonkit 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. -// Moonbeam is distributed in the hope that it will be useful, +// Moonkit 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 Moonbeam. If not, see . +// along with Moonkit. If not, see . //! Test utilities use crate::{ProxyPrecompile, ProxyPrecompileCall}; diff --git a/precompiles/proxy/src/tests.rs b/precompiles/proxy/src/tests.rs index dc0cf682..d4617936 100644 --- a/precompiles/proxy/src/tests.rs +++ b/precompiles/proxy/src/tests.rs @@ -1,18 +1,18 @@ -// Copyright 2019-2022 PureStake Inc. -// This file is part of Moonbeam. +// Copyright Moonsong Labs +// This file is part of Moonkit. -// Moonbeam is free software: you can redistribute it and/or modify +// Moonkit 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. -// Moonbeam is distributed in the hope that it will be useful, +// Moonkit 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 Moonbeam. If not, see . +// along with Moonkit. If not, see . use crate::mock::{ AccountId, ExtBuilder, PCall, PrecompilesValue, ProxyType, Runtime, RuntimeCall, RuntimeEvent, From b4aceb709323e896b59edd3cb9bff69de8dfbadb Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Mon, 27 May 2024 11:04:51 +0200 Subject: [PATCH 5/7] Fix mock --- precompiles/proxy/src/mock.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/precompiles/proxy/src/mock.rs b/precompiles/proxy/src/mock.rs index c2d7b8e2..9bfea17a 100644 --- a/precompiles/proxy/src/mock.rs +++ b/precompiles/proxy/src/mock.rs @@ -82,6 +82,11 @@ impl frame_system::Config for Runtime { type SS58Prefix = SS58Prefix; type OnSetCode = (); type MaxConsumers = frame_support::traits::ConstU32<16>; + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); } parameter_types! { pub const ExistentialDeposit: u128 = 0; @@ -152,10 +157,6 @@ parameter_types! { let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); block_gas_limit.saturating_div(MAX_POV_SIZE) }; - pub GasLimitStorageGrowthRatio: u64 = { - let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); - block_gas_limit.saturating_div(BLOCK_STORAGE_LIMIT) - }; } impl pallet_evm::Config for Runtime { type FeeCalculator = (); @@ -177,7 +178,6 @@ impl pallet_evm::Config for Runtime { type OnCreate = (); type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type SuicideQuickClearLimit = ConstU32<0>; - type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; type Timestamp = Timestamp; type WeightInfo = pallet_evm::weights::SubstrateWeight; } From b647284e9078af8fd99133c950ed19f0b1452614 Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Mon, 27 May 2024 16:00:39 +0200 Subject: [PATCH 6/7] asserts --- precompiles/proxy/src/mock.rs | 34 ++++++++++++++++++++++++++++++++++ precompiles/proxy/src/tests.rs | 12 +++++++----- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/precompiles/proxy/src/mock.rs b/precompiles/proxy/src/mock.rs index 9bfea17a..2b75cfd8 100644 --- a/precompiles/proxy/src/mock.rs +++ b/precompiles/proxy/src/mock.rs @@ -299,3 +299,37 @@ pub(crate) fn events() -> Vec { .map(|r| r.event) .collect::>() } + +/// Panics if an event is not found in the system log of events +#[macro_export] +macro_rules! assert_event_emitted { + ($event:expr) => { + match &$event.into() { + 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/precompiles/proxy/src/tests.rs b/precompiles/proxy/src/tests.rs index d4617936..587600e6 100644 --- a/precompiles/proxy/src/tests.rs +++ b/precompiles/proxy/src/tests.rs @@ -14,17 +14,19 @@ // You should have received a copy of the GNU General Public License // along with Moonkit. If not, see . -use crate::mock::{ - AccountId, ExtBuilder, PCall, PrecompilesValue, ProxyType, Runtime, RuntimeCall, RuntimeEvent, - RuntimeOrigin, +use crate::{ + assert_event_emitted, assert_event_not_emitted, + mock::{ + AccountId, ExtBuilder, PCall, PrecompilesValue, ProxyType, Runtime, RuntimeCall, + RuntimeEvent, RuntimeOrigin, + }, }; use frame_support::assert_ok; use pallet_evm::Call as EvmCall; use pallet_proxy::{ Call as ProxyCall, Event as ProxyEvent, Pallet as ProxyPallet, ProxyDefinition, }; -use precompile_utils::precompile_set::AddressU64; -use precompile_utils::{assert_event_emitted, assert_event_not_emitted, prelude::*, testing::*}; +use precompile_utils::{precompile_set::AddressU64, prelude::*, testing::*}; use sp_core::{Get, H160, H256, U256}; use sp_runtime::traits::Dispatchable; use std::cell::Cell; From 385224e9736bbc5ab111b10579632ec1283ee91e Mon Sep 17 00:00:00 2001 From: Francisco Gamundi Date: Mon, 27 May 2024 16:57:39 +0200 Subject: [PATCH 7/7] warn --- precompiles/proxy/src/mock.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/precompiles/proxy/src/mock.rs b/precompiles/proxy/src/mock.rs index 2b75cfd8..e978cb3a 100644 --- a/precompiles/proxy/src/mock.rs +++ b/precompiles/proxy/src/mock.rs @@ -146,8 +146,6 @@ impl EnsureAddressOrigin for EnsureAddressAlways { } const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; -/// Block storage limit in bytes. Set to 40 KB. -const BLOCK_STORAGE_LIMIT: u64 = 40 * 1024; parameter_types! { pub BlockGasLimit: U256 = U256::from(u64::MAX);