diff --git a/Cargo.lock b/Cargo.lock index 45d4359cc..d887033e4 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" @@ -15246,6 +15261,7 @@ dependencies = [ name = "zrml-futarchy" version = "0.5.5" dependencies = [ + "arbitrary", "env_logger 0.10.2", "frame-benchmarking", "frame-support", @@ -15253,6 +15269,7 @@ dependencies = [ "pallet-balances", "parity-scale-codec", "scale-info", + "sp-core", "sp-io", "sp-runtime", "test-case", @@ -15260,6 +15277,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" @@ -15382,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 eac6a6327..103dcfac1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,12 +39,15 @@ members = [ "runtime/zeitgeist", "zrml/authorized", "zrml/combinatorial-tokens", + "zrml/combinatorial-tokens/fuzz", "zrml/court", "zrml/futarchy", + "zrml/futarchy/fuzz", "zrml/hybrid-router", "zrml/global-disputes", "zrml/market-commons", "zrml/neo-swaps", + "zrml/neo-swaps/fuzz", "zrml/orderbook", "zrml/orderbook/fuzz", "zrml/parimutuel", 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/scripts/tests/fuzz.sh b/scripts/tests/fuzz.sh index beff95f26..c70909ac1 100755 --- a/scripts/tests/fuzz.sh +++ b/scripts/tests/fuzz.sh @@ -57,3 +57,13 @@ 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 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 +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 +cargo fuzz run --release --fuzz-dir zrml/neo-swaps/fuzz combo_sell -- -runs=$RUNS diff --git a/zrml/combinatorial-tokens/fuzz/Cargo.toml b/zrml/combinatorial-tokens/fuzz/Cargo.toml new file mode 100644 index 000000000..32d76bf87 --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/Cargo.toml @@ -0,0 +1,38 @@ +[[bin]] +doc = false +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"] } +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..b4a0e297d --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/common.rs @@ -0,0 +1,35 @@ +use zeitgeist_primitives::{ + traits::MarketOf, + types::{Market, MarketCreation, MarketPeriod, MarketStatus, MarketType, ScoringRule}, +}; +use zrml_combinatorial_tokens::{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/combinatorial-tokens/fuzz/merge_position.rs b/zrml/combinatorial-tokens/fuzz/merge_position.rs new file mode 100644 index 000000000..6a7524d04 --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/merge_position.rs @@ -0,0 +1,120 @@ +#![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 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, + 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 asset_count = if let Some(member) = params.partition.first() { + member.len().max(2) as u16 + } else { + 2u16 // In this case the index set doesn't fit the market. + }; + let market = common::market::( + params.market_id, + collateral, + MarketType::Categorical(asset_count), + ); + <::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/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 new file mode 100644 index 000000000..02a718866 --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/split_position.rs @@ -0,0 +1,101 @@ +#![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}, + }, + 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 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, + 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 asset_count = if let Some(member) = params.partition.first() { + member.len().max(2) as u16 + } else { + 2u16 // In this case the index set doesn't fit the market. + }; + 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(); + + 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..9a146c797 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); @@ -526,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() } @@ -624,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, 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; 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; 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 }) + } +} diff --git a/zrml/neo-swaps/fuzz/Cargo.toml b/zrml/neo-swaps/fuzz/Cargo.toml new file mode 100644 index 000000000..7b5c6dfd0 --- /dev/null +++ b/zrml/neo-swaps/fuzz/Cargo.toml @@ -0,0 +1,39 @@ +[[bin]] +doc = false +name = "deploy_combinatorial_pool" +path = "deploy_combinatorial_pool.rs" +test = false + +[[bin]] +doc = false +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"] } +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/combo_buy.rs b/zrml/neo-swaps/fuzz/combo_buy.rs new file mode 100644 index 000000000..a220f4346 --- /dev/null +++ b/zrml/neo-swaps/fuzz/combo_buy.rs @@ -0,0 +1,177 @@ +#![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, +}; +use sp_runtime::traits::Zero; + + +#[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, + keep: Vec, + sell: Vec, + amount_buy: BalanceOf, + amount_keep: 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); + } + + // 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 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 min_amount_out = Arbitrary::arbitrary(u)?; + + let params = ComboBuyFuzzParams { + account_id, + pool_id, + market_ids, + spot_prices, + swap_fee, + category_counts, + asset_count, + buy, + keep, + sell, + amount_buy, + amount_keep, + 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 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::( + 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_buy, + ) + .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_buy, + params.spot_prices, + params.swap_fee, + false, + ) + .unwrap(); + + // 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_sell( + RuntimeOrigin::signed(params.account_id), + params.pool_id, + params.asset_count, + buy, + keep, + sell, + params.amount_buy, + params.amount_keep, + params.min_amount_out, + ); + }); + + let _ = ext.commit_all(); +}); 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(); +}); 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..07fbce302 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,30 @@ 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 = - <::MarketCommons as MarketCommonsPalletApi>::MarketId; - pub(crate) type LiquidityTreeOf = LiquidityTree::MaxLiquidityTreeDepth>; - pub(crate) type PoolOf = Pool, MaxAssets>; - pub(crate) type AmmTradeOf = AmmTrade>; + pub type AssetIndexType = u16; + pub type MarketIdOf = <::MarketCommons as MarketCommonsPalletApi>::MarketId; + pub type LiquidityTreeOf = LiquidityTree::MaxLiquidityTreeDepth>; + pub type PoolOf = Pool, MaxAssets>; + pub type AmmTradeOf = AmmTrade>; #[pallet::config] pub trait Config: frame_system::Config { @@ -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