diff --git a/Cargo.lock b/Cargo.lock index bd4ed5efda..eb459a289e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7519,6 +7519,7 @@ dependencies = [ name = "pallet-foreign-investments" version = "1.0.0" dependencies = [ + "cfg-mocks", "cfg-primitives", "cfg-traits", "cfg-types", @@ -7527,6 +7528,7 @@ dependencies = [ "frame-system", "log", "parity-scale-codec 3.6.4", + "rand 0.8.5", "scale-info", "sp-core", "sp-io", diff --git a/libs/mock-builder/src/lib.rs b/libs/mock-builder/src/lib.rs index 9a24d707cc..5522b1f88e 100644 --- a/libs/mock-builder/src/lib.rs +++ b/libs/mock-builder/src/lib.rs @@ -122,8 +122,10 @@ //! } //! ``` //! -//! Take a look to the [pallet tests](`tests/pallet.rs`) to have a user view of -//! how to use a *mock pallet*. It supports any kind of trait, with reference +//! Take a look to the [pallet +//! tests](https://github.com/foss3/runtime-pallet-library/blob/main/mock-builder/tests/pallet.rs) +//! to have a user view of how to use a *mock pallet*. +//! It supports any kind of trait, with reference //! parameters and generics at trait level and method level. //! //! ## Mock pallet creation @@ -345,9 +347,6 @@ macro_rules! execute_call { ($input:expr) => {{ $crate::execute::, _, _, _>(|| (), $input) }}; - ($input:expr, $gen:expr) => {{ - $crate::execute::, _, _, _>(|| (), $input) - }}; } /// Execute a function from the function storage for a pallet with instances. diff --git a/libs/mocks/src/currency_conversion.rs b/libs/mocks/src/currency_conversion.rs new file mode 100644 index 0000000000..d6495b8dee --- /dev/null +++ b/libs/mocks/src/currency_conversion.rs @@ -0,0 +1,47 @@ +#[frame_support::pallet] +pub mod pallet { + use cfg_traits::IdentityCurrencyConversion; + use frame_support::pallet_prelude::*; + use mock_builder::{execute_call, register_call}; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Balance; + type CurrencyId; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::storage] + pub(super) type CallIds = StorageMap< + _, + Blake2_128Concat, + ::Output, + mock_builder::CallId, + >; + + impl Pallet { + pub fn mock_stable_to_stable( + f: impl Fn(T::CurrencyId, T::CurrencyId, T::Balance) -> Result + + 'static, + ) { + register_call!(move |(a, b, c)| f(a, b, c)); + } + } + + impl IdentityCurrencyConversion for Pallet { + type Balance = T::Balance; + type Currency = T::CurrencyId; + type Error = DispatchError; + + fn stable_to_stable( + a: Self::Currency, + b: Self::Currency, + c: Self::Balance, + ) -> Result { + execute_call!((a, b, c)) + } + } +} diff --git a/libs/mocks/src/investment.rs b/libs/mocks/src/investment.rs new file mode 100644 index 0000000000..1b5a778309 --- /dev/null +++ b/libs/mocks/src/investment.rs @@ -0,0 +1,160 @@ +#[frame_support::pallet] +pub mod pallet { + use cfg_traits::investments::{Investment, InvestmentCollector}; + use frame_support::pallet_prelude::*; + use mock_builder::{execute_call, register_call}; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Amount; + type CurrencyId; + type InvestmentId; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::storage] + pub(super) type CallIds = StorageMap< + _, + Blake2_128Concat, + ::Output, + mock_builder::CallId, + >; + + impl Pallet { + pub fn mock_update_investment( + f: impl Fn(&T::AccountId, T::InvestmentId, T::Amount) -> DispatchResult + 'static, + ) { + register_call!(move |(a, b, c)| f(a, b, c)); + } + + pub fn mock_accepted_payment_currency( + f: impl Fn(T::InvestmentId, T::CurrencyId) -> bool + 'static, + ) { + register_call!(move |(a, b)| f(a, b)); + } + + pub fn mock_investment( + f: impl Fn(&T::AccountId, T::InvestmentId) -> Result + 'static, + ) { + register_call!(move |(a, b)| f(a, b)); + } + + pub fn mock_update_redemption( + f: impl Fn(&T::AccountId, T::InvestmentId, T::Amount) -> DispatchResult + 'static, + ) { + register_call!(move |(a, b, c)| f(a, b, c)); + } + + pub fn mock_accepted_payout_currency( + f: impl Fn(T::InvestmentId, T::CurrencyId) -> bool + 'static, + ) { + register_call!(move |(a, b)| f(a, b)); + } + + pub fn mock_redemption( + f: impl Fn(&T::AccountId, T::InvestmentId) -> Result + 'static, + ) { + register_call!(move |(a, b)| f(a, b)); + } + + pub fn mock_collect_investment( + f: impl Fn(T::AccountId, T::InvestmentId) -> Result<(), DispatchError> + 'static, + ) { + register_call!(move |(a, b)| f(a, b)); + } + + pub fn mock_collect_redemption( + f: impl Fn(T::AccountId, T::InvestmentId) -> Result<(), DispatchError> + 'static, + ) { + register_call!(move |(a, b)| f(a, b)); + } + + pub fn mock_investment_requires_collect( + f: impl Fn(&T::AccountId, T::InvestmentId) -> bool + 'static, + ) { + register_call!(move |(a, b)| f(a, b)); + } + + pub fn mock_redemption_requires_collect( + f: impl Fn(&T::AccountId, T::InvestmentId) -> bool + 'static, + ) { + register_call!(move |(a, b)| f(a, b)); + } + } + + impl Investment for Pallet { + type Amount = T::Amount; + type CurrencyId = T::CurrencyId; + type Error = DispatchError; + type InvestmentId = T::InvestmentId; + + fn update_investment( + a: &T::AccountId, + b: Self::InvestmentId, + c: Self::Amount, + ) -> DispatchResult { + execute_call!((a, b, c)) + } + + fn accepted_payment_currency(a: Self::InvestmentId, b: Self::CurrencyId) -> bool { + execute_call!((a, b)) + } + + fn investment( + a: &T::AccountId, + b: Self::InvestmentId, + ) -> Result { + execute_call!((a, b)) + } + + fn update_redemption( + a: &T::AccountId, + b: Self::InvestmentId, + c: Self::Amount, + ) -> DispatchResult { + execute_call!((a, b, c)) + } + + fn accepted_payout_currency(a: Self::InvestmentId, b: Self::CurrencyId) -> bool { + execute_call!((a, b)) + } + + fn redemption( + a: &T::AccountId, + b: Self::InvestmentId, + ) -> Result { + execute_call!((a, b)) + } + + fn investment_requires_collect(a: &T::AccountId, b: T::InvestmentId) -> bool { + execute_call!((a, b)) + } + + fn redemption_requires_collect(a: &T::AccountId, b: T::InvestmentId) -> bool { + execute_call!((a, b)) + } + } + + impl InvestmentCollector for Pallet { + type Error = DispatchError; + type InvestmentId = T::InvestmentId; + type Result = (); + + fn collect_investment( + a: T::AccountId, + b: Self::InvestmentId, + ) -> Result { + execute_call!((a, b)) + } + + fn collect_redemption( + a: T::AccountId, + b: Self::InvestmentId, + ) -> Result { + execute_call!((a, b)) + } + } +} diff --git a/libs/mocks/src/lib.rs b/libs/mocks/src/lib.rs index 1fc48e7e8a..c563799276 100644 --- a/libs/mocks/src/lib.rs +++ b/libs/mocks/src/lib.rs @@ -1,24 +1,32 @@ mod change_guard; +mod currency_conversion; mod data; mod fees; +mod investment; mod liquidity_pools; mod liquidity_pools_gateway_routers; mod permissions; mod pools; mod rewards; +mod status_notification; mod time; +mod token_swaps; mod try_convert; mod write_off_policy; pub use change_guard::pallet_mock_change_guard; +pub use currency_conversion::pallet as pallet_mock_currency_conversion; pub use data::pallet as pallet_mock_data; pub use fees::pallet as pallet_mock_fees; +pub use investment::pallet as pallet_mock_investment; pub use liquidity_pools::{pallet as pallet_mock_liquidity_pools, MessageMock}; pub use liquidity_pools_gateway_routers::{pallet as pallet_mock_routers, RouterMock}; pub use permissions::pallet as pallet_mock_permissions; pub use pools::pallet as pallet_mock_pools; pub use rewards::pallet as pallet_mock_rewards; +pub use status_notification::pallet as pallet_mock_status_notification; pub use time::pallet as pallet_mock_time; +pub use token_swaps::pallet as pallet_mock_token_swaps; pub use try_convert::pallet as pallet_mock_try_convert; pub use write_off_policy::pallet as pallet_mock_write_off_policy; diff --git a/libs/mocks/src/status_notification.rs b/libs/mocks/src/status_notification.rs new file mode 100644 index 0000000000..b6cab902f3 --- /dev/null +++ b/libs/mocks/src/status_notification.rs @@ -0,0 +1,40 @@ +#[frame_support::pallet] +pub mod pallet { + use cfg_traits::StatusNotificationHook; + use frame_support::pallet_prelude::*; + use mock_builder::{execute_call_instance, register_call_instance}; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Id; + type Status; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::storage] + pub(super) type CallIds, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + ::Output, + mock_builder::CallId, + >; + + impl, I: 'static> Pallet { + pub fn mock_notify_status_change(f: impl Fn(T::Id, T::Status) -> DispatchResult + 'static) { + register_call_instance!(move |(a, b)| f(a, b)); + } + } + + impl, I: 'static> StatusNotificationHook for Pallet { + type Error = DispatchError; + type Id = T::Id; + type Status = T::Status; + + fn notify_status_change(a: Self::Id, b: Self::Status) -> DispatchResult { + execute_call_instance!((a, b)) + } + } +} diff --git a/libs/mocks/src/token_swaps.rs b/libs/mocks/src/token_swaps.rs new file mode 100644 index 0000000000..cd6eef7aee --- /dev/null +++ b/libs/mocks/src/token_swaps.rs @@ -0,0 +1,113 @@ +#[frame_support::pallet] +pub mod pallet { + use cfg_traits::TokenSwaps; + use frame_support::pallet_prelude::*; + use mock_builder::{execute_call, register_call}; + + #[pallet::config] + pub trait Config: frame_system::Config { + type CurrencyId; + type Balance; + type SellRatio; + type OrderId; + type OrderDetails; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::storage] + pub(super) type CallIds = StorageMap< + _, + Blake2_128Concat, + ::Output, + mock_builder::CallId, + >; + + impl Pallet { + pub fn mock_place_order( + f: impl Fn( + T::AccountId, + T::CurrencyId, + T::CurrencyId, + T::Balance, + T::SellRatio, + T::Balance, + ) -> Result + + 'static, + ) { + register_call!(move |(a, b, c, d, e, g)| f(a, b, c, d, e, g)); + } + + pub fn mock_update_order( + f: impl Fn(T::AccountId, T::OrderId, T::Balance, T::SellRatio, T::Balance) -> DispatchResult + + 'static, + ) { + register_call!(move |(a, b, c, d, e)| f(a, b, c, d, e)); + } + + pub fn mock_cancel_order(f: impl Fn(T::OrderId) -> DispatchResult + 'static) { + register_call!(f); + } + + pub fn mock_is_active(f: impl Fn(T::OrderId) -> bool + 'static) { + register_call!(f); + } + + pub fn mock_valid_pair( + f: impl Fn(T::CurrencyId, T::CurrencyId) -> DispatchResult + 'static, + ) { + register_call!(move |(a, b)| f(a, b)); + } + + pub fn mock_get_order_details(f: impl Fn(T::OrderId) -> Option + 'static) { + register_call!(f); + } + } + + impl TokenSwaps for Pallet { + type Balance = T::Balance; + type CurrencyId = T::CurrencyId; + type OrderDetails = T::OrderDetails; + type OrderId = T::OrderId; + type SellRatio = T::SellRatio; + + fn place_order( + a: T::AccountId, + b: Self::CurrencyId, + c: Self::CurrencyId, + d: Self::Balance, + e: Self::SellRatio, + f: Self::Balance, + ) -> Result { + execute_call!((a, b, c, d, e, f)) + } + + fn update_order( + a: T::AccountId, + b: Self::OrderId, + c: Self::Balance, + d: Self::SellRatio, + e: Self::Balance, + ) -> DispatchResult { + execute_call!((a, b, c, d, e)) + } + + fn cancel_order(a: Self::OrderId) -> DispatchResult { + execute_call!(a) + } + + fn is_active(a: Self::OrderId) -> bool { + execute_call!(a) + } + + fn valid_pair(a: Self::CurrencyId, b: Self::CurrencyId) -> bool { + execute_call!((a, b)) + } + + fn get_order_details(a: Self::OrderId) -> Option { + execute_call!(a) + } + } +} diff --git a/libs/traits/src/investments.rs b/libs/traits/src/investments.rs index 6d42bdc062..95cad1d49f 100644 --- a/libs/traits/src/investments.rs +++ b/libs/traits/src/investments.rs @@ -105,7 +105,7 @@ pub trait Investment { pub trait InvestmentCollector { type Error: Debug; type InvestmentId; - type Result: Debug; + type Result; /// Collect the results of a user's invest orders for the given /// investment. If any amounts are not fulfilled they are directly diff --git a/libs/types/src/investments.rs b/libs/types/src/investments.rs index 99bdbd2885..90eee5fff7 100644 --- a/libs/types/src/investments.rs +++ b/libs/types/src/investments.rs @@ -138,7 +138,7 @@ impl RedeemCollection { /// The collected investment/redemption amount for an account #[derive(Encode, Default, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] -pub struct CollectedAmount { +pub struct CollectedAmount { /// The amount which was collected /// * If investment: Tranche tokens /// * If redemption: Payment currency diff --git a/pallets/foreign-investments/Cargo.toml b/pallets/foreign-investments/Cargo.toml index 6b9a02c758..237039c23a 100644 --- a/pallets/foreign-investments/Cargo.toml +++ b/pallets/foreign-investments/Cargo.toml @@ -30,9 +30,12 @@ sp-std = { git = "https://github.com/paritytech/substrate", default-features = f frame-benchmarking = { git = "https://github.com/paritytech/substrate", default-features = false, optional = true, branch = "polkadot-v0.9.38" } [dev-dependencies] +rand = "0.8" sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } sp-io = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.38" } +cfg-mocks = { path = "../../libs/mocks" } + [features] default = ["std"] std = [ @@ -56,6 +59,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", + "cfg-mocks/runtime-benchmarks", ] try-runtime = [ "cfg-primitives/try-runtime", @@ -64,4 +68,5 @@ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "sp-runtime/try-runtime", + "cfg-mocks/try-runtime", ] diff --git a/pallets/foreign-investments/src/hooks.rs b/pallets/foreign-investments/src/hooks.rs index 150915e41a..17daddd943 100644 --- a/pallets/foreign-investments/src/hooks.rs +++ b/pallets/foreign-investments/src/hooks.rs @@ -26,8 +26,8 @@ use sp_std::marker::PhantomData; use crate::{ errors::{InvestError, RedeemError}, types::{InvestState, InvestTransition, RedeemState, RedeemTransition, TokenSwapReason}, - Config, Error, ForeignInvestmentInfo as ForeignInvestmentInfoStorage, InvestmentState, Pallet, - RedemptionState, SwapOf, + Config, Error, ForeignInvestmentInfo as ForeignInvestmentInfoStorage, InvestmentState, Of, + Pallet, RedemptionState, SwapOf, }; /// The hook struct which acts upon a fulfilled swap order. Depending on the @@ -211,7 +211,7 @@ impl StatusNotificationHook for CollectedInvestmentHook { let pre_state = InvestmentState::::get(&investor, investment_id); // Exit early if there is no foreign investment - if pre_state == InvestState::::NoState { + if pre_state == InvestState::>::NoState { return Ok(()); } diff --git a/pallets/foreign-investments/src/impls/invest.rs b/pallets/foreign-investments/src/impls/invest.rs index 4c3a98a6eb..1df8c4f15f 100644 --- a/pallets/foreign-investments/src/impls/invest.rs +++ b/pallets/foreign-investments/src/impls/invest.rs @@ -1117,3 +1117,349 @@ where } } } + +#[cfg(test)] +mod tests { + use cfg_traits::IdentityCurrencyConversion; + use frame_support::{assert_err, assert_ok}; + use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng}; + + use super::*; + + #[derive(Clone, Copy, PartialEq, Debug)] + enum CurrencyId { + Foreign, + Pool, + } + + const CONVERSION_RATE: u128 = 3; // 300% + + fn to_pool(foreign_amount: u128) -> u128 { + foreign_amount * CONVERSION_RATE + } + + fn to_foreign(pool_amount: u128) -> u128 { + pool_amount / CONVERSION_RATE + } + + struct TestCurrencyConverter; + + impl IdentityCurrencyConversion for TestCurrencyConverter { + type Balance = u128; + type Currency = CurrencyId; + type Error = DispatchError; + + fn stable_to_stable( + currency_in: Self::Currency, + currency_out: Self::Currency, + amount: Self::Balance, + ) -> Result { + match (currency_out, currency_in) { + (CurrencyId::Foreign, CurrencyId::Pool) => Ok(to_pool(amount)), + (CurrencyId::Pool, CurrencyId::Foreign) => Ok(to_foreign(amount)), + _ => panic!("Same currency"), + } + } + } + + #[derive(PartialEq)] + struct TestConfig; + + impl InvestStateConfig for TestConfig { + type Balance = u128; + type CurrencyConverter = TestCurrencyConverter; + type CurrencyId = CurrencyId; + } + + type InvestState = super::InvestState; + type InvestTransition = super::InvestTransition; + + #[test] + fn increase_with_pool_swap() { + let done_amount = 60; + let invest_amount = 600; + let pool_swap = Swap { + currency_in: CurrencyId::Pool, + currency_out: CurrencyId::Foreign, + amount: 120, + }; + let foreign_swap = Swap { + currency_in: CurrencyId::Foreign, + currency_out: CurrencyId::Pool, + amount: 240, + }; + let increase = InvestTransition::IncreaseInvestOrder(pool_swap); + + assert_ok!( + InvestState::NoState.transition(increase.clone()), + InvestState::ActiveSwapIntoPoolCurrency { swap: pool_swap } + ); + + assert_ok!( + InvestState::InvestmentOngoing { invest_amount }.transition(increase.clone()), + InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { + swap: pool_swap, + invest_amount + } + ); + + assert_ok!( + InvestState::ActiveSwapIntoPoolCurrency { swap: pool_swap } + .transition(increase.clone()), + InvestState::ActiveSwapIntoPoolCurrency { + swap: Swap { + amount: pool_swap.amount + pool_swap.amount, + ..pool_swap + } + } + ); + + assert_ok!( + InvestState::ActiveSwapIntoForeignCurrency { swap: foreign_swap } + .transition(increase.clone()), + InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { + swap: Swap { + amount: foreign_swap.amount - to_foreign(pool_swap.amount), + ..foreign_swap + }, + done_amount: to_foreign(pool_swap.amount), + invest_amount: pool_swap.amount, + } + ); + + assert_ok!( + InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { + swap: pool_swap, + invest_amount + } + .transition(increase.clone()), + InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { + swap: Swap { + amount: pool_swap.amount + pool_swap.amount, + ..pool_swap + }, + invest_amount + } + ); + + assert_ok!( + InvestState::ActiveSwapIntoForeignCurrencyAndInvestmentOngoing { + swap: foreign_swap, + invest_amount + } + .transition(increase.clone()), + InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { + swap: Swap { + amount: foreign_swap.amount - to_foreign(pool_swap.amount), + ..foreign_swap + }, + done_amount: to_foreign(pool_swap.amount), + invest_amount: invest_amount + pool_swap.amount, + } + ); + + assert_err!( + InvestState::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { + swap: pool_swap, + done_amount, + } + .transition(increase.clone()), + DispatchError::Other( + "Invalid invest state, should automatically be transitioned into \ + ActiveSwapIntoPoolCurrencyAndInvestmentOngoing", + ) + ); + + assert_ok!( + InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { + swap: foreign_swap, + done_amount, + } + .transition(increase.clone()), + InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { + swap: Swap { + amount: foreign_swap.amount - to_foreign(pool_swap.amount), + ..foreign_swap + }, + done_amount: done_amount + to_foreign(pool_swap.amount), + invest_amount: pool_swap.amount + } + ); + + assert_ok!( + InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { + swap: foreign_swap, + done_amount, + invest_amount, + } + .transition(increase.clone()), + InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { + swap: Swap { + amount: foreign_swap.amount - to_foreign(pool_swap.amount), + ..foreign_swap + }, + done_amount: done_amount + to_foreign(pool_swap.amount), + invest_amount: invest_amount + pool_swap.amount + } + ); + } + + impl InvestState { + fn get_done_amount(&self) -> u128 { + match *self { + Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDone { done_amount, .. } => { + to_pool(done_amount) + } + Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { + done_amount, .. + } => to_pool(done_amount), + Self::ActiveSwapIntoPoolCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { + done_amount, + .. + } => to_pool(done_amount), + Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDoneAndInvestmentOngoing { + done_amount, + .. + } => to_pool(done_amount), + Self::SwapIntoForeignDone { done_swap } => to_pool(done_swap.amount), + Self::SwapIntoForeignDoneAndInvestmentOngoing { done_swap, .. } => { + to_pool(done_swap.amount) + } + _ => 0, + } + } + + fn get_swap_pool_amount(&self) -> u128 { + self.get_active_swap_amount_pool_denominated().unwrap() + } + + fn has_active_pool_swap(&self) -> bool { + self.get_active_swap() + .map(|swap| swap.currency_in == CurrencyId::Pool) + .unwrap_or(false) + } + + fn total(&self) -> u128 { + self.get_investing_amount() + self.get_done_amount() + self.get_swap_pool_amount() + } + } + + struct Checker { + old_state: InvestState, + } + + impl Checker { + fn new(initial_state: InvestState, use_case: &[InvestTransition]) -> Self { + println!("Testing use case: {:#?}", use_case); + + Self { + old_state: initial_state, + } + } + + /// Invariants from: https://centrifuge.hackmd.io/IPtRlOrOSrOF9MHjEY48BA?view#Without-storage + fn check_delta_invariant(&self, transition: &InvestTransition, new_state: &InvestState) { + println!("Transition: {:#?}", transition); + println!("New state: {:#?}, total: {}", new_state, new_state.total()); + + match *transition { + InvestTransition::IncreaseInvestOrder(swap) => { + let diff = new_state.total() - self.old_state.total(); + assert_eq!(diff, swap.amount); + } + InvestTransition::DecreaseInvestOrder(_) => { + let diff = new_state.total() - self.old_state.total(); + assert_eq!(diff, 0); + } + InvestTransition::FulfillSwapOrder(swap) => { + let diff = new_state.total() - self.old_state.total(); + assert_eq!(diff, 0); + + if self.old_state.has_active_pool_swap() { + let invest_diff = new_state.get_investing_amount() + - self.old_state.get_investing_amount(); + assert_eq!(invest_diff, swap.amount) + } else { + let done_diff = + new_state.get_done_amount() - self.old_state.get_done_amount(); + assert_eq!(done_diff, to_pool(swap.amount)) + } + } + InvestTransition::CollectInvestment(value) => { + if self.old_state.get_investing_amount() == 0 { + assert_eq!(new_state.get_investing_amount(), 0) + } else { + assert_eq!(new_state.get_investing_amount(), value); + + assert_eq!( + new_state.get_done_amount(), + self.old_state.get_done_amount() + ); + assert_eq!( + new_state.get_swap_pool_amount(), + self.old_state.get_swap_pool_amount(), + ); + } + } + } + } + } + + #[test] + fn fuzzer() { + let pool_swap_big = Swap { + currency_in: CurrencyId::Pool, + currency_out: CurrencyId::Foreign, + amount: 120, + }; + let pool_swap_small = Swap { + currency_in: CurrencyId::Pool, + currency_out: CurrencyId::Foreign, + amount: 60, + }; + let foreign_swap_big = Swap { + currency_in: CurrencyId::Foreign, + currency_out: CurrencyId::Pool, + amount: to_foreign(pool_swap_big.amount), + }; + let foreign_swap_small = Swap { + currency_in: CurrencyId::Foreign, + currency_out: CurrencyId::Pool, + amount: to_foreign(pool_swap_small.amount), + }; + + let transitions = [ + InvestTransition::IncreaseInvestOrder(pool_swap_big), + InvestTransition::IncreaseInvestOrder(pool_swap_small), + InvestTransition::DecreaseInvestOrder(foreign_swap_big), + InvestTransition::DecreaseInvestOrder(foreign_swap_small), + InvestTransition::FulfillSwapOrder(pool_swap_big), + InvestTransition::FulfillSwapOrder(pool_swap_small), + InvestTransition::FulfillSwapOrder(foreign_swap_big), + InvestTransition::FulfillSwapOrder(foreign_swap_small), + InvestTransition::CollectInvestment(60), + InvestTransition::CollectInvestment(120), + ]; + + let mut rng = StdRng::seed_from_u64(42); // Determinism for reproduction + + for _ in 0..100000 { + let mut use_case = transitions.clone(); + let use_case = use_case.partial_shuffle(&mut rng, 8).0; + let mut state = InvestState::NoState; + let mut checker = Checker::new(state.clone(), use_case); + + for transition in use_case { + state = match state.transition(transition.clone()) { + Ok(state) => { + checker.check_delta_invariant(&transition, &state); + checker.old_state = state.clone(); + state + } + // We skip the imposible transition and continues with the use case + Err(_) => state, + } + } + } + } +} diff --git a/pallets/foreign-investments/src/impls/mod.rs b/pallets/foreign-investments/src/impls/mod.rs index 4c52984090..093a2c5d17 100644 --- a/pallets/foreign-investments/src/impls/mod.rs +++ b/pallets/foreign-investments/src/impls/mod.rs @@ -29,7 +29,7 @@ use crate::{ errors::{InvestError, RedeemError}, types::{InvestState, InvestTransition, RedeemState, RedeemTransition, TokenSwapReason}, CollectedInvestment, CollectedRedemption, Config, Error, Event, ForeignInvestmentInfo, - ForeignInvestmentInfoOf, InvestmentPaymentCurrency, InvestmentState, Pallet, + ForeignInvestmentInfoOf, InvestmentPaymentCurrency, InvestmentState, Of, Pallet, RedemptionPayoutCurrency, RedemptionState, SwapOf, TokenSwapOrderIds, }; @@ -342,7 +342,7 @@ impl Pallet { pub(crate) fn apply_invest_state_transition( who: &T::AccountId, investment_id: T::InvestmentId, - state: InvestState, + state: InvestState>, update_swap_order: bool, ) -> DispatchResult { // Must not send executed decrease notification before updating redemption @@ -409,8 +409,8 @@ impl Pallet { }, } .map(|(invest_state, maybe_swap, invest_amount)| { - // Must update investment amount before handling swap as in case of decrease, - // updating the swap transfers the currency from the investment account to the + // Must update investment amount before handling swap as in case of decrease, + // updating the swap transfers the currency from the investment account to the // investor which is required for placing the swap order if T::Investment::investment(who, investment_id)? != invest_amount { T::Investment::update_investment(who, investment_id, invest_amount)?; @@ -544,7 +544,7 @@ impl Pallet { fn deposit_investment_event( who: &T::AccountId, investment_id: T::InvestmentId, - maybe_state: Option>, + maybe_state: Option>>, ) { match maybe_state { Some(state) if state == InvestState::NoState => { @@ -674,7 +674,7 @@ impl Pallet { reason: TokenSwapReason, ) -> Result< ( - Option>, + Option>>, Option>, ), DispatchError, @@ -875,7 +875,7 @@ impl Pallet { ) -> Result< ( Option>, - Option>, + Option>>, Option>, Option, ), diff --git a/pallets/foreign-investments/src/impls/redeem.rs b/pallets/foreign-investments/src/impls/redeem.rs index 8c7fc9e6d2..105129994e 100644 --- a/pallets/foreign-investments/src/impls/redeem.rs +++ b/pallets/foreign-investments/src/impls/redeem.rs @@ -584,3 +584,138 @@ where } } } + +#[cfg(test)] +mod tests { + use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng}; + + use super::*; + + #[derive(Clone, Copy, PartialEq, Debug)] + enum CurrencyId { + Foreign, + Pool, + } + + type RedeemState = super::RedeemState; + type RedeemTransition = super::RedeemTransition; + + impl RedeemState { + fn get_done_amount(&self) -> u128 { + match *self { + Self::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { + done_amount, .. + } => done_amount, + Self::RedeemingAndActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { + done_amount, + .. + } => done_amount, + Self::SwapIntoForeignDone { done_swap } => done_swap.amount, + Self::RedeemingAndSwapIntoForeignDone { done_swap, .. } => done_swap.amount, + _ => 0, + } + } + + fn get_swap_amount(&self) -> u128 { + self.get_active_swap().map(|swap| swap.amount).unwrap_or(0) + } + + fn total(&self) -> u128 { + self.get_redeeming_amount() + self.get_done_amount() + self.get_swap_amount() + } + } + + struct Checker { + old_state: RedeemState, + } + + impl Checker { + fn new(initial_state: RedeemState, use_case: &[RedeemTransition]) -> Self { + println!("Testing use case: {:#?}", use_case); + + Self { + old_state: initial_state, + } + } + + /// Invariants from: https://centrifuge.hackmd.io/IPtRlOrOSrOF9MHjEY48BA?view#Without-storage + fn check_delta_invariant(&self, transition: &RedeemTransition, new_state: &RedeemState) { + println!("Transition: {:#?}", transition); + println!("New state: {:#?}", new_state); + + match *transition { + RedeemTransition::IncreaseRedeemOrder(amount) => { + let diff = new_state.total() - self.old_state.total(); + assert_eq!(diff, amount); + } + RedeemTransition::DecreaseRedeemOrder(amount) => { + let diff = self.old_state.total() - new_state.total(); + assert_eq!(diff, amount); + } + RedeemTransition::FulfillSwapOrder(swap) => { + let diff = new_state.total() - self.old_state.total(); + assert_eq!(diff, 0); + + let done_diff = new_state.get_done_amount() - self.old_state.get_done_amount(); + assert_eq!(done_diff, swap.amount) + } + RedeemTransition::CollectRedemption(value, swap) => { + if self.old_state.get_redeeming_amount() == 0 { + assert_eq!(new_state.get_redeeming_amount(), 0) + } else { + assert_eq!(new_state.get_redeeming_amount(), value); + } + + let swap_diff = new_state.get_swap_amount() - self.old_state.get_swap_amount(); + assert_eq!(swap_diff, swap.amount) + } + }; + } + } + + #[test] + fn fuzzer() { + let foreign_swap_big = Swap { + currency_in: CurrencyId::Foreign, + currency_out: CurrencyId::Pool, + amount: 120, + }; + let foreign_swap_small = Swap { + currency_in: CurrencyId::Foreign, + currency_out: CurrencyId::Pool, + amount: 60, + }; + + let transitions = [ + RedeemTransition::IncreaseRedeemOrder(120), + RedeemTransition::IncreaseRedeemOrder(60), + RedeemTransition::DecreaseRedeemOrder(120), + RedeemTransition::DecreaseRedeemOrder(60), + RedeemTransition::FulfillSwapOrder(foreign_swap_big), + RedeemTransition::FulfillSwapOrder(foreign_swap_small), + RedeemTransition::CollectRedemption(30, foreign_swap_big), + RedeemTransition::CollectRedemption(30, foreign_swap_small), + ]; + + let mut rng = StdRng::seed_from_u64(42); // Determinism for reproduction + + for _ in 0..100000 { + let mut use_case = transitions.clone(); + let use_case = use_case.partial_shuffle(&mut rng, 8).0; + let mut state = RedeemState::NoState; + let mut checker = Checker::new(state.clone(), &use_case); + + for transition in use_case { + state = match state.transition(transition.clone()) { + Ok(state) => { + checker.check_delta_invariant(&transition, &state); + checker.old_state = state.clone(); + state + } + // We skip the imposible transition and continues with the use case + Err(_) => state, + } + } + } + } +} diff --git a/pallets/foreign-investments/src/lib.rs b/pallets/foreign-investments/src/lib.rs index ccba92f841..7ee3f8e485 100644 --- a/pallets/foreign-investments/src/lib.rs +++ b/pallets/foreign-investments/src/lib.rs @@ -54,6 +54,12 @@ pub mod hooks; pub mod impls; pub mod types; +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + pub type SwapOf = Swap<::Balance, ::CurrencyId>; pub type ForeignInvestmentInfoOf = cfg_types::investments::ForeignInvestmentInfo< ::AccountId, @@ -73,7 +79,7 @@ pub mod pallet { use errors::{InvestError, RedeemError}; use frame_support::{dispatch::HasCompact, pallet_prelude::*}; use sp_runtime::traits::AtLeast32BitUnsigned; - use types::{InvestState, RedeemState}; + use types::{InvestState, InvestStateConfig, RedeemState}; use super::*; @@ -100,13 +106,7 @@ pub mod pallet { + MaxEncodedLen; /// The currency type of transferrable tokens - type CurrencyId: Parameter - + Member - + Copy - + MaybeSerializeDeserialize - + Ord - + TypeInfo - + MaxEncodedLen; + type CurrencyId: Parameter + Member + Copy + TypeInfo + MaxEncodedLen; /// The pool id type required for the investment identifier type PoolId: Member @@ -118,17 +118,10 @@ pub mod pallet { + core::fmt::Debug; /// The tranche id type required for the investment identifier - type TrancheId: Member - + Parameter - + Default - + Copy - + MaxEncodedLen - + TypeInfo - + From<[u8; 16]>; + type TrancheId: Member + Parameter + Default + Copy + MaxEncodedLen + TypeInfo; /// The investment identifying type required for the investment type type InvestmentId: TrancheCurrency - + Into + Clone + Member + Parameter @@ -249,6 +242,16 @@ pub mod pallet { >; } + /// Aux type for configurations that inherents from `Config` + #[derive(PartialEq)] + pub struct Of(PhantomData); + + impl InvestStateConfig for Of { + type Balance = T::Balance; + type CurrencyConverter = T::CurrencyConverter; + type CurrencyId = T::CurrencyId; + } + /// Maps an investor and their `InvestmentId` to the corresponding /// `InvestState`. /// @@ -263,7 +266,7 @@ pub mod pallet { T::AccountId, Blake2_128Concat, T::InvestmentId, - InvestState, + InvestState>, ValueQuery, >; @@ -400,7 +403,7 @@ pub mod pallet { ForeignInvestmentUpdated { investor: T::AccountId, investment_id: T::InvestmentId, - state: InvestState, + state: InvestState>, }, ForeignInvestmentCleared { investor: T::AccountId, diff --git a/pallets/foreign-investments/src/mock.rs b/pallets/foreign-investments/src/mock.rs new file mode 100644 index 0000000000..c9f49bc976 --- /dev/null +++ b/pallets/foreign-investments/src/mock.rs @@ -0,0 +1,182 @@ +use cfg_mocks::{ + pallet_mock_currency_conversion, pallet_mock_investment, pallet_mock_pools, + pallet_mock_status_notification, pallet_mock_token_swaps, +}; +use cfg_traits::investments::TrancheCurrency; +use cfg_types::investments::{ + ExecutedForeignCollect, ExecutedForeignDecreaseInvest, ForeignInvestmentInfo, Swap, +}; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::traits::{ConstU16, ConstU32, ConstU64}; +use scale_info::TypeInfo; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + FixedU128, +}; + +use crate::pallet as pallet_foreign_investments; + +// ============= +// Types +// ============= + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +pub type AccountId = u64; +pub type Balance = u128; +pub type TrancheId = u32; +pub type PoolId = u64; +pub type OrderId = u64; +pub type CurrencyId = u8; +pub type Ratio = FixedU128; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] +pub struct InvestmentId(pub PoolId, pub TrancheId); + +impl TrancheCurrency for InvestmentId { + fn generate(pool_id: PoolId, tranche_id: TrancheId) -> Self { + Self(pool_id, tranche_id) + } + + fn of_pool(&self) -> PoolId { + self.0 + } + + fn of_tranche(&self) -> TrancheId { + self.1 + } +} + +frame_support::parameter_types! { + pub DefaultTokenSellRatio: Ratio = FixedU128::from_float(1.5); +} + +// ====================== +// Runtime config +// ====================== + +frame_support::construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + MockInvestment: pallet_mock_investment, + MockTokenSwaps: pallet_mock_token_swaps, + MockDecreaseInvestHook: pallet_mock_status_notification::, + MockCollectRedeemHook: pallet_mock_status_notification::, + MockCollectInvestHook: pallet_mock_status_notification::, + MockCurrencyConversion: pallet_mock_currency_conversion, + MockPools: pallet_mock_pools, + ForeignInvestment: pallet_foreign_investments, + } +); + +impl frame_system::Config for Runtime { + type AccountData = (); + type AccountId = AccountId; + type BaseCallFilter = frame_support::traits::Everything; + type BlockHashCount = ConstU64<250>; + type BlockLength = (); + type BlockNumber = u64; + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Header = Header; + type Index = u64; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<16>; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = ConstU16<42>; + type SystemWeightInfo = (); + type Version = (); +} + +impl pallet_mock_investment::Config for Runtime { + type Amount = Balance; + type CurrencyId = CurrencyId; + type InvestmentId = InvestmentId; +} + +impl pallet_mock_token_swaps::Config for Runtime { + type Balance = Balance; + type CurrencyId = CurrencyId; + type OrderDetails = Swap; + type OrderId = OrderId; + type SellRatio = FixedU128; +} + +type Hook1 = pallet_mock_status_notification::Instance1; +impl pallet_mock_status_notification::Config for Runtime { + type Id = ForeignInvestmentInfo; + type Status = ExecutedForeignDecreaseInvest; +} + +type Hook2 = pallet_mock_status_notification::Instance2; +impl pallet_mock_status_notification::Config for Runtime { + type Id = ForeignInvestmentInfo; + type Status = ExecutedForeignCollect; +} + +type Hook3 = pallet_mock_status_notification::Instance3; +impl pallet_mock_status_notification::Config for Runtime { + type Id = ForeignInvestmentInfo; + type Status = ExecutedForeignCollect; +} + +impl pallet_mock_currency_conversion::Config for Runtime { + type Balance = Balance; + type CurrencyId = CurrencyId; +} + +impl pallet_mock_pools::Config for Runtime { + type Balance = Balance; + type BalanceRatio = Ratio; + type CurrencyId = CurrencyId; + type PoolId = PoolId; + type TrancheCurrency = InvestmentId; + type TrancheId = TrancheId; +} + +impl pallet_foreign_investments::Config for Runtime { + type Balance = Balance; + type BalanceRatio = Ratio; + type CollectedForeignInvestmentHook = MockCollectInvestHook; + type CollectedForeignRedemptionHook = MockCollectRedeemHook; + type CurrencyConverter = MockCurrencyConversion; + type CurrencyId = CurrencyId; + type DecreasedForeignInvestOrderHook = MockDecreaseInvestHook; + type DefaultTokenSellRatio = DefaultTokenSellRatio; + type Investment = MockInvestment; + type InvestmentId = InvestmentId; + type PoolId = PoolId; + type PoolInspect = MockPools; + type RuntimeEvent = RuntimeEvent; + type TokenSwapOrderId = OrderId; + type TokenSwaps = MockTokenSwaps; + type TrancheId = TrancheId; + type WeightInfo = (); +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(storage); + ext.execute_with(|| { + // Initialize default mocking methods here + }); + ext +} diff --git a/pallets/foreign-investments/src/tests.rs b/pallets/foreign-investments/src/tests.rs new file mode 100644 index 0000000000..81f3a0fafc --- /dev/null +++ b/pallets/foreign-investments/src/tests.rs @@ -0,0 +1,328 @@ +use cfg_traits::{investments::ForeignInvestment as ForeignInvestmentT, StatusNotificationHook}; +use cfg_types::investments::ForeignInvestmentInfo as ForeignInvestmentInfoS; +use frame_support::assert_ok; + +use crate::{ + hooks::FulfilledSwapOrderHook, + mock::*, + types::{InvestState, TokenSwapReason}, + *, +}; + +const USER: AccountId = 1; +const INVESTMENT_ID: InvestmentId = InvestmentId(42, 23); +const USER_CURR: CurrencyId = 5; +const POOL_CURR: CurrencyId = 10; +const ORDER_ID: OrderId = 1; + +mod util { + use super::*; + + pub fn new_invest(order_id: OrderId, amount: Balance) { + MockInvestment::mock_investment_requires_collect(|_, _| false); + MockInvestment::mock_investment(|_, _| Ok(0)); + MockInvestment::mock_update_investment(|_, _, _| Ok(())); + MockTokenSwaps::mock_place_order(move |_, _, _, _, _, _| Ok(order_id)); + MockCurrencyConversion::mock_stable_to_stable(move |_, _, _| Ok(amount) /* 1:1 */); + + ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + amount, + USER_CURR, + POOL_CURR, + ) + .unwrap(); + + MockInvestment::mock_investment_requires_collect(|_, _| unimplemented!("no mock")); + MockInvestment::mock_investment(|_, _| unimplemented!("no mock")); + MockInvestment::mock_update_investment(|_, _, _| unimplemented!("no mock")); + MockTokenSwaps::mock_place_order(|_, _, _, _, _, _| unimplemented!("no mock")); + MockCurrencyConversion::mock_stable_to_stable(|_, _, _| unimplemented!("no mock")); + } + + pub fn notify_swaped(order_id: OrderId, amount: Balance) { + MockInvestment::mock_investment_requires_collect(|_, _| false); + MockInvestment::mock_investment(|_, _| Ok(0)); + MockInvestment::mock_update_investment(|_, _, _| Ok(())); + MockTokenSwaps::mock_cancel_order(|_| Ok(())); + MockTokenSwaps::mock_is_active(|_| true); + MockCurrencyConversion::mock_stable_to_stable(move |_, _, _| Ok(amount) /* 1:1 */); + + FulfilledSwapOrderHook::::notify_status_change( + order_id, + Swap { + currency_out: USER_CURR, + currency_in: POOL_CURR, + amount, + }, + ) + .unwrap(); + + MockInvestment::mock_investment_requires_collect(|_, _| unimplemented!("no mock")); + MockInvestment::mock_investment(|_, _| unimplemented!("no mock")); + MockInvestment::mock_update_investment(|_, _, _| unimplemented!("no mock")); + MockTokenSwaps::mock_cancel_order(|_| unimplemented!("no mock")); + MockTokenSwaps::mock_is_active(|_| unimplemented!("no mock")); + MockCurrencyConversion::mock_stable_to_stable(|_, _, _| unimplemented!("no mock")); + } +} + +mod increase_investment { + use super::*; + + #[test] + fn create_new() { + const AMOUNT: Balance = 100; + + new_test_ext().execute_with(|| { + MockInvestment::mock_investment_requires_collect(|account_id, investment_id| { + assert_eq!(account_id, &USER); + assert_eq!(investment_id, INVESTMENT_ID); + false + }); + MockInvestment::mock_investment(|account_id, investment_id| { + assert_eq!(account_id, &USER); + assert_eq!(investment_id, INVESTMENT_ID); + Ok(0) // Nothing initially invested + }); + MockInvestment::mock_update_investment(|account_id, investment_id, amount| { + assert_eq!(account_id, &USER); + assert_eq!(investment_id, INVESTMENT_ID); + assert_eq!(amount, 0); // We still do not have the swap done. + Ok(()) + }); + MockTokenSwaps::mock_place_order( + |account_id, curr_in, curr_out, amount, limit, min| { + assert_eq!(account_id, USER); + assert_eq!(curr_in, POOL_CURR); + assert_eq!(curr_out, USER_CURR); + assert_eq!(amount, AMOUNT); + assert_eq!(limit, DefaultTokenSellRatio::get()); + assert_eq!(min, AMOUNT); + Ok(ORDER_ID) + }, + ); + MockCurrencyConversion::mock_stable_to_stable(|curr_in, curr_out, amount_out| { + assert_eq!(curr_in, POOL_CURR); + assert_eq!(curr_out, USER_CURR); + assert_eq!(amount_out, AMOUNT); + Ok(amount_out) // 1:1 + }); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + AMOUNT, + USER_CURR, + POOL_CURR, + )); + + assert_eq!( + InvestmentState::::get(USER, INVESTMENT_ID), + InvestState::ActiveSwapIntoPoolCurrency { + swap: Swap { + currency_out: USER_CURR, + currency_in: POOL_CURR, + amount: AMOUNT, + } + } + ); + assert_eq!( + TokenSwapOrderIds::::get(USER, INVESTMENT_ID), + Some(ORDER_ID) + ); + assert_eq!( + ForeignInvestmentInfo::::get(ORDER_ID), + Some(ForeignInvestmentInfoS { + owner: USER, + id: INVESTMENT_ID, + last_swap_reason: Some(TokenSwapReason::Investment), + }) + ); + }); + } + + #[test] + fn over_pending() { + const INITIAL_AMOUNT: Balance = 100; + const INCREASE_AMOUNT: Balance = 500; + + new_test_ext().execute_with(|| { + util::new_invest(ORDER_ID, INITIAL_AMOUNT); + + MockInvestment::mock_investment_requires_collect(|account_id, investment_id| { + assert_eq!(account_id, &USER); + assert_eq!(investment_id, INVESTMENT_ID); + false + }); + MockInvestment::mock_investment(|_, _| Ok(0)); + MockInvestment::mock_update_investment(|_, _, amount| { + assert_eq!(amount, 0); + Ok(()) + }); + MockTokenSwaps::mock_is_active(|order_id| { + assert_eq!(order_id, ORDER_ID); + true + }); + MockTokenSwaps::mock_get_order_details(|order_id| { + assert_eq!(order_id, ORDER_ID); + Some(Swap { + currency_out: USER_CURR, + currency_in: POOL_CURR, + amount: INITIAL_AMOUNT, + }) + }); + MockTokenSwaps::mock_update_order(|account_id, order_id, amount, limit, min| { + assert_eq!(account_id, USER); + assert_eq!(order_id, ORDER_ID); + assert_eq!(amount, INITIAL_AMOUNT + INCREASE_AMOUNT); + assert_eq!(limit, DefaultTokenSellRatio::get()); + assert_eq!(min, INITIAL_AMOUNT + INCREASE_AMOUNT); + Ok(()) + }); + MockCurrencyConversion::mock_stable_to_stable(|curr_in, curr_out, amount_out| { + assert_eq!(curr_in, POOL_CURR); + assert_eq!(curr_out, USER_CURR); + assert_eq!(amount_out, INCREASE_AMOUNT); + Ok(amount_out) // 1:1 + }); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + INCREASE_AMOUNT, + USER_CURR, + POOL_CURR, + )); + + assert_eq!( + InvestmentState::::get(USER, INVESTMENT_ID), + InvestState::ActiveSwapIntoPoolCurrency { + swap: Swap { + currency_out: USER_CURR, + currency_in: POOL_CURR, + amount: INITIAL_AMOUNT + INCREASE_AMOUNT, + } + } + ); + }); + } + + #[test] + fn over_ongoing() { + const INITIAL_AMOUNT: Balance = 100; + const INCREASE_AMOUNT: Balance = 500; + + new_test_ext().execute_with(|| { + util::new_invest(ORDER_ID, INITIAL_AMOUNT); + util::notify_swaped(ORDER_ID, INITIAL_AMOUNT); + + MockInvestment::mock_investment_requires_collect(|_, _| false); + MockInvestment::mock_investment(|_, _| Ok(INITIAL_AMOUNT)); + MockTokenSwaps::mock_is_active(|order_id| { + assert_eq!(order_id, ORDER_ID); + false + }); + MockTokenSwaps::mock_place_order( + |account_id, curr_in, curr_out, amount, limit, min| { + assert_eq!(account_id, USER); + assert_eq!(curr_in, POOL_CURR); + assert_eq!(curr_out, USER_CURR); + assert_eq!(amount, INCREASE_AMOUNT); + assert_eq!(limit, DefaultTokenSellRatio::get()); + assert_eq!(min, INCREASE_AMOUNT); + Ok(ORDER_ID) + }, + ); + MockInvestment::mock_update_investment(|_, _, amount| { + assert_eq!(amount, 0); + Ok(()) + }); + MockCurrencyConversion::mock_stable_to_stable(|curr_in, curr_out, amount_out| { + assert_eq!(curr_in, POOL_CURR); + assert_eq!(curr_out, USER_CURR); + assert_eq!(amount_out, INCREASE_AMOUNT); + Ok(amount_out) // 1:1 + }); + + assert_ok!(ForeignInvestment::increase_foreign_investment( + &USER, + INVESTMENT_ID, + INCREASE_AMOUNT, + USER_CURR, + POOL_CURR, + )); + + assert_eq!( + InvestmentState::::get(USER, INVESTMENT_ID), + InvestState::ActiveSwapIntoPoolCurrencyAndInvestmentOngoing { + swap: Swap { + currency_out: USER_CURR, + currency_in: POOL_CURR, + amount: INCREASE_AMOUNT, + }, + invest_amount: INITIAL_AMOUNT + } + ); + }); + } +} + +mod fulfilled_swap { + use super::*; + + #[test] + fn pending_investment_to_ongoing() { + const AMOUNT: Balance = 100; + + new_test_ext().execute_with(|| { + util::new_invest(ORDER_ID, AMOUNT); + + MockInvestment::mock_investment_requires_collect(|_, _| false); + MockInvestment::mock_investment(|account_id, investment_id| { + assert_eq!(account_id, &USER); + assert_eq!(investment_id, INVESTMENT_ID); + Ok(0) // Nothing initially invested + }); + MockInvestment::mock_update_investment(|account_id, investment_id, amount| { + assert_eq!(account_id, &USER); + assert_eq!(investment_id, INVESTMENT_ID); + assert_eq!(amount, AMOUNT); + Ok(()) + }); + MockTokenSwaps::mock_is_active(|order_id| { + assert_eq!(order_id, ORDER_ID); + true + }); + MockTokenSwaps::mock_cancel_order(|order_id| { + assert_eq!(order_id, ORDER_ID); + Ok(()) + }); + MockCurrencyConversion::mock_stable_to_stable(|curr_in, curr_out, amount_out| { + assert_eq!(curr_in, POOL_CURR); + assert_eq!(curr_out, USER_CURR); + assert_eq!(amount_out, AMOUNT); + Ok(amount_out) // 1:1 + }); + + assert_ok!(FulfilledSwapOrderHook::::notify_status_change( + ORDER_ID, + Swap { + currency_out: USER_CURR, + currency_in: POOL_CURR, + amount: AMOUNT, + }, + )); + + assert_eq!( + InvestmentState::::get(USER, INVESTMENT_ID), + InvestState::InvestmentOngoing { + invest_amount: AMOUNT + }, + ); + assert_eq!(TokenSwapOrderIds::::get(USER, INVESTMENT_ID), None); + assert_eq!(ForeignInvestmentInfo::::get(ORDER_ID), None); + }); + } +} diff --git a/pallets/foreign-investments/src/types.rs b/pallets/foreign-investments/src/types.rs index 4b2de308c1..5f6686602b 100644 --- a/pallets/foreign-investments/src/types.rs +++ b/pallets/foreign-investments/src/types.rs @@ -18,8 +18,6 @@ use frame_support::{dispatch::fmt::Debug, RuntimeDebugNoBound}; use scale_info::TypeInfo; use sp_runtime::traits::{EnsureAdd, EnsureSub, Zero}; -use crate::Config; - /// Reflects the reason for the last token swap update such that it can be /// updated accordingly if the last and current reason mismatch. #[derive( @@ -43,12 +41,6 @@ pub trait InvestStateConfig { >; } -impl InvestStateConfig for T { - type Balance = T::Balance; - type CurrencyConverter = T::CurrencyConverter; - type CurrencyId = T::CurrencyId; -} - /// Reflects all states a foreign investment can have until it is processed as /// an investment via `::Investment`. This includes swapping it /// into a pool currency or back, if the investment is decreased before it is