diff --git a/Cargo.lock b/Cargo.lock index 2ec96fc9d..5dede8ecb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5908,6 +5908,25 @@ dependencies = [ "sp-core", ] +[[package]] +name = "pallet-currency-swap" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "mockall", + "pallet-balances", + "pallet-evm-balances", + "pallet-evm-system", + "parity-scale-codec", + "primitives-currency-swap", + "scale-info", + "sp-core", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-dynamic-fee" version = "4.0.0-dev" diff --git a/crates/pallet-currency-swap/Cargo.toml b/crates/pallet-currency-swap/Cargo.toml new file mode 100644 index 000000000..9f437c5ae --- /dev/null +++ b/crates/pallet-currency-swap/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "pallet-currency-swap" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +primitives-currency-swap = { version = "0.1", path = "../primitives-currency-swap", default-features = false } + +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +frame-benchmarking = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38", optional = true } +frame-support = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } +frame-system = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +sp-runtime = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } +sp-std = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } + +[dev-dependencies] +mockall = "0.11" +pallet-balances = { git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } +pallet-evm-balances = { git = "https://github.com/humanode-network/frontier", branch = "locked/polkadot-v0.9.38" } +pallet-evm-system = { git = "https://github.com/humanode-network/frontier", branch = "locked/polkadot-v0.9.38" } +sp-core = { git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "primitives-currency-swap/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances/try-runtime", + "pallet-evm-balances/try-runtime", + "pallet-evm-system/try-runtime", + "primitives-currency-swap/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/crates/pallet-currency-swap/src/benchmarking.rs b/crates/pallet-currency-swap/src/benchmarking.rs new file mode 100644 index 000000000..df90412d4 --- /dev/null +++ b/crates/pallet-currency-swap/src/benchmarking.rs @@ -0,0 +1,119 @@ +//! The benchmarks for the pallet. + +use frame_benchmarking::benchmarks; +use frame_support::{assert_ok, dispatch::DispatchResult, traits::Currency}; +use frame_system::RawOrigin; + +use crate::*; + +/// The benchmark interface into the environment. +pub trait Interface: super::Config { + /// The data to be passed from `prepare` to `verify`. + type Data; + + /// Prepare currency swap environment. + fn prepare() -> Self::Data; + + /// Verify currency swap environment, + fn verify(data: Self::Data) -> DispatchResult; + + /// Obtain the Account ID the balance is swapped from. + fn from_account_id() -> ::AccountId; + + /// Obtain the amount of balance to withdraw from the swap source account. + fn from_balance() -> FromBalanceOf; + + /// Obtain the Account ID the balance is swapped to. + fn to_account_id() -> ::AccountIdTo; + + /// Obtain the amount of balance to deposit to the swap destination account. + fn to_balance() -> ToBalanceOf; +} + +benchmarks! { + where_clause { + where + T: Interface, + } + + swap { + let from = ::from_account_id(); + let from_balance = ::from_balance(); + let to = ::to_account_id(); + let to_balance = ::to_balance(); + let init_balance: u32 = 1000; + + let _ = >::deposit_creating(&from, init_balance.into()); + + let from_balance_before = >::total_balance(&from); + let to_balance_before = >::total_balance(&to); + + let currency_swap = ::prepare(); + + let origin = RawOrigin::Signed(from.clone()); + + }: _(origin, to.clone(), from_balance) + verify { + let from_balance_after = >::total_balance(&from); + let to_balance_after = >::total_balance(&to); + + assert_eq!(from_balance_before - from_balance_after, from_balance); + assert_eq!(to_balance_after - to_balance_before, to_balance); + + assert_ok!(::verify(currency_swap)); + } + + impl_benchmark_test_suite!( + Pallet, + crate::mock::new_test_ext(), + crate::mock::Test, + ); +} + +#[cfg(test)] +impl Interface for crate::mock::Test { + type Data = ( + std::sync::MutexGuard<'static, ()>, + mock::__mock_MockCurrencySwap_CurrencySwap_9230394375286242749::__swap::Context, + ); + + fn prepare() -> Self::Data { + let mock_runtime_guard = mock::runtime_lock(); + + let swap_ctx = mock::MockCurrencySwap::swap_context(); + swap_ctx.expect().times(1..).return_once(move |_| { + Ok( + >::NegativeImbalance::new( + Self::to_balance().into(), + ), + ) + }); + + (mock_runtime_guard, swap_ctx) + } + + fn verify(data: Self::Data) -> DispatchResult { + let (mock_runtime_guard, swap_ctx) = data; + swap_ctx.checkpoint(); + drop(mock_runtime_guard); + Ok(()) + } + + fn from_account_id() -> ::AccountId { + 42 + } + + fn from_balance() -> FromBalanceOf { + 100 + } + + fn to_account_id() -> ::AccountIdTo { + use sp_std::str::FromStr; + + mock::EvmAccountId::from_str("1000000000000000000000000000000000000001").unwrap() + } + + fn to_balance() -> ToBalanceOf { + 100 + } +} diff --git a/crates/pallet-currency-swap/src/lib.rs b/crates/pallet-currency-swap/src/lib.rs new file mode 100644 index 000000000..cc14ed219 --- /dev/null +++ b/crates/pallet-currency-swap/src/lib.rs @@ -0,0 +1,140 @@ +//! A substrate pallet containing the currency swap integration. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::traits::Currency; +pub use pallet::*; +use primitives_currency_swap::CurrencySwap as CurrencySwapT; +pub use weights::*; + +pub mod weights; + +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarking; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +/// Utility alias for easy access to [`primitives_currency_swap::CurrencySwap::From`] type from a given config. +type FromCurrencyOf = <::CurrencySwap as CurrencySwapT< + ::AccountId, + ::AccountIdTo, +>>::From; + +/// Utility alias for easy access to the [`Currency::Balance`] of +/// the [`primitives_currency_swap::CurrencySwap::From`] type. +type FromBalanceOf = + as Currency<::AccountId>>::Balance; + +/// Utility alias for easy access to [`primitives_currency_swap::CurrencySwap::To`] type from a given config. +type ToCurrencyOf = <::CurrencySwap as CurrencySwapT< + ::AccountId, + ::AccountIdTo, +>>::To; + +/// Utility alias for easy access to the [`Currency::Balance`] of +/// the [`primitives_currency_swap::CurrencySwap::To`] type. +type ToBalanceOf = as Currency<::AccountIdTo>>::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::*, + storage::with_storage_layer, + traits::{ExistenceRequirement, Imbalance, WithdrawReasons}, + }; + use frame_system::pallet_prelude::*; + use sp_runtime::traits::MaybeDisplay; + use sp_std::fmt::Debug; + + use super::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + /// Configuration trait of this pallet. + #[pallet::config] + pub trait Config: frame_system::Config { + /// Overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The user account identifier type to convert to. + type AccountIdTo: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + MaybeDisplay + + Ord + + MaxEncodedLen; + + /// Interface into currency swap implementation. + type CurrencySwap: CurrencySwapT; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Balances were swapped. + BalancesSwapped { + /// The account id balances withdrawed from. + from: T::AccountId, + /// The withdrawed balances amount. + withdrawed_amount: FromBalanceOf, + /// The account id balances deposited to. + to: T::AccountIdTo, + /// The deposited balances amount. + deposited_amount: ToBalanceOf, + }, + } + + #[pallet::call] + impl Pallet { + /// Swap balances. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::swap())] + pub fn swap( + origin: OriginFor, + to: T::AccountIdTo, + amount: FromBalanceOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + with_storage_layer(move || { + let withdrawed_imbalance = FromCurrencyOf::::withdraw( + &who, + amount, + WithdrawReasons::TRANSFER, + ExistenceRequirement::AllowDeath, + )?; + let withdrawed_amount = withdrawed_imbalance.peek(); + + let deposited_imbalance = + T::CurrencySwap::swap(withdrawed_imbalance).map_err(|error| { + // Here we undo the withdrawl to avoid having a dangling imbalance. + FromCurrencyOf::::resolve_creating(&who, error.incoming_imbalance); + error.cause.into() + })?; + let deposited_amount = deposited_imbalance.peek(); + + ToCurrencyOf::::resolve_creating(&to, deposited_imbalance); + + Self::deposit_event(Event::BalancesSwapped { + from: who, + withdrawed_amount, + to, + deposited_amount, + }); + + Ok(()) + }) + } + } +} diff --git a/crates/pallet-currency-swap/src/mock.rs b/crates/pallet-currency-swap/src/mock.rs new file mode 100644 index 000000000..99b987586 --- /dev/null +++ b/crates/pallet-currency-swap/src/mock.rs @@ -0,0 +1,161 @@ +//! The mock for the pallet. + +// Allow simple integer arithmetic in tests. +#![allow(clippy::integer_arithmetic)] + +use frame_support::{ + sp_io, + sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, DispatchError, + }, + traits::{ConstU32, ConstU64, Currency}, +}; +use mockall::mock; +use sp_core::{H160, H256}; + +use crate::{self as pallet_currency_swap}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +pub(crate) type AccountId = u64; +pub(crate) type EvmAccountId = H160; +type Balance = u64; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + EvmSystem: pallet_evm_system, + EvmBalances: pallet_evm_balances, + CurrencySwap: pallet_currency_swap, + } +); + +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 = 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 = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type Balance = u64; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<1>; + 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 = ConstU64<1>; + type AccountStore = EvmSystem; + type DustRemoval = (); +} + +mock! { + #[derive(Debug)] + pub CurrencySwap {} + impl primitives_currency_swap::CurrencySwap for CurrencySwap { + type From = Balances; + type To = EvmBalances; + type Error = DispatchError; + + fn swap( + imbalance: >::NegativeImbalance, + ) -> Result< + primitives_currency_swap::ToNegativeImbalanceFor, + primitives_currency_swap::ErrorFor + >; + } +} + +impl pallet_currency_swap::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AccountIdTo = H160; + type CurrencySwap = MockCurrencySwap; + type WeightInfo = (); +} + +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/pallet-currency-swap/src/tests.rs b/crates/pallet-currency-swap/src/tests.rs new file mode 100644 index 000000000..6b143a248 --- /dev/null +++ b/crates/pallet-currency-swap/src/tests.rs @@ -0,0 +1,107 @@ +//! The tests for the pallet. + +use frame_support::{assert_noop, assert_ok, traits::Currency}; +use mockall::predicate; +use sp_core::H160; +use sp_runtime::DispatchError; +use sp_std::str::FromStr; + +use crate::{mock::*, *}; + +/// This test verifies that swap call works in the happy path. +#[test] +fn swap_works() { + new_test_ext().execute_with_ext(|_| { + let alice = 42; + let alice_evm = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + let alice_balance = 1000; + let swap_balance = 100; + + // Prepare the test state. + Balances::make_free_balance_be(&alice, alice_balance); + + // Check test preconditions. + assert_eq!(Balances::total_balance(&alice), alice_balance); + assert_eq!(EvmBalances::total_balance(&alice_evm), 0); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + let swap_ctx = MockCurrencySwap::swap_context(); + swap_ctx + .expect() + .once() + .with(predicate::eq( + >::NegativeImbalance::new(swap_balance), + )) + .return_once(move |_| { + Ok(>::NegativeImbalance::new(swap_balance)) + }); + + // Invoke the function under test. + assert_ok!(CurrencySwap::swap( + RuntimeOrigin::signed(alice), + alice_evm, + swap_balance + )); + + // Assert state changes. + assert_eq!( + Balances::total_balance(&alice), + alice_balance - swap_balance + ); + assert_eq!(EvmBalances::total_balance(&alice_evm), swap_balance); + System::assert_has_event(RuntimeEvent::CurrencySwap(Event::BalancesSwapped { + from: alice, + withdrawed_amount: swap_balance, + to: alice_evm, + deposited_amount: swap_balance, + })); + + // Assert mock invocations. + swap_ctx.checkpoint(); + }); +} + +/// This test verifies that swap call fails in case some error happens during the actual swap logic. +#[test] +fn swap_fails() { + new_test_ext().execute_with_ext(|_| { + let alice = 42; + let alice_evm = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + let alice_balance = 1000; + let swap_balance = 100; + + // Prepare the test state. + Balances::make_free_balance_be(&alice, alice_balance); + + // Set mock expectations. + let swap_ctx = MockCurrencySwap::swap_context(); + swap_ctx + .expect() + .once() + .with(predicate::eq( + >::NegativeImbalance::new(swap_balance), + )) + .return_once(move |incoming_imbalance| { + Err(primitives_currency_swap::Error { + cause: sp_runtime::DispatchError::Other("currency swap failed"), + incoming_imbalance, + }) + }); + + // Invoke the function under test. + assert_noop!( + CurrencySwap::swap(RuntimeOrigin::signed(alice), alice_evm, swap_balance), + DispatchError::Other("currency swap failed") + ); + + // Assert state changes. + assert_eq!(Balances::total_balance(&alice), alice_balance); + assert_eq!(EvmBalances::total_balance(&alice_evm), 0); + + // Assert mock invocations. + swap_ctx.checkpoint(); + }); +} diff --git a/crates/pallet-currency-swap/src/weights.rs b/crates/pallet-currency-swap/src/weights.rs new file mode 100644 index 000000000..37daf0790 --- /dev/null +++ b/crates/pallet-currency-swap/src/weights.rs @@ -0,0 +1,15 @@ +//! Weights definition for pallet-currency-swap. + +use frame_support::weights::Weight; + +/// Weight functions needed for pallet-currency-swap. +pub trait WeightInfo { + /// A function to calculate required weights for swap call. + fn swap() -> Weight; +} + +impl WeightInfo for () { + fn swap() -> Weight { + Weight::zero() + } +}