From 0ff469f2d1f6f42dbe6a758a04394237013e7eca Mon Sep 17 00:00:00 2001 From: Konrad Stepniak Date: Fri, 14 Jun 2024 17:08:00 +0200 Subject: [PATCH] test(pallet-market): add unit tests --- pallets/market/Cargo.toml | 4 +- pallets/market/src/lib.rs | 81 +++++++++++-------- pallets/market/src/mock.rs | 44 +++++++--- pallets/market/src/test.rs | 162 +++++++++++++++++++++++++++++++++++++ 4 files changed, 247 insertions(+), 44 deletions(-) create mode 100644 pallets/market/src/test.rs diff --git a/pallets/market/Cargo.toml b/pallets/market/Cargo.toml index a70328f4e..da739f40e 100644 --- a/pallets/market/Cargo.toml +++ b/pallets/market/Cargo.toml @@ -25,10 +25,10 @@ frame-support = { workspace = true, default-features = false } frame-system = { workspace = true, default-features = false } [dev-dependencies] +pallet-balances = { workspace = true, default-features = false } sp-core = { workspace = true, default-features = false } sp-io = { workspace = true } sp-runtime = { workspace = true, default-features = false } -pallet-balances = { workspace = true, default-features = false } [features] default = ["std"] @@ -41,9 +41,9 @@ runtime-benchmarks = [ std = [ "codec/std", "frame-benchmarking?/std", - "pallet-balances/std", "frame-support/std", "frame-system/std", + "pallet-balances/std", "scale-info/std", "sp-core/std", "sp-io/std", diff --git a/pallets/market/src/lib.rs b/pallets/market/src/lib.rs index e95c8fdc5..f1a74a65f 100644 --- a/pallets/market/src/lib.rs +++ b/pallets/market/src/lib.rs @@ -12,6 +12,10 @@ pub use pallet::*; #[cfg(test)] mod mock; +#[cfg(test)] +mod test; + +// TODO(@th7nder,#77,14/06/2024): take the pallet out of dev mode #[frame_support::pallet(dev_mode)] pub mod pallet { use codec::{Decode, Encode}; @@ -19,15 +23,22 @@ pub mod pallet { dispatch::DispatchResult, ensure, pallet_prelude::*, - sp_runtime::{RuntimeDebug,traits::{AccountIdConversion,CheckedAdd},ArithmeticError}, - traits::{Currency, ReservableCurrency,ExistenceRequirement::KeepAlive,ExistenceRequirement::AllowDeath}, + sp_runtime::{ + traits::{AccountIdConversion, CheckedAdd, CheckedSub}, + ArithmeticError, RuntimeDebug, + }, + traits::{ + Currency, + ExistenceRequirement::{AllowDeath, KeepAlive}, + ReservableCurrency, + }, PalletId, }; use frame_system::{pallet_prelude::*, Config as SystemConfig}; use scale_info::TypeInfo; - // Allows to extract Balance of an account via the Config::Currency associated type. - // BalanceOf is a sophisticated way of getting an u128. + /// Allows to extract Balance of an account via the Config::Currency associated type. + /// BalanceOf is a sophisticated way of getting an u128. type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; @@ -45,7 +56,7 @@ pub mod pallet { } /// Stores balances info for both Storage Providers and Storage Users - /// We do not use the ReservableCurrency::reserve mechanism, + /// We do not use the ReservableCurrency::reserve mechanism, /// as the Market works as a liaison between Storage Providers and Storage Clients. /// Market has its own account on which funds of all parties are stored. /// It's Market reposibility to manage deposited funds, lock/unlock and pay them out when necessary. @@ -55,10 +66,10 @@ pub mod pallet { pub struct BalanceEntry { /// Amount of Balance that has been deposited for future deals/earned from deals. /// It can be withdrawn at any time. - free: Balance, + pub(crate) free: Balance, /// Amount of Balance that has been staked as Deal Collateral /// It's locked to a deal and cannot be withdrawn until the deal ends. - locked: Balance, + pub(crate) locked: Balance, } #[pallet::pallet] @@ -81,7 +92,7 @@ pub mod pallet { BalanceWithdrawn { who: T::AccountId, amount: BalanceOf, - } + }, } #[pallet::error] @@ -89,31 +100,32 @@ pub mod pallet { /// When a Market Participant tries to withdraw more /// funds than they have available on the Market, because: /// - they never deposited the amount they want to withdraw - // - the funds they deposited were locked as part of a deal + /// - the funds they deposited were locked as part of a deal InsufficientFreeFunds, } /// Extrinsics exposed by the pallet #[pallet::call] impl Pallet { - /// Transfers `amount` of Balance from the `origin` to the Market Pallet account. /// It is marked as _free_ in the Market bookkeeping. /// Free balance can be withdrawn at any moment from the Market. pub fn add_balance(origin: OriginFor, amount: BalanceOf) -> DispatchResult { let caller = ensure_signed(origin)?; - BalanceTable::::try_mutate( - &caller, - |balance| -> DispatchResult { - T::Currency::transfer(&caller, &Self::account_id(), amount, KeepAlive)?; - balance.free = balance.free.checked_add(&amount) - .ok_or(ArithmeticError::Overflow)?; + BalanceTable::::try_mutate(&caller, |balance| -> DispatchResult { + balance.free = balance + .free + .checked_add(&amount) + .ok_or(ArithmeticError::Overflow)?; + T::Currency::transfer(&caller, &Self::account_id(), amount, KeepAlive)?; - Self::deposit_event(Event::::BalanceAdded { who: caller.clone(), amount }); - Ok(()) - } - )?; + Self::deposit_event(Event::::BalanceAdded { + who: caller.clone(), + amount, + }); + Ok(()) + })?; Ok(()) } @@ -123,18 +135,21 @@ pub mod pallet { pub fn withdraw_balance(origin: OriginFor, amount: BalanceOf) -> DispatchResult { let caller = ensure_signed(origin)?; - BalanceTable::::try_mutate( - &caller, - |balance| -> DispatchResult { - ensure!(balance.free >= amount, Error::::InsufficientFreeFunds); - balance.free -= amount; - // The Market Pallet account will be reaped if no one is participating in the market. - T::Currency::transfer(&Self::account_id(), &caller, amount, AllowDeath)?; - - Self::deposit_event(Event::::BalanceWithdrawn { who: caller.clone(), amount }); - Ok(()) - } - )?; + BalanceTable::::try_mutate(&caller, |balance| -> DispatchResult { + ensure!(balance.free >= amount, Error::::InsufficientFreeFunds); + balance.free = balance + .free + .checked_sub(&amount) + .ok_or(ArithmeticError::Underflow)?; + // The Market Pallet account will be reaped if no one is participating in the market. + T::Currency::transfer(&Self::account_id(), &caller, amount, AllowDeath)?; + + Self::deposit_event(Event::::BalanceWithdrawn { + who: caller.clone(), + amount, + }); + Ok(()) + })?; Ok(()) } @@ -144,7 +159,7 @@ pub mod pallet { impl Pallet { /// Account Id of the Market /// - /// This actually does computation. + /// This actually does computation. /// If you need to keep using it, make sure you cache it and call it once. pub fn account_id() -> T::AccountId { T::PalletId::get().into_account_truncating() diff --git a/pallets/market/src/mock.rs b/pallets/market/src/mock.rs index 349deac79..a9150c8ea 100644 --- a/pallets/market/src/mock.rs +++ b/pallets/market/src/mock.rs @@ -1,23 +1,24 @@ -use crate as pallet_market; use frame_support::{derive_impl, parameter_types, PalletId}; use frame_system as system; use sp_runtime::BuildStorage; +use crate as pallet_market; + type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( - pub enum Test - { - System: frame_system, + pub enum Test + { + System: frame_system, Balances: pallet_balances, Market: pallet_market, - } + } ); #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl system::Config for Test { - type Block = Block; + type Block = Block; type AccountData = pallet_balances::AccountData; } @@ -31,12 +32,37 @@ parameter_types! { } impl crate::Config for Test { - type RuntimeEvent = RuntimeEvent; + type RuntimeEvent = RuntimeEvent; type PalletId = MarketPalletId; type Currency = Balances; } +pub const ALICE: u64 = 0; +pub const BOB: u64 = 1; +pub const INITIAL_FUNDS: u64 = 100; + /// Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - system::GenesisConfig::::default().build_storage().unwrap().into() -} \ No newline at end of file + let mut t = system::GenesisConfig::::default() + .build_storage() + .unwrap() + .into(); + pallet_balances::GenesisConfig:: { + balances: vec![(ALICE, INITIAL_FUNDS), (BOB, INITIAL_FUNDS)], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub fn events() -> Vec { + let evt = System::events() + .into_iter() + .map(|evt| evt.event) + .collect::>(); + System::reset_events(); + evt +} diff --git a/pallets/market/src/test.rs b/pallets/market/src/test.rs new file mode 100644 index 000000000..b6c8fde9c --- /dev/null +++ b/pallets/market/src/test.rs @@ -0,0 +1,162 @@ +use frame_support::{ + assert_noop, assert_ok, + sp_runtime::{ArithmeticError, TokenError}, +}; + +use crate::{mock::*, BalanceEntry, BalanceTable, Error, Event}; + +#[test] +fn initial_state() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(Market::account_id()), 0); + assert_eq!( + BalanceTable::::get(ALICE), + BalanceEntry:: { free: 0, locked: 0 } + ); + }); +} + +#[test] +fn basic_end_to_end_works() { + new_test_ext().execute_with(|| { + // Adds funds from an account to the Market + assert_ok!(Market::add_balance(RuntimeOrigin::signed(ALICE), 10)); + assert_eq!(Balances::free_balance(Market::account_id()), 10); + assert_eq!(Balances::free_balance(ALICE), INITIAL_FUNDS - 10); + assert_eq!( + BalanceTable::::get(ALICE), + BalanceEntry:: { + free: 10, + locked: 0, + } + ); + + // Is able to withdraw added funds back + assert_ok!(Market::withdraw_balance(RuntimeOrigin::signed(ALICE), 10)); + assert_eq!(Balances::free_balance(Market::account_id()), 0); + assert_eq!(Balances::free_balance(ALICE), INITIAL_FUNDS); + assert_eq!( + BalanceTable::::get(ALICE), + BalanceEntry:: { free: 0, locked: 0 } + ); + }); +} + +#[test] +fn adds_balance() { + new_test_ext().execute_with(|| { + assert_ok!(Market::add_balance(RuntimeOrigin::signed(ALICE), 10)); + assert_eq!(Balances::free_balance(Market::account_id()), 10); + assert_eq!(Balances::free_balance(ALICE), INITIAL_FUNDS - 10); + assert_eq!( + BalanceTable::::get(ALICE), + BalanceEntry:: { + free: 10, + locked: 0, + } + ); + + assert_eq!( + events(), + [ + RuntimeEvent::System(frame_system::Event::::NewAccount { + account: Market::account_id() + }), + RuntimeEvent::Balances(pallet_balances::Event::::Endowed { + account: Market::account_id(), + free_balance: 10 + }), + RuntimeEvent::Balances(pallet_balances::Event::::Transfer { + from: ALICE, + to: Market::account_id(), + amount: 10 + }), + RuntimeEvent::Market(Event::::BalanceAdded { + who: ALICE, + amount: 10 + }) + ] + ); + + // Makes sure other accounts are unaffected + assert_eq!( + BalanceTable::::get(BOB), + BalanceEntry:: { free: 0, locked: 0 } + ); + }); +} + +#[test] +fn fails_to_add_balance_insufficient_funds() { + new_test_ext().execute_with(|| { + assert_noop!( + Market::add_balance(RuntimeOrigin::signed(ALICE), INITIAL_FUNDS + 1), + TokenError::FundsUnavailable, + ); + }); +} + +#[test] +fn fails_to_add_balance_overflow() { + new_test_ext().execute_with(|| { + // Hard to do this without setting it explicitly in the map + BalanceTable::::set( + BOB, + BalanceEntry:: { + free: u64::MAX, + locked: 0, + }, + ); + + assert_noop!( + Market::add_balance(RuntimeOrigin::signed(BOB), 1), + ArithmeticError::Overflow + ); + }); +} + +#[test] +fn withdraws_balance() { + new_test_ext().execute_with(|| { + let _ = Market::add_balance(RuntimeOrigin::signed(ALICE), 10); + System::reset_events(); + + assert_ok!(Market::withdraw_balance(RuntimeOrigin::signed(ALICE), 10)); + assert_eq!(Balances::free_balance(Market::account_id()), 0); + assert_eq!(Balances::free_balance(ALICE), INITIAL_FUNDS); + assert_eq!( + BalanceTable::::get(ALICE), + BalanceEntry:: { free: 0, locked: 0 } + ); + + assert_eq!( + events(), + [ + RuntimeEvent::System(frame_system::Event::::KilledAccount { + account: Market::account_id() + }), + RuntimeEvent::Balances(pallet_balances::Event::::Transfer { + from: Market::account_id(), + to: ALICE, + amount: 10 + }), + RuntimeEvent::Market(Event::::BalanceWithdrawn { + who: ALICE, + amount: 10 + }) + ] + ); + }); +} + +#[test] +fn fails_to_withdraw_balance() { + new_test_ext().execute_with(|| { + assert_noop!( + Market::withdraw_balance(RuntimeOrigin::signed(BOB), 10), + Error::::InsufficientFreeFunds + ); + + assert_eq!(events(), []); + }); +}