From 8ad9edaccdf9f87aa470c0af0dd80995360b4b2f Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 11 Oct 2024 13:32:55 +0200 Subject: [PATCH] feat: minimum chunk size setting (#5314) --- state-chain/pallets/cf-swapping/src/lib.rs | 27 +++++++-- state-chain/pallets/cf-swapping/src/tests.rs | 7 +-- .../pallets/cf-swapping/src/tests/config.rs | 59 +++---------------- .../pallets/cf-swapping/src/tests/dca.rs | 54 +++++++++++++++++ 4 files changed, 86 insertions(+), 61 deletions(-) diff --git a/state-chain/pallets/cf-swapping/src/lib.rs b/state-chain/pallets/cf-swapping/src/lib.rs index 90ec5f3e7d1..46c9a793011 100644 --- a/state-chain/pallets/cf-swapping/src/lib.rs +++ b/state-chain/pallets/cf-swapping/src/lib.rs @@ -401,7 +401,7 @@ pub enum CcmFailReason { #[scale_info(skip_type_params(T, I))] pub enum PalletConfigUpdate { /// Set the maximum amount allowed to be put into a swap. Excess amounts are confiscated. - MaximumSwapAmount { asset: Asset, amount: Option }, + SetMinimumChunkSize { asset: Asset, amount: Option }, /// Set the delay in blocks before retrying a previously failed swap. SwapRetryDelay { delay: BlockNumberFor }, /// Set the interval at which we buy FLIP in order to burn it. @@ -543,6 +543,14 @@ pub mod pallet { ConstU32, >; + /// The minimum chunk size for DCA swaps. The number of chunks of a DCA swap will be reduced + /// so that the chunk size is greater than or equal to this value. Setting to zero will disable + /// the check for that asset. + #[pallet::storage] + #[pallet::getter(fn minimum_chunk_size)] + pub type MinimumChunkSize = + StorageMap<_, Twox64Concat, Asset, AssetAmount, ValueQuery>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -999,9 +1007,8 @@ pub mod pallet { for update in updates { match update { - PalletConfigUpdate::MaximumSwapAmount { asset, amount } => { - MaximumSwapAmount::::set(asset, amount); - Self::deposit_event(Event::::MaximumSwapAmountSet { asset, amount }); + PalletConfigUpdate::SetMinimumChunkSize { asset, amount } => { + MinimumChunkSize::::set(asset, amount.unwrap_or_default()); }, PalletConfigUpdate::SwapRetryDelay { delay } => { ensure!( @@ -2111,6 +2118,18 @@ pub mod pallet { swap_amount }; + // Restrict the number of chunks based on the minimum chunk size. + let dca_params = dca_params.map(|mut dca_params| { + let minimum_chunk_size = MinimumChunkSize::::get(input_asset); + if minimum_chunk_size > 0 { + dca_params.number_of_chunks = core::cmp::min( + max((input_amount / minimum_chunk_size) as u32, 1), + dca_params.number_of_chunks, + ); + } + dca_params + }); + Self::deposit_event(Event::::SwapRequested { swap_request_id: request_id, input_asset, diff --git a/state-chain/pallets/cf-swapping/src/tests.rs b/state-chain/pallets/cf-swapping/src/tests.rs index adf27c2cd8c..c6deebebda9 100644 --- a/state-chain/pallets/cf-swapping/src/tests.rs +++ b/state-chain/pallets/cf-swapping/src/tests.rs @@ -52,12 +52,7 @@ static EVM_OUTPUT_ADDRESS: LazyLock = LazyLock::new(|| ForeignChainAddress::Eth([1; 20].into())); fn set_maximum_swap_amount(asset: Asset, amount: Option) { - assert_ok!(Swapping::update_pallet_config( - OriginTrait::root(), - vec![PalletConfigUpdate::MaximumSwapAmount { asset, amount }] - .try_into() - .unwrap() - )); + MaximumSwapAmount::::set(asset, amount); } struct TestSwapParams { diff --git a/state-chain/pallets/cf-swapping/src/tests/config.rs b/state-chain/pallets/cf-swapping/src/tests/config.rs index f33a49d6d5d..ef6a53c06e1 100644 --- a/state-chain/pallets/cf-swapping/src/tests/config.rs +++ b/state-chain/pallets/cf-swapping/src/tests/config.rs @@ -3,61 +3,46 @@ use super::*; #[test] fn can_update_all_config_items() { new_test_ext().execute_with(|| { - const NEW_MAX_SWAP_AMOUNT_BTC: Option = Some(100); - const NEW_MAX_SWAP_AMOUNT_DOT: Option = Some(69); let new_swap_retry_delay = BlockNumberFor::::from(1234u32); let new_flip_buy_interval = BlockNumberFor::::from(5678u32); const NEW_MAX_SWAP_RETRY_DURATION: u32 = 69_u32; const MAX_SWAP_REQUEST_DURATION: u32 = 420_u32; + const NEW_MINIMUM_CHUNK_SIZE: AssetAmount = 1; // Check that the default values are different from the new ones - assert!(MaximumSwapAmount::::get(Asset::Btc).is_none()); - assert!(MaximumSwapAmount::::get(Asset::Dot).is_none()); assert_ne!(SwapRetryDelay::::get(), new_swap_retry_delay); assert_ne!(FlipBuyInterval::::get(), new_flip_buy_interval); assert_ne!(MaxSwapRetryDurationBlocks::::get(), NEW_MAX_SWAP_RETRY_DURATION); assert_ne!(MaxSwapRequestDurationBlocks::::get(), MAX_SWAP_REQUEST_DURATION); + assert_ne!(MinimumChunkSize::::get(Asset::Eth), NEW_MINIMUM_CHUNK_SIZE); // Update all config items at the same time, and updates 2 separate max swap amounts. assert_ok!(Swapping::update_pallet_config( OriginTrait::root(), vec![ - PalletConfigUpdate::MaximumSwapAmount { - asset: Asset::Btc, - amount: NEW_MAX_SWAP_AMOUNT_BTC - }, - PalletConfigUpdate::MaximumSwapAmount { - asset: Asset::Dot, - amount: NEW_MAX_SWAP_AMOUNT_DOT - }, PalletConfigUpdate::SwapRetryDelay { delay: new_swap_retry_delay }, PalletConfigUpdate::FlipBuyInterval { interval: new_flip_buy_interval }, PalletConfigUpdate::SetMaxSwapRetryDuration { blocks: NEW_MAX_SWAP_RETRY_DURATION }, PalletConfigUpdate::SetMaxSwapRequestDuration { blocks: MAX_SWAP_REQUEST_DURATION }, + PalletConfigUpdate::SetMinimumChunkSize { + asset: Asset::Eth, + amount: Some(NEW_MINIMUM_CHUNK_SIZE) + }, ] .try_into() .unwrap() )); // Check that the new values were set - assert_eq!(MaximumSwapAmount::::get(Asset::Btc), NEW_MAX_SWAP_AMOUNT_BTC); - assert_eq!(MaximumSwapAmount::::get(Asset::Dot), NEW_MAX_SWAP_AMOUNT_DOT); assert_eq!(SwapRetryDelay::::get(), new_swap_retry_delay); assert_eq!(FlipBuyInterval::::get(), new_flip_buy_interval); assert_eq!(MaxSwapRetryDurationBlocks::::get(), NEW_MAX_SWAP_RETRY_DURATION); assert_eq!(MaxSwapRequestDurationBlocks::::get(), MAX_SWAP_REQUEST_DURATION); + assert_eq!(MinimumChunkSize::::get(Asset::Eth), NEW_MINIMUM_CHUNK_SIZE); // Check that the events were emitted assert_events_eq!( Test, - RuntimeEvent::Swapping(crate::Event::MaximumSwapAmountSet { - asset: Asset::Btc, - amount: NEW_MAX_SWAP_AMOUNT_BTC, - }), - RuntimeEvent::Swapping(crate::Event::MaximumSwapAmountSet { - asset: Asset::Dot, - amount: NEW_MAX_SWAP_AMOUNT_DOT, - }), RuntimeEvent::Swapping(crate::Event::SwapRetryDelaySet { swap_retry_delay: new_swap_retry_delay }), @@ -69,7 +54,7 @@ fn can_update_all_config_items() { }), RuntimeEvent::Swapping(crate::Event::MaxSwapRequestDurationSet { blocks: MAX_SWAP_REQUEST_DURATION - }) + }), ); // Make sure that only governance can update the config @@ -132,34 +117,6 @@ fn max_swap_amount_can_be_removed() { }); } -#[test] -fn can_set_maximum_swap_amount() { - new_test_ext().execute_with(|| { - let asset = Asset::Eth; - let amount = Some(1_000u128); - assert!(MaximumSwapAmount::::get(asset).is_none()); - - // Set the new maximum swap_amount - set_maximum_swap_amount(asset, amount); - - assert_eq!(MaximumSwapAmount::::get(asset), amount); - assert_eq!(Swapping::maximum_swap_amount(asset), amount); - - System::assert_last_event(RuntimeEvent::Swapping(Event::::MaximumSwapAmountSet { - asset, - amount, - })); - - // Can remove maximum swap amount - set_maximum_swap_amount(asset, None); - assert!(MaximumSwapAmount::::get(asset).is_none()); - System::assert_last_event(RuntimeEvent::Swapping(Event::::MaximumSwapAmountSet { - asset, - amount: None, - })); - }); -} - #[test] fn can_swap_below_max_amount() { new_test_ext().execute_with(|| { diff --git a/state-chain/pallets/cf-swapping/src/tests/dca.rs b/state-chain/pallets/cf-swapping/src/tests/dca.rs index f93a8dc7a47..f5bdb737733 100644 --- a/state-chain/pallets/cf-swapping/src/tests/dca.rs +++ b/state-chain/pallets/cf-swapping/src/tests/dca.rs @@ -1001,3 +1001,57 @@ mod ccm_tests { }); } } + +#[test] +fn test_minimum_chunk_size() { + #[track_caller] + fn set_and_test_chunk_size( + asset_amount: AssetAmount, + number_of_chunks: u32, + expected_number_of_chunks: u32, + minimum_chunk_size: AssetAmount, + ) { + // Update the minimum chunk size + assert_ok!(Swapping::update_pallet_config( + OriginTrait::root(), + vec![PalletConfigUpdate::SetMinimumChunkSize { + asset: Asset::Eth, + amount: Some(minimum_chunk_size) + },] + .try_into() + .unwrap() + )); + + // Init a swap, this is where the minimum chunk size will kick in + let dca_params = DcaParameters { number_of_chunks, chunk_interval: CHUNK_INTERVAL }; + let expected_swap_request_id = Swapping::init_swap_request( + Asset::Eth, + asset_amount, + Asset::Btc, + SwapRequestType::Regular { output_address: ForeignChainAddress::Eth([1; 20].into()) }, + vec![].try_into().unwrap(), + None, + Some(dca_params), + SwapOrigin::Vault { tx_hash: Default::default() }, + ); + + // Check that the swap was initiated with the updated number of chunks + let expected_dca_params = DcaParameters { + number_of_chunks: expected_number_of_chunks, + chunk_interval: CHUNK_INTERVAL, + }; + assert_has_matching_event!( + Test, + RuntimeEvent::Swapping(Event::SwapRequested {swap_request_id, dca_parameters, .. }) + if dca_parameters == &Some(expected_dca_params.clone()) && Ok(*swap_request_id) == expected_swap_request_id + ); + } + + new_test_ext().execute_with(|| { + set_and_test_chunk_size(100, 10, 10, 9); + set_and_test_chunk_size(100, 10, 10, 10); + set_and_test_chunk_size(100, 10, 9, 11); + set_and_test_chunk_size(1, 10, 1, 10); + set_and_test_chunk_size(1, 1000, 1000, 0); + }); +}