From 33a85db0c68a869e77728d98991a5b78514521ca Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Wed, 20 Nov 2024 14:57:09 +0100 Subject: [PATCH 01/12] Implement fuzz tests for futarchy --- Cargo.lock | 17 ++++++++++ Cargo.toml | 1 + scripts/tests/fuzz.sh | 2 ++ zrml/futarchy/Cargo.toml | 6 ++++ zrml/futarchy/fuzz/Cargo.toml | 26 +++++++++++++++ zrml/futarchy/fuzz/submit_proposal.rs | 44 ++++++++++++++++++++++++++ zrml/futarchy/src/mock/mod.rs | 4 +-- zrml/futarchy/src/mock/types/oracle.rs | 16 ++++++++++ zrml/futarchy/src/types/proposal.rs | 27 ++++++++++++++++ 9 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 zrml/futarchy/fuzz/Cargo.toml create mode 100644 zrml/futarchy/fuzz/submit_proposal.rs diff --git a/Cargo.lock b/Cargo.lock index 45d4359cc..8ad88800b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15246,6 +15246,7 @@ dependencies = [ name = "zrml-futarchy" version = "0.5.5" dependencies = [ + "arbitrary", "env_logger 0.10.2", "frame-benchmarking", "frame-support", @@ -15253,6 +15254,7 @@ dependencies = [ "pallet-balances", "parity-scale-codec", "scale-info", + "sp-core", "sp-io", "sp-runtime", "test-case", @@ -15260,6 +15262,21 @@ dependencies = [ "zrml-futarchy", ] +[[package]] +name = "zrml-futarchy-fuzz" +version = "0.5.5" +dependencies = [ + "arbitrary", + "frame-support", + "frame-system", + "libfuzzer-sys", + "orml-traits", + "rand 0.8.5", + "sp-runtime", + "zeitgeist-primitives", + "zrml-futarchy", +] + [[package]] name = "zrml-global-disputes" version = "0.5.5" diff --git a/Cargo.toml b/Cargo.toml index eac6a6327..540356d64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ members = [ "zrml/combinatorial-tokens", "zrml/court", "zrml/futarchy", + "zrml/futarchy/fuzz", "zrml/hybrid-router", "zrml/global-disputes", "zrml/market-commons", diff --git a/scripts/tests/fuzz.sh b/scripts/tests/fuzz.sh index beff95f26..ba5877eab 100755 --- a/scripts/tests/fuzz.sh +++ b/scripts/tests/fuzz.sh @@ -57,3 +57,5 @@ cargo fuzz run --release --fuzz-dir zrml/swaps/fuzz pool_exit -- -runs=$(($(($RU # --- Orderbook-v1 Pallet fuzz tests --- cargo fuzz run --release --fuzz-dir zrml/orderbook/fuzz orderbook_v1_full_workflow -- -runs=$RUNS + +cargo fuzz run --release --fuzz-dir zrml/futarchy/fuzz futarchy_full_workflow -- -runs=$RUNS diff --git a/zrml/futarchy/Cargo.toml b/zrml/futarchy/Cargo.toml index ab9b7ceff..1fd6f7bfd 100644 --- a/zrml/futarchy/Cargo.toml +++ b/zrml/futarchy/Cargo.toml @@ -13,12 +13,18 @@ env_logger = { workspace = true, optional = true } pallet-balances = { workspace = true, optional = true } sp-io = { workspace = true, optional = true } +# fuzz + +arbitrary = { workspace = true, features = ["derive"], optional = true } +sp-core = { workspace = true, optional = true } + [dev-dependencies] test-case = { workspace = true } zrml-futarchy = { workspace = true, features = ["default", "mock"] } [features] default = ["std"] +fuzzing = ["arbitrary", "sp-core"] mock = [ "env_logger/default", "sp-io/default", diff --git a/zrml/futarchy/fuzz/Cargo.toml b/zrml/futarchy/fuzz/Cargo.toml new file mode 100644 index 000000000..e12990f80 --- /dev/null +++ b/zrml/futarchy/fuzz/Cargo.toml @@ -0,0 +1,26 @@ +[[bin]] +doc = false +name = "submit_proposal" +path = "submit_proposal.rs" +test = false + +[dependencies] +arbitrary = { workspace = true, features = ["derive"] } +frame-support = { workspace = true, features = ["default"] } +frame-system = { workspace = true } +libfuzzer-sys = { workspace = true } +orml-traits = { workspace = true, features = ["default"] } +rand = { workspace = true, features = ["default"] } +sp-runtime = { workspace = true, features = ["default"] } +zeitgeist-primitives = { workspace = true, features = ["default", "mock"] } +zrml-futarchy = { workspace = true, features = ["default", "fuzzing", "mock"] } + +[package] +authors = ["Forecasting Technologies Ltd"] +edition.workspace = true +name = "zrml-futarchy-fuzz" +publish = false +version = "0.5.5" + +[package.metadata] +cargo-fuzz = true diff --git a/zrml/futarchy/fuzz/submit_proposal.rs b/zrml/futarchy/fuzz/submit_proposal.rs new file mode 100644 index 000000000..22a0c0bbd --- /dev/null +++ b/zrml/futarchy/fuzz/submit_proposal.rs @@ -0,0 +1,44 @@ +#![no_main] + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use frame_system::pallet_prelude::{BlockNumberFor, OriginFor}; +use libfuzzer_sys::fuzz_target; +use zrml_futarchy::{ + mock::{ + ext_builder::ExtBuilder, + runtime::{Futarchy, Runtime, RuntimeOrigin}, + }, + types::Proposal, +}; + +#[derive(Debug)] +struct SubmitProposalParams { + origin: OriginFor, + duration: BlockNumberFor, + proposal: Proposal, +} + +impl<'a> Arbitrary<'a> for SubmitProposalParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + let origin = RuntimeOrigin::signed(account_id); + + let duration = Arbitrary::arbitrary(u)?; + + let proposal = Arbitrary::arbitrary(u)?; + + let params = SubmitProposalParams { origin, duration, proposal }; + + Ok(params) + } +} + +fuzz_target!(|params: SubmitProposalParams| { + let mut ext = ExtBuilder::build(); + + ext.execute_with(|| { + let _ = Futarchy::submit_proposal(params.origin, params.duration, params.proposal); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/futarchy/src/mock/mod.rs b/zrml/futarchy/src/mock/mod.rs index 546789d7e..6c3be09e1 100644 --- a/zrml/futarchy/src/mock/mod.rs +++ b/zrml/futarchy/src/mock/mod.rs @@ -18,6 +18,6 @@ #![cfg(feature = "mock")] pub mod ext_builder; -pub(crate) mod runtime; -pub(crate) mod types; +pub mod runtime; +pub mod types; pub mod utility; diff --git a/zrml/futarchy/src/mock/types/oracle.rs b/zrml/futarchy/src/mock/types/oracle.rs index 4b8a7f7e4..63efe2da8 100644 --- a/zrml/futarchy/src/mock/types/oracle.rs +++ b/zrml/futarchy/src/mock/types/oracle.rs @@ -21,6 +21,9 @@ use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use zeitgeist_primitives::traits::FutarchyOracle; +#[cfg(feature = "fuzzing")] +use arbitrary::{Arbitrary, Unstructured, Result as ArbitraryResult}; + #[derive(Clone, Debug, Decode, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo)] pub struct MockOracle { weight: Weight, @@ -44,3 +47,16 @@ impl FutarchyOracle for MockOracle { (self.weight, self.value) } } + +#[cfg(feature = "fuzzing")] +impl<'a> Arbitrary<'a> for MockOracle { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let ref_time = u64::arbitrary(u)?; + let proof_size = u64::arbitrary(u)?; + let weight = Weight::from_parts(ref_time, proof_size); + + let value = bool::arbitrary(u)?; + + Ok(MockOracle::new(weight, value)) + } +} diff --git a/zrml/futarchy/src/types/proposal.rs b/zrml/futarchy/src/types/proposal.rs index 84ddf6748..fb7360fb5 100644 --- a/zrml/futarchy/src/types/proposal.rs +++ b/zrml/futarchy/src/types/proposal.rs @@ -21,6 +21,13 @@ use frame_system::pallet_prelude::BlockNumberFor; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; +#[cfg(feature = "fuzzing")] +use { + arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}, + frame_support::traits::{Bounded}, + sp_core::H256, +}; + // TODO Make config a generic, keeps things simple. #[derive( CloneNoBound, Decode, Encode, Eq, MaxEncodedLen, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo, @@ -34,3 +41,23 @@ where pub call: BoundedCallOf, pub oracle: OracleOf, } + +#[cfg(feature = "fuzzing")] +impl<'a, T> Arbitrary<'a> for Proposal +where + OracleOf: Arbitrary<'a>, + T: Config, +{ + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let when = u32::arbitrary(u)?.into(); + + let raw: [u8; 32] = Arbitrary::arbitrary(u)?; + let hash = H256(raw); + let len = u32::arbitrary(u)?; + let call = Bounded::Lookup { hash, len }; + + let oracle = Arbitrary::arbitrary(u)?; + + Ok(Proposal { when, call, oracle }) + } +} From 541a4a65f6057318f6675cd5f2e00437219429f7 Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Wed, 20 Nov 2024 15:05:51 +0100 Subject: [PATCH 02/12] Minor fixes --- scripts/tests/fuzz.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tests/fuzz.sh b/scripts/tests/fuzz.sh index ba5877eab..354a5f4d4 100755 --- a/scripts/tests/fuzz.sh +++ b/scripts/tests/fuzz.sh @@ -58,4 +58,4 @@ cargo fuzz run --release --fuzz-dir zrml/swaps/fuzz pool_exit -- -runs=$(($(($RU # --- Orderbook-v1 Pallet fuzz tests --- cargo fuzz run --release --fuzz-dir zrml/orderbook/fuzz orderbook_v1_full_workflow -- -runs=$RUNS -cargo fuzz run --release --fuzz-dir zrml/futarchy/fuzz futarchy_full_workflow -- -runs=$RUNS +cargo fuzz run --release --fuzz-dir zrml/futarchy/fuzz submit_proposal -- -runs=$RUNS From 8c26d3612d4ae30583c4ce72a78d6b9ee3ceda12 Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Fri, 22 Nov 2024 15:10:26 +0100 Subject: [PATCH 03/12] add first fuzz test for combo tokens --- Cargo.lock | 15 ++++ Cargo.toml | 1 + .../src/traits/market_commons_pallet_api.rs | 2 +- zrml/combinatorial-tokens/fuzz/Cargo.toml | 26 ++++++ zrml/combinatorial-tokens/fuzz/common.rs | 36 ++++++++ .../fuzz/split_position.rs | 88 +++++++++++++++++++ zrml/combinatorial-tokens/src/lib.rs | 15 ++-- zrml/combinatorial-tokens/src/mock/mod.rs | 4 +- 8 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 zrml/combinatorial-tokens/fuzz/Cargo.toml create mode 100644 zrml/combinatorial-tokens/fuzz/common.rs create mode 100644 zrml/combinatorial-tokens/fuzz/split_position.rs diff --git a/Cargo.lock b/Cargo.lock index 8ad88800b..93a36046e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15215,6 +15215,21 @@ dependencies = [ "zrml-market-commons", ] +[[package]] +name = "zrml-combinatorial-tokens-fuzz" +version = "0.5.5" +dependencies = [ + "arbitrary", + "frame-support", + "frame-system", + "libfuzzer-sys", + "orml-traits", + "rand 0.8.5", + "sp-runtime", + "zeitgeist-primitives", + "zrml-combinatorial-tokens", +] + [[package]] name = "zrml-court" version = "0.5.5" diff --git a/Cargo.toml b/Cargo.toml index 540356d64..e4f13383f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ members = [ "runtime/zeitgeist", "zrml/authorized", "zrml/combinatorial-tokens", + "zrml/combinatorial-tokens/fuzz", "zrml/court", "zrml/futarchy", "zrml/futarchy/fuzz", diff --git a/primitives/src/traits/market_commons_pallet_api.rs b/primitives/src/traits/market_commons_pallet_api.rs index a24eecefa..38c9ccb0f 100644 --- a/primitives/src/traits/market_commons_pallet_api.rs +++ b/primitives/src/traits/market_commons_pallet_api.rs @@ -37,7 +37,7 @@ use sp_runtime::{ // Abstraction of the market type, which is not a part of `MarketCommonsPalletApi` because Rust // doesn't support type aliases in traits. -type MarketOf = Market< +pub type MarketOf = Market< ::AccountId, ::Balance, ::BlockNumber, diff --git a/zrml/combinatorial-tokens/fuzz/Cargo.toml b/zrml/combinatorial-tokens/fuzz/Cargo.toml new file mode 100644 index 000000000..b927c441e --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/Cargo.toml @@ -0,0 +1,26 @@ +[[bin]] +doc = false +name = "split_position" +path = "split_position.rs" +test = false + +[dependencies] +arbitrary = { workspace = true, features = ["derive"] } +frame-support = { workspace = true, features = ["default"] } +frame-system = { workspace = true } +libfuzzer-sys = { workspace = true } +orml-traits = { workspace = true, features = ["default"] } +rand = { workspace = true, features = ["default"] } +sp-runtime = { workspace = true, features = ["default"] } +zeitgeist-primitives = { workspace = true, features = ["default", "mock"] } +zrml-combinatorial-tokens = { workspace = true, features = ["default", "mock"] } + +[package] +authors = ["Forecasting Technologies Ltd"] +edition.workspace = true +name = "zrml-combinatorial-tokens-fuzz" +publish = false +version = "0.5.5" + +[package.metadata] +cargo-fuzz = true diff --git a/zrml/combinatorial-tokens/fuzz/common.rs b/zrml/combinatorial-tokens/fuzz/common.rs new file mode 100644 index 000000000..543e8248f --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/common.rs @@ -0,0 +1,36 @@ +use frame_system::pallet_prelude::BlockNumberFor; +use zeitgeist_primitives::{ + traits::MarketOf, + types::{Asset, Market, MarketCreation, MarketPeriod, MarketStatus, MarketType, ScoringRule}, +}; +use zrml_combinatorial_tokens::{AssetOf, BalanceOf, Config, MarketIdOf}; + +pub(crate) fn market( + market_id: MarketIdOf, + base_asset: AssetOf, + market_type: MarketType, +) -> MarketOf<::MarketCommons> +where + T: Config, + ::AccountId: Default, +{ + Market { + market_id, + base_asset, + creator: Default::default(), + creation: MarketCreation::Permissionless, + creator_fee: Default::default(), + oracle: Default::default(), + metadata: Default::default(), + market_type, + period: MarketPeriod::Block(0u8.into()..10u8.into()), + deadlines: Default::default(), + scoring_rule: ScoringRule::AmmCdaHybrid, + status: MarketStatus::Active, + report: None, + resolved_outcome: None, + dispute_mechanism: None, + bonds: Default::default(), + early_close: None, + } +} diff --git a/zrml/combinatorial-tokens/fuzz/split_position.rs b/zrml/combinatorial-tokens/fuzz/split_position.rs new file mode 100644 index 000000000..2f07f6fe8 --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/split_position.rs @@ -0,0 +1,88 @@ +#![no_main] + +mod common; + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use frame_system::pallet_prelude::{BlockNumberFor, OriginFor}; +use libfuzzer_sys::fuzz_target; +use orml_traits::currency::MultiCurrency; +use zeitgeist_primitives::{ + traits::MarketCommonsPalletApi, + types::{Asset, MarketType}, +}; +use zrml_combinatorial_tokens::{ + mock::{ + ext_builder::ExtBuilder, + runtime::{CombinatorialTokens, Runtime, RuntimeOrigin}, + }, + traits::CombinatorialIdManager, + AccountIdOf, BalanceOf, CombinatorialIdOf, Config, MarketIdOf, +}; + +#[derive(Debug)] +struct SplitPositionFuzzParams { + account_id: AccountIdOf, + parent_collection_id: Option>, + market_id: MarketIdOf, + partition: Vec>, + amount: BalanceOf, + force_max_work: bool, +} + +impl<'a> Arbitrary<'a> for SplitPositionFuzzParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + let parent_collection_id = Arbitrary::arbitrary(u)?; + let market_id = 0u8.into(); + let partition = Arbitrary::arbitrary(u)?; + let amount = Arbitrary::arbitrary(u)?; + let force_max_work = Arbitrary::arbitrary(u)?; + + let params = SplitPositionFuzzParams { + account_id, + parent_collection_id, + market_id, + partition, + amount, + force_max_work, + }; + + Ok(params) + } +} + +fuzz_target!(|params: SplitPositionFuzzParams| { + let mut ext = ExtBuilder::build(); + + ext.execute_with(|| { + // We create a market and equip the user with the tokens they require to make the + // `split_position` call meaningful. + let collateral = Asset::Ztg; + let market = + common::market::(params.market_id, collateral, MarketType::Categorical(7)); + <::MarketCommons as MarketCommonsPalletApi>::push_market(market) + .unwrap(); + + let position = if let Some(pci) = params.parent_collection_id { + let position_id = + ::CombinatorialIdManager::get_position_id(collateral, pci); + + Asset::CombinatorialToken(position_id) + } else { + Asset::Ztg + }; + <::MultiCurrency>::deposit(position, ¶ms.account_id, params.amount) + .unwrap(); + + let _ = CombinatorialTokens::split_position( + RuntimeOrigin::signed(params.account_id), + params.parent_collection_id, + params.market_id, + params.partition, + params.amount, + params.force_max_work, + ); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/combinatorial-tokens/src/lib.rs b/zrml/combinatorial-tokens/src/lib.rs index 7feb249b3..cb0fa7424 100644 --- a/zrml/combinatorial-tokens/src/lib.rs +++ b/zrml/combinatorial-tokens/src/lib.rs @@ -30,7 +30,7 @@ extern crate alloc; mod benchmarking; pub mod mock; mod tests; -mod traits; +pub mod traits; pub mod types; pub mod weights; @@ -100,15 +100,14 @@ mod pallet { #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(PhantomData); - pub(crate) type AccountIdOf = ::AccountId; - pub(crate) type AssetOf = Asset>; - pub(crate) type BalanceOf = + pub type AccountIdOf = ::AccountId; + pub type AssetOf = Asset>; + pub type BalanceOf = <::MultiCurrency as MultiCurrency>>::Balance; - pub(crate) type CombinatorialIdOf = + pub type CombinatorialIdOf = <::CombinatorialIdManager as CombinatorialIdManager>::CombinatorialId; - pub(crate) type MarketIdOf = - <::MarketCommons as MarketCommonsPalletApi>::MarketId; - pub(crate) type SplitPositionDispatchInfoOf = + pub type MarketIdOf = <::MarketCommons as MarketCommonsPalletApi>::MarketId; + pub type SplitPositionDispatchInfoOf = SplitPositionDispatchInfo, MarketIdOf>; pub(crate) const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); diff --git a/zrml/combinatorial-tokens/src/mock/mod.rs b/zrml/combinatorial-tokens/src/mock/mod.rs index f2933dea7..b29cf33f0 100644 --- a/zrml/combinatorial-tokens/src/mock/mod.rs +++ b/zrml/combinatorial-tokens/src/mock/mod.rs @@ -19,5 +19,5 @@ pub(crate) mod consts; pub mod ext_builder; -pub(crate) mod runtime; -pub(crate) mod types; +pub mod runtime; +pub mod types; From da91c5372e4a423684f334847c446d929c56785f Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Fri, 22 Nov 2024 15:14:52 +0100 Subject: [PATCH 04/12] Add test to script --- scripts/tests/fuzz.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/tests/fuzz.sh b/scripts/tests/fuzz.sh index 354a5f4d4..8e5ec7db6 100755 --- a/scripts/tests/fuzz.sh +++ b/scripts/tests/fuzz.sh @@ -59,3 +59,5 @@ cargo fuzz run --release --fuzz-dir zrml/swaps/fuzz pool_exit -- -runs=$(($(($RU cargo fuzz run --release --fuzz-dir zrml/orderbook/fuzz orderbook_v1_full_workflow -- -runs=$RUNS cargo fuzz run --release --fuzz-dir zrml/futarchy/fuzz submit_proposal -- -runs=$RUNS + +cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz split_position -- -runs=$RUNS From 4fc55d5f594c6f3f715bd0ae295fdd7ae2ac464d Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Fri, 22 Nov 2024 19:00:18 +0100 Subject: [PATCH 05/12] Add fuzz test for merge position --- zrml/combinatorial-tokens/fuzz/Cargo.toml | 12 ++ zrml/combinatorial-tokens/fuzz/common.rs | 5 +- .../fuzz/merge_position.rs | 104 ++++++++++++++++++ .../fuzz/split_position.rs | 1 - zrml/combinatorial-tokens/src/lib.rs | 4 +- 5 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 zrml/combinatorial-tokens/fuzz/merge_position.rs diff --git a/zrml/combinatorial-tokens/fuzz/Cargo.toml b/zrml/combinatorial-tokens/fuzz/Cargo.toml index b927c441e..32d76bf87 100644 --- a/zrml/combinatorial-tokens/fuzz/Cargo.toml +++ b/zrml/combinatorial-tokens/fuzz/Cargo.toml @@ -4,6 +4,18 @@ name = "split_position" path = "split_position.rs" test = false +[[bin]] +doc = false +name = "merge_position" +path = "merge_position.rs" +test = false + +[[bin]] +doc = false +name = "redeem_position" +path = "redeem_position.rs" +test = false + [dependencies] arbitrary = { workspace = true, features = ["derive"] } frame-support = { workspace = true, features = ["default"] } diff --git a/zrml/combinatorial-tokens/fuzz/common.rs b/zrml/combinatorial-tokens/fuzz/common.rs index 543e8248f..b4a0e297d 100644 --- a/zrml/combinatorial-tokens/fuzz/common.rs +++ b/zrml/combinatorial-tokens/fuzz/common.rs @@ -1,9 +1,8 @@ -use frame_system::pallet_prelude::BlockNumberFor; use zeitgeist_primitives::{ traits::MarketOf, - types::{Asset, Market, MarketCreation, MarketPeriod, MarketStatus, MarketType, ScoringRule}, + types::{Market, MarketCreation, MarketPeriod, MarketStatus, MarketType, ScoringRule}, }; -use zrml_combinatorial_tokens::{AssetOf, BalanceOf, Config, MarketIdOf}; +use zrml_combinatorial_tokens::{AssetOf, Config, MarketIdOf}; pub(crate) fn market( market_id: MarketIdOf, diff --git a/zrml/combinatorial-tokens/fuzz/merge_position.rs b/zrml/combinatorial-tokens/fuzz/merge_position.rs new file mode 100644 index 000000000..f266d2e5f --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/merge_position.rs @@ -0,0 +1,104 @@ +#![no_main] + +mod common; + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use libfuzzer_sys::fuzz_target; +use orml_traits::currency::MultiCurrency; +use zeitgeist_primitives::{ + traits::MarketCommonsPalletApi, + types::{Asset, MarketType}, +}; +use zrml_combinatorial_tokens::{ + mock::{ + ext_builder::ExtBuilder, + runtime::{CombinatorialTokens, Runtime, RuntimeOrigin}, + }, + AccountIdOf, BalanceOf, CombinatorialIdOf, Config, MarketIdOf, +}; + +#[derive(Debug)] +struct MergePositionFuzzParams { + account_id: AccountIdOf, + parent_collection_id: Option>, + market_id: MarketIdOf, + partition: Vec>, + amount: BalanceOf, + force_max_work: bool, +} + +impl<'a> Arbitrary<'a> for MergePositionFuzzParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + let parent_collection_id = Arbitrary::arbitrary(u)?; + let market_id = 0u8.into(); + let partition = Arbitrary::arbitrary(u)?; + let amount = Arbitrary::arbitrary(u)?; + let force_max_work = Arbitrary::arbitrary(u)?; + + let params = MergePositionFuzzParams { + account_id, + parent_collection_id, + market_id, + partition, + amount, + force_max_work, + }; + + Ok(params) + } +} + +fuzz_target!(|params: MergePositionFuzzParams| { + let mut ext = ExtBuilder::build(); + + ext.execute_with(|| { + // We create a market and equip the user with the tokens they require to make the + // `merge_position` call meaningful, and deposit collateral in the pallet account. + let collateral = Asset::Ztg; + let market = + common::market::(params.market_id, collateral, MarketType::Categorical(7)); + <::MarketCommons as MarketCommonsPalletApi>::push_market(market) + .unwrap(); + + let positions = params.partition + .iter() + .cloned() + .map(|index_set| { + CombinatorialTokens::position_from_parent_collection( + params.parent_collection_id, + params.market_id, + index_set, + false, + ) + }) + .collect::, _>>() + .unwrap(); + for &position in positions.iter() { + <::MultiCurrency>::deposit( + position, + ¶ms.account_id, + params.amount, + ) + .unwrap(); + } + + // Is not required if `parent_collection_id.is_some()`, but we're doing it anyways. + <::MultiCurrency>::deposit( + collateral, + &CombinatorialTokens::account_id(), + params.amount, + ).unwrap(); + + let _ = CombinatorialTokens::merge_position( + RuntimeOrigin::signed(params.account_id), + params.parent_collection_id, + params.market_id, + params.partition, + params.amount, + params.force_max_work, + ); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/combinatorial-tokens/fuzz/split_position.rs b/zrml/combinatorial-tokens/fuzz/split_position.rs index 2f07f6fe8..145801878 100644 --- a/zrml/combinatorial-tokens/fuzz/split_position.rs +++ b/zrml/combinatorial-tokens/fuzz/split_position.rs @@ -3,7 +3,6 @@ mod common; use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; -use frame_system::pallet_prelude::{BlockNumberFor, OriginFor}; use libfuzzer_sys::fuzz_target; use orml_traits::currency::MultiCurrency; use zeitgeist_primitives::{ diff --git a/zrml/combinatorial-tokens/src/lib.rs b/zrml/combinatorial-tokens/src/lib.rs index cb0fa7424..9a146c797 100644 --- a/zrml/combinatorial-tokens/src/lib.rs +++ b/zrml/combinatorial-tokens/src/lib.rs @@ -525,7 +525,7 @@ mod pallet { Ok(Some(weight).into()) } - pub(crate) fn account_id() -> T::AccountId { + pub fn account_id() -> T::AccountId { T::PalletId::get().into_account_truncating() } @@ -623,7 +623,7 @@ mod pallet { Ok(asset) } - pub(crate) fn position_from_parent_collection( + pub fn position_from_parent_collection( parent_collection_id: Option>, market_id: MarketIdOf, index_set: Vec, From e647e741fff3ff4a3c9724fc91332f16b115bb18 Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Fri, 22 Nov 2024 19:16:49 +0100 Subject: [PATCH 06/12] . --- scripts/tests/fuzz.sh | 1 + .../fuzz/merge_position.rs | 26 +++++++++++++++---- .../fuzz/split_position.rs | 20 +++++++++++--- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/scripts/tests/fuzz.sh b/scripts/tests/fuzz.sh index 8e5ec7db6..39c194af1 100755 --- a/scripts/tests/fuzz.sh +++ b/scripts/tests/fuzz.sh @@ -61,3 +61,4 @@ cargo fuzz run --release --fuzz-dir zrml/orderbook/fuzz orderbook_v1_full_workfl cargo fuzz run --release --fuzz-dir zrml/futarchy/fuzz submit_proposal -- -runs=$RUNS cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz split_position -- -runs=$RUNS +cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz merge_position -- -runs=$RUNS diff --git a/zrml/combinatorial-tokens/fuzz/merge_position.rs b/zrml/combinatorial-tokens/fuzz/merge_position.rs index f266d2e5f..9b6811c78 100644 --- a/zrml/combinatorial-tokens/fuzz/merge_position.rs +++ b/zrml/combinatorial-tokens/fuzz/merge_position.rs @@ -32,10 +32,16 @@ impl<'a> Arbitrary<'a> for MergePositionFuzzParams { let account_id = u128::arbitrary(u)?; let parent_collection_id = Arbitrary::arbitrary(u)?; let market_id = 0u8.into(); - let partition = Arbitrary::arbitrary(u)?; let amount = Arbitrary::arbitrary(u)?; let force_max_work = Arbitrary::arbitrary(u)?; + // Note: This might result in members of unequal length, but that's OK. + let min_len = 0; + let max_len = 10; + let len = u.int_in_range(0..=max_len)?; + let partition = + (min_len..len).map(|_| Arbitrary::arbitrary(u)).collect::>>()?; + let params = MergePositionFuzzParams { account_id, parent_collection_id, @@ -56,12 +62,21 @@ fuzz_target!(|params: MergePositionFuzzParams| { // We create a market and equip the user with the tokens they require to make the // `merge_position` call meaningful, and deposit collateral in the pallet account. let collateral = Asset::Ztg; - let market = - common::market::(params.market_id, collateral, MarketType::Categorical(7)); + let asset_count = if let Some(member) = params.partition.first() { + member.len().max(2) as u16 + } else { + return; + }; + let market = common::market::( + params.market_id, + collateral, + MarketType::Categorical(asset_count), + ); <::MarketCommons as MarketCommonsPalletApi>::push_market(market) .unwrap(); - let positions = params.partition + let positions = params + .partition .iter() .cloned() .map(|index_set| { @@ -88,7 +103,8 @@ fuzz_target!(|params: MergePositionFuzzParams| { collateral, &CombinatorialTokens::account_id(), params.amount, - ).unwrap(); + ) + .unwrap(); let _ = CombinatorialTokens::merge_position( RuntimeOrigin::signed(params.account_id), diff --git a/zrml/combinatorial-tokens/fuzz/split_position.rs b/zrml/combinatorial-tokens/fuzz/split_position.rs index 145801878..4d7314a75 100644 --- a/zrml/combinatorial-tokens/fuzz/split_position.rs +++ b/zrml/combinatorial-tokens/fuzz/split_position.rs @@ -33,10 +33,16 @@ impl<'a> Arbitrary<'a> for SplitPositionFuzzParams { let account_id = u128::arbitrary(u)?; let parent_collection_id = Arbitrary::arbitrary(u)?; let market_id = 0u8.into(); - let partition = Arbitrary::arbitrary(u)?; let amount = Arbitrary::arbitrary(u)?; let force_max_work = Arbitrary::arbitrary(u)?; + // Note: This might result in members of unequal length, but that's OK. + let min_len = 0; + let max_len = 10; + let len = u.int_in_range(0..=max_len)?; + let partition = + (min_len..len).map(|_| Arbitrary::arbitrary(u)).collect::>>()?; + let params = SplitPositionFuzzParams { account_id, parent_collection_id, @@ -57,8 +63,16 @@ fuzz_target!(|params: SplitPositionFuzzParams| { // We create a market and equip the user with the tokens they require to make the // `split_position` call meaningful. let collateral = Asset::Ztg; - let market = - common::market::(params.market_id, collateral, MarketType::Categorical(7)); + let asset_count = if let Some(member) = params.partition.first() { + member.len().max(2) as u16 + } else { + return; + }; + let market = common::market::( + params.market_id, + collateral, + MarketType::Categorical(asset_count), + ); <::MarketCommons as MarketCommonsPalletApi>::push_market(market) .unwrap(); From cb4718d62b9ef579c25cf9a0c65c7fe9a14ee549 Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Fri, 22 Nov 2024 19:33:27 +0100 Subject: [PATCH 07/12] Add redeem fuzz test --- scripts/tests/fuzz.sh | 1 + .../fuzz/merge_position.rs | 2 +- .../fuzz/redeem_position.rs | 119 ++++++++++++++++++ .../fuzz/split_position.rs | 2 +- .../src/mock/types/mod.rs | 2 +- 5 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 zrml/combinatorial-tokens/fuzz/redeem_position.rs diff --git a/scripts/tests/fuzz.sh b/scripts/tests/fuzz.sh index 39c194af1..c2cb28593 100755 --- a/scripts/tests/fuzz.sh +++ b/scripts/tests/fuzz.sh @@ -62,3 +62,4 @@ cargo fuzz run --release --fuzz-dir zrml/futarchy/fuzz submit_proposal -- -runs= cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz split_position -- -runs=$RUNS cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz merge_position -- -runs=$RUNS +cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz redeem_position -- -runs=$RUNS diff --git a/zrml/combinatorial-tokens/fuzz/merge_position.rs b/zrml/combinatorial-tokens/fuzz/merge_position.rs index 9b6811c78..6a7524d04 100644 --- a/zrml/combinatorial-tokens/fuzz/merge_position.rs +++ b/zrml/combinatorial-tokens/fuzz/merge_position.rs @@ -65,7 +65,7 @@ fuzz_target!(|params: MergePositionFuzzParams| { let asset_count = if let Some(member) = params.partition.first() { member.len().max(2) as u16 } else { - return; + 2u16 // In this case the index set doesn't fit the market. }; let market = common::market::( params.market_id, diff --git a/zrml/combinatorial-tokens/fuzz/redeem_position.rs b/zrml/combinatorial-tokens/fuzz/redeem_position.rs new file mode 100644 index 000000000..bda833955 --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/redeem_position.rs @@ -0,0 +1,119 @@ +#![no_main] + +mod common; + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use libfuzzer_sys::fuzz_target; +use orml_traits::currency::MultiCurrency; +use zeitgeist_primitives::{ + constants::base_multiples::*, + traits::MarketCommonsPalletApi, + types::{Asset, MarketType}, +}; +use zrml_combinatorial_tokens::{ + mock::{ + ext_builder::ExtBuilder, + runtime::{CombinatorialTokens, Runtime, RuntimeOrigin}, + types::MockPayout, + }, + traits::CombinatorialIdManager, + AccountIdOf, BalanceOf, CombinatorialIdOf, Config, MarketIdOf, +}; + +#[derive(Debug)] +struct RedeemPositionFuzzParams { + account_id: AccountIdOf, + parent_collection_id: Option>, + market_id: MarketIdOf, + index_set: Vec, + force_max_work: bool, + payout_vector: Option>>, + amount: BalanceOf, +} + +impl<'a> Arbitrary<'a> for RedeemPositionFuzzParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + let parent_collection_id = Arbitrary::arbitrary(u)?; + let market_id = 0u8.into(); + let amount = Arbitrary::arbitrary(u)?; + let force_max_work = Arbitrary::arbitrary(u)?; + + let min_len = 2; + let max_len = 1000; + let len = u.int_in_range(0..=max_len)?; + let index_set = + (min_len..len).map(|_| bool::arbitrary(u)).collect::>>()?; + + // Clamp every value of the payout vector to [0..1]. That doesn't ensure that the payout + // vector is valid, but it's valid enough to avoid most overflows. + let payout_vector = Some( + (min_len..len) + .map(|_| Ok(u128::arbitrary(u)? % _1)) + .collect::>>()?, + ); + + let params = RedeemPositionFuzzParams { + account_id, + parent_collection_id, + market_id, + index_set, + force_max_work, + payout_vector, + amount, + }; + + Ok(params) + } +} + +fuzz_target!(|params: RedeemPositionFuzzParams| { + let mut ext = ExtBuilder::build(); + + ext.execute_with(|| { + // We create a market and equip the user with the tokens they require to make the + // `redeem_position` call meaningful. We also provide the pallet account with collateral in + // case it's required. + let collateral = Asset::Ztg; + let asset_count = params.index_set.len() as u16; + let market = common::market::( + params.market_id, + collateral, + MarketType::Categorical(asset_count), + ); + <::MarketCommons as MarketCommonsPalletApi>::push_market(market) + .unwrap(); + + let position = if let Some(pci) = params.parent_collection_id { + let position_id = + ::CombinatorialIdManager::get_position_id(collateral, pci); + + Asset::CombinatorialToken(position_id) + } else { + Asset::Ztg + }; + <::MultiCurrency>::deposit(position, ¶ms.account_id, params.amount) + .unwrap(); + + // Is not required if `parent_collection_id.is_some()`, but we're doing it anyways. + <::MultiCurrency>::deposit( + collateral, + &CombinatorialTokens::account_id(), + params.amount * asset_count as u128, + ) + .unwrap(); + + // Mock up the payout vector. + MockPayout::set_return_value(params.payout_vector); + + let _ = CombinatorialTokens::redeem_position( + RuntimeOrigin::signed(params.account_id), + params.parent_collection_id, + params.market_id, + params.index_set, + params.force_max_work, + ); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/combinatorial-tokens/fuzz/split_position.rs b/zrml/combinatorial-tokens/fuzz/split_position.rs index 4d7314a75..02a718866 100644 --- a/zrml/combinatorial-tokens/fuzz/split_position.rs +++ b/zrml/combinatorial-tokens/fuzz/split_position.rs @@ -66,7 +66,7 @@ fuzz_target!(|params: SplitPositionFuzzParams| { let asset_count = if let Some(member) = params.partition.first() { member.len().max(2) as u16 } else { - return; + 2u16 // In this case the index set doesn't fit the market. }; let market = common::market::( params.market_id, diff --git a/zrml/combinatorial-tokens/src/mock/types/mod.rs b/zrml/combinatorial-tokens/src/mock/types/mod.rs index 6f3afbeaf..0eb340eff 100644 --- a/zrml/combinatorial-tokens/src/mock/types/mod.rs +++ b/zrml/combinatorial-tokens/src/mock/types/mod.rs @@ -21,4 +21,4 @@ mod payout; #[cfg(feature = "runtime-benchmarks")] pub(crate) use benchmark_helper::BenchmarkHelper; -pub(crate) use payout::MockPayout; +pub use payout::MockPayout; From 0eb01f7a11b1837d4fe08dcb44b4b75a1cbb5df6 Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Sun, 24 Nov 2024 22:21:38 +0100 Subject: [PATCH 08/12] Add combinatorial pool fuzz tests --- Cargo.lock | 15 +++ Cargo.toml | 1 + zrml/neo-swaps/fuzz/Cargo.toml | 27 ++++ zrml/neo-swaps/fuzz/common.rs | 35 +++++ .../fuzz/deploy_combinatorial_pool.rs | 121 ++++++++++++++++++ zrml/neo-swaps/src/lib.rs | 28 ++-- 6 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 zrml/neo-swaps/fuzz/Cargo.toml create mode 100644 zrml/neo-swaps/fuzz/common.rs create mode 100644 zrml/neo-swaps/fuzz/deploy_combinatorial_pool.rs diff --git a/Cargo.lock b/Cargo.lock index 93a36046e..d887033e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15414,6 +15414,21 @@ dependencies = [ "zrml-prediction-markets-runtime-api", ] +[[package]] +name = "zrml-neo-swaps-fuzz" +version = "0.5.5" +dependencies = [ + "arbitrary", + "frame-support", + "frame-system", + "libfuzzer-sys", + "orml-traits", + "rand 0.8.5", + "sp-runtime", + "zeitgeist-primitives", + "zrml-neo-swaps", +] + [[package]] name = "zrml-orderbook" version = "0.5.5" diff --git a/Cargo.toml b/Cargo.toml index e4f13383f..103dcfac1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ members = [ "zrml/global-disputes", "zrml/market-commons", "zrml/neo-swaps", + "zrml/neo-swaps/fuzz", "zrml/orderbook", "zrml/orderbook/fuzz", "zrml/parimutuel", diff --git a/zrml/neo-swaps/fuzz/Cargo.toml b/zrml/neo-swaps/fuzz/Cargo.toml new file mode 100644 index 000000000..b7f5f9fa9 --- /dev/null +++ b/zrml/neo-swaps/fuzz/Cargo.toml @@ -0,0 +1,27 @@ +[[bin]] +doc = false +name = "deploy_combinatorial_pool" +path = "deploy_combinatorial_pool.rs" +test = false + +[dependencies] +arbitrary = { workspace = true, features = ["derive"] } +frame-support = { workspace = true, features = ["default"] } +frame-system = { workspace = true } +libfuzzer-sys = { workspace = true } +orml-traits = { workspace = true, features = ["default"] } +rand = { workspace = true, features = ["default"] } +sp-runtime = { workspace = true, features = ["default"] } +zeitgeist-primitives = { workspace = true, features = ["default", "mock"] } +zrml-neo-swaps = { workspace = true, features = ["default", "mock"] } + +[package] +authors = ["Forecasting Technologies Ltd"] +edition.workspace = true +name = "zrml-neo-swaps-fuzz" +publish = false +version = "0.5.5" + +[package.metadata] +cargo-fuzz = true + diff --git a/zrml/neo-swaps/fuzz/common.rs b/zrml/neo-swaps/fuzz/common.rs new file mode 100644 index 000000000..583dc430c --- /dev/null +++ b/zrml/neo-swaps/fuzz/common.rs @@ -0,0 +1,35 @@ +use zeitgeist_primitives::{ + traits::MarketOf, + types::{Market, MarketCreation, MarketPeriod, MarketStatus, MarketType, ScoringRule}, +}; +use zrml_neo_swaps::{AssetOf, Config, MarketIdOf}; + +pub(crate) fn market( + market_id: MarketIdOf, + base_asset: AssetOf, + market_type: MarketType, +) -> MarketOf<::MarketCommons> +where + T: Config, + ::AccountId: Default, +{ + Market { + market_id, + base_asset, + creator: Default::default(), + creation: MarketCreation::Permissionless, + creator_fee: Default::default(), + oracle: Default::default(), + metadata: Default::default(), + market_type, + period: MarketPeriod::Block(0u8.into()..10u8.into()), + deadlines: Default::default(), + scoring_rule: ScoringRule::AmmCdaHybrid, + status: MarketStatus::Active, + report: None, + resolved_outcome: None, + dispute_mechanism: None, + bonds: Default::default(), + early_close: None, + } +} diff --git a/zrml/neo-swaps/fuzz/deploy_combinatorial_pool.rs b/zrml/neo-swaps/fuzz/deploy_combinatorial_pool.rs new file mode 100644 index 000000000..50b78f7c3 --- /dev/null +++ b/zrml/neo-swaps/fuzz/deploy_combinatorial_pool.rs @@ -0,0 +1,121 @@ +#![no_main] + +mod common; + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use libfuzzer_sys::fuzz_target; +use orml_traits::currency::MultiCurrency; +use zeitgeist_primitives::{ + constants::base_multiples::*, + traits::MarketCommonsPalletApi, + types::{Asset, MarketType}, +}; +use zrml_neo_swaps::{ + mock::{ExtBuilder, NeoSwaps, Runtime, RuntimeOrigin}, + AccountIdOf, BalanceOf, Config, MarketIdOf, COMBO_MAX_SPOT_PRICE, COMBO_MIN_SPOT_PRICE, + MIN_SWAP_FEE, +}; + +#[derive(Debug)] +struct DeployCombinatorialPoolFuzzParams { + account_id: AccountIdOf, + asset_count: u16, + market_ids: Vec>, + category_counts: Vec, + amount: BalanceOf, + spot_prices: Vec>, + swap_fee: BalanceOf, + force_max_work: bool, +} + +impl<'a> Arbitrary<'a> for DeployCombinatorialPoolFuzzParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + + let min_market_ids = 1; + let max_market_ids = 16; + let market_ids_len: usize = u.int_in_range(min_market_ids..=max_market_ids)?; + let market_ids = + (0..market_ids_len).map(|x| (x as u32).into()).collect::>>(); + + let min_category_count = 2; + let max_category_count = 16; + let mut category_counts = vec![]; + for _ in market_ids.iter() { + let category_count = u.int_in_range(min_category_count..=max_category_count)? as u16; + category_counts.push(category_count); + } + + let amount = Arbitrary::arbitrary(u)?; + + let asset_count: u16 = category_counts.iter().product(); + let asset_count_usize = asset_count as usize; + + // Create arbitrary spot price vector by creating a vector of `MinSpotPrice` and then adding + // value to them in increments until a total spot price of one is reached. It's possible + // that this results in invalid spot prices, for example if `total_assets` is too large. + let mut spot_prices = vec![COMBO_MIN_SPOT_PRICE; asset_count_usize]; + let increment = COMBO_MIN_SPOT_PRICE; + while spot_prices.iter().sum::() < _1 { + let index = u.int_in_range(0..=asset_count_usize - 1)?; + if spot_prices[index] < COMBO_MAX_SPOT_PRICE { + spot_prices[index] += increment; + } + } + + let swap_fee = u.int_in_range(MIN_SWAP_FEE..=::MaxSwapFee::get())?; + + let force_max_work = Arbitrary::arbitrary(u)?; + + let params = DeployCombinatorialPoolFuzzParams { + account_id, + asset_count: asset_count as u16, + market_ids, + category_counts, + amount, + spot_prices, + swap_fee, + force_max_work, + }; + + Ok(params) + } +} + +fuzz_target!(|params: DeployCombinatorialPoolFuzzParams| { + let mut ext = ExtBuilder::default().build(); + + ext.execute_with(|| { + // We create the required markets and deposit enough funds for the user. + let collateral = Asset::Ztg; + for (&market_id, &category_count) in + params.market_ids.iter().zip(params.category_counts.iter()) + { + let market = common::market::( + market_id, + collateral, + MarketType::Categorical(category_count), + ); + <::MarketCommons as MarketCommonsPalletApi>::push_market(market) + .unwrap(); + } + <::MultiCurrency>::deposit( + collateral, + ¶ms.account_id, + params.amount, + ) + .unwrap(); + + let _ = NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(params.account_id), + params.asset_count, + params.market_ids, + params.amount, + params.spot_prices, + params.swap_fee, + params.force_max_work, + ); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/neo-swaps/src/lib.rs b/zrml/neo-swaps/src/lib.rs index 2fc47f05a..512718e6a 100644 --- a/zrml/neo-swaps/src/lib.rs +++ b/zrml/neo-swaps/src/lib.rs @@ -29,7 +29,7 @@ mod liquidity_tree; mod macros; mod math; pub mod migration; -mod mock; +pub mod mock; mod pool_storage; mod tests; pub mod traits; @@ -99,31 +99,31 @@ mod pallet { pub(crate) const EXIT_FEE: u128 = CENT / 10; /// The minimum allowed swap fee. Hardcoded to avoid misconfigurations which may lead to /// exploits. - pub(crate) const MIN_SWAP_FEE: u128 = BASE / 1_000; // 0.1%. + pub const MIN_SWAP_FEE: u128 = BASE / 1_000; // 0.1%. /// The maximum allowed spot price when creating a pool. - pub(crate) const MAX_SPOT_PRICE: u128 = BASE - CENT / 2; + pub const MAX_SPOT_PRICE: u128 = BASE - CENT / 2; /// The minimum allowed spot price when creating a pool. - pub(crate) const MIN_SPOT_PRICE: u128 = CENT / 2; + pub const MIN_SPOT_PRICE: u128 = CENT / 2; /// The maximum value the spot price is allowed to take in a combinatorial market. - pub(crate) const COMBO_MAX_SPOT_PRICE: u128 = BASE - CENT / 10; + pub const COMBO_MAX_SPOT_PRICE: u128 = BASE - CENT / 10; /// The minimum value the spot price is allowed to take in a combinatorial market. - pub(crate) const COMBO_MIN_SPOT_PRICE: u128 = CENT / 10; + pub const COMBO_MIN_SPOT_PRICE: u128 = CENT / 10; /// The minimum vallowed value of a pool's liquidity parameter. pub(crate) const MIN_LIQUIDITY: u128 = BASE; /// The minimum percentage each new LP position must increase the liquidity by, represented as /// fractional (0.0139098411 represents 1.39098411%). pub(crate) const MIN_RELATIVE_LP_POSITION_VALUE: u128 = 139098411; // 1.39098411% - pub(crate) type AccountIdOf = ::AccountId; - pub(crate) type AssetOf = Asset>; - pub(crate) type BalanceOf = + pub type AccountIdOf = ::AccountId; + pub type AssetOf = Asset>; + pub type BalanceOf = <::MultiCurrency as MultiCurrency>>::Balance; - pub(crate) type AssetIndexType = u16; - pub(crate) type MarketIdOf = + pub type AssetIndexType = u16; + pub type MarketIdOf = <::MarketCommons as MarketCommonsPalletApi>::MarketId; - pub(crate) type LiquidityTreeOf = LiquidityTree::MaxLiquidityTreeDepth>; - pub(crate) type PoolOf = Pool, MaxAssets>; - pub(crate) type AmmTradeOf = AmmTrade>; + pub type LiquidityTreeOf = LiquidityTree::MaxLiquidityTreeDepth>; + pub type PoolOf = Pool, MaxAssets>; + pub type AmmTradeOf = AmmTrade>; #[pallet::config] pub trait Config: frame_system::Config { From d8892953c6416a779734e29ee9f6c2e64598ce59 Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Sun, 24 Nov 2024 22:26:11 +0100 Subject: [PATCH 09/12] Add neo-swaps fuzz to script --- scripts/tests/fuzz.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/tests/fuzz.sh b/scripts/tests/fuzz.sh index c2cb28593..d458d3dd5 100755 --- a/scripts/tests/fuzz.sh +++ b/scripts/tests/fuzz.sh @@ -63,3 +63,5 @@ cargo fuzz run --release --fuzz-dir zrml/futarchy/fuzz submit_proposal -- -runs= cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz split_position -- -runs=$RUNS cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz merge_position -- -runs=$RUNS cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz redeem_position -- -runs=$RUNS + +cargo fuzz run --release --fuzz-dir zrml/neo-swaps/fuzz deploy_combinatorial_pool -- -runs=$RUNS From 2aa19729a2f48121f9a3f4f789ed4ec4e3cccf36 Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Mon, 25 Nov 2024 18:57:00 +0100 Subject: [PATCH 10/12] combo buy --- zrml/neo-swaps/fuzz/Cargo.toml | 6 ++ zrml/neo-swaps/fuzz/combo_buy.rs | 150 +++++++++++++++++++++++++++++++ zrml/neo-swaps/src/lib.rs | 10 ++- 3 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 zrml/neo-swaps/fuzz/combo_buy.rs diff --git a/zrml/neo-swaps/fuzz/Cargo.toml b/zrml/neo-swaps/fuzz/Cargo.toml index b7f5f9fa9..a5929ac9d 100644 --- a/zrml/neo-swaps/fuzz/Cargo.toml +++ b/zrml/neo-swaps/fuzz/Cargo.toml @@ -4,6 +4,12 @@ name = "deploy_combinatorial_pool" path = "deploy_combinatorial_pool.rs" test = false +[[bin]] +doc = false +name = "combo_buy" +path = "combo_buy.rs" +test = false + [dependencies] arbitrary = { workspace = true, features = ["derive"] } frame-support = { workspace = true, features = ["default"] } diff --git a/zrml/neo-swaps/fuzz/combo_buy.rs b/zrml/neo-swaps/fuzz/combo_buy.rs new file mode 100644 index 000000000..37c15317c --- /dev/null +++ b/zrml/neo-swaps/fuzz/combo_buy.rs @@ -0,0 +1,150 @@ +#![no_main] + +mod common; + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use libfuzzer_sys::fuzz_target; +use orml_traits::currency::MultiCurrency; +use rand::seq::SliceRandom; +use zeitgeist_primitives::{ + constants::base_multiples::*, + traits::MarketCommonsPalletApi, + types::{Asset, MarketType}, +}; +use zrml_neo_swaps::{ + mock::{ExtBuilder, NeoSwaps, Runtime, RuntimeOrigin}, + AccountIdOf, BalanceOf, Config, MarketIdOf, COMBO_MAX_SPOT_PRICE, COMBO_MIN_SPOT_PRICE, + MIN_SWAP_FEE, +}; + +#[derive(Debug)] +struct ComboBuyFuzzParams { + account_id: AccountIdOf, + pool_id: ::PoolId, + market_ids: Vec>, + spot_prices: Vec>, + swap_fee: BalanceOf, + category_counts: Vec, + asset_count: u16, + buy: Vec, + sell: Vec, + amount_in: BalanceOf, + min_amount_out: BalanceOf, +} + +impl<'a> Arbitrary<'a> for ComboBuyFuzzParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + let pool_id = 0; + let market_ids = vec![0, 1, 2]; + + let min_category_count = 2; + let max_category_count = 16; + let mut category_counts = vec![]; + for _ in market_ids.iter() { + // We're just assuming three markets here! + let category_count = u.int_in_range(min_category_count..=max_category_count)? as u16; + category_counts.push(category_count); + } + + let asset_count = category_counts.iter().product(); + let asset_count_usize = asset_count as usize; + + // Create arbitrary spot price vector by creating a vector of `MinSpotPrice` and then adding + // value to them in increments until a total spot price of one is reached. It's possible + // that this results in invalid spot prices, for example if `total_assets` is too large. + let mut spot_prices = vec![COMBO_MIN_SPOT_PRICE; asset_count_usize]; + let increment = COMBO_MIN_SPOT_PRICE; + while spot_prices.iter().sum::() < _1 { + let index = u.int_in_range(0..=asset_count_usize - 1)?; + if spot_prices[index] < COMBO_MAX_SPOT_PRICE { + spot_prices[index] += increment; + } + } + + let swap_fee = u.int_in_range(MIN_SWAP_FEE..=::MaxSwapFee::get())?; + + // Shuffle 0..asset_count_usize and then obtain `buy` and `sell` from the result. + let mut indices: Vec = (0..asset_count_usize).collect(); + for i in (1..indices.len()).rev() { + let j = u.int_in_range(0..=i)?; + indices.swap(i, j); + } + let buy_len = u.int_in_range(1..=asset_count_usize - 1)?; + let sell_len = asset_count_usize - buy_len; + let buy = indices[0..buy_len].to_vec(); + let sell = indices[buy_len..asset_count_usize].to_vec(); + + let amount_in = Arbitrary::arbitrary(u)?; + let min_amount_out = Arbitrary::arbitrary(u)?; + + let params = ComboBuyFuzzParams { + account_id, + pool_id, + market_ids, + spot_prices, + swap_fee, + category_counts, + asset_count, + buy, + sell, + amount_in, + min_amount_out, + }; + + Ok(params) + } +} + +fuzz_target!(|params: ComboBuyFuzzParams| { + let mut ext = ExtBuilder::default().build(); + + ext.execute_with(|| { + // We create the required markets and deposit enough funds for the user. + let collateral = Asset::Ztg; + for (market_id, &category_count) in params.category_counts.iter().enumerate() { + let market = common::market::( + market_id as u128, + collateral, + MarketType::Categorical(category_count), + ); + <::MarketCommons as MarketCommonsPalletApi>::push_market(market) + .unwrap(); + } + <::MultiCurrency>::deposit( + collateral, + ¶ms.account_id, + 100 * params.amount_in, + ) + .unwrap(); + + // Create a pool to trade on. + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(params.account_id), + params.asset_count, + params.market_ids, + 10 * params.amount_in, + params.spot_prices, + params.swap_fee, + false, + ) + .unwrap(); + + // Convert indices to assets. + let assets = NeoSwaps::assets(params.pool_id).unwrap(); + let buy = params.buy.into_iter().map(|i| assets[i]).collect(); + let sell = params.sell.into_iter().map(|i| assets[i]).collect(); + + let _ = NeoSwaps::combo_buy( + RuntimeOrigin::signed(params.account_id), + params.pool_id, + params.asset_count, + buy, + sell, + params.amount_in, + params.min_amount_out, + ); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/neo-swaps/src/lib.rs b/zrml/neo-swaps/src/lib.rs index 512718e6a..07fbce302 100644 --- a/zrml/neo-swaps/src/lib.rs +++ b/zrml/neo-swaps/src/lib.rs @@ -119,8 +119,7 @@ mod pallet { pub type BalanceOf = <::MultiCurrency as MultiCurrency>>::Balance; pub type AssetIndexType = u16; - pub type MarketIdOf = - <::MarketCommons as MarketCommonsPalletApi>::MarketId; + pub type MarketIdOf = <::MarketCommons as MarketCommonsPalletApi>::MarketId; pub type LiquidityTreeOf = LiquidityTree::MaxLiquidityTreeDepth>; pub type PoolOf = Pool, MaxAssets>; pub type AmmTradeOf = AmmTrade>; @@ -1528,6 +1527,13 @@ mod pallet { T::PalletId::get().into_sub_account_truncating((*pool_id).saturated_into::()) } + /// Returns the assets contained in the pool given by `pool_id`. + pub fn assets(pool_id: T::PoolId) -> Result>, DispatchError> { + let pool = ::get(pool_id)?; + + Ok(pool.assets.into_inner()) + } + /// Distribute swap fees and external fees and returns the remaining amount. /// /// # Arguments From 7b01564c9863f92b0c24488d5d87756167760607 Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Mon, 25 Nov 2024 19:32:32 +0100 Subject: [PATCH 11/12] Minor fixes, add to script --- scripts/tests/fuzz.sh | 1 + zrml/neo-swaps/fuzz/combo_buy.rs | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/tests/fuzz.sh b/scripts/tests/fuzz.sh index d458d3dd5..34d6e5a8c 100755 --- a/scripts/tests/fuzz.sh +++ b/scripts/tests/fuzz.sh @@ -65,3 +65,4 @@ cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz merge_positio cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz redeem_position -- -runs=$RUNS cargo fuzz run --release --fuzz-dir zrml/neo-swaps/fuzz deploy_combinatorial_pool -- -runs=$RUNS +cargo fuzz run --release --fuzz-dir zrml/neo-swaps/fuzz combo_buy -- -runs=$RUNS diff --git a/zrml/neo-swaps/fuzz/combo_buy.rs b/zrml/neo-swaps/fuzz/combo_buy.rs index 37c15317c..51016eb00 100644 --- a/zrml/neo-swaps/fuzz/combo_buy.rs +++ b/zrml/neo-swaps/fuzz/combo_buy.rs @@ -13,7 +13,7 @@ use zeitgeist_primitives::{ }; use zrml_neo_swaps::{ mock::{ExtBuilder, NeoSwaps, Runtime, RuntimeOrigin}, - AccountIdOf, BalanceOf, Config, MarketIdOf, COMBO_MAX_SPOT_PRICE, COMBO_MIN_SPOT_PRICE, + AccountIdOf, BalanceOf, Config, MarketIdOf, MAX_SPOT_PRICE, MIN_SPOT_PRICE, MIN_SWAP_FEE, }; @@ -53,11 +53,11 @@ impl<'a> Arbitrary<'a> for ComboBuyFuzzParams { // Create arbitrary spot price vector by creating a vector of `MinSpotPrice` and then adding // value to them in increments until a total spot price of one is reached. It's possible // that this results in invalid spot prices, for example if `total_assets` is too large. - let mut spot_prices = vec![COMBO_MIN_SPOT_PRICE; asset_count_usize]; - let increment = COMBO_MIN_SPOT_PRICE; + let mut spot_prices = vec![MIN_SPOT_PRICE; asset_count_usize]; + let increment = MIN_SPOT_PRICE; while spot_prices.iter().sum::() < _1 { let index = u.int_in_range(0..=asset_count_usize - 1)?; - if spot_prices[index] < COMBO_MAX_SPOT_PRICE { + if spot_prices[index] < MAX_SPOT_PRICE { spot_prices[index] += increment; } } @@ -71,11 +71,10 @@ impl<'a> Arbitrary<'a> for ComboBuyFuzzParams { indices.swap(i, j); } let buy_len = u.int_in_range(1..=asset_count_usize - 1)?; - let sell_len = asset_count_usize - buy_len; let buy = indices[0..buy_len].to_vec(); let sell = indices[buy_len..asset_count_usize].to_vec(); - let amount_in = Arbitrary::arbitrary(u)?; + let amount_in = u.int_in_range(_1..=_100)?; let min_amount_out = Arbitrary::arbitrary(u)?; let params = ComboBuyFuzzParams { From 5a74401fa9d0b38233964c41da50ffea0074fd56 Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Mon, 25 Nov 2024 19:50:58 +0100 Subject: [PATCH 12/12] Add combo sell --- scripts/tests/fuzz.sh | 1 + zrml/neo-swaps/fuzz/Cargo.toml | 6 ++ zrml/neo-swaps/fuzz/combo_buy.rs | 48 ++++++++-- zrml/neo-swaps/fuzz/combo_sell.rs | 149 ++++++++++++++++++++++++++++++ 4 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 zrml/neo-swaps/fuzz/combo_sell.rs diff --git a/scripts/tests/fuzz.sh b/scripts/tests/fuzz.sh index 34d6e5a8c..c70909ac1 100755 --- a/scripts/tests/fuzz.sh +++ b/scripts/tests/fuzz.sh @@ -66,3 +66,4 @@ cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz redeem_positi cargo fuzz run --release --fuzz-dir zrml/neo-swaps/fuzz deploy_combinatorial_pool -- -runs=$RUNS cargo fuzz run --release --fuzz-dir zrml/neo-swaps/fuzz combo_buy -- -runs=$RUNS +cargo fuzz run --release --fuzz-dir zrml/neo-swaps/fuzz combo_sell -- -runs=$RUNS diff --git a/zrml/neo-swaps/fuzz/Cargo.toml b/zrml/neo-swaps/fuzz/Cargo.toml index a5929ac9d..7b5c6dfd0 100644 --- a/zrml/neo-swaps/fuzz/Cargo.toml +++ b/zrml/neo-swaps/fuzz/Cargo.toml @@ -10,6 +10,12 @@ name = "combo_buy" path = "combo_buy.rs" test = false +[[bin]] +doc = false +name = "combo_sell" +path = "combo_sell.rs" +test = false + [dependencies] arbitrary = { workspace = true, features = ["derive"] } frame-support = { workspace = true, features = ["default"] } diff --git a/zrml/neo-swaps/fuzz/combo_buy.rs b/zrml/neo-swaps/fuzz/combo_buy.rs index 51016eb00..a220f4346 100644 --- a/zrml/neo-swaps/fuzz/combo_buy.rs +++ b/zrml/neo-swaps/fuzz/combo_buy.rs @@ -16,6 +16,8 @@ use zrml_neo_swaps::{ AccountIdOf, BalanceOf, Config, MarketIdOf, MAX_SPOT_PRICE, MIN_SPOT_PRICE, MIN_SWAP_FEE, }; +use sp_runtime::traits::Zero; + #[derive(Debug)] struct ComboBuyFuzzParams { @@ -27,8 +29,10 @@ struct ComboBuyFuzzParams { category_counts: Vec, asset_count: u16, buy: Vec, + keep: Vec, sell: Vec, - amount_in: BalanceOf, + amount_buy: BalanceOf, + amount_keep: BalanceOf, min_amount_out: BalanceOf, } @@ -70,11 +74,21 @@ impl<'a> Arbitrary<'a> for ComboBuyFuzzParams { let j = u.int_in_range(0..=i)?; indices.swap(i, j); } + + // This isn't perfectly random, but biased towards producing larger `buy` sets. let buy_len = u.int_in_range(1..=asset_count_usize - 1)?; + let keep_len = u.int_in_range(0..=asset_count_usize - 1 - buy_len)?; let buy = indices[0..buy_len].to_vec(); - let sell = indices[buy_len..asset_count_usize].to_vec(); + let keep = indices[buy_len..buy_len + keep_len].to_vec(); + let sell = indices[buy_len + keep_len..asset_count_usize].to_vec(); + + let amount_buy = u.int_in_range(_1..=_100)?; + let amount_keep = if keep.is_empty() { + Zero::zero() + } else { + u.int_in_range(_1..=amount_buy)? + }; - let amount_in = u.int_in_range(_1..=_100)?; let min_amount_out = Arbitrary::arbitrary(u)?; let params = ComboBuyFuzzParams { @@ -86,8 +100,10 @@ impl<'a> Arbitrary<'a> for ComboBuyFuzzParams { category_counts, asset_count, buy, + keep, sell, - amount_in, + amount_buy, + amount_keep, min_amount_out, }; @@ -99,7 +115,7 @@ fuzz_target!(|params: ComboBuyFuzzParams| { let mut ext = ExtBuilder::default().build(); ext.execute_with(|| { - // We create the required markets and deposit enough funds for the user. + // We create the required markets and deposit collateral in the user's account. let collateral = Asset::Ztg; for (market_id, &category_count) in params.category_counts.iter().enumerate() { let market = common::market::( @@ -113,7 +129,7 @@ fuzz_target!(|params: ComboBuyFuzzParams| { <::MultiCurrency>::deposit( collateral, ¶ms.account_id, - 100 * params.amount_in, + 100 * params.amount_buy, ) .unwrap(); @@ -122,25 +138,37 @@ fuzz_target!(|params: ComboBuyFuzzParams| { RuntimeOrigin::signed(params.account_id), params.asset_count, params.market_ids, - 10 * params.amount_in, + 10 * params.amount_buy, params.spot_prices, params.swap_fee, false, ) .unwrap(); - // Convert indices to assets. + // Convert indices to assets an deposit funds for the user. let assets = NeoSwaps::assets(params.pool_id).unwrap(); + for &asset in assets.iter() { + <::MultiCurrency>::deposit( + asset, + ¶ms.account_id, + params.amount_buy, + ) + .unwrap(); + } + let buy = params.buy.into_iter().map(|i| assets[i]).collect(); + let keep = params.keep.into_iter().map(|i| assets[i]).collect(); let sell = params.sell.into_iter().map(|i| assets[i]).collect(); - let _ = NeoSwaps::combo_buy( + let _ = NeoSwaps::combo_sell( RuntimeOrigin::signed(params.account_id), params.pool_id, params.asset_count, buy, + keep, sell, - params.amount_in, + params.amount_buy, + params.amount_keep, params.min_amount_out, ); }); diff --git a/zrml/neo-swaps/fuzz/combo_sell.rs b/zrml/neo-swaps/fuzz/combo_sell.rs new file mode 100644 index 000000000..51016eb00 --- /dev/null +++ b/zrml/neo-swaps/fuzz/combo_sell.rs @@ -0,0 +1,149 @@ +#![no_main] + +mod common; + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use libfuzzer_sys::fuzz_target; +use orml_traits::currency::MultiCurrency; +use rand::seq::SliceRandom; +use zeitgeist_primitives::{ + constants::base_multiples::*, + traits::MarketCommonsPalletApi, + types::{Asset, MarketType}, +}; +use zrml_neo_swaps::{ + mock::{ExtBuilder, NeoSwaps, Runtime, RuntimeOrigin}, + AccountIdOf, BalanceOf, Config, MarketIdOf, MAX_SPOT_PRICE, MIN_SPOT_PRICE, + MIN_SWAP_FEE, +}; + +#[derive(Debug)] +struct ComboBuyFuzzParams { + account_id: AccountIdOf, + pool_id: ::PoolId, + market_ids: Vec>, + spot_prices: Vec>, + swap_fee: BalanceOf, + category_counts: Vec, + asset_count: u16, + buy: Vec, + sell: Vec, + amount_in: BalanceOf, + min_amount_out: BalanceOf, +} + +impl<'a> Arbitrary<'a> for ComboBuyFuzzParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + let pool_id = 0; + let market_ids = vec![0, 1, 2]; + + let min_category_count = 2; + let max_category_count = 16; + let mut category_counts = vec![]; + for _ in market_ids.iter() { + // We're just assuming three markets here! + let category_count = u.int_in_range(min_category_count..=max_category_count)? as u16; + category_counts.push(category_count); + } + + let asset_count = category_counts.iter().product(); + let asset_count_usize = asset_count as usize; + + // Create arbitrary spot price vector by creating a vector of `MinSpotPrice` and then adding + // value to them in increments until a total spot price of one is reached. It's possible + // that this results in invalid spot prices, for example if `total_assets` is too large. + let mut spot_prices = vec![MIN_SPOT_PRICE; asset_count_usize]; + let increment = MIN_SPOT_PRICE; + while spot_prices.iter().sum::() < _1 { + let index = u.int_in_range(0..=asset_count_usize - 1)?; + if spot_prices[index] < MAX_SPOT_PRICE { + spot_prices[index] += increment; + } + } + + let swap_fee = u.int_in_range(MIN_SWAP_FEE..=::MaxSwapFee::get())?; + + // Shuffle 0..asset_count_usize and then obtain `buy` and `sell` from the result. + let mut indices: Vec = (0..asset_count_usize).collect(); + for i in (1..indices.len()).rev() { + let j = u.int_in_range(0..=i)?; + indices.swap(i, j); + } + let buy_len = u.int_in_range(1..=asset_count_usize - 1)?; + let buy = indices[0..buy_len].to_vec(); + let sell = indices[buy_len..asset_count_usize].to_vec(); + + let amount_in = u.int_in_range(_1..=_100)?; + let min_amount_out = Arbitrary::arbitrary(u)?; + + let params = ComboBuyFuzzParams { + account_id, + pool_id, + market_ids, + spot_prices, + swap_fee, + category_counts, + asset_count, + buy, + sell, + amount_in, + min_amount_out, + }; + + Ok(params) + } +} + +fuzz_target!(|params: ComboBuyFuzzParams| { + let mut ext = ExtBuilder::default().build(); + + ext.execute_with(|| { + // We create the required markets and deposit enough funds for the user. + let collateral = Asset::Ztg; + for (market_id, &category_count) in params.category_counts.iter().enumerate() { + let market = common::market::( + market_id as u128, + collateral, + MarketType::Categorical(category_count), + ); + <::MarketCommons as MarketCommonsPalletApi>::push_market(market) + .unwrap(); + } + <::MultiCurrency>::deposit( + collateral, + ¶ms.account_id, + 100 * params.amount_in, + ) + .unwrap(); + + // Create a pool to trade on. + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(params.account_id), + params.asset_count, + params.market_ids, + 10 * params.amount_in, + params.spot_prices, + params.swap_fee, + false, + ) + .unwrap(); + + // Convert indices to assets. + let assets = NeoSwaps::assets(params.pool_id).unwrap(); + let buy = params.buy.into_iter().map(|i| assets[i]).collect(); + let sell = params.sell.into_iter().map(|i| assets[i]).collect(); + + let _ = NeoSwaps::combo_buy( + RuntimeOrigin::signed(params.account_id), + params.pool_id, + params.asset_count, + buy, + sell, + params.amount_in, + params.min_amount_out, + ); + }); + + let _ = ext.commit_all(); +});