From c58d5a399207a29bae55195c471a98ddf60530c2 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov <39522748+dmitrylavrenov@users.noreply.github.com> Date: Mon, 18 Sep 2023 23:19:19 +0300 Subject: [PATCH] Add eHMND ERC20 precompile (#746) * Add basic implementation for total_supply and balance_of calls * Integrate initial precompile-evm-balances-erc20 implementation into humanode-runtime * Add metadata erc20 methods * Add approvals related logic * Add transfer related method * Add transferFrom related method * Add event logs * Add docs to interface actions * Fix some clippy * Add docs for approves logic * Fix typos * Remove unnecesary docs * Introduce helper functions to simplify code * Some refactoring * Update features snapshot * Fix docs * Add ERC20 solidity interface * Rename erc20 metadata at frontier precompiles * Add mocked environment * Add metadata related tests * Add balance_of_works and total_supply_works tests * Add approve_works test * Rename some tests * Add transfer_works test * Add transfer_from_works test * Rename symbol Co-authored-by: MOZGIII * Edit name erc20 metadata Co-authored-by: MOZGIII * Introduce pallet-erc20 to store approvals data * Use approvals logic from pallet-erc20 at precompile * Use currency config instead of pallet-evm-balances config * Add erc20 related logic into pallet-erc20 * Add utility aliases * Integrate pallet-erc20 logic into precompile * Fix mock at pallet-erc20 * Fix mock at precompile * Remove redundant dependency * Rename pallet-erc20 to pallet-token-wrapper * Rename precompile-evm-balances-erc20 into precompile-token-wrapper * Integrate introduced changes into humanode-runtime * Use wrapped-token instead for token-wrapper * Rename EvmBalancesErc20 into WrappedEvmBalances * Add total_supply_works test to pallet-wrapped-token * Improve approvals related test at pallet-wrapped-token * Add transfer_works test * Add transfer_from_works test * Add fails tests for transfer from logic * Add with_storage_layer usage at pallet-wrapped-token * Fix features * Use approvals aliases * Rename pallet-wrapped-token into pallet-erc20-support * Some renaming and docs improvements * Update features snapshot * Add comments for tests at pallet-erc20-support * Improve dispatch error handling * Add more tests at precompile-erc20-support * Rename precompile-erc20-support into precompile-native-currency * Rename Erc20EvmBalancesMetadata into EvmBalancesErc20Metadata * Rename Erc20EvmBalances into EvmBalancesErc20Support * Revert "Use approvals aliases" This reverts commit 654dc6523c41bb6598fa23e34b3ca80dcec31b42. * Properly handle transferFrom logic * Improve dispatch error handling * Add approve_overwrite_works test * Fix transfer * Add tests to check approve logic with sending full approved balance * Add approve_approval_value_more_than_balance_works test --------- Co-authored-by: MOZGIII --- Cargo.lock | 39 +- crates/humanode-runtime/Cargo.toml | 5 + .../src/frontier_precompiles.rs | 13 +- crates/humanode-runtime/src/lib.rs | 23 ++ crates/pallet-erc20-support/Cargo.toml | 20 + crates/pallet-erc20-support/src/lib.rs | 165 ++++++++ crates/pallet-erc20-support/src/mock.rs | 136 +++++++ crates/pallet-erc20-support/src/tests.rs | 335 +++++++++++++++++ crates/precompile-native-currency/Cargo.toml | 47 +++ crates/precompile-native-currency/ERC20.sol | 117 ++++++ crates/precompile-native-currency/src/lib.rs | 345 +++++++++++++++++ crates/precompile-native-currency/src/mock.rs | 225 +++++++++++ .../precompile-native-currency/src/tests.rs | 351 ++++++++++++++++++ utils/checks/snapshots/features.yaml | 43 ++- 14 files changed, 1859 insertions(+), 5 deletions(-) create mode 100644 crates/pallet-erc20-support/Cargo.toml create mode 100644 crates/pallet-erc20-support/src/lib.rs create mode 100644 crates/pallet-erc20-support/src/mock.rs create mode 100644 crates/pallet-erc20-support/src/tests.rs create mode 100644 crates/precompile-native-currency/Cargo.toml create mode 100644 crates/precompile-native-currency/ERC20.sol create mode 100644 crates/precompile-native-currency/src/lib.rs create mode 100644 crates/precompile-native-currency/src/mock.rs create mode 100644 crates/precompile-native-currency/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index c53550a5a..8790606ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3708,6 +3708,7 @@ dependencies = [ "pallet-chain-properties", "pallet-chain-start-moment", "pallet-currency-swap", + "pallet-erc20-support", "pallet-ethereum", "pallet-ethereum-chain-id", "pallet-evm", @@ -3735,6 +3736,7 @@ dependencies = [ "precompile-bioauth", "precompile-currency-swap", "precompile-evm-accounts-mapping", + "precompile-native-currency", "precompile-utils", "primitives-auth-ticket", "primitives-currency-swap-proxy", @@ -5937,6 +5939,18 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-erc20-support" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core", +] + [[package]] name = "pallet-ethereum" version = "4.0.0-dev" @@ -6747,6 +6761,27 @@ dependencies = [ "sp-std", ] +[[package]] +name = "precompile-native-currency" +version = "0.1.0" +dependencies = [ + "fp-evm", + "frame-support", + "frame-system", + "hex-literal", + "num_enum 0.6.0", + "pallet-balances", + "pallet-erc20-support", + "pallet-evm", + "pallet-evm-balances", + "pallet-evm-system", + "pallet-timestamp", + "parity-scale-codec", + "precompile-utils", + "scale-info", + "sp-core", +] + [[package]] name = "precompile-utils" version = "0.1.0" @@ -6918,9 +6953,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] diff --git a/crates/humanode-runtime/Cargo.toml b/crates/humanode-runtime/Cargo.toml index d7d336b49..e01326bee 100644 --- a/crates/humanode-runtime/Cargo.toml +++ b/crates/humanode-runtime/Cargo.toml @@ -23,6 +23,7 @@ pallet-bootnodes = { path = "../pallet-bootnodes", default-features = false } pallet-chain-properties = { path = "../pallet-chain-properties", default-features = false } pallet-chain-start-moment = { path = "../pallet-chain-start-moment", default-features = false } pallet-currency-swap = { path = "../pallet-currency-swap", default-features = false } +pallet-erc20-support = { path = "../pallet-erc20-support", default-features = false } pallet-ethereum-chain-id = { path = "../pallet-ethereum-chain-id", default-features = false } pallet-evm-accounts-mapping = { path = "../pallet-evm-accounts-mapping", default-features = false } pallet-humanode-session = { path = "../pallet-humanode-session", default-features = false } @@ -32,6 +33,7 @@ pallet-vesting = { path = "../pallet-vesting", default-features = false } precompile-bioauth = { path = "../precompile-bioauth", default-features = false } precompile-currency-swap = { path = "../precompile-currency-swap", default-features = false } precompile-evm-accounts-mapping = { path = "../precompile-evm-accounts-mapping", default-features = false } +precompile-native-currency = { path = "../precompile-native-currency", default-features = false } precompile-utils = { path = "../precompile-utils", default-features = false } primitives-auth-ticket = { path = "../primitives-auth-ticket", default-features = false } primitives-currency-swap-proxy = { path = "../primitives-currency-swap-proxy", default-features = false } @@ -164,6 +166,7 @@ std = [ "pallet-chain-properties/std", "pallet-chain-start-moment/std", "pallet-currency-swap/std", + "pallet-erc20-support/std", "pallet-ethereum-chain-id/std", "pallet-ethereum/std", "pallet-evm-accounts-mapping/std", @@ -190,6 +193,7 @@ std = [ "precompile-bioauth/std", "precompile-currency-swap/std", "precompile-evm-accounts-mapping/std", + "precompile-native-currency/std", "precompile-utils/std", "primitives-auth-ticket/std", "primitives-currency-swap-proxy/std", @@ -234,6 +238,7 @@ try-runtime = [ "pallet-chain-properties/try-runtime", "pallet-chain-start-moment/try-runtime", "pallet-currency-swap/try-runtime", + "pallet-erc20-support/try-runtime", "pallet-ethereum-chain-id/try-runtime", "pallet-ethereum/try-runtime", "pallet-evm-accounts-mapping/try-runtime", diff --git a/crates/humanode-runtime/src/frontier_precompiles.rs b/crates/humanode-runtime/src/frontier_precompiles.rs index 451844e10..265b7986d 100644 --- a/crates/humanode-runtime/src/frontier_precompiles.rs +++ b/crates/humanode-runtime/src/frontier_precompiles.rs @@ -1,3 +1,4 @@ +use frame_support::traits::Currency; use pallet_evm::{Precompile, PrecompileHandle, PrecompileResult, PrecompileSet}; use pallet_evm_precompile_modexp::Modexp; use pallet_evm_precompile_sha3fips::Sha3FIPS256; @@ -5,7 +6,8 @@ use pallet_evm_precompile_simple::{ECRecover, ECRecoverPublicKey, Identity, Ripe use precompile_bioauth::Bioauth; use precompile_currency_swap::CurrencySwap; use precompile_evm_accounts_mapping::EvmAccountsMapping; -use sp_core::H160; +use precompile_native_currency::NativeCurrency; +use sp_core::{H160, U256}; use sp_std::marker::PhantomData; use crate::{currency_swap, AccountId, ConstU64, EvmAccountId}; @@ -23,7 +25,7 @@ where R: pallet_evm::Config, { pub fn used_addresses() -> sp_std::vec::Vec { - sp_std::vec![1_u64, 2, 3, 4, 5, 1024, 1025, 2048, 2049, 2304] + sp_std::vec![1_u64, 2, 3, 4, 5, 1024, 1025, 2048, 2049, 2050, 2304] .into_iter() .map(hash) .collect() @@ -35,6 +37,12 @@ where R: pallet_evm::Config, R: pallet_bioauth::Config, R: pallet_evm_accounts_mapping::Config, + R: pallet_evm_balances::Config, + R: pallet_erc20_support::Config, + ::AccountId: From, + <::Currency as Currency< + ::AccountId, + >>::Balance: Into + TryFrom, R::ValidatorPublicKey: for<'a> TryFrom<&'a [u8]> + Eq, { fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { @@ -51,6 +59,7 @@ where // Humanode precompiles: a if a == hash(2048) => Some(Bioauth::::execute(handle)), a if a == hash(2049) => Some(EvmAccountsMapping::::execute(handle)), + a if a == hash(2050) => Some(NativeCurrency::>::execute(handle)), a if a == hash(2304) => Some(CurrencySwap::< currency_swap::EvmToNativeOneToOne, EvmAccountId, diff --git a/crates/humanode-runtime/src/lib.rs b/crates/humanode-runtime/src/lib.rs index 8f1ebaf48..2c54e3aff 100644 --- a/crates/humanode-runtime/src/lib.rs +++ b/crates/humanode-runtime/src/lib.rs @@ -788,6 +788,28 @@ impl pallet_balanced_currency_swap_bridges_initializer::Config for Runtime { type WeightInfo = (); } +pub struct EvmBalancesErc20Metadata; + +impl pallet_erc20_support::Metadata for EvmBalancesErc20Metadata { + fn name() -> &'static str { + "Wrapped eHMND" + } + + fn symbol() -> &'static str { + "WeHMND" + } + + fn decimals() -> u8 { + 18 + } +} + +impl pallet_erc20_support::Config for Runtime { + type AccountId = EvmAccountId; + type Currency = EvmBalances; + type Metadata = EvmBalancesErc20Metadata; +} + // Create the runtime by composing the FRAME pallets that were previously // configured. construct_runtime!( @@ -832,6 +854,7 @@ construct_runtime!( EvmToNativeSwapBridgePot: pallet_pot:: = 34, CurrencySwap: pallet_currency_swap = 35, BalancedCurrencySwapBridgesInitializer: pallet_balanced_currency_swap_bridges_initializer = 36, + EvmBalancesErc20Support: pallet_erc20_support = 37, } ); diff --git a/crates/pallet-erc20-support/Cargo.toml b/crates/pallet-erc20-support/Cargo.toml new file mode 100644 index 000000000..2ff44dc97 --- /dev/null +++ b/crates/pallet-erc20-support/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "pallet-erc20-support" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +codec = { workspace = true, package = "parity-scale-codec", features = ["derive"] } +frame-support = { workspace = true } +frame-system = { workspace = true } +scale-info = { workspace = true, features = ["derive"] } + +[dev-dependencies] +pallet-balances = { workspace = true } +sp-core = { workspace = true } + +[features] +default = ["std"] +std = ["codec/std", "frame-support/std", "frame-system/std", "pallet-balances/std", "scale-info/std", "sp-core/std"] +try-runtime = ["frame-support/try-runtime", "frame-system/try-runtime", "pallet-balances/try-runtime"] diff --git a/crates/pallet-erc20-support/src/lib.rs b/crates/pallet-erc20-support/src/lib.rs new file mode 100644 index 000000000..dffb7aa78 --- /dev/null +++ b/crates/pallet-erc20-support/src/lib.rs @@ -0,0 +1,165 @@ +//! A substrate pallet that exposes currency instance using the ERC20 interface standard.. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + sp_runtime::{traits::CheckedSub, DispatchResult}, + storage::with_storage_layer, + traits::{Currency, StorageVersion}, +}; +pub use pallet::*; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +/// Metadata of an ERC20 token. +pub trait Metadata { + /// Returns the name of the token. + fn name() -> &'static str; + + /// Returns the symbol of the token. + fn symbol() -> &'static str; + + /// Returns the decimals places of the token. + fn decimals() -> u8; +} + +/// The current storage version. +const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + +/// Utility alias for easy access to the [`Config::AccountId`]. +type AccountIdOf = >::AccountId; + +/// Utility alias for easy access to the [`Currency::Balance`] of the [`Config::Currency`] type. +type BalanceOf = <>::Currency as Currency>>::Balance; + +// We have to temporarily allow some clippy lints. Later on we'll send patches to substrate to +// fix them at their end. +#[allow(clippy::missing_docs_in_private_items)] +#[frame_support::pallet] +pub mod pallet { + use frame_support::{pallet_prelude::*, sp_runtime::traits::MaybeDisplay, sp_std::fmt::Debug}; + + use super::*; + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + /// Configuration trait of this pallet. + #[pallet::config] + pub trait Config: frame_system::Config { + /// The user account identifier type. + type AccountId: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + MaybeDisplay + + Ord + + MaxEncodedLen; + + /// The currency to be exposed as ERC20 token. + type Currency: Currency>; + + /// Interface into ERC20 metadata implementation. + type Metadata: Metadata; + } + + /// ERC20-style approvals data. + /// (Owner => Allowed => Amount). + #[pallet::storage] + #[pallet::getter(fn approvals)] + pub type Approvals, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + AccountIdOf, + Blake2_128Concat, + AccountIdOf, + BalanceOf, + ValueQuery, + >; + + /// Possible errors. + #[pallet::error] + pub enum Error { + /// Spender can't transfer tokens more than allowed. + SpendMoreThanAllowed, + } +} + +impl, I: 'static> Pallet { + /// Returns the amount of tokens in existence. + pub fn total_supply() -> BalanceOf { + T::Currency::total_issuance() + } + + /// Returns the amount of tokens owned by provided account. + pub fn balance_of(owner: &AccountIdOf) -> BalanceOf { + T::Currency::total_balance(owner) + } + + /// Returns the remaining number of tokens that spender will be allowed to spend on behalf of + /// owner. This is zero by default. + pub fn allowance(owner: &AccountIdOf, spender: &AccountIdOf) -> BalanceOf { + >::get(owner, spender) + } + + /// Sets amount as the allowance of spender over the caller’s tokens. + pub fn approve(owner: AccountIdOf, spender: AccountIdOf, amount: BalanceOf) { + >::insert(owner, spender, amount); + } + + /// Moves amount tokens from the caller’s account to recipient. + pub fn transfer( + caller: AccountIdOf, + recipient: AccountIdOf, + amount: BalanceOf, + ) -> DispatchResult { + with_storage_layer(move || { + T::Currency::transfer( + &caller, + &recipient, + amount, + frame_support::traits::ExistenceRequirement::AllowDeath, + )?; + + Ok(()) + }) + } + + /// Moves amount tokens from sender to recipient using the allowance mechanism, + /// amount is then deducted from the caller’s allowance. + pub fn transfer_from( + caller: AccountIdOf, + sender: AccountIdOf, + recipient: AccountIdOf, + amount: BalanceOf, + ) -> DispatchResult { + with_storage_layer(move || { + >::mutate(sender.clone(), caller, |entry| { + // Remove "value" from allowed, exit if underflow. + let allowed = entry + .checked_sub(&amount) + .ok_or(Error::::SpendMoreThanAllowed)?; + + // Update allowed value. + *entry = allowed; + + Ok::<(), Error>(()) + })?; + + T::Currency::transfer( + &sender, + &recipient, + amount, + frame_support::traits::ExistenceRequirement::AllowDeath, + )?; + + Ok(()) + }) + } +} diff --git a/crates/pallet-erc20-support/src/mock.rs b/crates/pallet-erc20-support/src/mock.rs new file mode 100644 index 000000000..77e481679 --- /dev/null +++ b/crates/pallet-erc20-support/src/mock.rs @@ -0,0 +1,136 @@ +//! The mock for the pallet. + +use frame_support::{ + sp_io, + sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, + }, + traits::{ConstU32, ConstU64}, +}; +use sp_core::H256; + +use crate::{self as pallet_erc20_support}; + +pub(crate) const NAME: &str = "Wrapped HMND"; +pub(crate) const SYMBOL: &str = "WHMND"; +pub(crate) const DECIMALS: u8 = 18; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + Erc20Balances: pallet_erc20_support, + } +); + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type Balance = u64; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<10>; + type AccountStore = System; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +pub struct BalancesErc20BalancesMetadata; + +impl crate::Metadata for BalancesErc20BalancesMetadata { + fn name() -> &'static str { + NAME + } + + fn symbol() -> &'static str { + SYMBOL + } + + fn decimals() -> u8 { + DECIMALS + } +} + +impl pallet_erc20_support::Config for Test { + type AccountId = u64; + type Currency = Balances; + type Metadata = BalancesErc20BalancesMetadata; +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let genesis_config = GenesisConfig::default(); + new_test_ext_with(genesis_config) +} + +// This function basically just builds a genesis storage key/value store according to +// our desired mockup. +pub fn new_test_ext_with(genesis_config: GenesisConfig) -> sp_io::TestExternalities { + let storage = genesis_config.build_storage().unwrap(); + storage.into() +} + +fn runtime_lock() -> std::sync::MutexGuard<'static, ()> { + static MOCK_RUNTIME_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + // Ignore the poisoning for the tests that panic. + // We only care about concurrency here, not about the poisoning. + match MOCK_RUNTIME_MUTEX.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } +} + +pub trait TestExternalitiesExt { + fn execute_with_ext(&mut self, execute: E) -> R + where + E: for<'e> FnOnce(&'e ()) -> R; +} + +impl TestExternalitiesExt for frame_support::sp_io::TestExternalities { + fn execute_with_ext(&mut self, execute: E) -> R + where + E: for<'e> FnOnce(&'e ()) -> R, + { + let guard = runtime_lock(); + let result = self.execute_with(|| execute(&guard)); + drop(guard); + result + } +} diff --git a/crates/pallet-erc20-support/src/tests.rs b/crates/pallet-erc20-support/src/tests.rs new file mode 100644 index 000000000..bed47e837 --- /dev/null +++ b/crates/pallet-erc20-support/src/tests.rs @@ -0,0 +1,335 @@ +use frame_support::{assert_noop, assert_ok, traits::Currency}; + +use crate::{mock::*, *}; + +/// This test verifies that getting total supply works as expected. +#[test] +fn total_supply_works() { + new_test_ext().execute_with_ext(|_| { + let alice = 42; + let alice_balance = 1000; + let bob = 43; + let bob_balance = 2000; + + // Prepare the test state. + Balances::make_free_balance_be(&alice, alice_balance); + Balances::make_free_balance_be(&bob, bob_balance); + + // Check total supply. + assert_eq!(Erc20Balances::total_supply(), alice_balance + bob_balance); + }); +} + +/// This test verifies that getting balance of provided account works as expected. +#[test] +fn balance_of_works() { + new_test_ext().execute_with_ext(|_| { + let alice = 42; + let alice_balance = 1000; + + // Prepare the test state. + Balances::make_free_balance_be(&alice, alice_balance); + + // Check Alice's balance value. + assert_eq!(Erc20Balances::balance_of(&alice), alice_balance); + }); +} + +/// This test verifies that approval logic works as expected. +#[test] +fn approve_works() { + new_test_ext().execute_with_ext(|_| { + let alice = 42_u64; + let bob = 52_u64; + let approved_balance = 999; + + // Check test preconditions. + assert_eq!(Erc20Balances::approvals(alice, bob), 0); + + // Store alice-bob approval. + Erc20Balances::approve(alice, bob, approved_balance); + + // Verify alice-bob approval existence. + assert_eq!(Erc20Balances::approvals(alice, bob), approved_balance); + }) +} + +/// This test verifies that approval logic works as expected in case approval has been overwritten. +#[test] +fn approve_overwrite_works() { + new_test_ext().execute_with_ext(|_| { + let alice = 42_u64; + let bob = 52_u64; + let approved_balance = 999; + let approved_balance_new = 1000; + + // Check test preconditions. + assert_eq!(Erc20Balances::approvals(alice, bob), 0); + + // Alice approves balance value for Bob. + Erc20Balances::approve(alice, bob, approved_balance); + // Verify alice-bob approval existence. + assert_eq!(Erc20Balances::approvals(alice, bob), approved_balance); + + // Alice approves new balance value for Bob. + Erc20Balances::approve(alice, bob, approved_balance_new); + // Verify alice-bob approval existence. + assert_eq!(Erc20Balances::approvals(alice, bob), approved_balance_new); + }) +} + +/// This test verifies that approval logic works as expected in case approved balanced +/// has been transferred in single transaction. +#[test] +fn approve_spend_all_in_single_transaction_works() { + new_test_ext().execute_with_ext(|_| { + let alice = 42_u64; + let bob = 52_u64; + let charlie = 62_u64; + let alice_balance = 10000; + let approved_balance = 1000; + + // Prepare the test state. + Balances::make_free_balance_be(&alice, alice_balance); + Erc20Balances::approve(alice, bob, approved_balance); + + // Check test preconditions. + assert_eq!(Erc20Balances::approvals(&alice, &bob), approved_balance); + + // Execute transfer_from. + assert_ok!(Erc20Balances::transfer_from( + bob, + alice, + charlie, + approved_balance + )); + + // Check resulted approvals. + assert_eq!(Erc20Balances::approvals(&alice, &bob), 0); + // Check resulted balances. + assert_eq!( + Balances::total_balance(&alice), + alice_balance - approved_balance + ); + assert_eq!(Balances::total_balance(&bob), 0); + assert_eq!(Balances::total_balance(&charlie), approved_balance); + // Check transfer_from failed execution. + assert_noop!( + Erc20Balances::transfer_from(bob, alice, charlie, 1), + >::SpendMoreThanAllowed + ); + }) +} + +/// This test verifies that approval logic works as expected in case approved balanced +/// has been transferred in several transactions. +#[test] +fn approve_spend_all_in_several_transactions_works() { + new_test_ext().execute_with_ext(|_| { + let alice = 42_u64; + let bob = 52_u64; + let charlie = 62_u64; + let alice_balance = 10000; + let approved_balance = 1000; + + // Prepare the test state. + Balances::make_free_balance_be(&alice, alice_balance); + Erc20Balances::approve(alice, bob, approved_balance); + + // Check test preconditions. + assert_eq!(Erc20Balances::approvals(&alice, &bob), approved_balance); + + // Execute transfer_from. + assert_ok!(Erc20Balances::transfer_from(bob, alice, charlie, 500)); + + // Check resulted approvals. + assert_eq!( + Erc20Balances::approvals(&alice, &bob), + approved_balance - 500 + ); + // Check resulted balances. + assert_eq!(Balances::total_balance(&alice), alice_balance - 500); + assert_eq!(Balances::total_balance(&bob), 0); + assert_eq!(Balances::total_balance(&charlie), 500); + + // Execute transfer_from again. + assert_ok!(Erc20Balances::transfer_from( + bob, + alice, + charlie, + approved_balance - 500 + )); + + // Check resulted approvals. + assert_eq!(Erc20Balances::approvals(&alice, &bob), 0); + // Check resulted balances. + assert_eq!( + Balances::total_balance(&alice), + alice_balance - approved_balance + ); + assert_eq!(Balances::total_balance(&bob), 0); + assert_eq!(Balances::total_balance(&charlie), approved_balance); + // Check transfer_from failed execution. + assert_noop!( + Erc20Balances::transfer_from(bob, alice, charlie, 1), + >::SpendMoreThanAllowed + ); + }) +} + +/// This test verifies that approval logic works as expected in case approved balance value +/// is more than owner's balance value initially. +#[test] +fn approve_approval_value_more_than_balance_works() { + new_test_ext().execute_with_ext(|_| { + let alice = 42_u64; + let alice_stash = 43_u64; + let bob = 52_u64; + let charlie = 62_u64; + let alice_balance_initial = 1000; + let alice_stash_balance = 1000; + let approved_balance = 2000; + + // Prepare the test state. + Balances::make_free_balance_be(&alice, alice_balance_initial); + Balances::make_free_balance_be(&alice_stash, alice_stash_balance); + Erc20Balances::approve(alice, bob, approved_balance); + + // Check test preconditions. + assert_eq!(Erc20Balances::approvals(&alice, &bob), approved_balance); + + // Try to execute transfer_from with all approved balance. + assert_noop!( + Erc20Balances::transfer_from(bob, alice, charlie, approved_balance), + pallet_balances::Error::::InsufficientBalance + ); + + // Execute transfer_from with alice initial balance value. + assert_ok!(Erc20Balances::transfer_from( + bob, + alice, + charlie, + alice_balance_initial + )); + + // Check resulted approvals. + assert_eq!( + Erc20Balances::approvals(&alice, &bob), + approved_balance - alice_balance_initial + ); + // Check resulted balances. + assert_eq!(Balances::total_balance(&alice), 0); + assert_eq!(Balances::total_balance(&bob), 0); + assert_eq!(Balances::total_balance(&charlie), alice_balance_initial); + + // Send more tokens to alice. + assert_ok!(Erc20Balances::transfer( + alice_stash, + alice, + alice_stash_balance + )); + + // Execute transfer_from with the rest approved balance value. + assert_ok!(Erc20Balances::transfer_from( + bob, + alice, + charlie, + approved_balance - alice_balance_initial + )); + + // Check resulted approvals. + assert_eq!(Erc20Balances::approvals(&alice, &bob), 0); + // Check transfer_from failed execution. + assert_noop!( + Erc20Balances::transfer_from(bob, alice, charlie, 1), + >::SpendMoreThanAllowed + ); + }) +} + +/// This test verifies that transferring logic works as expected. +#[test] +fn transfer_works() { + new_test_ext().execute_with_ext(|_| { + let alice = 42; + let alice_balance = 10000; + let bob = 43; + let transferred_balance = 5000; + + // Prepare the test state. + Balances::make_free_balance_be(&alice, alice_balance); + + // Execute transfer. + assert_ok!(Erc20Balances::transfer(alice, bob, transferred_balance)); + + // Check resulted balances. + assert_eq!( + Balances::total_balance(&alice), + alice_balance - transferred_balance + ); + assert_eq!(Balances::total_balance(&bob), transferred_balance); + }); +} + +/// This test verifies that transferring logic on behalf of provided account works as expected. +#[test] +fn transfer_from_works() { + new_test_ext().execute_with_ext(|_| { + let alice = 42; + let alice_balance = 10000; + let bob = 43; + let approved_balance = 5000; + let charlie = 44; + let transferred_from_balance = 1000; + + // Prepare the test state. + Balances::make_free_balance_be(&alice, alice_balance); + Erc20Balances::approve(alice, bob, approved_balance); + + // Execute transfer_from. + assert_ok!(Erc20Balances::transfer_from( + bob, + alice, + charlie, + transferred_from_balance + )); + + // Check resulted balances. + assert_eq!( + Balances::total_balance(&alice), + alice_balance - transferred_from_balance + ); + assert_eq!(Balances::total_balance(&bob), 0); + assert_eq!(Balances::total_balance(&charlie), transferred_from_balance); + + // Check updated approvals changes. + assert_eq!( + Erc20Balances::approvals(alice, bob), + approved_balance - transferred_from_balance + ); + }); +} + +/// This test verifies that transferring logic on behalf of provided account fails in case +/// the corresponding approval doesn't have sufficient balance. +#[test] +fn transfer_from_fails_spend_more_than_allowed() { + new_test_ext().execute_with_ext(|_| { + let alice = 42; + let alice_balance = 10000; + let bob = 43; + let approved_balance = 500; + let charlie = 44; + let transferred_from_balance = 1000; + + // Prepare the test state. + Balances::make_free_balance_be(&alice, alice_balance); + Erc20Balances::approve(alice, bob, approved_balance); + + // Execute transfer_from. + assert_noop!( + Erc20Balances::transfer_from(bob, alice, charlie, transferred_from_balance), + >::SpendMoreThanAllowed + ); + }); +} diff --git a/crates/precompile-native-currency/Cargo.toml b/crates/precompile-native-currency/Cargo.toml new file mode 100644 index 000000000..eb7fe32e8 --- /dev/null +++ b/crates/precompile-native-currency/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "precompile-native-currency" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +pallet-erc20-support = { path = "../pallet-erc20-support", default-features = false } +precompile-utils = { path = "../precompile-utils", default-features = false } + +codec = { workspace = true, package = "parity-scale-codec", features = ["derive"] } +fp-evm = { workspace = true } +frame-support = { workspace = true } +num_enum = { workspace = true } +pallet-evm = { workspace = true } +pallet-evm-balances = { workspace = true } +scale-info = { workspace = true, features = ["derive"] } +sp-core = { workspace = true } + +[dev-dependencies] +precompile-utils = { path = "../precompile-utils", features = ["testing"] } + +frame-system = { workspace = true } +hex-literal = { workspace = true } +pallet-balances = { workspace = true, features = ["default"] } +pallet-evm = { workspace = true } +pallet-evm-system = { workspace = true, features = ["default"] } +pallet-timestamp = { workspace = true, features = ["default"] } + +[features] +default = ["std"] +std = [ + "codec/std", + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "num_enum/std", + "pallet-balances/std", + "pallet-erc20-support/std", + "pallet-evm-balances/std", + "pallet-evm-system/std", + "pallet-evm/std", + "pallet-timestamp/std", + "precompile-utils/std", + "scale-info/std", + "sp-core/std", +] diff --git a/crates/precompile-native-currency/ERC20.sol b/crates/precompile-native-currency/ERC20.sol new file mode 100644 index 000000000..ebf6f927a --- /dev/null +++ b/crates/precompile-native-currency/ERC20.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity >=0.7.0 <0.9.0; + +/** + * @title ERC20 Interface + * + * An interface exposing native eHMND tokens as ERC20 tokens. + * + * Address: 0x0000000000000000000000000000000000000802 + */ +interface IERC20 { + /** + * Returns the name of the token. + * Selector: 06fdde03 + */ + function name() external view returns (string memory); + + /** + * Returns the symbol of the token. + * Selector: 95d89b41 + */ + function symbol() external view returns (string memory); + + /** + * Returns the decimals places of the token. + * Selector: 313ce567 + */ + function decimals() external view returns (uint8); + + /** + * Total number of tokens in existence. + * Selector: 18160ddd + */ + function totalSupply() external view returns (uint256); + + /** + * Gets the balance of the specified address. + * Selector: 70a08231 + * + * @param owner The address to query the balance of. + * @return uint256 The amount owned by the passed address. + */ + function balanceOf(address owner) external view returns (uint256); + + /** + * Function to check the amount of tokens that an owner allowed to a spender. + * Selector: dd62ed3e + * + * @param owner The address which owns the funds. + * @param spender The address which will spend the funds. + * @return uint256 The amount of tokens still available for the spender. + */ + function allowance( + address owner, + address spender + ) external view returns (uint256); + + /** + * Transfer token for a specified address. + * Selector: a9059cbb + * + * @param to The address to transfer tokens to. + * @param value The amount to be transferred. + * @return true if the transfer was succesful, revert otherwise. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. + * Selector: 095ea7b3 + * + * @param spender The address which will spend the funds. + * @param value The amount of tokens to be spent. + * @return true, this cannot fail. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * Transfer tokens from one address to another. + * Selector: 23b872dd + * + * @param from The address which you want to send tokens from. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @return true if the transfer was succesful, revert otherwise. + */ + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + + /** + * Event emited when a transfer has been performed. + * Selector: ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + * + * @param from The address sending the tokens + * @param to The address receiving the tokens. + * @param value The amount of tokens transfered. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * Event emited when an approval has been registered. + * Selector: 8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 + * + * @param owner The owner address of the tokens. + * @param spender The allowed spender address. + * @param value The amount of tokens approved. + */ + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} diff --git a/crates/precompile-native-currency/src/lib.rs b/crates/precompile-native-currency/src/lib.rs new file mode 100644 index 000000000..b8319cf4a --- /dev/null +++ b/crates/precompile-native-currency/src/lib.rs @@ -0,0 +1,345 @@ +//! A precompile to interact with currency instance using the ERC20 interface standard. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + sp_runtime::{self, DispatchError}, + sp_std::{marker::PhantomData, prelude::*}, + traits::Currency, +}; +use pallet_erc20_support::Metadata; +use pallet_evm::{ + ExitError, Precompile, PrecompileFailure, PrecompileHandle, PrecompileOutput, PrecompileResult, +}; +use precompile_utils::{ + keccak256, succeed, Address, Bytes, EvmDataReader, EvmDataWriter, EvmResult, LogExt, + LogsBuilder, PrecompileHandleExt, +}; +use sp_core::{Get, H160, U256}; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +/// Solidity selector of the Transfer log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_TRANSFER: [u8; 32] = keccak256!("Transfer(address,address,uint256)"); + +/// Solidity selector of the Approval log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_APPROVAL: [u8; 32] = keccak256!("Approval(address,address,uint256)"); + +/// Utility alias for easy access to the [`pallet_erc20_support::Config::AccountId`]. +type AccountIdOf = ::AccountId; + +/// Utility alias for easy access to the [`Currency::Balance`] of the [`pallet_erc20_support::Config::Currency`] type. +type BalanceOf = + <::Currency as Currency>>::Balance; + +/// Possible actions for this interface. +#[precompile_utils::generate_function_selector] +#[derive(Debug, PartialEq)] +pub enum Action { + /// Returns the name of the token. + Name = "name()", + /// Returns the symbol of the token. + Symbol = "symbol()", + /// Returns the decimals places of the token. + Decimals = "decimals()", + /// Returns the amount of tokens in existence. + TotalSupply = "totalSupply()", + /// Returns the amount of tokens owned by provided account. + BalanceOf = "balanceOf(address)", + /// Returns the remaining number of tokens that spender will be allowed to spend on behalf of + /// owner through transferFrom. This is zero by default. + Allowance = "allowance(address,address)", + /// Sets amount as the allowance of spender over the caller’s tokens. + Approve = "approve(address,uint256)", + /// Moves amount tokens from the caller’s account to recipient. + Transfer = "transfer(address,uint256)", + /// Moves amount tokens from sender to recipient using the allowance mechanism, + /// amount is then deducted from the caller’s allowance. + TransferFrom = "transferFrom(address,address,uint256)", +} + +/// Precompile exposing currency instance as ERC20. +pub struct NativeCurrency(PhantomData<(Erc20SupportT, GasCost)>) +where + GasCost: Get; + +impl Precompile for NativeCurrency +where + Erc20SupportT: pallet_erc20_support::Config, + AccountIdOf: From, + BalanceOf: Into + TryFrom, + GasCost: Get, +{ + fn execute(handle: &mut impl PrecompileHandle) -> PrecompileResult { + let selector = handle + .read_selector() + .map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("invalid function selector".into()), + })?; + + match selector { + Action::Name => Self::name(handle), + Action::Symbol => Self::symbol(handle), + Action::Decimals => Self::decimals(handle), + Action::TotalSupply => Self::total_supply(handle), + Action::BalanceOf => Self::balance_of(handle), + Action::Allowance => Self::allowance(handle), + Action::Approve => Self::approve(handle), + Action::Transfer => Self::transfer(handle), + Action::TransferFrom => Self::transfer_from(handle), + } + } +} + +impl NativeCurrency +where + Erc20SupportT: pallet_erc20_support::Config, + AccountIdOf: From, + BalanceOf: Into + TryFrom, + GasCost: Get, +{ + /// Returns the name of the token. + fn name(_handle: &mut impl PrecompileHandle) -> EvmResult { + let name: Bytes = Erc20SupportT::Metadata::name().into(); + + Ok(succeed(EvmDataWriter::new().write(name).build())) + } + + /// Returns the symbol of the token. + fn symbol(_handle: &mut impl PrecompileHandle) -> EvmResult { + let symbol: Bytes = Erc20SupportT::Metadata::symbol().into(); + + Ok(succeed(EvmDataWriter::new().write(symbol).build())) + } + + /// Returns the decimals places of the token. + fn decimals(_handle: &mut impl PrecompileHandle) -> EvmResult { + let decimals: u8 = Erc20SupportT::Metadata::decimals(); + + Ok(succeed(EvmDataWriter::new().write(decimals).build())) + } + + /// Returns the amount of tokens in existence. + fn total_supply(_handle: &mut impl PrecompileHandle) -> EvmResult { + let total_supply: U256 = + pallet_erc20_support::Pallet::::total_supply().into(); + + Ok(succeed(EvmDataWriter::new().write(total_supply).build())) + } + + /// Returns the amount of tokens owned by provided account. + fn balance_of(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + check_input(&mut input, 1)?; + + let owner: Address = input.read()?; + let owner: H160 = owner.into(); + + check_input_end(&mut input)?; + + let total_balance: U256 = + pallet_erc20_support::Pallet::::balance_of(&owner.into()).into(); + + Ok(succeed(EvmDataWriter::new().write(total_balance).build())) + } + + /// Returns the remaining number of tokens that spender will be allowed to spend on behalf of + /// owner through transferFrom. This is zero by default. + fn allowance(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + + check_input(&mut input, 2)?; + + let owner: Address = input.read()?; + let owner: H160 = owner.into(); + + let spender: Address = input.read()?; + let spender: H160 = spender.into(); + + check_input_end(&mut input)?; + + Ok(succeed( + EvmDataWriter::new() + .write( + pallet_erc20_support::Pallet::::allowance( + &owner.into(), + &spender.into(), + ) + .into(), + ) + .build(), + )) + } + + /// Sets amount as the allowance of spender over the caller’s tokens. + fn approve(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.record_cost(GasCost::get())?; + + let mut input = handle.read_input()?; + + let owner = handle.context().caller; + + check_input(&mut input, 2)?; + + let spender: Address = input.read()?; + let spender: H160 = spender.into(); + + let amount: U256 = input.read()?; + + check_input_end(&mut input)?; + + pallet_erc20_support::Pallet::::approve( + owner.into(), + spender.into(), + amount.try_into().map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("value is out of bounds".into()), + })?, + ); + + let logs_builder = LogsBuilder::new(handle.context().address); + + logs_builder + .log3( + SELECTOR_LOG_APPROVAL, + handle.context().caller, + spender, + EvmDataWriter::new().write(amount).build(), + ) + .record(handle)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + /// Moves amount tokens from the caller’s account to recipient. + fn transfer(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.record_cost(GasCost::get())?; + + let mut input = handle.read_input()?; + + let caller = handle.context().caller; + + check_input(&mut input, 2)?; + + let recipient: Address = input.read()?; + let recipient: H160 = recipient.into(); + + let amount: U256 = input.read()?; + + check_input_end(&mut input)?; + + pallet_erc20_support::Pallet::::transfer( + caller.into(), + recipient.into(), + amount.try_into().map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("value is out of bounds".into()), + })?, + ) + .map_err(process_dispatch_error::)?; + + let logs_builder = LogsBuilder::new(handle.context().address); + + logs_builder + .log3( + SELECTOR_LOG_TRANSFER, + caller, + recipient, + EvmDataWriter::new().write(amount).build(), + ) + .record(handle)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + /// Moves amount tokens from sender to recipient using the allowance mechanism, + /// amount is then deducted from the caller’s allowance. + fn transfer_from(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.record_cost(GasCost::get())?; + + let mut input = handle.read_input()?; + + let caller = handle.context().caller; + + check_input(&mut input, 3)?; + + let sender: Address = input.read()?; + let sender: H160 = sender.into(); + + let recipient: Address = input.read()?; + let recipient: H160 = recipient.into(); + + let amount: U256 = input.read()?; + + check_input_end(&mut input)?; + + pallet_erc20_support::Pallet::::transfer_from( + caller.into(), + sender.into(), + recipient.into(), + amount.try_into().map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("value is out of bounds".into()), + })?, + ) + .map_err(process_dispatch_error::)?; + + let logs_builder = LogsBuilder::new(handle.context().address); + + logs_builder + .log4( + SELECTOR_LOG_TRANSFER, + caller, + sender, + recipient, + EvmDataWriter::new().write(amount).build(), + ) + .record(handle)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } +} + +/// A helper function to process dispatch related errors. +fn process_dispatch_error( + error: DispatchError, +) -> PrecompileFailure { + if error == pallet_erc20_support::Error::SpendMoreThanAllowed::.into() { + PrecompileFailure::Error { + exit_status: ExitError::Other("spend more than allowed".into()), + } + } else { + match error { + DispatchError::Token(sp_runtime::TokenError::NoFunds) => PrecompileFailure::Error { + exit_status: ExitError::OutOfFund, + }, + _ => PrecompileFailure::Error { + exit_status: ExitError::Other("unable to transfer funds".into()), + }, + } + } +} + +/// A helper function to check expected arguments number. +fn check_input(input: &mut EvmDataReader, args_number: u8) -> Result<(), PrecompileFailure> { + input + .expect_arguments(args_number.into()) + .map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("not expected arguments number".into()), + })?; + + Ok(()) +} + +/// A helper function that verifies possible junk at the end of input. +fn check_input_end(input: &mut EvmDataReader) -> Result<(), PrecompileFailure> { + let junk_data = input.read_till_end()?; + if !junk_data.is_empty() { + return Err(PrecompileFailure::Error { + exit_status: ExitError::Other("junk at the end of input".into()), + }); + } + + Ok(()) +} diff --git a/crates/precompile-native-currency/src/mock.rs b/crates/precompile-native-currency/src/mock.rs new file mode 100644 index 000000000..7e3b72543 --- /dev/null +++ b/crates/precompile-native-currency/src/mock.rs @@ -0,0 +1,225 @@ +//! The mock for the precompile. + +use frame_support::{ + once_cell::sync::Lazy, + sp_io, + sp_runtime::{ + self, + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, + }, + traits::{ConstU16, ConstU32, ConstU64}, + weights::Weight, +}; +use frame_system as system; +use precompile_utils::precompile_set::{PrecompileAt, PrecompileSetBuilder}; +use sp_core::{ConstU128, H160, H256, U256}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +pub(crate) type AccountId = sp_runtime::AccountId32; +pub(crate) type EvmAccountId = H160; +pub(crate) type Balance = u128; + +pub(crate) const NAME: &str = "Wrapped eHMND"; +pub(crate) const SYMBOL: &str = "WeHMND"; +pub(crate) const DECIMALS: u8 = 18; +pub(crate) const GAS_COST: u64 = 200; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Timestamp: pallet_timestamp, + Balances: pallet_balances, + EvmSystem: pallet_evm_system, + EvmBalances: pallet_evm_balances, + EVM: pallet_evm, + Erc20: pallet_erc20_support, + } +); + +impl system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = ConstU16<1>; + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +frame_support::parameter_types! { + pub const MinimumPeriod: u64 = 1000; +} +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +impl pallet_balances::Config for Test { + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<2>; + type AccountStore = System; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +impl pallet_evm_system::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AccountId = EvmAccountId; + type Index = u64; + type AccountData = pallet_evm_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); +} + +impl pallet_evm_balances::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AccountId = EvmAccountId; + type Balance = Balance; + type ExistentialDeposit = ConstU128<1>; + type AccountStore = EvmSystem; + type DustRemoval = (); +} + +pub struct EvmBalancesErc20Metadata; + +impl pallet_erc20_support::Metadata for EvmBalancesErc20Metadata { + fn name() -> &'static str { + NAME + } + + fn symbol() -> &'static str { + SYMBOL + } + + fn decimals() -> u8 { + DECIMALS + } +} + +impl pallet_erc20_support::Config for Test { + type AccountId = EvmAccountId; + type Currency = EvmBalances; + type Metadata = EvmBalancesErc20Metadata; +} + +pub(crate) static GAS_PRICE: Lazy = Lazy::new(|| 1_000_000_000u128.into()); + +pub struct FixedGasPrice; +impl fp_evm::FeeCalculator for FixedGasPrice { + fn min_gas_price() -> (U256, Weight) { + // Return some meaningful gas price and weight + (*GAS_PRICE, Weight::from_ref_time(7u64)) + } +} + +pub(crate) static PRECOMPILE_ADDRESS: Lazy = Lazy::new(|| H160::from_low_u64_be(0x802)); + +pub(crate) type EvmBalancesErc20Precompile = crate::NativeCurrency>; + +pub type Precompiles = + PrecompileSetBuilder,)>; + +frame_support::parameter_types! { + pub BlockGasLimit: U256 = U256::max_value(); + pub WeightPerGas: Weight = Weight::from_ref_time(20_000); + pub PrecompileAddress: H160 = *PRECOMPILE_ADDRESS; + pub PrecompilesValue: Precompiles = Precompiles::new(); +} + +impl pallet_evm::Config for Test { + type AccountProvider = EvmSystem; + type FeeCalculator = FixedGasPrice; + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type CallOrigin = pallet_evm::EnsureAddressNever< + ::AccountId, + >; + type WithdrawOrigin = pallet_evm::EnsureAddressNever< + ::AccountId, + >; + type AddressMapping = pallet_evm::IdentityAddressMapping; + type Currency = EvmBalances; + type RuntimeEvent = RuntimeEvent; + type PrecompilesType = Precompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = (); + type BlockGasLimit = BlockGasLimit; + type Runner = pallet_evm::runner::stack::Runner; + type OnChargeTransaction = (); + type OnCreate = (); + type FindAuthor = (); +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let genesis_config = GenesisConfig::default(); + new_test_ext_with(genesis_config) +} + +// This function basically just builds a genesis storage key/value store according to +// our desired mockup. +pub fn new_test_ext_with(genesis_config: GenesisConfig) -> sp_io::TestExternalities { + let storage = genesis_config.build_storage().unwrap(); + storage.into() +} + +pub fn runtime_lock() -> std::sync::MutexGuard<'static, ()> { + static MOCK_RUNTIME_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + // Ignore the poisoning for the tests that panic. + // We only care about concurrency here, not about the poisoning. + match MOCK_RUNTIME_MUTEX.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } +} + +pub trait TestExternalitiesExt { + fn execute_with_ext(&mut self, execute: E) -> R + where + E: for<'e> FnOnce(&'e ()) -> R; +} + +impl TestExternalitiesExt for frame_support::sp_io::TestExternalities { + fn execute_with_ext(&mut self, execute: E) -> R + where + E: for<'e> FnOnce(&'e ()) -> R, + { + let guard = runtime_lock(); + let result = self.execute_with(|| execute(&guard)); + drop(guard); + result + } +} diff --git a/crates/precompile-native-currency/src/tests.rs b/crates/precompile-native-currency/src/tests.rs new file mode 100644 index 000000000..ad30bbf74 --- /dev/null +++ b/crates/precompile-native-currency/src/tests.rs @@ -0,0 +1,351 @@ +#![allow(clippy::integer_arithmetic)] // not a problem in tests + +use precompile_utils::{testing::*, EvmDataWriter}; + +use crate::{mock::*, *}; + +fn precompiles() -> Precompiles { + PrecompilesValue::get() +} + +#[test] +fn metadata_name_works() { + new_test_ext().execute_with_ext(|_| { + let alice_evm = H160::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )); + + let name_action = EvmDataWriter::new_with_selector(Action::Name).build(); + + precompiles() + .prepare_test(alice_evm, *PRECOMPILE_ADDRESS, name_action) + .expect_cost(0) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(Bytes::from(NAME)).build()); + }); +} + +#[test] +fn metadata_symbol_works() { + new_test_ext().execute_with_ext(|_| { + let alice_evm = H160::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )); + + let symbol_action = EvmDataWriter::new_with_selector(Action::Symbol).build(); + + precompiles() + .prepare_test(alice_evm, *PRECOMPILE_ADDRESS, symbol_action) + .expect_cost(0) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(Bytes::from(SYMBOL)).build()); + }); +} + +#[test] +fn metadata_decimals_works() { + new_test_ext().execute_with_ext(|_| { + let alice_evm = H160::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )); + + let decimals_action = EvmDataWriter::new_with_selector(Action::Decimals).build(); + + precompiles() + .prepare_test(alice_evm, *PRECOMPILE_ADDRESS, decimals_action) + .expect_cost(0) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(DECIMALS).build()); + }); +} + +#[test] +fn total_supply_works() { + new_test_ext().execute_with_ext(|_| { + let alice_evm = H160::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )); + let alice_evm_balance = 100 * 10u128.pow(18); + let bob_evm = H160::from(hex_literal::hex!( + "7000000000000000000000000000000000000007" + )); + let bob_evm_balance = 200 * 10u128.pow(18); + + // Prepare the test state. + EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); + EvmBalances::make_free_balance_be(&bob_evm, bob_evm_balance); + + let total_supply_action = EvmDataWriter::new_with_selector(Action::TotalSupply).build(); + + precompiles() + .prepare_test(bob_evm, *PRECOMPILE_ADDRESS, total_supply_action) + .expect_cost(0) + .expect_no_logs() + .execute_returns( + EvmDataWriter::new() + .write(U256::from(alice_evm_balance + bob_evm_balance)) + .build(), + ); + }); +} + +#[test] +fn balance_of_works() { + new_test_ext().execute_with_ext(|_| { + let alice_evm = H160::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )); + let alice_evm_balance = 100 * 10u128.pow(18); + let bob_evm = H160::from(hex_literal::hex!( + "7000000000000000000000000000000000000007" + )); + + // Prepare the test state. + EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); + + let balance_of_action = EvmDataWriter::new_with_selector(Action::BalanceOf) + .write(Address::from(alice_evm)) + .build(); + + precompiles() + .prepare_test(bob_evm, *PRECOMPILE_ADDRESS, balance_of_action) + .expect_cost(0) + .expect_no_logs() + .execute_returns( + EvmDataWriter::new() + .write(U256::from(alice_evm_balance)) + .build(), + ); + }); +} + +#[test] +fn approve_works() { + new_test_ext().execute_with_ext(|_| { + let alice_evm = H160::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )); + let alice_evm_balance = 100 * 10u128.pow(18); + let bob_evm = H160::from(hex_literal::hex!( + "7000000000000000000000000000000000000007" + )); + let approved_alice_bob_balance = 10 * 10u128.pow(18); + let charlie_evm = H160::from(hex_literal::hex!( + "9000000000000000000000000000000000000009" + )); + + // Prepare the test state. + EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); + + let approve_action = EvmDataWriter::new_with_selector(Action::Approve) + .write(Address::from(bob_evm)) + .write(U256::from(approved_alice_bob_balance)) + .build(); + + precompiles() + .prepare_test(alice_evm, *PRECOMPILE_ADDRESS, approve_action) + .expect_cost(GAS_COST) + .expect_log( + LogsBuilder::new(*PRECOMPILE_ADDRESS).log3( + SELECTOR_LOG_APPROVAL, + alice_evm, + bob_evm, + EvmDataWriter::new() + .write(approved_alice_bob_balance) + .build(), + ), + ) + .execute_returns(EvmDataWriter::new().write(true).build()); + + let allowance_action = EvmDataWriter::new_with_selector(Action::Allowance) + .write(Address::from(alice_evm)) + .write(Address::from(bob_evm)) + .build(); + + precompiles() + .prepare_test(charlie_evm, *PRECOMPILE_ADDRESS, allowance_action) + .expect_cost(0) + .expect_no_logs() + .execute_returns( + EvmDataWriter::new() + .write(U256::from(approved_alice_bob_balance)) + .build(), + ); + }); +} + +#[test] +fn transfer_works() { + new_test_ext().execute_with_ext(|_| { + let alice_evm = H160::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )); + let alice_evm_balance = 100 * 10u128.pow(18); + let bob_evm = H160::from(hex_literal::hex!( + "7000000000000000000000000000000000000007" + )); + let alice_bob_transfer_balance = 10 * 10u128.pow(18); + + // Prepare the test state. + EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); + + let transfer_action = EvmDataWriter::new_with_selector(Action::Transfer) + .write(Address::from(bob_evm)) + .write(U256::from(alice_bob_transfer_balance)) + .build(); + + precompiles() + .prepare_test(alice_evm, *PRECOMPILE_ADDRESS, transfer_action) + .expect_cost(GAS_COST) + .expect_log( + LogsBuilder::new(*PRECOMPILE_ADDRESS).log3( + SELECTOR_LOG_TRANSFER, + alice_evm, + bob_evm, + EvmDataWriter::new() + .write(alice_bob_transfer_balance) + .build(), + ), + ) + .execute_returns(EvmDataWriter::new().write(true).build()); + + assert_eq!( + EvmBalances::total_balance(&alice_evm), + alice_evm_balance - alice_bob_transfer_balance + ); + assert_eq!( + EvmBalances::total_balance(&bob_evm), + alice_bob_transfer_balance + ); + }); +} + +#[test] +fn transfer_from_works() { + new_test_ext().execute_with_ext(|_| { + let alice_evm = H160::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )); + let alice_evm_balance = 100 * 10u128.pow(18); + let bob_evm = H160::from(hex_literal::hex!( + "7000000000000000000000000000000000000007" + )); + let approved_alice_bob_balance = 10 * 10u128.pow(18); + let charlie_evm = H160::from(hex_literal::hex!( + "9000000000000000000000000000000000000009" + )); + let bob_charlie_transfer_from_alice_balance = 5 * 10u128.pow(18); + + // Prepare the test state. + EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); + + let approve_action = EvmDataWriter::new_with_selector(Action::Approve) + .write(Address::from(bob_evm)) + .write(U256::from(approved_alice_bob_balance)) + .build(); + + precompiles() + .prepare_test(alice_evm, *PRECOMPILE_ADDRESS, approve_action) + .expect_cost(GAS_COST) + .expect_log( + LogsBuilder::new(*PRECOMPILE_ADDRESS).log3( + SELECTOR_LOG_APPROVAL, + alice_evm, + bob_evm, + EvmDataWriter::new() + .write(approved_alice_bob_balance) + .build(), + ), + ) + .execute_returns(EvmDataWriter::new().write(true).build()); + + let transfer_from_action = EvmDataWriter::new_with_selector(Action::TransferFrom) + .write(Address::from(alice_evm)) + .write(Address::from(charlie_evm)) + .write(U256::from(bob_charlie_transfer_from_alice_balance)) + .build(); + + precompiles() + .prepare_test(bob_evm, *PRECOMPILE_ADDRESS, transfer_from_action) + .expect_cost(GAS_COST) + .expect_log( + LogsBuilder::new(*PRECOMPILE_ADDRESS).log4( + SELECTOR_LOG_TRANSFER, + bob_evm, + alice_evm, + charlie_evm, + EvmDataWriter::new() + .write(bob_charlie_transfer_from_alice_balance) + .build(), + ), + ) + .execute_returns(EvmDataWriter::new().write(true).build()); + + assert_eq!( + EvmBalances::total_balance(&alice_evm), + alice_evm_balance - bob_charlie_transfer_from_alice_balance + ); + assert_eq!(EvmBalances::total_balance(&bob_evm), 0); + assert_eq!( + EvmBalances::total_balance(&charlie_evm), + bob_charlie_transfer_from_alice_balance + ); + }); +} + +#[test] +fn transfer_from_fails_spend_more_than_allowed() { + new_test_ext().execute_with_ext(|_| { + let alice_evm = H160::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )); + let alice_evm_balance = 100 * 10u128.pow(18); + let bob_evm = H160::from(hex_literal::hex!( + "7000000000000000000000000000000000000007" + )); + let approved_alice_bob_balance = 2 * 10u128.pow(18); + let charlie_evm = H160::from(hex_literal::hex!( + "9000000000000000000000000000000000000009" + )); + let bob_charlie_transfer_from_alice_balance = 5 * 10u128.pow(18); + + // Prepare the test state. + EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); + + let approve_action = EvmDataWriter::new_with_selector(Action::Approve) + .write(Address::from(bob_evm)) + .write(U256::from(approved_alice_bob_balance)) + .build(); + + precompiles() + .prepare_test(alice_evm, *PRECOMPILE_ADDRESS, approve_action) + .expect_cost(GAS_COST) + .expect_log( + LogsBuilder::new(*PRECOMPILE_ADDRESS).log3( + SELECTOR_LOG_APPROVAL, + alice_evm, + bob_evm, + EvmDataWriter::new() + .write(approved_alice_bob_balance) + .build(), + ), + ) + .execute_returns(EvmDataWriter::new().write(true).build()); + + let transfer_from_action = EvmDataWriter::new_with_selector(Action::TransferFrom) + .write(Address::from(alice_evm)) + .write(Address::from(charlie_evm)) + .write(U256::from(bob_charlie_transfer_from_alice_balance)) + .build(); + + precompiles() + .prepare_test(bob_evm, *PRECOMPILE_ADDRESS, transfer_from_action) + .expect_cost(GAS_COST) + .expect_no_logs() + .execute_error(ExitError::Other("spend more than allowed".into())); + + assert_eq!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(EvmBalances::total_balance(&bob_evm), 0); + assert_eq!(EvmBalances::total_balance(&charlie_evm), 0); + }); +} diff --git a/utils/checks/snapshots/features.yaml b/utils/checks/snapshots/features.yaml index b770c1d7d..8ecd41580 100644 --- a/utils/checks/snapshots/features.yaml +++ b/utils/checks/snapshots/features.yaml @@ -247,6 +247,11 @@ - default - sha2 - std +- name: bstr 0.2.17 + features: + - lazy_static + - regex-automata + - unicode - name: bstr 1.4.0 features: - alloc @@ -389,6 +394,8 @@ features: - default - std +- name: console 0.15.5 + features: [] - name: const-oid 0.9.2 features: [] - name: constant_time_eq 0.2.5 @@ -750,6 +757,10 @@ - hazmat - sec1 - std +- name: encode_unicode 0.3.6 + features: + - default + - std - name: encoding_rs 0.8.32 features: - alloc @@ -2001,6 +2012,10 @@ features: - default - std +- name: pallet-erc20-support 0.1.0 + features: + - default + - std - name: pallet-ethereum 4.0.0-dev features: - std @@ -2215,10 +2230,16 @@ features: - default - std +- name: precompile-native-currency 0.1.0 + features: + - default + - std - name: precompile-utils 0.1.0 features: - default + - similar-asserts - std + - testing - name: precompile-utils-macro 0.1.0 features: [] - name: predicates 2.1.5 @@ -2277,7 +2298,7 @@ - syn-error - name: proc-macro-error-attr 1.0.4 features: [] -- name: proc-macro2 1.0.66 +- name: proc-macro2 1.0.56 features: - default - proc-macro @@ -2855,6 +2876,18 @@ - name: simba 0.5.1 features: - std +- name: similar 2.2.1 + features: + - bstr + - default + - inline + - text + - unicode + - unicode-segmentation +- name: similar-asserts 1.4.2 + features: + - default + - unicode - name: siphasher 0.3.10 features: - default @@ -3490,6 +3523,8 @@ features: - default - std +- name: unicode-segmentation 1.10.1 + features: [] - name: unicode-width 0.1.10 features: - default @@ -3809,9 +3844,15 @@ - Win32_Security_Authentication_Identity - Win32_Security_Credentials - Win32_Security_Cryptography + - Win32_Storage + - Win32_Storage_FileSystem - Win32_System + - Win32_System_Console - Win32_System_Memory - Win32_System_Threading + - Win32_UI + - Win32_UI_Input + - Win32_UI_Input_KeyboardAndMouse - default - name: windows-sys 0.45.0 features: