From e607c937263926084eb204bfd6c872b2327ba648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Thu, 16 Apr 2026 18:00:51 +0000 Subject: [PATCH 01/10] Add more try_state checks to PSM pallet --- substrate/frame/psm/src/lib.rs | 97 ++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 3 deletions(-) diff --git a/substrate/frame/psm/src/lib.rs b/substrate/frame/psm/src/lib.rs index c166077d49ccc..265e9a2cba29c 100644 --- a/substrate/frame/psm/src/lib.rs +++ b/substrate/frame/psm/src/lib.rs @@ -943,7 +943,7 @@ pub mod pallet { } } - #[cfg(any(feature = "try-runtime", test))] + #[cfg(any(feature = "try-runtime", test, feature = "fuzzing"))] pub(crate) fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> { use sp_runtime::traits::CheckedAdd; @@ -978,8 +978,7 @@ pub mod pallet { ); // Check 4: Per-asset debt should not exceed its ceiling. - // (May be transiently violated if governance lowers ceilings, but - // should hold under normal operation.) + // (May be transiently violated if governance lowers ceilings.) for (asset_id, status) in ExternalAssets::::iter() { if status.allows_minting() { let debt = PsmDebt::::get(asset_id); @@ -988,6 +987,98 @@ pub mod pallet { } } + // Check 5: No non-zero PsmDebt entry for a non-approved asset. + // Note: if this is violated, check 3 will also fail because total_psm_debt() + // iterates all PsmDebt entries while the sum above only covers ExternalAssets. + // Both checks are retained since they report distinct invariant violations. + for (asset_id, debt) in PsmDebt::::iter() { + if !debt.is_zero() { + ensure!( + ExternalAssets::::contains_key(asset_id), + "PsmDebt entry exists for non-approved asset" + ); + } + } + + // Check 6: Orphan fee/ceiling storage entries (warn only — may be intentional + // pre-configuration via setMintingFee before addExternalAsset). + for asset_id in MintingFee::::iter_keys() { + if !ExternalAssets::::contains_key(asset_id) { + log::warn!( + target: "runtime::psm", + "MintingFee entry for non-approved asset {:?}", + asset_id + ); + } + } + for asset_id in RedemptionFee::::iter_keys() { + if !ExternalAssets::::contains_key(asset_id) { + log::warn!( + target: "runtime::psm", + "RedemptionFee entry for non-approved asset {:?}", + asset_id + ); + } + } + for asset_id in AssetCeilingWeight::::iter_keys() { + if !ExternalAssets::::contains_key(asset_id) { + log::warn!( + target: "runtime::psm", + "AssetCeilingWeight entry for non-approved asset {:?}", + asset_id + ); + } + } + + // Check 7: PSM account must exist. + let psm_account = Self::account_id(); + ensure!( + frame_system::Pallet::::account_exists(&psm_account), + "PSM account does not exist" + ); + + // Check 8: ExternalAssets count within bound. + let count = ExternalAssets::::iter_keys().count() as u32; + ensure!( + count <= T::MaxExternalAssets::get(), + "ExternalAssets count exceeds MaxExternalAssets" + ); + + // Check 9: Zero ceiling weight + zero debt implies zero reserve. + // Non-zero reserve under these conditions is likely a donation or bug. + for (asset_id, _) in ExternalAssets::::iter() { + if AssetCeilingWeight::::get(asset_id).is_zero() + && PsmDebt::::get(asset_id).is_zero() + { + let reserve = Self::get_reserve(asset_id); + if !reserve.is_zero() { + log::warn!( + target: "runtime::psm", + "Asset {:?} has zero ceiling weight and zero debt but non-zero reserve {:?}", + asset_id, reserve + ); + } + } + } + + // Check 10: Total PSM debt should not exceed MaxPsmDebtOfTotal ceiling. + // May be transiently violated if governance lowers MaxPsmDebtOfTotal. + let total_debt = Self::total_psm_debt(); + let max_debt = Self::max_psm_debt(); + if total_debt > max_debt { + log::warn!( + target: "runtime::psm", + "Total PSM debt {:?} exceeds max {:?}", + total_debt, max_debt + ); + } + + // Check 11: Fee destination account must exist. + ensure!( + frame_system::Pallet::::account_exists(&T::FeeDestination::get()), + "Fee destination account does not exist" + ); + Ok(()) } } From 26114172e9ed1124931ac1eb1f7fedf8221ec98f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Thu, 16 Apr 2026 21:49:56 +0000 Subject: [PATCH 02/10] Test PSM pallet's try_state checks --- substrate/frame/psm/Cargo.toml | 1 + substrate/frame/psm/src/tests.rs | 128 +++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/substrate/frame/psm/Cargo.toml b/substrate/frame/psm/Cargo.toml index 484dea4002254..6e56f24d9bab3 100644 --- a/substrate/frame/psm/Cargo.toml +++ b/substrate/frame/psm/Cargo.toml @@ -56,3 +56,4 @@ try-runtime = [ "pallet-balances/try-runtime", "sp-runtime/try-runtime", ] +fuzzing = [] diff --git a/substrate/frame/psm/src/tests.rs b/substrate/frame/psm/src/tests.rs index 30cc7966f65eb..c7d3dd06531fa 100644 --- a/substrate/frame/psm/src/tests.rs +++ b/substrate/frame/psm/src/tests.rs @@ -1599,3 +1599,131 @@ mod cycles { }); } } + +mod try_state { + use super::*; + + #[test] + fn passes_on_valid_state() { + new_test_ext().execute_with(|| { + assert_ok!(crate::Pallet::::do_try_state()); + }); + } + + #[test] + fn detects_reserve_deficit() { + new_test_ext().execute_with(|| { + use frame_support::traits::{ + fungibles::Mutate, + tokens::{Fortitude, Precision, Preservation}, + }; + + assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, 1_000 * PUSD_UNIT)); + assert_ok!(crate::Pallet::::do_try_state()); + + let psm = psm_account(); + let reserve = Assets::balance(USDC_ASSET_ID, psm); + let _ = Assets::burn_from( + USDC_ASSET_ID, + &psm, + reserve, + Preservation::Expendable, + Precision::BestEffort, + Fortitude::Force, + ); + + assert_eq!( + crate::Pallet::::do_try_state().unwrap_err(), + DispatchError::Other("PSM reserve is less than tracked debt for an asset") + ); + }); + } + + #[test] + fn detects_orphan_debt() { + new_test_ext().execute_with(|| { + // Register the asset fully so all earlier checks pass, then remove it from + // ExternalAssets to leave an orphan PsmDebt entry. Check 3 (sum mismatch) fires + // first because total_psm_debt() includes the orphan while the approved-asset + // sum does not; check 5 (orphan debt) would fire if check 3 did not. Both checks + // cover the same underlying invariant violation from different angles. + create_asset_with_metadata(UNSUPPORTED_ASSET_ID); + ExternalAssets::::insert(UNSUPPORTED_ASSET_ID, CircuitBreakerLevel::AllEnabled); + set_asset_ceiling_weight(UNSUPPORTED_ASSET_ID, Permill::from_percent(50)); + let debt = 1_000 * PUSD_UNIT; + PsmDebt::::insert(UNSUPPORTED_ASSET_ID, debt); + fund_external_asset(UNSUPPORTED_ASSET_ID, psm_account(), debt); + assert_ok!(crate::Pallet::::do_try_state()); + ExternalAssets::::remove(UNSUPPORTED_ASSET_ID); + + assert_eq!( + crate::Pallet::::do_try_state().unwrap_err(), + DispatchError::Other("total_psm_debt() does not match sum of per-asset debts") + ); + }); + } + + #[test] + fn zero_orphan_debt_does_not_error() { + new_test_ext().execute_with(|| { + PsmDebt::::insert(UNSUPPORTED_ASSET_ID, 0u128); + assert_ok!(crate::Pallet::::do_try_state()); + }); + } + + #[test] + fn detects_missing_psm_account() { + new_test_ext().execute_with(|| { + let psm = psm_account(); + let _ = frame_system::Pallet::::dec_providers(&psm); + + assert_eq!( + crate::Pallet::::do_try_state().unwrap_err(), + DispatchError::Other("PSM account does not exist") + ); + }); + } + + #[test] + fn detects_missing_fee_destination() { + new_test_ext().execute_with(|| { + let _ = frame_system::Pallet::::dec_providers(&INSURANCE_FUND); + + assert_eq!( + crate::Pallet::::do_try_state().unwrap_err(), + DispatchError::Other("Fee destination account does not exist") + ); + }); + } + + #[test] + fn detects_asset_count_exceeds_bound() { + new_test_ext().execute_with(|| { + // Check 1 verifies decimals of all approved assets. Create assets with matching + // decimals before inserting them into ExternalAssets, so checks 1-4 pass and + // check 8 fires. + for id in 10u32..20u32 { + create_asset_with_metadata(id); + ExternalAssets::::insert(id, CircuitBreakerLevel::AllEnabled); + } + + assert_eq!( + crate::Pallet::::do_try_state().unwrap_err(), + DispatchError::Other("ExternalAssets count exceeds MaxExternalAssets") + ); + }); + } + + #[test] + fn detects_debt_exceeds_asset_ceiling() { + new_test_ext().execute_with(|| { + assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, 1_000 * PUSD_UNIT)); + set_max_psm_debt_ratio(Permill::from_parts(1)); + + assert_eq!( + crate::Pallet::::do_try_state().unwrap_err(), + DispatchError::Other("Per-asset PSM debt exceeds its ceiling") + ); + }); + } +} From b6d8e57a9263881beb885d933b6e2479db2287c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Thu, 16 Apr 2026 21:50:28 +0000 Subject: [PATCH 03/10] Improve try_state checks' unit tests Fix a check from `log::warn! -> ensure!`, add more comments to each, and order them for legibility. --- substrate/frame/psm/src/lib.rs | 33 ++++----- substrate/frame/psm/src/tests.rs | 119 ++++++++++++++++++++++++++----- 2 files changed, 116 insertions(+), 36 deletions(-) diff --git a/substrate/frame/psm/src/lib.rs b/substrate/frame/psm/src/lib.rs index 265e9a2cba29c..987aae54eaa8c 100644 --- a/substrate/frame/psm/src/lib.rs +++ b/substrate/frame/psm/src/lib.rs @@ -977,7 +977,16 @@ pub mod pallet { "total_psm_debt() does not match sum of per-asset debts" ); - // Check 4: Per-asset debt should not exceed its ceiling. + // Check 4: Total PSM debt must not exceed the global ceiling. + // May be transiently violated if governance lowers MaxPsmDebtOfTotal after debt + // has already been accumulated. Checked before per-asset ceilings so it fires + // independently when only the global ceiling has been lowered. + ensure!( + Self::total_psm_debt() <= Self::max_psm_debt(), + "Total PSM debt exceeds global ceiling" + ); + + // Check 5: Per-asset debt should not exceed its ceiling. // (May be transiently violated if governance lowers ceilings.) for (asset_id, status) in ExternalAssets::::iter() { if status.allows_minting() { @@ -987,7 +996,7 @@ pub mod pallet { } } - // Check 5: No non-zero PsmDebt entry for a non-approved asset. + // Check 6: No non-zero PsmDebt entry for a non-approved asset. // Note: if this is violated, check 3 will also fail because total_psm_debt() // iterates all PsmDebt entries while the sum above only covers ExternalAssets. // Both checks are retained since they report distinct invariant violations. @@ -1000,7 +1009,7 @@ pub mod pallet { } } - // Check 6: Orphan fee/ceiling storage entries (warn only — may be intentional + // Check 7: Orphan fee/ceiling storage entries (warn only; may be intentional // pre-configuration via setMintingFee before addExternalAsset). for asset_id in MintingFee::::iter_keys() { if !ExternalAssets::::contains_key(asset_id) { @@ -1030,21 +1039,21 @@ pub mod pallet { } } - // Check 7: PSM account must exist. + // Check 8: PSM account must exist. let psm_account = Self::account_id(); ensure!( frame_system::Pallet::::account_exists(&psm_account), "PSM account does not exist" ); - // Check 8: ExternalAssets count within bound. + // Check 9: ExternalAssets count within bound. let count = ExternalAssets::::iter_keys().count() as u32; ensure!( count <= T::MaxExternalAssets::get(), "ExternalAssets count exceeds MaxExternalAssets" ); - // Check 9: Zero ceiling weight + zero debt implies zero reserve. + // Check 10: Zero ceiling weight + zero debt implies zero reserve. // Non-zero reserve under these conditions is likely a donation or bug. for (asset_id, _) in ExternalAssets::::iter() { if AssetCeilingWeight::::get(asset_id).is_zero() @@ -1061,18 +1070,6 @@ pub mod pallet { } } - // Check 10: Total PSM debt should not exceed MaxPsmDebtOfTotal ceiling. - // May be transiently violated if governance lowers MaxPsmDebtOfTotal. - let total_debt = Self::total_psm_debt(); - let max_debt = Self::max_psm_debt(); - if total_debt > max_debt { - log::warn!( - target: "runtime::psm", - "Total PSM debt {:?} exceeds max {:?}", - total_debt, max_debt - ); - } - // Check 11: Fee destination account must exist. ensure!( frame_system::Pallet::::account_exists(&T::FeeDestination::get()), diff --git a/substrate/frame/psm/src/tests.rs b/substrate/frame/psm/src/tests.rs index c7d3dd06531fa..4f5dc0b71f827 100644 --- a/substrate/frame/psm/src/tests.rs +++ b/substrate/frame/psm/src/tests.rs @@ -1198,13 +1198,13 @@ mod ceiling_redistribution { // PSM ceiling = 50% of 20M = 10M let mint_amount = 1000 * PUSD_UNIT; - // Set USDC weight to 30% — with a single asset this normalizes to 100% + // Set USDC weight to 30%; with a single asset this normalizes to 100% set_asset_ceiling_weight(USDC_ASSET_ID, Permill::from_percent(30)); let ceiling_at_30 = crate::Pallet::::max_asset_debt(USDC_ASSET_ID); assert_eq!(ceiling_at_30, 10_000_000 * PUSD_UNIT); assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, mint_amount)); - // Change weight to 80% — still normalizes to 100% + // Change weight to 80%; still normalizes to 100% set_asset_ceiling_weight(USDC_ASSET_ID, Permill::from_percent(80)); let ceiling_at_80 = crate::Pallet::::max_asset_debt(USDC_ASSET_ID); assert_eq!(ceiling_at_80, 10_000_000 * PUSD_UNIT); @@ -1603,6 +1603,7 @@ mod cycles { mod try_state { use super::*; + // Covers all checks: verifies do_try_state returns Ok on a clean genesis state. #[test] fn passes_on_valid_state() { new_test_ext().execute_with(|| { @@ -1610,6 +1611,29 @@ mod try_state { }); } + // Check 1: approved asset has mismatched decimals. + #[test] + fn detects_decimal_mismatch() { + new_test_ext().execute_with(|| { + use frame_support::traits::fungibles::{metadata::Mutate as MetadataMutate, Create}; + let _ = >::create(UNSUPPORTED_ASSET_ID, ALICE, true, 1); + let _ = >::set( + UNSUPPORTED_ASSET_ID, + &ALICE, + b"Test".to_vec(), + b"TST".to_vec(), + 18, + ); + ExternalAssets::::insert(UNSUPPORTED_ASSET_ID, CircuitBreakerLevel::AllEnabled); + + assert_eq!( + crate::Pallet::::do_try_state().unwrap_err(), + DispatchError::Other("External asset decimals do not match stable asset decimals") + ); + }); + } + + // Check 2: reserve < debt. #[test] fn detects_reserve_deficit() { new_test_ext().execute_with(|| { @@ -1639,13 +1663,14 @@ mod try_state { }); } + // Checks 3 and 6: orphan PsmDebt fires check 3 (sum mismatch) first; check 6 would follow. #[test] fn detects_orphan_debt() { new_test_ext().execute_with(|| { // Register the asset fully so all earlier checks pass, then remove it from // ExternalAssets to leave an orphan PsmDebt entry. Check 3 (sum mismatch) fires // first because total_psm_debt() includes the orphan while the approved-asset - // sum does not; check 5 (orphan debt) would fire if check 3 did not. Both checks + // sum does not; check 6 (orphan debt) would fire if check 3 did not. Both checks // cover the same underlying invariant violation from different angles. create_asset_with_metadata(UNSUPPORTED_ASSET_ID); ExternalAssets::::insert(UNSUPPORTED_ASSET_ID, CircuitBreakerLevel::AllEnabled); @@ -1663,45 +1688,91 @@ mod try_state { }); } + // Check 4: total debt exceeds global ceiling. #[test] - fn zero_orphan_debt_does_not_error() { + fn detects_total_debt_exceeds_global_ceiling() { new_test_ext().execute_with(|| { - PsmDebt::::insert(UNSUPPORTED_ASSET_ID, 0u128); - assert_ok!(crate::Pallet::::do_try_state()); + assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, 1_000 * PUSD_UNIT)); + // Lower MaxPsmDebtOfTotal below accumulated debt via storage. + // Per-asset ceilings (derived from max_psm_debt) also shrink, so check 5 + // would fire too, but check 4 fires first since it is ordered before check 5. + MaxPsmDebtOfTotal::::put(Permill::from_parts(1)); + + assert_eq!( + crate::Pallet::::do_try_state().unwrap_err(), + DispatchError::Other("Total PSM debt exceeds global ceiling") + ); }); } + /// Check 5: per-asset debt exceeds its ceiling. + /// + /// Mints 1000 UNIT of USDC, then lowers `MaxPsmDebtOfTotal` such that the global ceiling + /// (check 4) still passes (`max_psm_debt` = 1100 UNIT > 1000 UNIT debt), but the + /// per-asset ceiling for USDC (`max_asset_debt` = 1100 * 60% = 660 UNIT) falls below the + /// accumulated debt. The ratio is derived dynamically from `MockMaximumIssuance` to avoid + /// sensitivity to thread-local state across tests. #[test] - fn detects_missing_psm_account() { + fn detects_debt_exceeds_asset_ceiling() { new_test_ext().execute_with(|| { - let psm = psm_account(); - let _ = frame_system::Pallet::::dec_providers(&psm); + assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, 1_000 * PUSD_UNIT)); + let max_issuance = crate::mock::MockMaximumIssuance::get(); + let ratio = Permill::from_rational(1_100 * PUSD_UNIT, max_issuance); + MaxPsmDebtOfTotal::::put(ratio); + assert!(crate::Pallet::::max_psm_debt() > 1_000 * PUSD_UNIT); + assert!(crate::Pallet::::max_asset_debt(USDC_ASSET_ID) < 1_000 * PUSD_UNIT); assert_eq!( crate::Pallet::::do_try_state().unwrap_err(), - DispatchError::Other("PSM account does not exist") + DispatchError::Other("Per-asset PSM debt exceeds its ceiling") ); }); } + // Check 6: zero debt for a non-approved asset is not a violation. #[test] - fn detects_missing_fee_destination() { + fn zero_orphan_debt_does_not_error() { new_test_ext().execute_with(|| { - let _ = frame_system::Pallet::::dec_providers(&INSURANCE_FUND); + PsmDebt::::insert(UNSUPPORTED_ASSET_ID, 0u128); + assert_ok!(crate::Pallet::::do_try_state()); + }); + } + + // Check 7: orphan fee/ceiling entries; warn only, so do_try_state still returns Ok. + #[test] + fn orphan_fee_entries_do_not_error() { + new_test_ext().execute_with(|| { + // Insert fee entries for an asset not in ExternalAssets. + MintingFee::::insert(UNSUPPORTED_ASSET_ID, Permill::from_percent(1)); + RedemptionFee::::insert(UNSUPPORTED_ASSET_ID, Permill::from_percent(1)); + AssetCeilingWeight::::insert(UNSUPPORTED_ASSET_ID, Permill::from_percent(50)); + + // Warnings are emitted but no error is returned. + assert_ok!(crate::Pallet::::do_try_state()); + }); + } + + // Check 8: PSM account missing. + #[test] + fn detects_missing_psm_account() { + new_test_ext().execute_with(|| { + let psm = psm_account(); + let _ = frame_system::Pallet::::dec_providers(&psm); assert_eq!( crate::Pallet::::do_try_state().unwrap_err(), - DispatchError::Other("Fee destination account does not exist") + DispatchError::Other("PSM account does not exist") ); }); } + // Check 9: ExternalAssets count exceeds MaxExternalAssets. #[test] fn detects_asset_count_exceeds_bound() { new_test_ext().execute_with(|| { // Check 1 verifies decimals of all approved assets. Create assets with matching // decimals before inserting them into ExternalAssets, so checks 1-4 pass and - // check 8 fires. + // check 9 fires. for id in 10u32..20u32 { create_asset_with_metadata(id); ExternalAssets::::insert(id, CircuitBreakerLevel::AllEnabled); @@ -1714,15 +1785,27 @@ mod try_state { }); } + // Check 10 (warn only): zero ceiling + zero debt + non-zero reserve. #[test] - fn detects_debt_exceeds_asset_ceiling() { + fn zero_ceiling_zero_debt_nonzero_reserve_does_not_error() { new_test_ext().execute_with(|| { - assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, 1_000 * PUSD_UNIT)); - set_max_psm_debt_ratio(Permill::from_parts(1)); + set_asset_ceiling_weight(USDC_ASSET_ID, Permill::zero()); + assert_eq!(PsmDebt::::get(USDC_ASSET_ID), 0u128); + fund_external_asset(USDC_ASSET_ID, psm_account(), 1_000 * PUSD_UNIT); + + assert_ok!(crate::Pallet::::do_try_state()); + }); + } + + // Check 11: fee destination account missing. + #[test] + fn detects_missing_fee_destination() { + new_test_ext().execute_with(|| { + let _ = frame_system::Pallet::::dec_providers(&INSURANCE_FUND); assert_eq!( crate::Pallet::::do_try_state().unwrap_err(), - DispatchError::Other("Per-asset PSM debt exceeds its ceiling") + DispatchError::Other("Fee destination account does not exist") ); }); } From 3b85a9099194436cbcd75bdc0b5592c1a2fa0dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Fri, 17 Apr 2026 17:09:55 +0000 Subject: [PATCH 04/10] Add more tests to multi-asset debt ceiling boundary interactions --- substrate/frame/psm/src/tests.rs | 95 ++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/substrate/frame/psm/src/tests.rs b/substrate/frame/psm/src/tests.rs index 4f5dc0b71f827..05e99a6ebfe64 100644 --- a/substrate/frame/psm/src/tests.rs +++ b/substrate/frame/psm/src/tests.rs @@ -1249,6 +1249,101 @@ mod ceiling_redistribution { } } +mod multi_asset_ceiling { + use super::*; + + const OVER: u128 = 100 * PUSD_UNIT; + + #[test] + fn usdc_at_ceiling_does_not_consume_usdt_ceiling() { + new_test_ext().execute_with(|| { + // 1. Set global ceiling to 50% of MaximumIssuance, split 60/40 between USDC and USDT + set_max_psm_debt_ratio(Permill::from_percent(50)); + set_asset_ceiling_weight(USDC_ASSET_ID, Permill::from_percent(60)); + set_asset_ceiling_weight(USDT_ASSET_ID, Permill::from_percent(40)); + let usdc_ceiling = crate::Pallet::::max_asset_debt(USDC_ASSET_ID); + let usdt_ceiling = crate::Pallet::::max_asset_debt(USDT_ASSET_ID); + fund_external_asset(USDC_ASSET_ID, ALICE, usdc_ceiling); + fund_external_asset(USDT_ASSET_ID, ALICE, usdt_ceiling); + + // 2. Mint USDC to its exact per-asset ceiling + assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, usdc_ceiling)); + assert_eq!(PsmDebt::::get(USDC_ASSET_ID), usdc_ceiling); + assert_eq!(crate::Pallet::::max_asset_debt(USDT_ASSET_ID), usdt_ceiling); + + // 3. USDT can still mint to its full ceiling + assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDT_ASSET_ID, usdt_ceiling)); + assert_eq!(PsmDebt::::get(USDT_ASSET_ID), usdt_ceiling); + }); + } + + #[test] + fn minting_past_per_asset_ceiling_blocked_regardless_of_global_headroom() { + new_test_ext().execute_with(|| { + // 1. 50% global ceiling, 60/40 split: USDC ceiling is 3M, global is 5M + set_max_psm_debt_ratio(Permill::from_percent(50)); + set_asset_ceiling_weight(USDC_ASSET_ID, Permill::from_percent(60)); + set_asset_ceiling_weight(USDT_ASSET_ID, Permill::from_percent(40)); + let usdc_ceiling = crate::Pallet::::max_asset_debt(USDC_ASSET_ID); + fund_external_asset(USDC_ASSET_ID, ALICE, usdc_ceiling + OVER); + + // 2. Mint USDC to its per-asset ceiling + assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, usdc_ceiling)); + + // 3. Global ceiling still has 2M headroom + assert!( + crate::Pallet::::total_psm_debt().saturating_add(OVER) + <= crate::Pallet::::max_psm_debt(), + "global headroom must exist for this test to be meaningful", + ); + + // 4. Further USDC mint blocked by per-asset ceiling, not global + assert_noop!( + Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, OVER), + Error::::ExceedsMaxPsmDebt + ); + }); + } + + #[test] + fn boundary_both_assets_min_swap_below_ceiling() { + new_test_ext().execute_with(|| { + // 1. 50% global ceiling, 60/40 split + set_max_psm_debt_ratio(Permill::from_percent(50)); + set_asset_ceiling_weight(USDC_ASSET_ID, Permill::from_percent(60)); + set_asset_ceiling_weight(USDT_ASSET_ID, Permill::from_percent(40)); + let usdc_ceiling = crate::Pallet::::max_asset_debt(USDC_ASSET_ID); + let usdt_ceiling = crate::Pallet::::max_asset_debt(USDT_ASSET_ID); + fund_external_asset(USDC_ASSET_ID, ALICE, usdc_ceiling + OVER); + fund_external_asset(USDT_ASSET_ID, ALICE, usdt_ceiling + OVER); + + // 2. Mint both to one MinSwapAmount below their ceilings + assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, usdc_ceiling - OVER)); + assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDT_ASSET_ID, usdt_ceiling - OVER)); + + // 3. Fill the remaining MinSwapAmount gap + assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, OVER)); + assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDT_ASSET_ID, OVER)); + + // 4. Per-asset ceilings sum to global ceiling exactly + assert_eq!( + crate::Pallet::::total_psm_debt(), + crate::Pallet::::max_psm_debt() + ); + + // 5. One more MinSwapAmount fails for both + assert_noop!( + Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, OVER), + Error::::ExceedsMaxPsmDebt + ); + assert_noop!( + Psm::mint(RuntimeOrigin::signed(ALICE), USDT_ASSET_ID, OVER), + Error::::ExceedsMaxPsmDebt + ); + }); + } +} + mod cycles { use super::*; From 6dab6dd187db4c9cdae8a5a9b6564c749fa4fa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Sat, 18 Apr 2026 15:09:41 +0000 Subject: [PATCH 05/10] Add README to PSM fuzzer project --- substrate/frame/psm/fuzz/README.md | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 substrate/frame/psm/fuzz/README.md diff --git a/substrate/frame/psm/fuzz/README.md b/substrate/frame/psm/fuzz/README.md new file mode 100644 index 0000000000000..2bdcc08771485 --- /dev/null +++ b/substrate/frame/psm/fuzz/README.md @@ -0,0 +1,69 @@ +# pallet-psm fuzz targets + +Two complementary fuzzers for the PSM pallet's mint/redeem/ceiling/governance +state space. Both exercise all 9 dispatchables and validate `do_try_state()` +after each block (libFuzzer) or each command (stateful). + +## `psm` — coverage-guided (libFuzzer) + +Generates random byte sequences, parsed into `Op` hints by `Arbitrary`. +Hints are resolved to concrete calls at execution time using current pallet +state (debt, ceilings, balances). Runs indefinitely; corpus accumulates in +`corpus/psm/`. Crashes (invariant violations) go to `artifacts/psm/`. + +Run: + cargo +nightly fuzz run psm -- -max_len=4096 + +Reproduce crash: + cargo +nightly fuzz run psm artifacts/psm/ + +Architecture: `Op` enum → `dispatch_op()` reads storage via `fuzz_helpers` +→ dispatches → `do_try_state()`. No storage access during `Arbitrary` parse. + +## `psm_stateful` — state-aware property tester + +Generates commands by reading actual pallet state before each decision. +Uses `rand::StdRng` with a fixed seed for reproducibility. Biases toward +interesting scenarios: ceiling violations, fee extremes, weight redistribution +with existing debt, circuit breaker toggling. + +Run: + cargo run --bin psm_stateful -- + cargo run --bin psm_stateful -- 42 10000 + +Deterministic: same seed produces the same sequence. Logs every command +with the state snapshot that led to it. Panics on invariant violation +with full reproduction trace. + +## When to use which + +- **libFuzzer**: long-running background campaigns, coverage tracking, corpus + minimization. Good for finding unexpected code paths. Limited by entropy + budget — each `Arbitrary` parse consumes 4+ bytes per op, so corpus entries + stay small and multi-op sequences are rare. + +- **Stateful**: targeted campaigns, reproducing specific scenarios, exploring + deep state interactions (governance→debt→redeem chains). Generates long + semantically rich sequences by design. No coverage feedback — relies on + domain knowledge for interestingness. + +## Genesis + +Both use the same genesis: 10 accounts, 5 pre-created external assets +(USDC, USDT, DAI, USDP, FRAX at IDs 2–6), PSM configured with USDC 60% / +USDT 40%, MaxPsmDebtOfTotal 50%, MaximumIssuance 20M pUSD. Assets 4–6 +are unapproved — available for `add_external_asset` during fuzzing. + +## Mock access + +The pallet's internal methods and storage are `pub(crate)`. The fuzz crate +accesses them via `pallet_psm::mock::fuzz_helpers`, a module gated behind +`#[cfg(feature = "fuzzing")]` that exposes `psm_debt()`, `max_psm_debt()`, +`total_psm_debt()`, `do_try_state()`, `approved_assets()`, etc. + +## `do_try_state` checks + +Checks 4 (global ceiling) and 5 (per-asset ceiling) are warnings, not hard +invariants — governance can transiently create these states. The fuzzers +exercise them freely. All other checks (reserve ≥ debt, debt sum integrity, +no orphan debt, etc.) are hard and will cause a panic. From ee6403de8c1d353e64475ccb6e39cbcd2963a464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Sat, 18 Apr 2026 15:27:48 +0000 Subject: [PATCH 06/10] Create `.gitignore` for PSM pallet's fuzzer --- substrate/frame/psm/fuzz/.gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 substrate/frame/psm/fuzz/.gitignore diff --git a/substrate/frame/psm/fuzz/.gitignore b/substrate/frame/psm/fuzz/.gitignore new file mode 100644 index 0000000000000..64a0aacfab9b4 --- /dev/null +++ b/substrate/frame/psm/fuzz/.gitignore @@ -0,0 +1,5 @@ +corpus/ +coverage/ +crash-* +*.log +replay.rs From 6e3d27eaf256d7d1d962902289f4b0a988cfef0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Sat, 18 Apr 2026 15:29:02 +0000 Subject: [PATCH 07/10] Move PSM's gitignore to pallet root --- substrate/frame/psm/{fuzz => }/.gitignore | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename substrate/frame/psm/{fuzz => }/.gitignore (100%) diff --git a/substrate/frame/psm/fuzz/.gitignore b/substrate/frame/psm/.gitignore similarity index 100% rename from substrate/frame/psm/fuzz/.gitignore rename to substrate/frame/psm/.gitignore From 831d92c0900aaa56c66be3ef6811d104e59e2ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Sat, 18 Apr 2026 15:30:45 +0000 Subject: [PATCH 08/10] Add coverage-guided and stateful PSM fuzzer harnesses --- substrate/frame/psm/fuzz/Cargo.lock | 3994 +++++++++++++++++ substrate/frame/psm/fuzz/Cargo.toml | 33 + substrate/frame/psm/fuzz/fuzz_targets/psm.rs | 522 +++ .../psm/fuzz/fuzz_targets/psm_stateful.rs | 749 ++++ 4 files changed, 5298 insertions(+) create mode 100644 substrate/frame/psm/fuzz/Cargo.lock create mode 100644 substrate/frame/psm/fuzz/Cargo.toml create mode 100644 substrate/frame/psm/fuzz/fuzz_targets/psm.rs create mode 100644 substrate/frame/psm/fuzz/fuzz_targets/psm_stateful.rs diff --git a/substrate/frame/psm/fuzz/Cargo.lock b/substrate/frame/psm/fuzz/Cargo.lock new file mode 100644 index 0000000000000..b301875caf504 --- /dev/null +++ b/substrate/frame/psm/fuzz/Cargo.lock @@ -0,0 +1,3994 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "aquamarine" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21cc1548309245035eb18aa7f0967da6bc65587005170c56e6ef2788a4cf3f4e" +dependencies = [ + "include_dir", + "itertools 0.10.5", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "ark-bls12-377" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb00293ba84f51ce3bd026bd0de55899c4e68f0a39a5728cebae3a73ffdc0a4f" +dependencies = [ + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-std 0.4.0", +] + +[[package]] +name = "ark-bls12-381" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c775f0d12169cba7aae4caeb547bb6a50781c7449a8aa53793827c9ec4abf488" +dependencies = [ + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", +] + +[[package]] +name = "ark-bls12-381" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff 0.4.2", + "ark-poly 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ed-on-bls12-381-bandersnatch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1786b2e3832f6f0f7c8d62d5d5a282f6952a1ab99981c54cd52b6ac1d8f02df5" +dependencies = [ + "ark-bls12-381 0.5.0", + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive 0.4.2", + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "ark-transcript" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c1c928edb9d8ff24cb5dcb7651d3a98494fff3099eee95c2404cd813a9139f" +dependencies = [ + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "digest 0.10.7", + "rand_core", + "sha3", +] + +[[package]] +name = "ark-vrf" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d815a2faa9e0fe6c342cc7f25f1f30016774cfbc966f56ea1967377a7ac2066" +dependencies = [ + "ark-bls12-381 0.5.0", + "ark-ec 0.5.0", + "ark-ed-on-bls12-381-bandersnatch", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "digest 0.10.7", + "rand_chacha", + "sha2 0.10.9", + "w3f-ring-proof", + "zeroize", +] + +[[package]] +name = "array-bytes" +version = "6.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5dde061bd34119e902bbb2d9b90c5692635cf59fb91d582c2b68043f1b8293" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "binary-merkle-tree" +version = "13.0.0" +dependencies = [ + "hash-db", + "log", + "parity-scale-codec", +] + +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand", + "rand_core", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bounded-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee8eddd066a8825ec5570528e6880471210fd5d88cb6abbe1cfdd51ca249c33" +dependencies = [ + "jam-codec", + "log", + "parity-scale-codec", + "scale-info", + "serde", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "common-path" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "docify" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a772b62b1837c8f060432ddcc10b17aae1453ef17617a99bc07789252d2a5896" +dependencies = [ + "docify_macros", +] + +[[package]] +name = "docify_macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e6be249b0a462a14784a99b19bf35a667bb5e09de611738bb7362fa4c95ff7" +dependencies = [ + "common-path", + "derive-syn-parse", + "once_cell", + "proc-macro2", + "quote", + "regex", + "syn 2.0.117", + "termcolor", + "toml", + "walkdir", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "ed25519-zebra" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775765289f7c6336c18d3d66127527820dd45ffd9eb3b6b8ee4708590e6c20f5" +dependencies = [ + "curve25519-dalek", + "ed25519", + "hashbrown 0.16.1", + "pkcs8", + "rand_core", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "environmental" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48c92028aaa870e83d51c64e5d4e0b6981b360c522198c23959f219a4e1b15b" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "expander" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2c470c71d91ecbd179935b24170459e926382eaaa86b590b78814e180d8a8e2" +dependencies = [ + "blake2", + "file-guard", + "fs-err", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "file-guard" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ef72acf95ec3d7dbf61275be556299490a245f017cf084bd23b4f68cf9407c" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "frame-benchmarking" +version = "28.0.0" +dependencies = [ + "frame-support", + "frame-support-procedural", + "frame-system", + "linregress", + "log", + "parity-scale-codec", + "paste", + "scale-info", + "serde", + "sp-api", + "sp-application-crypto", + "sp-core", + "sp-io", + "sp-runtime", + "sp-runtime-interface", + "sp-storage", + "static_assertions", +] + +[[package]] +name = "frame-metadata" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba5be0edbdb824843a0f9c6f0906ecfc66c5316218d74457003218b24909ed0" +dependencies = [ + "cfg-if", + "parity-scale-codec", + "scale-info", + "serde", +] + +[[package]] +name = "frame-support" +version = "28.0.0" +dependencies = [ + "aquamarine", + "array-bytes", + "binary-merkle-tree", + "bitflags 1.3.2", + "derive-where", + "docify", + "environmental", + "frame-metadata", + "frame-support-procedural", + "impl-trait-for-tuples", + "k256", + "log", + "macro_magic", + "parity-scale-codec", + "paste", + "scale-info", + "serde", + "serde_json", + "sp-api", + "sp-arithmetic", + "sp-core", + "sp-crypto-hashing-proc-macro", + "sp-debug-derive", + "sp-genesis-builder", + "sp-inherents", + "sp-io", + "sp-metadata-ir", + "sp-runtime", + "sp-staking", + "sp-state-machine", + "sp-std", + "sp-tracing", + "sp-trie", + "sp-weights", + "tt-call", +] + +[[package]] +name = "frame-support-procedural" +version = "23.0.0" +dependencies = [ + "Inflector", + "cfg-expr", + "derive-syn-parse", + "docify", + "expander", + "frame-support-procedural-tools", + "itertools 0.11.0", + "macro_magic", + "proc-macro-warning", + "proc-macro2", + "quote", + "sp-crypto-hashing", + "syn 2.0.117", +] + +[[package]] +name = "frame-support-procedural-tools" +version = "10.0.0" +dependencies = [ + "frame-support-procedural-tools-derive", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "frame-support-procedural-tools-derive" +version = "11.0.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "frame-system" +version = "28.0.0" +dependencies = [ + "cfg-if", + "docify", + "frame-support", + "log", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-version", + "sp-weights", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "getrandom_or_panic" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" +dependencies = [ + "rand", + "rand_core", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash-db" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7d7786361d7425ae2fe4f9e407eb0efaa0840f5212d109cc018c40c35c6ab4" + +[[package]] +name = "hash256-std-hasher" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "impl-codec" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d40b9d5e17727407e55028eafc22b2dc68781786e6d7eb8a21103f5058e3a14" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-num-traits" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d15461ab0dcc56706adf266158acbc44ccf719bf7d0af30705f58b90a4b8c" +dependencies = [ + "integer-sqrt", + "num-traits", + "uint", +] + +[[package]] +name = "impl-serde" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a143eada6a1ec4aefa5049037a26a6d597bfd64f8c026d07b77133e02b7dd0b" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", +] + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jam-codec" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb948eace373d99de60501a02fb17125d30ac632570de20dccc74370cdd611b9" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "jam-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "jam-codec-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "319af585c4c8a6b5552a52b7787a1ab3e4d59df7614190b1f85b9b842488789d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "serdect", + "sha2 0.10.9", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libsecp256k1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" +dependencies = [ + "arrayref", + "base64", + "digest 0.9.0", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand", + "serde", + "sha2 0.9.9", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "linregress" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9eda9dcf4f2a99787827661f312ac3219292549c2ee992bf9a6248ffb066bf7" +dependencies = [ + "nalgebra", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "macro_magic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" +dependencies = [ + "macro_magic_core", + "macro_magic_macros", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "macro_magic_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" +dependencies = [ + "const-random", + "derive-syn-parse", + "macro_magic_core_macros", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "macro_magic_core_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "macro_magic_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" +dependencies = [ + "macro_magic_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memory-db" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e300c54e3239a86f9c61cc63ab0f03862eb40b1c6e065dc6fd6ceaeff6da93d" +dependencies = [ + "foldhash 0.1.5", + "hash-db", + "hashbrown 0.15.5", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nalgebra" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d43ddcacf343185dfd6de2ee786d9e8b1c2301622afab66b6c73baf9882abfd" +dependencies = [ + "approx", + "matrixmultiply", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pallet-assets" +version = "29.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-runtime", +] + +[[package]] +name = "pallet-balances" +version = "28.0.0" +dependencies = [ + "docify", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-runtime", +] + +[[package]] +name = "pallet-psm" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-assets", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-io", + "sp-runtime", +] + +[[package]] +name = "pallet-psm-fuzz" +version = "0.0.0" +dependencies = [ + "arbitrary", + "frame-support", + "frame-system", + "libfuzzer-sys", + "pallet-assets", + "pallet-balances", + "pallet-psm", + "rand", + "sp-io", + "sp-runtime", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "bytes", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "parity-wasm" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1ad0aff30c1da14b1254fcb2af73e1fa9a28670e584a626f53a369d0e157304" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "password-hash", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "picosimd" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8cf1ae70818c6476eb2da0ac8f3f55ecdea41a7aa16824ea6efc4a31cccf41" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polkavm-common" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e44a9487003cf5b9fc4462bbcf105cc37d5d9b18b40edf5ed50dd20ed1fdb27" +dependencies = [ + "picosimd", +] + +[[package]] +name = "polkavm-derive" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef966bc8518a66ce12d4edb73f2c4094cae72bb23258bc9e9b2802cc9d6cd79" +dependencies = [ + "polkavm-derive-impl-macro", +] + +[[package]] +name = "polkavm-derive-impl" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c2166ad71dd7f51dcdd0d91b70d408a8b3610fa6e94d8202dd4b7185607181" +dependencies = [ + "polkavm-common", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "polkavm-derive-impl-macro" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7ac2ac8ec5b938e249fa97b5ebb1e6fa47000c81a25eba6bf0f13edb8d430e4" +dependencies = [ + "polkavm-derive-impl", + "syn 2.0.117", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-num-traits", + "impl-serde", + "scale-info", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.11+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-warning" +version = "1.84.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75eea531cfcd120e0851a3f8aed42c4841f78c889eefafd96339c72677ae42c3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "thiserror", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.14", + "regex-syntax 0.8.10", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.10", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scale-info" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" +dependencies = [ + "bitvec", + "cfg-if", + "derive_more", + "parity-scale-codec", + "scale-info-derive", + "serde", +] + +[[package]] +name = "scale-info-derive" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "schnellru" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649" +dependencies = [ + "ahash", + "cfg-if", + "hashbrown 0.13.2", +] + +[[package]] +name = "schnorrkel" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9fcb6c2e176e86ec703e22560d99d65a5ee9056ae45a08e13e84ebf796296f" +dependencies = [ + "aead", + "arrayref", + "arrayvec", + "curve25519-dalek", + "getrandom_or_panic", + "merlin", + "rand_core", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core", +] + +[[package]] +name = "simba" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "simple-mermaid" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "620a1d43d70e142b1d46a929af51d44f383db9c7a2ec122de2cd992ccfcf3c18" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "sp-api" +version = "26.0.0" +dependencies = [ + "docify", + "hash-db", + "log", + "parity-scale-codec", + "scale-info", + "sp-api-proc-macro", + "sp-core", + "sp-externalities", + "sp-metadata-ir", + "sp-runtime", + "sp-runtime-interface", + "sp-state-machine", + "sp-trie", + "sp-version", + "thiserror", +] + +[[package]] +name = "sp-api-proc-macro" +version = "15.0.0" +dependencies = [ + "Inflector", + "blake2", + "expander", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sp-application-crypto" +version = "30.0.0" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", +] + +[[package]] +name = "sp-arithmetic" +version = "23.0.0" +dependencies = [ + "docify", + "integer-sqrt", + "num-traits", + "parity-scale-codec", + "scale-info", + "serde", + "static_assertions", +] + +[[package]] +name = "sp-core" +version = "28.0.0" +dependencies = [ + "ark-vrf", + "array-bytes", + "bip39", + "bitflags 1.3.2", + "blake2", + "bounded-collections", + "bs58", + "dyn-clone", + "ed25519-zebra", + "futures", + "hash-db", + "hash256-std-hasher", + "impl-serde", + "itertools 0.11.0", + "k256", + "libsecp256k1", + "log", + "merlin", + "parity-scale-codec", + "parking_lot", + "paste", + "primitive-types", + "rand", + "scale-info", + "schnorrkel", + "secp256k1", + "secrecy", + "serde", + "sha2 0.10.9", + "sp-crypto-hashing", + "sp-debug-derive", + "sp-externalities", + "sp-std", + "sp-storage", + "ss58-registry", + "substrate-bip39", + "thiserror", + "tracing", + "w3f-bls", + "zeroize", +] + +[[package]] +name = "sp-crypto-hashing" +version = "0.1.0" +dependencies = [ + "blake2b_simd", + "byteorder", + "digest 0.10.7", + "sha2 0.10.9", + "sha3", + "twox-hash", +] + +[[package]] +name = "sp-crypto-hashing-proc-macro" +version = "0.1.0" +dependencies = [ + "quote", + "sp-crypto-hashing", + "syn 2.0.117", +] + +[[package]] +name = "sp-debug-derive" +version = "14.0.0" +dependencies = [ + "proc-macro-warning", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sp-externalities" +version = "0.25.0" +dependencies = [ + "environmental", + "parity-scale-codec", + "sp-storage", +] + +[[package]] +name = "sp-genesis-builder" +version = "0.8.0" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde_json", + "sp-api", + "sp-runtime", +] + +[[package]] +name = "sp-inherents" +version = "26.0.0" +dependencies = [ + "async-trait", + "impl-trait-for-tuples", + "parity-scale-codec", + "scale-info", + "sp-runtime", + "thiserror", +] + +[[package]] +name = "sp-io" +version = "30.0.0" +dependencies = [ + "bytes", + "docify", + "ed25519-dalek", + "libsecp256k1", + "log", + "parity-scale-codec", + "polkavm-derive", + "rustversion", + "secp256k1", + "sp-core", + "sp-crypto-hashing", + "sp-externalities", + "sp-keystore", + "sp-runtime-interface", + "sp-state-machine", + "sp-tracing", + "sp-trie", + "tracing", + "tracing-core", +] + +[[package]] +name = "sp-keystore" +version = "0.34.0" +dependencies = [ + "parity-scale-codec", + "parking_lot", + "sp-core", + "sp-externalities", +] + +[[package]] +name = "sp-metadata-ir" +version = "0.6.0" +dependencies = [ + "derive-where", + "frame-metadata", + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "sp-panic-handler" +version = "13.0.0" +dependencies = [ + "backtrace", + "regex", +] + +[[package]] +name = "sp-runtime" +version = "31.0.1" +dependencies = [ + "binary-merkle-tree", + "bytes", + "docify", + "either", + "hash256-std-hasher", + "impl-trait-for-tuples", + "log", + "num-traits", + "parity-scale-codec", + "paste", + "rand", + "scale-info", + "serde", + "simple-mermaid", + "sp-application-crypto", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-std", + "sp-trie", + "sp-weights", + "strum", + "tracing", + "tuplex", +] + +[[package]] +name = "sp-runtime-interface" +version = "24.0.0" +dependencies = [ + "bytes", + "impl-trait-for-tuples", + "parity-scale-codec", + "polkavm-derive", + "sp-externalities", + "sp-runtime-interface-proc-macro", + "sp-std", + "sp-storage", + "sp-tracing", + "sp-wasm-interface", + "static_assertions", +] + +[[package]] +name = "sp-runtime-interface-proc-macro" +version = "17.0.0" +dependencies = [ + "Inflector", + "expander", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sp-staking" +version = "26.0.0" +dependencies = [ + "impl-trait-for-tuples", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-runtime", +] + +[[package]] +name = "sp-state-machine" +version = "0.35.0" +dependencies = [ + "hash-db", + "log", + "parity-scale-codec", + "parking_lot", + "rand", + "smallvec", + "sp-core", + "sp-externalities", + "sp-panic-handler", + "sp-trie", + "thiserror", + "tracing", + "trie-db", +] + +[[package]] +name = "sp-std" +version = "14.0.0" + +[[package]] +name = "sp-storage" +version = "19.0.0" +dependencies = [ + "impl-serde", + "parity-scale-codec", + "ref-cast", + "serde", + "sp-debug-derive", +] + +[[package]] +name = "sp-tracing" +version = "16.0.0" +dependencies = [ + "parity-scale-codec", + "regex", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sp-trie" +version = "29.0.0" +dependencies = [ + "ahash", + "foldhash 0.1.5", + "hash-db", + "hashbrown 0.15.5", + "memory-db", + "nohash-hasher", + "parity-scale-codec", + "parking_lot", + "rand", + "scale-info", + "schnellru", + "sp-core", + "sp-externalities", + "substrate-prometheus-endpoint", + "thiserror", + "tracing", + "trie-db", + "trie-root", +] + +[[package]] +name = "sp-version" +version = "29.0.0" +dependencies = [ + "impl-serde", + "parity-scale-codec", + "parity-wasm", + "scale-info", + "serde", + "sp-core", + "sp-crypto-hashing-proc-macro", + "sp-runtime", + "sp-std", + "sp-version-proc-macro", + "thiserror", +] + +[[package]] +name = "sp-version-proc-macro" +version = "13.0.0" +dependencies = [ + "parity-scale-codec", + "proc-macro-warning", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sp-wasm-interface" +version = "20.0.0" +dependencies = [ + "anyhow", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", +] + +[[package]] +name = "sp-weights" +version = "27.0.0" +dependencies = [ + "bounded-collections", + "parity-scale-codec", + "scale-info", + "serde", + "smallvec", + "sp-arithmetic", + "sp-debug-derive", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ss58-registry" +version = "1.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19409f13998e55816d1c728395af0b52ec066206341d939e22e7766df9b494b8" +dependencies = [ + "Inflector", + "num-format", + "proc-macro2", + "quote", + "serde", + "serde_json", + "unicode-xid", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + +[[package]] +name = "substrate-bip39" +version = "0.4.7" +dependencies = [ + "hmac", + "pbkdf2", + "schnorrkel", + "sha2 0.10.9", + "zeroize", +] + +[[package]] +name = "substrate-prometheus-endpoint" +version = "0.17.0" +dependencies = [ + "http-body-util", + "hyper", + "hyper-util", + "log", + "prometheus", + "thiserror", + "tokio", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "socket2", + "windows-sys", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.1", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.1", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trie-db" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7795f2df2ef744e4ffb2125f09325e60a21d305cc3ecece0adeef03f7a9e560" +dependencies = [ + "hash-db", + "log", + "rustc-hex", + "smallvec", +] + +[[package]] +name = "trie-root" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ed310ef5ab98f5fa467900ed906cb9232dd5376597e00fd4cba2a449d06c0b" +dependencies = [ + "hash-db", +] + +[[package]] +name = "tt-call" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f195fd851901624eee5a58c4bb2b4f06399148fcd0ed336e6f1cb60a9881df" + +[[package]] +name = "tuplex" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "676ac81d5454c4dcf37955d34fa8626ede3490f744b86ca14a7b90168d2a08aa" + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "digest 0.10.7", + "rand", + "static_assertions", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "w3f-bls" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6bfb937b3d12077654a9e43e32a4e9c20177dd9fea0f3aba673e7840bb54f32" +dependencies = [ + "ark-bls12-377", + "ark-bls12-381 0.4.0", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-serialize-derive 0.4.2", + "arrayref", + "digest 0.10.7", + "rand", + "rand_chacha", + "rand_core", + "sha2 0.10.9", + "sha3", + "zeroize", +] + +[[package]] +name = "w3f-pcs" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea1046a1deb6d26c34ba2d1f1bab4222d695d126502ee765f80b021753cb674" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "merlin", +] + +[[package]] +name = "w3f-plonk-common" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077db25196f87773d7f0784c0ea5b11f18f38d336fa25c24bc67d7936af05d7a" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "getrandom_or_panic", + "rand_core", + "w3f-pcs", +] + +[[package]] +name = "w3f-ring-proof" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3afac5d485a6eed7c1762be4b7b70a6f9b346bd6eebe485f48b7d909a6773f5" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "ark-transcript", + "w3f-pcs", + "w3f-plonk-common", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/substrate/frame/psm/fuzz/Cargo.toml b/substrate/frame/psm/fuzz/Cargo.toml new file mode 100644 index 0000000000000..c8155e4fa2fb1 --- /dev/null +++ b/substrate/frame/psm/fuzz/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "pallet-psm-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[workspace] + +[dependencies] +libfuzzer-sys = "0.4" +arbitrary = { version = "1", features = ["derive"] } +rand = "0.8" +pallet-psm = { path = "..", features = ["fuzzing"] } +pallet-assets = { path = "../../assets" } +pallet-balances = { path = "../../balances" } +frame-support = { path = "../../support" } +frame-system = { path = "../../system" } +sp-runtime = { path = "../../../primitives/runtime" } +sp-io = { path = "../../../primitives/io" } + +[[bin]] +name = "psm" +path = "fuzz_targets/psm.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "psm_stateful" +path = "fuzz_targets/psm_stateful.rs" diff --git a/substrate/frame/psm/fuzz/fuzz_targets/psm.rs b/substrate/frame/psm/fuzz/fuzz_targets/psm.rs new file mode 100644 index 0000000000000..5e0f01da6465c --- /dev/null +++ b/substrate/frame/psm/fuzz/fuzz_targets/psm.rs @@ -0,0 +1,522 @@ +#![no_main] + +//! Coverage-guided fuzzer for pallet-psm. +//! +//! Generates multi-block sequences of PSM dispatchables, executes them with +//! state-aware amount generation, and validates invariants via `do_try_state` +//! after each block. Uses libFuzzer's coverage feedback to explore interesting +//! call sequences. + +use arbitrary::{Arbitrary, Unstructured}; +use frame_support::traits::fungibles::Inspect; +use libfuzzer_sys::fuzz_target; +use pallet_psm::mock::fuzz_helpers as fh; +use pallet_psm::mock::{ + Assets, Psm, RuntimeOrigin, System, Test, ALL_EXTERNAL_ASSETS, DAI_ASSET_ID, FRAX_ASSET_ID, + INSURANCE_FUND, PUSD_ASSET_ID, PUSD_UNIT, USDC_ASSET_ID, USDP_ASSET_ID, USDT_ASSET_ID, +}; +use pallet_psm::CircuitBreakerLevel; +use sp_runtime::{BuildStorage, Permill}; +use std::fs::OpenOptions; +use std::io::Write; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +// 10 accounts gives enough diversity for cross-account interactions (e.g. one account +// mints, a different one redeems) while keeping the fuzzer input compact — each account +// index fits in a u8 nibble. +const N_ACCOUNTS: u8 = 10; +const MIN_SWAP: u128 = 100 * PUSD_UNIT; +// Deliberately finite: if every account started with u128::MAX, the fuzzer could never +// discover "insufficient balance" rejection paths. 500K units is large enough to permit +// multi-transaction sequences but small enough that sustained minting can exhaust it. +const INITIAL_EXTERNAL_BALANCE: u128 = 500_000 * PUSD_UNIT; +const INITIAL_NATIVE_BALANCE: u128 = 1_000_000 * PUSD_UNIT; +// 20M units leaves headroom for ceiling exploration: with 50% MaxPsmDebtOfTotal the +// global debt cap starts at 10M, so the fuzzer can mint well past any single-asset +// ceiling without immediately hitting the issuance limit. +const MAX_PSM_ISSUANCE: u128 = 20_000_000 * PUSD_UNIT; + +// --------------------------------------------------------------------------- +// Hint enums (produced by Arbitrary, no storage access) +// --------------------------------------------------------------------------- + +// AmountTier is a *hint* produced during `Arbitrary` construction, where no storage is +// available. It is resolved to a concrete u128 later by `resolve_amount`, which runs inside +// externalities and can read the current ceiling/debt. Each variant targets a different +// pallet boundary condition (see `resolve_amount` for the mapping). +#[derive(Debug)] +enum AmountTier { + MinSwap, + NearCeiling, + AtCeiling, + OverCeiling, + Random(u128), +} + +// Op carries raw indices and entropy bytes rather than resolved asset IDs or amounts. +// This split is necessary because `Arbitrary` runs outside externalities — it cannot call +// into storage to enumerate approved assets or read current ceilings. The `dispatch_op` +// function resolves these opaque indices against live storage at execution time. +// +// Why full u8 for asset_idx instead of constraining to the known asset count? +// Three reasons. (1) The number of *approved* assets changes during a fuzz campaign +// (governance adds/removes them), so the count at generation time may not match the +// count at dispatch time. (2) The modulo in dispatch_op already maps every possible u8 +// to a valid asset, so there is no waste — each byte always resolves correctly. +// (3) Full u8 costs the same as a constrained range (both consume one byte of fuzz +// input), but gives libFuzzer more distinct byte→coverage edges to explore. +#[derive(Debug)] +enum Op { + Mint { account_idx: u8, asset_idx: u8, tier: AmountTier }, + Redeem { account_idx: u8, asset_idx: u8, tier: AmountTier }, + SetMaxPsmDebt { force_below_debt: bool, parts: u32 }, + SetAssetCeilingWeight { asset_idx: u8, parts: u32 }, + SetMintingFee { asset_idx: u8, parts: u32 }, + SetRedemptionFee { asset_idx: u8, parts: u32 }, + SetAssetStatus { asset_idx: u8, level: u8 }, + AddExternalAsset { asset_idx: u8 }, + RemoveExternalAsset { asset_idx: u8 }, +} + +// --------------------------------------------------------------------------- +// Weighted call selection (kitchensink pattern) +// --------------------------------------------------------------------------- + +struct CallSpec { + weight: u32, + generator: fn(&mut Unstructured) -> arbitrary::Result, +} + +// Mint and redeem carry the highest weight (15 each) because they are the core accounting +// paths — every other operation exists to set up interesting states for them. Governance +// ops (debt/ceiling/fee/status) get moderate weight (3-5) since they mutate state that +// subsequent mints/redeems must handle correctly. Asset management (add/remove) gets the +// lowest weight (1) because those calls are irreversible and low-yield once executed. +const CALL_SPECS: &[CallSpec] = &[ + CallSpec { weight: 15, generator: gen_mint }, + CallSpec { weight: 15, generator: gen_redeem }, + CallSpec { weight: 5, generator: gen_set_max_psm_debt }, + CallSpec { weight: 5, generator: gen_set_asset_ceiling_weight }, + CallSpec { weight: 3, generator: gen_set_minting_fee }, + CallSpec { weight: 3, generator: gen_set_redemption_fee }, + CallSpec { weight: 3, generator: gen_set_asset_status }, + CallSpec { weight: 1, generator: gen_add_external_asset }, + CallSpec { weight: 1, generator: gen_remove_external_asset }, +]; + +fn gen_mint(u: &mut Unstructured) -> arbitrary::Result { + Ok(Op::Mint { account_idx: u.arbitrary()?, asset_idx: u.arbitrary()?, tier: u.arbitrary()? }) +} + +fn gen_redeem(u: &mut Unstructured) -> arbitrary::Result { + Ok(Op::Redeem { account_idx: u.arbitrary()?, asset_idx: u.arbitrary()?, tier: u.arbitrary()? }) +} + +fn gen_set_max_psm_debt(u: &mut Unstructured) -> arbitrary::Result { + Ok(Op::SetMaxPsmDebt { force_below_debt: u.arbitrary()?, parts: u.arbitrary()? }) +} + +fn gen_set_asset_ceiling_weight(u: &mut Unstructured) -> arbitrary::Result { + Ok(Op::SetAssetCeilingWeight { asset_idx: u.arbitrary()?, parts: u.arbitrary()? }) +} + +fn gen_set_minting_fee(u: &mut Unstructured) -> arbitrary::Result { + Ok(Op::SetMintingFee { asset_idx: u.arbitrary()?, parts: u.arbitrary()? }) +} + +fn gen_set_redemption_fee(u: &mut Unstructured) -> arbitrary::Result { + Ok(Op::SetRedemptionFee { asset_idx: u.arbitrary()?, parts: u.arbitrary()? }) +} + +fn gen_set_asset_status(u: &mut Unstructured) -> arbitrary::Result { + Ok(Op::SetAssetStatus { asset_idx: u.arbitrary()?, level: u.arbitrary()? }) +} + +fn gen_add_external_asset(u: &mut Unstructured) -> arbitrary::Result { + Ok(Op::AddExternalAsset { asset_idx: u.arbitrary()? }) +} + +fn gen_remove_external_asset(u: &mut Unstructured) -> arbitrary::Result { + Ok(Op::RemoveExternalAsset { asset_idx: u.arbitrary()? }) +} + +// --------------------------------------------------------------------------- +// Arbitrary impls +// --------------------------------------------------------------------------- + +// Uniform 5-way distribution ensures each tier is equally likely. Each variant targets +// a distinct pallet code path: MinSwap triggers BelowMinimumSwap, NearCeiling probes +// the boundary just below the cap, AtCeiling exercises the exact-limit path, OverCeiling +// triggers the overdraft rejection, and Random exercises the general case. +impl<'a> Arbitrary<'a> for AmountTier { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + Ok(match u.int_in_range(0..=4)? { + 0 => AmountTier::MinSwap, + 1 => AmountTier::NearCeiling, + 2 => AmountTier::AtCeiling, + 3 => AmountTier::OverCeiling, + 4 => AmountTier::Random(u.arbitrary()?), + _ => unreachable!(), + }) + } +} + +// Weighted random selection borrowed from the substrate-runtime-fuzzer kitchensink pattern. +// A single u32 is drawn from the fuzz input and mapped to a call type proportionally, +// then the remaining bytes are consumed by that call's specific generator. This ensures +// the distribution of call types is deterministic relative to the input, which improves +// coverage guidance compared to uniform selection. +impl<'a> Arbitrary<'a> for Op { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let total_weight: u32 = CALL_SPECS.iter().map(|s| s.weight).sum(); + let rand_value: u32 = u.int_in_range(0..=u32::MAX)?; + let mut threshold = ((rand_value as u64 * total_weight as u64) / u32::MAX as u64) as u32; + + for spec in CALL_SPECS { + if threshold < spec.weight { + return (spec.generator)(u); + } + threshold -= spec.weight; + } + + // Fallback; CALL_SPECS is non-empty, so this is safe. + (CALL_SPECS[0].generator)(u) + } +} + +// --------------------------------------------------------------------------- +// Block / multi-block structure (tiered entropy from single byte) +// --------------------------------------------------------------------------- + +// Tiered entropy: a single byte is split into a 5-bit tier and a 3-bit variation, +// maximizing coverage per byte of fuzz input. The tier selects a rough magnitude +// (few/medium/many calls) and the variation adds fine-grained jitter within that tier. +// Without this split, the fuzzer would waste entire bytes on a single linear range. +#[derive(Debug)] +struct Block(Vec); + +impl<'a> Arbitrary<'a> for Block { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let decision: u8 = u.arbitrary()?; + let tier = decision & 0x1F; + let variation = (decision >> 5) & 0x07; + + let num_calls = match tier { + 0..=15 => 1 + (variation % 3), + 16..=28 => 3 + (variation % 6), + 29..=31 => 8 + variation, + _ => unreachable!(), + }; + + let calls = (0..num_calls).map(|_| u.arbitrary()).collect::, _>>()?; + + Ok(Block(calls)) + } +} + +// Same tiered-entropy approach as Block, but controls the number of blocks. +// Multi-block sequences are important because governance changes in one block +// can create transient violations that subsequent blocks must resolve. +#[derive(Debug)] +struct MultiBlockOps(Vec); + +impl<'a> Arbitrary<'a> for MultiBlockOps { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let decision: u8 = u.arbitrary()?; + let tier = decision & 0x1F; + let variation = (decision >> 5) & 0x07; + + let num_blocks = match tier { + 0..=9 => 1 + (variation % 5) as usize, + 10..=25 => 5 + (variation % 6) as usize, + 26..=31 => 10 + ((variation as usize * 10) / 7), + _ => unreachable!(), + }; + + let blocks = (0..num_blocks).map(|_| u.arbitrary()).collect::, _>>()?; + + Ok(MultiBlockOps(blocks)) + } +} + +// --------------------------------------------------------------------------- +// Amount resolution (called inside externalities with storage access) +// --------------------------------------------------------------------------- + +// Maps each AmountTier to a concrete amount that targets a specific pallet check: +// MinSwap -> exactly the pallet's minimum, may trigger BelowMinimumSwap if effective_cap < MIN_SWAP +// NearCeiling -> one MIN_SWAP below the cap, tests the "almost full" path +// AtCeiling -> exact cap, tests the boundary where debt == ceiling after mint +// OverCeiling -> cap + MIN_SWAP, triggers the OverCeiling rejection path +// Random -> arbitrary value in [MIN_SWAP, 2*cap], exercises the general interior +fn resolve_amount(tier: &AmountTier, effective_cap: u128) -> u128 { + match tier { + AmountTier::MinSwap => MIN_SWAP, + AmountTier::NearCeiling => effective_cap.saturating_sub(MIN_SWAP).max(MIN_SWAP), + AmountTier::AtCeiling => effective_cap, + AmountTier::OverCeiling => effective_cap.saturating_add(MIN_SWAP), + AmountTier::Random(raw) => { + let upper = (2u128 * effective_cap).max(MIN_SWAP); + let range = upper.saturating_sub(MIN_SWAP).max(1); + MIN_SWAP.saturating_add(raw % range) + }, + } +} + +// --------------------------------------------------------------------------- +// Genesis setup (10 accounts, 5 external assets, PSM with USDC/USDT) +// --------------------------------------------------------------------------- + +// Genesis creates 6 assets but only USDC and USDT are PSM-approved. The remaining +// three (DAI, USDP, FRAX) are pre-funded so the fuzzer can exercise AddExternalAsset +// without first needing to create the asset. All 5 external assets are funded across +// all 10 accounts so that newly-approved assets can be minted immediately by any account. +// +// PSM genesis: USDC gets 60% ceiling weight, USDT gets 40%, both with 1% minting and +// redemption fees. MaxPsmDebtOfTotal is 50%, meaning the total PSM debt cannot exceed +// half the pUSD issuance cap. This gives enough room for the fuzzer to explore ceiling +// violations without trivially saturating the global limit. +fn build_fuzzer_genesis() -> sp_io::TestExternalities { + let mut storage = as Default>::default() + .build_storage() + .expect("system genesis storage builds; qed"); + + let accounts: Vec = (1..=N_ACCOUNTS as u64).collect(); + + pallet_balances::GenesisConfig:: { + balances: accounts + .iter() + .map(|&a| (a, INITIAL_NATIVE_BALANCE)) + .chain(std::iter::once((INSURANCE_FUND, 1))) + .collect(), + ..Default::default() + } + .assimilate_storage(&mut storage) + .expect("balances genesis assimilates; qed"); + + let asset_owner: u64 = 1; + pallet_assets::GenesisConfig:: { + assets: vec![ + (PUSD_ASSET_ID, asset_owner, true, 1), + (USDC_ASSET_ID, asset_owner, true, 1), + (USDT_ASSET_ID, asset_owner, true, 1), + (DAI_ASSET_ID, asset_owner, true, 1), + (USDP_ASSET_ID, asset_owner, true, 1), + (FRAX_ASSET_ID, asset_owner, true, 1), + ], + metadata: vec![ + (PUSD_ASSET_ID, b"pUSD Stablecoin".to_vec(), b"pUSD".to_vec(), 6), + (USDC_ASSET_ID, b"USD Coin".to_vec(), b"USDC".to_vec(), 6), + (USDT_ASSET_ID, b"Tether USD".to_vec(), b"USDT".to_vec(), 6), + (DAI_ASSET_ID, b"Dai Stablecoin".to_vec(), b"DAI".to_vec(), 6), + (USDP_ASSET_ID, b"Pax Dollar".to_vec(), b"USDP".to_vec(), 6), + (FRAX_ASSET_ID, b"Frax".to_vec(), b"FRAX".to_vec(), 6), + ], + accounts: accounts + .iter() + .flat_map(|&a| { + ALL_EXTERNAL_ASSETS.iter().map(move |&id| (id, a, INITIAL_EXTERNAL_BALANCE)) + }) + .collect(), + ..Default::default() + } + .assimilate_storage(&mut storage) + .expect("assets genesis assimilates; qed"); + + pallet_psm::GenesisConfig:: { + max_psm_debt_of_total: Permill::from_percent(50), + asset_configs: [ + ( + USDC_ASSET_ID, + (Permill::from_percent(1), Permill::from_percent(1), Permill::from_percent(60)), + ), + ( + USDT_ASSET_ID, + (Permill::from_percent(1), Permill::from_percent(1), Permill::from_percent(40)), + ), + ] + .into_iter() + .collect(), + _marker: Default::default(), + } + .assimilate_storage(&mut storage) + .expect("PSM genesis assimilates; qed"); + + let mut ext: sp_io::TestExternalities = storage.into(); + ext.execute_with(|| { + System::set_block_number(1); + pallet_psm::mock::set_mock_maximum_issuance(MAX_PSM_ISSUANCE); + }); + ext +} + +// --------------------------------------------------------------------------- +// Dispatch (runs inside externalities, reads storage via fh::*) +// --------------------------------------------------------------------------- + +fn dispatch_op(op: &Op) { + match op { + Op::Mint { account_idx, asset_idx, tier } => { + // The effective cap is the minimum of four independent limits that must + // all be satisfied for a mint to succeed: per-asset ceiling remaining, + // global PSM debt remaining, pUSD issuance remaining, and the caller's + // external-asset balance. Taking the min ensures the fuzzer generates + // amounts that are feasible under at least one limit, letting coverage + // guidance discover which limit is the binding constraint. + let asset_id = + ALL_EXTERNAL_ASSETS[(*asset_idx % ALL_EXTERNAL_ASSETS.len() as u8) as usize]; + let account = (*account_idx % N_ACCOUNTS + 1) as u64; + if !fh::is_approved_asset(asset_id) { + return; + } + let debt = fh::psm_debt(asset_id); + let ceiling = fh::max_asset_debt(asset_id); + let remaining = ceiling.saturating_sub(debt); + let global_remaining = fh::max_psm_debt().saturating_sub(fh::total_psm_debt()); + let balance = Assets::balance(asset_id, account); + let issuance_remaining = pallet_psm::mock::MockMaximumIssuance::get() + .saturating_sub(Assets::total_issuance(PUSD_ASSET_ID)); + let effective_cap = + remaining.min(global_remaining).min(issuance_remaining).min(balance); + let amount = resolve_amount(tier, effective_cap); + if amount >= MIN_SWAP { + let _ = Psm::mint(RuntimeOrigin::signed(account), asset_id, amount); + } + }, + Op::Redeem { account_idx, asset_idx, tier } => { + let asset_id = + ALL_EXTERNAL_ASSETS[(*asset_idx % ALL_EXTERNAL_ASSETS.len() as u8) as usize]; + let account = (*account_idx % N_ACCOUNTS + 1) as u64; + if !fh::is_approved_asset(asset_id) { + return; + } + let debt = fh::psm_debt(asset_id); + let user_pusd = Assets::balance(PUSD_ASSET_ID, account); + let effective_cap = debt.min(user_pusd); + let amount = resolve_amount(tier, effective_cap); + if amount >= MIN_SWAP { + let _ = Psm::redeem(RuntimeOrigin::signed(account), asset_id, amount); + } + }, + // When force_below_debt is true, the ratio is computed so that max_psm_debt ends up + // below the current total_psm_debt, creating a transient invariant violation. This + // stresses the pallet's handling of the state where governance has overshot — + // subsequent mints must be rejected, and the violation must persist until either + // debt is redeemed or the ceiling is raised. + Op::SetMaxPsmDebt { force_below_debt, parts } => { + let ratio = if *force_below_debt { + let debt = fh::total_psm_debt(); + let max_issuance = pallet_psm::mock::MockMaximumIssuance::get(); + if debt == 0 || max_issuance == 0 { + Permill::from_parts(parts % 1_000_001) + } else { + let below_debt = Permill::from_rational(debt / 2, max_issuance); + below_debt.min(Permill::from_parts(parts % below_debt.deconstruct() as u32)) + } + } else { + Permill::from_parts(parts % 1_000_001) + }; + let _ = Psm::set_max_psm_debt(RuntimeOrigin::root(), ratio); + }, + Op::SetAssetCeilingWeight { asset_idx, parts } => { + let asset_id = + ALL_EXTERNAL_ASSETS[(*asset_idx % ALL_EXTERNAL_ASSETS.len() as u8) as usize]; + let weight = Permill::from_parts(parts % 1_000_001); + let _ = Psm::set_asset_ceiling_weight(RuntimeOrigin::root(), asset_id, weight); + }, + Op::SetMintingFee { asset_idx, parts } => { + let asset_id = + ALL_EXTERNAL_ASSETS[(*asset_idx % ALL_EXTERNAL_ASSETS.len() as u8) as usize]; + let fee = Permill::from_parts(parts % 1_000_001); + let _ = Psm::set_minting_fee(RuntimeOrigin::root(), asset_id, fee); + }, + Op::SetRedemptionFee { asset_idx, parts } => { + let asset_id = + ALL_EXTERNAL_ASSETS[(*asset_idx % ALL_EXTERNAL_ASSETS.len() as u8) as usize]; + let fee = Permill::from_parts(parts % 1_000_001); + let _ = Psm::set_redemption_fee(RuntimeOrigin::root(), asset_id, fee); + }, + Op::SetAssetStatus { asset_idx, level } => { + let asset_id = + ALL_EXTERNAL_ASSETS[(*asset_idx % ALL_EXTERNAL_ASSETS.len() as u8) as usize]; + if !fh::is_approved_asset(asset_id) { + return; + } + let status = match level % 3 { + 0 => CircuitBreakerLevel::AllEnabled, + 1 => CircuitBreakerLevel::MintingDisabled, + _ => CircuitBreakerLevel::AllDisabled, + }; + let _ = Psm::set_asset_status(RuntimeOrigin::root(), asset_id, status); + }, + // Newly-added assets receive a non-zero ceiling weight immediately via the paired + // set_asset_ceiling_weight call. Without this, a freshly-added asset has zero ceiling + // and every subsequent mint would fail trivially with OverCeiling — producing no + // useful coverage signal. + Op::AddExternalAsset { asset_idx } => { + let candidates: Vec = ALL_EXTERNAL_ASSETS + .iter() + .filter(|&&id| !fh::is_approved_asset(id)) + .copied() + .collect(); + if candidates.is_empty() { + return; + } + let asset_id = candidates[(*asset_idx as usize) % candidates.len()]; + if Psm::add_external_asset(RuntimeOrigin::root(), asset_id).is_ok() { + let weight = Permill::from_parts(((*asset_idx as u32) % 1_000_001).max(1)); + let _ = Psm::set_asset_ceiling_weight(RuntimeOrigin::root(), asset_id, weight); + } + }, + Op::RemoveExternalAsset { asset_idx } => { + let candidates: Vec = + fh::approved_assets().into_iter().filter(|&id| fh::psm_debt(id) == 0).collect(); + if candidates.is_empty() { + return; + } + let asset_id = candidates[(*asset_idx as usize) % candidates.len()]; + let _ = Psm::remove_external_asset(RuntimeOrigin::root(), asset_id); + }, + } +} + +// --------------------------------------------------------------------------- +// Fuzzer entry point +// --------------------------------------------------------------------------- + +// Each fuzz input produces a multi-block sequence. Within each block, all ops execute +// against the same block number, then do_try_state validates pallet invariants. The +// check runs per-block rather than per-call because PSM invariants are defined at block +// boundaries — governance can create transient violations mid-block (e.g. lowering the +// ceiling below current debt) that are resolved by subsequent calls within the same block. +fuzz_target!(|input: MultiBlockOps| { + let mut ext = build_fuzzer_genesis(); + let mut block_number: u32 = 1; + + // Log generated ops for post-mortem debugging. + if let Ok(mut log_file) = OpenOptions::new().create(true).append(true).open("psm-fuzz.log") { + let _ = writeln!(log_file, "\n{}", "=".repeat(80)); + let _ = writeln!(log_file, "NEW FUZZER INPUT - {} blocks", input.0.len()); + let _ = writeln!(log_file, "{}", "=".repeat(80)); + for (bi, block) in input.0.iter().enumerate() { + let _ = writeln!(log_file, "Block {} ({} ops):", bi + 1, block.0.len()); + for (oi, op) in block.0.iter().enumerate() { + let _ = writeln!(log_file, " Op {}: {:?}", oi + 1, op); + } + } + } + + for block in input.0.iter() { + ext.execute_with(|| { + System::set_block_number(block_number.into()); + for op in &block.0 { + dispatch_op(op); + } + fh::do_try_state().expect("PSM invariant violated"); + }); + block_number += 1; + } +}); diff --git a/substrate/frame/psm/fuzz/fuzz_targets/psm_stateful.rs b/substrate/frame/psm/fuzz/fuzz_targets/psm_stateful.rs new file mode 100644 index 0000000000000..031da9741abd3 --- /dev/null +++ b/substrate/frame/psm/fuzz/fuzz_targets/psm_stateful.rs @@ -0,0 +1,749 @@ +//! Stateful property-based tester for pallet-psm. +//! +//! Generates semantically rich, state-aware command sequences that stress PSM +//! invariants. Reads pallet storage between each generated command to produce +//! domain-informed inputs. Runs `do_try_state()` after every command and +//! panics on violation. Uses deterministic seeding for reproducibility. +//! +//! Usage: `psm_stateful [seed] [max_commands]` + +use frame_support::traits::fungibles::Inspect; +use pallet_psm::mock::fuzz_helpers as fh; +use pallet_psm::mock::{ + set_mock_maximum_issuance, Assets, MockMaximumIssuance, Psm, RuntimeOrigin, System, Test, + ALL_EXTERNAL_ASSETS, DAI_ASSET_ID, FRAX_ASSET_ID, INSURANCE_FUND, PUSD_ASSET_ID, PUSD_UNIT, + USDC_ASSET_ID, USDP_ASSET_ID, USDT_ASSET_ID, +}; +use pallet_psm::CircuitBreakerLevel; +use rand::seq::SliceRandom; +use rand::{rngs::StdRng, Rng, SeedableRng}; +use sp_io::TestExternalities; +use sp_runtime::{BuildStorage, Permill}; +use std::env; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +// Constants match psm.rs but are duplicated here because this is a separate binary +// with no shared state. The values have the same rationale: finite balances to allow +// the fuzzer to discover rejection paths, and a 20M issuance cap for ceiling headroom. +const N_ACCOUNTS: u8 = 10; +const MIN_SWAP: u128 = 100 * PUSD_UNIT; +const INITIAL_EXTERNAL_BALANCE: u128 = 500_000 * PUSD_UNIT; +const INITIAL_NATIVE_BALANCE: u128 = 1_000_000 * PUSD_UNIT; +const MAX_PSM_ISSUANCE: u128 = 20_000_000 * PUSD_UNIT; + +// --------------------------------------------------------------------------- +// Command enum +// --------------------------------------------------------------------------- + +#[derive(Debug)] +enum Command { + Mint { account: u64, asset_id: u32, amount: u128 }, + Redeem { account: u64, asset_id: u32, amount: u128 }, + SetMaxPsmDebt { ratio: Permill }, + SetAssetCeilingWeight { asset_id: u32, weight: Permill }, + SetMintingFee { asset_id: u32, fee: Permill }, + SetRedemptionFee { asset_id: u32, fee: Permill }, + SetAssetStatus { asset_id: u32, status: CircuitBreakerLevel }, + AddExternalAsset { asset_id: u32, weight: Permill }, + RemoveExternalAsset { asset_id: u32 }, +} + +// --------------------------------------------------------------------------- +// State snapshot structs +// --------------------------------------------------------------------------- + +// FuzzState captures ALL relevant pallet state in a single snapshot so that generators +// can make informed decisions. Per-account balances are included so generators can pick +// accounts that actually have sufficient funds to complete an operation, rather than +// wasting commands on trivially-failing mints from empty accounts. +#[derive(Debug)] +#[allow(dead_code)] +struct AssetState { + asset_id: u32, + debt: u128, + ceiling: u128, + remaining_ceiling: u128, + reserve: u128, + minting_fee: Permill, + redemption_fee: Permill, + weight: Permill, + account_external: Vec, + account_pusd: Vec, +} + +#[derive(Debug)] +#[allow(dead_code)] +struct FuzzState { + assets: Vec, + unapproved: Vec, + max_psm_debt: u128, + total_psm_debt: u128, + max_psm_debt_ratio: Permill, + max_issuance: u128, + total_pusd_issuance: u128, + block_number: u32, +} + +// --------------------------------------------------------------------------- +// FuzzState helper predicates +// --------------------------------------------------------------------------- + +impl FuzzState { + fn any_asset_can_mint(&self) -> bool { + self.assets.iter().any(|a| { + a.remaining_ceiling >= MIN_SWAP && a.account_external.iter().any(|&b| b >= MIN_SWAP) + }) + } + + fn any_asset_can_redeem(&self) -> bool { + self.assets + .iter() + .any(|a| a.debt >= MIN_SWAP && a.account_pusd.iter().any(|&b| b >= MIN_SWAP)) + } + + fn any_asset_near_ceiling(&self) -> bool { + self.assets + .iter() + .any(|a| a.ceiling > 0 && a.remaining_ceiling <= a.ceiling / 10) + } +} + +// --------------------------------------------------------------------------- +// Genesis setup (copied from psm.rs — separate binary, no shared state) +// --------------------------------------------------------------------------- + +// Genesis mirrors psm.rs exactly: 6 assets created, 2 PSM-approved (USDC 60%, USDT 40%), +// 3 unapproved for AddExternalAsset testing, all funded across all accounts. +// See psm.rs build_fuzzer_genesis comments for the full rationale. +fn build_fuzzer_genesis() -> TestExternalities { + let mut storage = as Default>::default() + .build_storage() + .expect("system genesis storage builds; qed"); + + let accounts: Vec = (1..=N_ACCOUNTS as u64).collect(); + + pallet_balances::GenesisConfig:: { + balances: accounts + .iter() + .map(|&a| (a, INITIAL_NATIVE_BALANCE)) + .chain(std::iter::once((INSURANCE_FUND, 1))) + .collect(), + ..Default::default() + } + .assimilate_storage(&mut storage) + .expect("balances genesis assimilates; qed"); + + let asset_owner: u64 = 1; + pallet_assets::GenesisConfig:: { + assets: vec![ + (PUSD_ASSET_ID, asset_owner, true, 1), + (USDC_ASSET_ID, asset_owner, true, 1), + (USDT_ASSET_ID, asset_owner, true, 1), + (DAI_ASSET_ID, asset_owner, true, 1), + (USDP_ASSET_ID, asset_owner, true, 1), + (FRAX_ASSET_ID, asset_owner, true, 1), + ], + metadata: vec![ + (PUSD_ASSET_ID, b"pUSD Stablecoin".to_vec(), b"pUSD".to_vec(), 6), + (USDC_ASSET_ID, b"USD Coin".to_vec(), b"USDC".to_vec(), 6), + (USDT_ASSET_ID, b"Tether USD".to_vec(), b"USDT".to_vec(), 6), + (DAI_ASSET_ID, b"Dai Stablecoin".to_vec(), b"DAI".to_vec(), 6), + (USDP_ASSET_ID, b"Pax Dollar".to_vec(), b"USDP".to_vec(), 6), + (FRAX_ASSET_ID, b"Frax".to_vec(), b"FRAX".to_vec(), 6), + ], + accounts: accounts + .iter() + .flat_map(|&a| { + ALL_EXTERNAL_ASSETS.iter().map(move |&id| (id, a, INITIAL_EXTERNAL_BALANCE)) + }) + .collect(), + ..Default::default() + } + .assimilate_storage(&mut storage) + .expect("assets genesis assimilates; qed"); + + pallet_psm::GenesisConfig:: { + max_psm_debt_of_total: Permill::from_percent(50), + asset_configs: [ + ( + USDC_ASSET_ID, + (Permill::from_percent(1), Permill::from_percent(1), Permill::from_percent(60)), + ), + ( + USDT_ASSET_ID, + (Permill::from_percent(1), Permill::from_percent(1), Permill::from_percent(40)), + ), + ] + .into_iter() + .collect(), + _marker: Default::default(), + } + .assimilate_storage(&mut storage) + .expect("PSM genesis assimilates; qed"); + + let mut ext: TestExternalities = storage.into(); + ext.execute_with(|| { + System::set_block_number(1); + set_mock_maximum_issuance(MAX_PSM_ISSUANCE); + }); + ext +} + +// --------------------------------------------------------------------------- +// State snapshot — reads ALL relevant storage before each command +// --------------------------------------------------------------------------- + +fn snapshot_state() -> FuzzState { + let approved = fh::approved_assets(); + let max_issuance = MockMaximumIssuance::get(); + let total_pusd_issuance = Assets::total_issuance(PUSD_ASSET_ID); + + let mut assets = Vec::with_capacity(approved.len()); + for &asset_id in &approved { + let debt = fh::psm_debt(asset_id); + let ceiling = fh::max_asset_debt(asset_id); + let remaining_ceiling = ceiling.saturating_sub(debt); + let reserve = fh::get_reserve(asset_id); + let minting_fee = fh::minting_fee(asset_id); + let redemption_fee = fh::redemption_fee(asset_id); + let weight = fh::asset_ceiling_weight(asset_id); + + let mut account_external = Vec::with_capacity(N_ACCOUNTS as usize); + let mut account_pusd = Vec::with_capacity(N_ACCOUNTS as usize); + for account in 1..=N_ACCOUNTS as u64 { + account_external.push(Assets::balance(asset_id, account)); + account_pusd.push(Assets::balance(PUSD_ASSET_ID, account)); + } + + assets.push(AssetState { + asset_id, + debt, + ceiling, + remaining_ceiling, + reserve, + minting_fee, + redemption_fee, + weight, + account_external, + account_pusd, + }); + } + + let unapproved: Vec = ALL_EXTERNAL_ASSETS + .iter() + .filter(|&&id| !fh::is_approved_asset(id)) + .copied() + .collect(); + + // Test block numbers always fit in u32; qed + let block_number: u32 = TryInto::::try_into(System::block_number()).unwrap_or(0); + + FuzzState { + assets, + unapproved, + max_psm_debt: fh::max_psm_debt(), + total_psm_debt: fh::total_psm_debt(), + max_psm_debt_ratio: fh::max_psm_debt_ratio(), + max_issuance, + total_pusd_issuance, + block_number, + } +} + +// --------------------------------------------------------------------------- +// Weighted selection helpers +// --------------------------------------------------------------------------- + +// Weighted selection: assets with more remaining capacity (or debt, depending on weight_fn) +// are picked more often. This biases toward operations that will actually change state +// rather than trivially fail, making each fuzz command more likely to advance coverage. +fn pick_asset_weighted(rng: &mut StdRng, state: &FuzzState, weight_fn: F) -> usize +where + F: Fn(&AssetState) -> u128, +{ + // Caller guarantees assets non-empty; qed + if state.assets.is_empty() { + return 0; + } + let weights: Vec = state.assets.iter().map(|a| weight_fn(a).max(1)).collect(); + let total: u128 = weights.iter().sum(); + if total == 0 { + return 0; + } + let mut pick = rng.gen_range(0..total); + for (i, w) in weights.iter().enumerate() { + if pick < *w { + return i; + } + pick -= w; + } + 0 +} + +// Same weighted selection for accounts: accounts with higher balances are more likely +// to produce interesting mints (they can actually complete the operation). Accounts with +// zero balance still have weight 1 (via .max(1)) so they are occasionally selected, +// exercising the insufficient-balance rejection path. +fn pick_richest_account(rng: &mut StdRng, balances: &[u128]) -> (u64, u128) { + // balances always has N_ACCOUNTS entries; qed + if balances.is_empty() { + return (1, 0); + } + let weights: Vec = balances.iter().map(|&b| b.max(1)).collect(); + let total: u128 = weights.iter().sum(); + if total == 0 { + return (1, 0); + } + let mut pick = rng.gen_range(0..total); + for (i, w) in weights.iter().enumerate() { + if pick < *w { + return ((i + 1) as u64, balances[i]); + } + pick -= w; + } + (1, balances[0]) +} + +// --------------------------------------------------------------------------- +// Boundary-aware amount pickers +// --------------------------------------------------------------------------- + +// 10-way boundary distribution: each variant targets a specific pallet check path. +// Variant 0: BelowMinimumSwap minimum. 1: exact ceiling hit. 2: one step below ceiling. +// 3: overdraft past ceiling. 4-5: mid-range (half/third). 6-7: off-by-one boundaries. +// 8: uniform random within valid range. 9: near-boundary with random offset. +fn pick_mint_amount( + rng: &mut StdRng, + asset: &AssetState, + account_balance: u128, + state: &FuzzState, +) -> u128 { + let remaining = asset.remaining_ceiling; + let global_remaining = state.max_psm_debt.saturating_sub(state.total_psm_debt); + let issuance_remaining = state.max_issuance.saturating_sub(state.total_pusd_issuance); + let effective_cap = + remaining.min(global_remaining).min(issuance_remaining).min(account_balance); + + if effective_cap < MIN_SWAP { + return MIN_SWAP; // BelowMinimumSwap path + } + + match rng.gen_range(0..=9) { + 0 => MIN_SWAP, // smallest valid + 1 => effective_cap, // exact ceiling hit + 2 => effective_cap.saturating_sub(MIN_SWAP).max(MIN_SWAP), // one step below + 3 => effective_cap.saturating_add(MIN_SWAP), // overdraft + 4 => effective_cap / 2, // half + 5 => effective_cap / 3, // third + 6 => effective_cap.saturating_sub(1).max(MIN_SWAP), // off-by-one below + 7 => effective_cap.saturating_add(1), // off-by-one over + 8 => rng.gen_range(MIN_SWAP..=effective_cap), // uniform in range + _ => effective_cap.saturating_sub(rng.gen_range(0..MIN_SWAP)).max(MIN_SWAP), // near boundary + } +} + +fn pick_redeem_amount(rng: &mut StdRng, asset: &AssetState, pusd_balance: u128) -> u128 { + let effective_cap = asset.debt.min(pusd_balance); + + if effective_cap < MIN_SWAP { + return MIN_SWAP; // BelowMinimumSwap path + } + + match rng.gen_range(0..=7) { + 0 => MIN_SWAP, + 1 => effective_cap, + 2 => effective_cap.saturating_sub(MIN_SWAP).max(MIN_SWAP), + 3 => effective_cap.saturating_add(MIN_SWAP), + 4 => effective_cap / 2, + 5 => effective_cap.saturating_sub(1).max(MIN_SWAP), + 6 => effective_cap.saturating_add(1), + _ => rng.gen_range(MIN_SWAP..=effective_cap), + } +} + +// --------------------------------------------------------------------------- +// Specific generators (domain knowledge) +// --------------------------------------------------------------------------- + +fn gen_mint(rng: &mut StdRng, state: &FuzzState) -> Command { + if state.assets.is_empty() { + return Command::Mint { account: 1, asset_id: USDC_ASSET_ID, amount: MIN_SWAP }; + } + let idx = pick_asset_weighted(rng, state, |a| a.remaining_ceiling); + let asset = &state.assets[idx]; + let (account, balance) = pick_richest_account(rng, &asset.account_external); + let amount = pick_mint_amount(rng, asset, balance, state); + Command::Mint { account, asset_id: asset.asset_id, amount } +} + +fn gen_redeem(rng: &mut StdRng, state: &FuzzState) -> Command { + if state.assets.is_empty() { + return Command::Redeem { account: 1, asset_id: USDC_ASSET_ID, amount: MIN_SWAP }; + } + let idx = pick_asset_weighted(rng, state, |a| a.debt); + let asset = &state.assets[idx]; + let (account, pusd_balance) = pick_richest_account(rng, &asset.account_pusd); + let amount = pick_redeem_amount(rng, asset, pusd_balance); + Command::Redeem { account, asset_id: asset.asset_id, amount } +} + +// The most interesting generator: deliberately lowers max_psm_debt below the current +// total_psm_debt, creating a transient invariant violation. The ratio is computed as a +// random fraction (10-90%) of the debt-to-issuance ratio, guaranteeing the new cap is +// strictly below the outstanding debt. Subsequent operations must handle this state +// correctly — mints should be blocked, and the violation should persist until redeemed. +fn gen_lower_ceiling_below_debt(rng: &mut StdRng, state: &FuzzState) -> Command { + let debt = state.total_psm_debt; + let max_issuance = state.max_issuance; + // max_issuance > 0: guarded by debt > 0 and debt <= max_psm_debt <= ratio * max_issuance; qed + let max_ratio = Permill::from_rational(debt, max_issuance.max(1)); + let factor = Permill::from_percent(rng.gen_range(10..90)); + // Permill * Permill: multiply raw parts then divide by 1M + let max_raw = max_ratio.deconstruct() as u128; + let factor_raw = factor.deconstruct() as u128; + let target_raw = max_raw * factor_raw / 1_000_000; + let ratio = Permill::from_parts(target_raw.min(1_000_000) as u32); + Command::SetMaxPsmDebt { ratio } +} + +fn gen_raise_ceiling(rng: &mut StdRng, state: &FuzzState) -> Command { + let current_parts = state.max_psm_debt_ratio.deconstruct() as u128; + let increment = rng.gen_range(1..500_000) as u128; + let new_parts = (current_parts + increment).min(1_000_000); + Command::SetMaxPsmDebt { ratio: Permill::from_parts(new_parts as u32) } +} + +// 50/50 split in weight assignment: half the time the new weight is set below what is +// needed for the current debt (creating a per-asset ceiling violation), and half the +// time it is set to a random value. The ceiling-violation path is the most interesting +// because it exercises the pallet's handling of debt that exceeds its allocated ceiling. +fn gen_redistribute_weights(rng: &mut StdRng, state: &FuzzState) -> Command { + let debt_assets: Vec<&AssetState> = state.assets.iter().filter(|a| a.debt > 0).collect(); + // Non-empty guaranteed by guard condition; qed + let asset = debt_assets.choose(rng).expect("at least one asset with debt"); + + let new_weight = if state.max_psm_debt > 0 { + let debt_ratio = Permill::from_rational(asset.debt, state.max_psm_debt); + if rng.gen_bool(0.5) { + // Set weight below what's needed for current debt + Permill::from_parts((debt_ratio.deconstruct() as u128 / 2).min(1_000_000) as u32) + } else { + Permill::from_parts(rng.gen_range(1..=1_000_000)) + } + } else { + Permill::from_parts(rng.gen_range(1..=1_000_000)) + }; + + Command::SetAssetCeilingWeight { asset_id: asset.asset_id, weight: new_weight } +} + +fn gen_toggle_breaker_with_debt(rng: &mut StdRng, state: &FuzzState) -> Command { + let debt_assets: Vec<&AssetState> = state.assets.iter().filter(|a| a.debt > 0).collect(); + // Non-empty guaranteed by guard condition; qed + let asset = debt_assets.choose(rng).expect("at least one"); + + let status = match rng.gen_range(0..=2) { + 0 => CircuitBreakerLevel::MintingDisabled, // most interesting: blocks new debt + 1 => CircuitBreakerLevel::AllDisabled, + _ => CircuitBreakerLevel::AllEnabled, // re-enable + }; + + Command::SetAssetStatus { asset_id: asset.asset_id, status } +} + +// The 100% fee variant is important because the mint still succeeds and increments debt, +// but the user receives zero pUSD output. This exercises the pallet's fee-application +// logic at its extreme — the debt accounting must remain correct even when the entire +// mint amount is absorbed by the fee. +fn gen_extreme_fee(rng: &mut StdRng, state: &FuzzState) -> Command { + if state.assets.is_empty() { + return Command::SetMintingFee { asset_id: USDC_ASSET_ID, fee: Permill::zero() }; + } + // Non-empty verified above; qed + let asset = state.assets.choose(rng).expect("at least one asset"); + let fee = match rng.gen_range(0..=4) { + 0 => Permill::zero(), // no fee + 1 => Permill::from_percent(1), // 1% + 2 => Permill::from_percent(50), // 50% + 3 => Permill::from_parts(999_900), // 99.99% + _ => Permill::one(), // 100% — zero output, still counts + }; + + if rng.gen_bool(0.5) { + Command::SetMintingFee { asset_id: asset.asset_id, fee } + } else { + Command::SetRedemptionFee { asset_id: asset.asset_id, fee } + } +} + +fn gen_add_asset_with_weight(rng: &mut StdRng, state: &FuzzState) -> Command { + // Non-empty guaranteed by guard condition; qed + let &asset_id = state.unapproved.choose(rng).expect("at least one unapproved"); + let weight = Permill::from_parts(rng.gen_range(1..=1_000_000)); + Command::AddExternalAsset { asset_id, weight } +} + +fn gen_remove_zero_debt_asset(rng: &mut StdRng, state: &FuzzState) -> Command { + let zero_debt: Vec<&AssetState> = state.assets.iter().filter(|a| a.debt == 0).collect(); + // Non-empty guaranteed by guard condition; qed + let asset = zero_debt.choose(rng).expect("at least one zero-debt asset"); + Command::RemoveExternalAsset { asset_id: asset.asset_id } +} + +// --------------------------------------------------------------------------- +// State-aware command generation with weighted selection +// --------------------------------------------------------------------------- + +// Dynamic candidate list: generators are only added when their preconditions are met. +// For example, "lower ceiling below debt" is only offered when total_psm_debt > 0, +// and "add external asset" only when unapproved assets exist. This avoids wasting +// commands on operations that would trivially fail due to unsatisfied preconditions, +// directing entropy toward state-changing operations instead. +fn gen_command(rng: &mut StdRng, state: &FuzzState) -> Command { + let mut candidates: Vec<(u32, fn(&mut StdRng, &FuzzState) -> Command)> = Vec::new(); + + // Mint: interesting if any asset has remaining ceiling AND any account has balance + if state.any_asset_can_mint() { + candidates.push((20, gen_mint)); + } + + // Redeem: interesting if any asset has debt AND any account has pUSD + if state.any_asset_can_redeem() { + candidates.push((20, gen_redeem)); + } + + // Lower ceiling below current debt: HIGH interestingness — tests transient violations + if state.total_psm_debt > 0 && state.max_psm_debt > 0 { + candidates.push((8, gen_lower_ceiling_below_debt)); + } + + // Raise ceiling: interesting if debt is near ceiling + if state.any_asset_near_ceiling() { + candidates.push((5, gen_raise_ceiling)); + } + + // Change weights while debt exists: redistributes ceilings + if state.assets.iter().any(|a| a.debt > 0 && a.weight > Permill::zero()) { + candidates.push((8, gen_redistribute_weights)); + } + + // Toggle circuit breaker while debt exists + if state.assets.iter().any(|a| a.debt > 0) { + candidates.push((6, gen_toggle_breaker_with_debt)); + } + + // Set fee to extreme values (0%, 1%, 50%, 99.99%, 100%) + candidates.push((4, gen_extreme_fee)); + + // Add external asset (if any unapproved exist) + if !state.unapproved.is_empty() { + candidates.push((2, gen_add_asset_with_weight)); + } + + // Remove asset with zero debt + if state.assets.iter().any(|a| a.debt == 0) { + candidates.push((2, gen_remove_zero_debt_asset)); + } + + // Always have at least one candidate + if candidates.is_empty() { + candidates.push((1, gen_mint)); + } + + // Weighted random selection + let total: u32 = candidates.iter().map(|(w, _)| *w).sum(); + if total == 0 { + return gen_mint(rng, state); + } + let mut pick = rng.gen_range(0..total); + for (weight, gen_fn) in &candidates { + if pick < *weight { + return gen_fn(rng, state); + } + pick -= weight; + } + (candidates[0].1)(rng, state) +} + +// --------------------------------------------------------------------------- +// Command execution (dispatches via Psm::*, ignores expected errors) +// --------------------------------------------------------------------------- + +fn execute_command(cmd: &Command) { + match cmd { + Command::Mint { account, asset_id, amount } => { + if fh::is_approved_asset(*asset_id) { + let _ = Psm::mint(RuntimeOrigin::signed(*account), *asset_id, *amount); + } + }, + Command::Redeem { account, asset_id, amount } => { + if fh::is_approved_asset(*asset_id) { + let _ = Psm::redeem(RuntimeOrigin::signed(*account), *asset_id, *amount); + } + }, + Command::SetMaxPsmDebt { ratio } => { + let _ = Psm::set_max_psm_debt(RuntimeOrigin::root(), *ratio); + }, + Command::SetAssetCeilingWeight { asset_id, weight } => { + if fh::is_approved_asset(*asset_id) { + let _ = Psm::set_asset_ceiling_weight(RuntimeOrigin::root(), *asset_id, *weight); + } + }, + Command::SetMintingFee { asset_id, fee } => { + if fh::is_approved_asset(*asset_id) { + let _ = Psm::set_minting_fee(RuntimeOrigin::root(), *asset_id, *fee); + } + }, + Command::SetRedemptionFee { asset_id, fee } => { + if fh::is_approved_asset(*asset_id) { + let _ = Psm::set_redemption_fee(RuntimeOrigin::root(), *asset_id, *fee); + } + }, + Command::SetAssetStatus { asset_id, status } => { + if fh::is_approved_asset(*asset_id) { + let _ = Psm::set_asset_status(RuntimeOrigin::root(), *asset_id, *status); + } + }, + // Paired add+weight: mirrors the libFuzzer target's approach. Without setting + // a non-zero ceiling weight immediately after adding, the new asset would have + // zero ceiling and all subsequent mints would fail trivially. + Command::AddExternalAsset { asset_id, weight } => { + if Psm::add_external_asset(RuntimeOrigin::root(), *asset_id).is_ok() { + let _ = Psm::set_asset_ceiling_weight(RuntimeOrigin::root(), *asset_id, *weight); + } + }, + Command::RemoveExternalAsset { asset_id } => { + let _ = Psm::remove_external_asset(RuntimeOrigin::root(), *asset_id); + }, + } +} + +// --------------------------------------------------------------------------- +// Logging helpers +// --------------------------------------------------------------------------- + +fn asset_name(id: u32) -> &'static str { + match id { + USDC_ASSET_ID => "USDC", + USDT_ASSET_ID => "USDT", + DAI_ASSET_ID => "DAI", + USDP_ASSET_ID => "USDP", + FRAX_ASSET_ID => "FRAX", + _ => "???", + } +} + +fn format_amount(raw: u128) -> String { + let tokens = raw / PUSD_UNIT; + if tokens >= 1_000_000 { + format!("{:.1}M", tokens as f64 / 1_000_000.0) + } else if tokens >= 1_000 { + format!("{:.1}K", tokens as f64 / 1_000.0) + } else { + format!("{}", tokens) + } +} + +fn format_command(cmd: &Command) -> String { + match cmd { + Command::Mint { account, asset_id, amount } => { + format!("Mint(acct={}, {}, {})", account, asset_name(*asset_id), amount) + }, + Command::Redeem { account, asset_id, amount } => { + format!("Redeem(acct={}, {}, {})", account, asset_name(*asset_id), amount) + }, + Command::SetMaxPsmDebt { ratio } => { + format!("SetMaxPsmDebt({:.3}%)", ratio.deconstruct() as f64 / 10_000.0) + }, + Command::SetAssetCeilingWeight { asset_id, weight } => format!( + "SetWeight({}, {:.3}%)", + asset_name(*asset_id), + weight.deconstruct() as f64 / 10_000.0 + ), + Command::SetMintingFee { asset_id, fee } => format!( + "SetMintFee({}, {:.3}%)", + asset_name(*asset_id), + fee.deconstruct() as f64 / 10_000.0 + ), + Command::SetRedemptionFee { asset_id, fee } => format!( + "SetRedeemFee({}, {:.3}%)", + asset_name(*asset_id), + fee.deconstruct() as f64 / 10_000.0 + ), + Command::SetAssetStatus { asset_id, status } => { + format!("SetStatus({}, {:?})", asset_name(*asset_id), status) + }, + Command::AddExternalAsset { asset_id, weight } => format!( + "AddAsset({}, w={:.3}%)", + asset_name(*asset_id), + weight.deconstruct() as f64 / 10_000.0 + ), + Command::RemoveExternalAsset { asset_id } => { + format!("RemoveAsset({})", asset_name(*asset_id)) + }, + } +} + +fn log_command(step: usize, cmd: &Command, state: &FuzzState) { + let total_reserve: u128 = state.assets.iter().map(|a| a.reserve).sum(); + eprintln!( + "[{:>4}] {} | debt={}/{} issuance={}/{} reserve={}", + step, + format_command(cmd), + format_amount(state.total_psm_debt), + format_amount(state.max_psm_debt), + format_amount(state.total_pusd_issuance), + format_amount(state.max_issuance), + format_amount(total_reserve), + ); +} + +// --------------------------------------------------------------------------- +// Campaign runner +// --------------------------------------------------------------------------- + +// Per-command try_state is stricter than the libFuzzer target's per-block check. Since +// this tester generates semantically aware commands (not raw fuzz bytes), it can assert +// invariants after every single call. A violation here means a specific command sequence +// broke a pallet invariant, and the deterministic seed makes it reproducible. +fn run_campaign(seed: u64, max_commands: usize) { + let mut ext = build_fuzzer_genesis(); + let mut rng = StdRng::seed_from_u64(seed); + let mut block_number: u32 = 1; + + // Random block advancement interval + let mut next_block_at: usize = rng.gen_range(10..50); + + ext.execute_with(|| { + for i in 0..max_commands { + // Multi-block: advance block number periodically + if i > 0 && i == next_block_at { + block_number += 1; + System::set_block_number(block_number.into()); + next_block_at = i + rng.gen_range(10..50); + } + + let state = snapshot_state(); + let cmd = gen_command(&mut rng, &state); + log_command(i, &cmd, &state); + execute_command(&cmd); + fh::do_try_state().expect("PSM invariant violated — see command log above"); + } + }); +} + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- + +fn main() { + let seed: u64 = env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or_else(rand::random); + let max_commands: usize = env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(10_000); + + eprintln!("PSM stateful tester: seed={}, max_commands={}", seed, max_commands); + run_campaign(seed, max_commands); + eprintln!("Campaign complete: {} commands, 0 invariant violations", max_commands); +} From e41240bf046832d69bba5dbabd8e3bde1032d905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Sat, 18 Apr 2026 15:31:38 +0000 Subject: [PATCH 09/10] Change one try_state check to warn, not error --- substrate/frame/psm/src/lib.rs | 37 +++++++++++++++++++++----------- substrate/frame/psm/src/tests.rs | 34 ++++++++++++----------------- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/substrate/frame/psm/src/lib.rs b/substrate/frame/psm/src/lib.rs index 987aae54eaa8c..913ec2e7ddb31 100644 --- a/substrate/frame/psm/src/lib.rs +++ b/substrate/frame/psm/src/lib.rs @@ -72,8 +72,8 @@ pub mod weights; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; -#[cfg(test)] -mod mock; +#[cfg(any(test, feature = "fuzzing"))] +pub mod mock; #[cfg(test)] mod tests; @@ -926,7 +926,7 @@ pub mod pallet { } /// Check if an asset is approved for PSM swaps. - #[cfg(test)] + #[cfg(any(test, feature = "fuzzing"))] pub(crate) fn is_approved_asset(asset_id: &T::AssetId) -> bool { ExternalAssets::::contains_key(asset_id) } @@ -978,21 +978,32 @@ pub mod pallet { ); // Check 4: Total PSM debt must not exceed the global ceiling. - // May be transiently violated if governance lowers MaxPsmDebtOfTotal after debt - // has already been accumulated. Checked before per-asset ceilings so it fires - // independently when only the global ceiling has been lowered. - ensure!( - Self::total_psm_debt() <= Self::max_psm_debt(), - "Total PSM debt exceeds global ceiling" - ); + // This is a warning, not a hard invariant: governance may lower MaxPsmDebtOfTotal + // below current debt, creating a transient state that redemptions or further + // governance action can resolve. + if Self::total_psm_debt() > Self::max_psm_debt() { + log::warn!( + "PSM: total debt ({:?}) exceeds global ceiling ({:?})", + Self::total_psm_debt(), + Self::max_psm_debt(), + ); + } - // Check 5: Per-asset debt should not exceed its ceiling. - // (May be transiently violated if governance lowers ceilings.) + // Check 5: Per-asset debt exceeding its ceiling is a warning. + // Governance may change weights or MaxPsmDebtOfTotal, transiently causing + // per-asset debt to exceed the newly computed ceiling. for (asset_id, status) in ExternalAssets::::iter() { if status.allows_minting() { let debt = PsmDebt::::get(asset_id); let ceiling = Self::max_asset_debt(asset_id); - ensure!(debt <= ceiling, "Per-asset PSM debt exceeds its ceiling"); + if debt > ceiling { + log::warn!( + "PSM: asset {:?} debt ({:?}) exceeds ceiling ({:?})", + asset_id, + debt, + ceiling, + ); + } } } diff --git a/substrate/frame/psm/src/tests.rs b/substrate/frame/psm/src/tests.rs index 05e99a6ebfe64..b1daf711a35ba 100644 --- a/substrate/frame/psm/src/tests.rs +++ b/substrate/frame/psm/src/tests.rs @@ -248,6 +248,9 @@ mod mint { }); } + // Companion to multi_asset_ceiling::minting_past_per_asset_ceiling_blocked_regardless_of_global_headroom: + // this test hits the global ceiling (per-asset ceiling allows the mint), that one hits the per-asset + // ceiling (global ceiling allows the mint). Both error ExceedsMaxPsmDebt from different ensure! gates. #[test] fn fails_mint_exceeds_aggregate_psm_ceiling() { new_test_ext().execute_with(|| { @@ -1118,7 +1121,7 @@ mod ceiling_redistribution { fn multiple_assets_share_redistributed_ceiling() { new_test_ext().execute_with(|| { // Add a third asset - let bridged_usdc_asset_id = 4u32; + let bridged_usdc_asset_id = 7u32; create_asset_with_metadata(bridged_usdc_asset_id); assert_ok!(Psm::add_external_asset(RuntimeOrigin::root(), bridged_usdc_asset_id)); @@ -1277,6 +1280,8 @@ mod multi_asset_ceiling { }); } + // Companion to mint::fails_mint_exceeds_aggregate_psm_ceiling: that test hits the global ceiling + // (per-asset allows), this one hits the per-asset ceiling (global allows). Same error, different gate. #[test] fn minting_past_per_asset_ceiling_blocked_regardless_of_global_headroom() { new_test_ext().execute_with(|| { @@ -1783,32 +1788,24 @@ mod try_state { }); } - // Check 4: total debt exceeds global ceiling. + // Check 4: total debt exceeding global ceiling is a warning, not an error. + // Governance may transiently create this state; it is logged, not rejected. #[test] - fn detects_total_debt_exceeds_global_ceiling() { + fn warns_on_total_debt_exceeds_global_ceiling() { new_test_ext().execute_with(|| { assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, 1_000 * PUSD_UNIT)); - // Lower MaxPsmDebtOfTotal below accumulated debt via storage. - // Per-asset ceilings (derived from max_psm_debt) also shrink, so check 5 - // would fire too, but check 4 fires first since it is ordered before check 5. MaxPsmDebtOfTotal::::put(Permill::from_parts(1)); - assert_eq!( - crate::Pallet::::do_try_state().unwrap_err(), - DispatchError::Other("Total PSM debt exceeds global ceiling") - ); + assert_ok!(crate::Pallet::::do_try_state()); }); } /// Check 5: per-asset debt exceeds its ceiling. /// - /// Mints 1000 UNIT of USDC, then lowers `MaxPsmDebtOfTotal` such that the global ceiling - /// (check 4) still passes (`max_psm_debt` = 1100 UNIT > 1000 UNIT debt), but the - /// per-asset ceiling for USDC (`max_asset_debt` = 1100 * 60% = 660 UNIT) falls below the - /// accumulated debt. The ratio is derived dynamically from `MockMaximumIssuance` to avoid - /// sensitivity to thread-local state across tests. + /// Per-asset debt exceeding ceiling is a warning, not an error. + /// Governance may change weights or MaxPsmDebtOfTotal, transiently causing this. #[test] - fn detects_debt_exceeds_asset_ceiling() { + fn warns_on_debt_exceeds_asset_ceiling() { new_test_ext().execute_with(|| { assert_ok!(Psm::mint(RuntimeOrigin::signed(ALICE), USDC_ASSET_ID, 1_000 * PUSD_UNIT)); let max_issuance = crate::mock::MockMaximumIssuance::get(); @@ -1817,10 +1814,7 @@ mod try_state { assert!(crate::Pallet::::max_psm_debt() > 1_000 * PUSD_UNIT); assert!(crate::Pallet::::max_asset_debt(USDC_ASSET_ID) < 1_000 * PUSD_UNIT); - assert_eq!( - crate::Pallet::::do_try_state().unwrap_err(), - DispatchError::Other("Per-asset PSM debt exceeds its ceiling") - ); + assert_ok!(crate::Pallet::::do_try_state()); }); } From 583378a58b34bf29eb54bd33bfdbd9751d923b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Sat, 18 Apr 2026 19:12:07 +0000 Subject: [PATCH 10/10] Add some helpers to fuzz harnesses --- substrate/frame/psm/Cargo.toml | 5 +- substrate/frame/psm/fuzz/fuzz_targets/psm.rs | 32 +++++---- .../psm/fuzz/fuzz_targets/psm_stateful.rs | 39 +++++------ substrate/frame/psm/src/mock.rs | 65 +++++++++++++++++++ 4 files changed, 107 insertions(+), 34 deletions(-) diff --git a/substrate/frame/psm/Cargo.toml b/substrate/frame/psm/Cargo.toml index 6e56f24d9bab3..c9af161dfc137 100644 --- a/substrate/frame/psm/Cargo.toml +++ b/substrate/frame/psm/Cargo.toml @@ -22,7 +22,10 @@ frame-benchmarking = { workspace = true, optional = true } frame-support = { workspace = true } frame-system = { workspace = true } log = { workspace = true } +pallet-assets = { workspace = true, optional = true } +pallet-balances = { workspace = true, optional = true } scale-info = { features = ["derive"], workspace = true } +sp-io = { workspace = true, optional = true } sp-runtime = { workspace = true } [dev-dependencies] @@ -56,4 +59,4 @@ try-runtime = [ "pallet-balances/try-runtime", "sp-runtime/try-runtime", ] -fuzzing = [] +fuzzing = ["pallet-assets", "pallet-balances", "sp-io"] diff --git a/substrate/frame/psm/fuzz/fuzz_targets/psm.rs b/substrate/frame/psm/fuzz/fuzz_targets/psm.rs index 5e0f01da6465c..68fb37a037bc4 100644 --- a/substrate/frame/psm/fuzz/fuzz_targets/psm.rs +++ b/substrate/frame/psm/fuzz/fuzz_targets/psm.rs @@ -10,12 +10,13 @@ use arbitrary::{Arbitrary, Unstructured}; use frame_support::traits::fungibles::Inspect; use libfuzzer_sys::fuzz_target; -use pallet_psm::mock::fuzz_helpers as fh; +use pallet_psm::mock::fuzz_helpers; use pallet_psm::mock::{ Assets, Psm, RuntimeOrigin, System, Test, ALL_EXTERNAL_ASSETS, DAI_ASSET_ID, FRAX_ASSET_ID, INSURANCE_FUND, PUSD_ASSET_ID, PUSD_UNIT, USDC_ASSET_ID, USDP_ASSET_ID, USDT_ASSET_ID, }; use pallet_psm::CircuitBreakerLevel; +use pallet_psm::PsmDebt; use sp_runtime::{BuildStorage, Permill}; use std::fs::OpenOptions; use std::io::Write; @@ -354,7 +355,7 @@ fn build_fuzzer_genesis() -> sp_io::TestExternalities { } // --------------------------------------------------------------------------- -// Dispatch (runs inside externalities, reads storage via fh::*) +// Dispatch (runs inside externalities, reads storage via fuzz_helpers::*) // --------------------------------------------------------------------------- fn dispatch_op(op: &Op) { @@ -369,13 +370,14 @@ fn dispatch_op(op: &Op) { let asset_id = ALL_EXTERNAL_ASSETS[(*asset_idx % ALL_EXTERNAL_ASSETS.len() as u8) as usize]; let account = (*account_idx % N_ACCOUNTS + 1) as u64; - if !fh::is_approved_asset(asset_id) { + if !fuzz_helpers::is_approved_asset(asset_id) { return; } - let debt = fh::psm_debt(asset_id); - let ceiling = fh::max_asset_debt(asset_id); + let debt = PsmDebt::::get(asset_id); + let ceiling = fuzz_helpers::max_asset_debt(asset_id); let remaining = ceiling.saturating_sub(debt); - let global_remaining = fh::max_psm_debt().saturating_sub(fh::total_psm_debt()); + let global_remaining = + fuzz_helpers::max_psm_debt().saturating_sub(fuzz_helpers::total_psm_debt()); let balance = Assets::balance(asset_id, account); let issuance_remaining = pallet_psm::mock::MockMaximumIssuance::get() .saturating_sub(Assets::total_issuance(PUSD_ASSET_ID)); @@ -390,10 +392,10 @@ fn dispatch_op(op: &Op) { let asset_id = ALL_EXTERNAL_ASSETS[(*asset_idx % ALL_EXTERNAL_ASSETS.len() as u8) as usize]; let account = (*account_idx % N_ACCOUNTS + 1) as u64; - if !fh::is_approved_asset(asset_id) { + if !fuzz_helpers::is_approved_asset(asset_id) { return; } - let debt = fh::psm_debt(asset_id); + let debt = PsmDebt::::get(asset_id); let user_pusd = Assets::balance(PUSD_ASSET_ID, account); let effective_cap = debt.min(user_pusd); let amount = resolve_amount(tier, effective_cap); @@ -408,7 +410,7 @@ fn dispatch_op(op: &Op) { // debt is redeemed or the ceiling is raised. Op::SetMaxPsmDebt { force_below_debt, parts } => { let ratio = if *force_below_debt { - let debt = fh::total_psm_debt(); + let debt = fuzz_helpers::total_psm_debt(); let max_issuance = pallet_psm::mock::MockMaximumIssuance::get(); if debt == 0 || max_issuance == 0 { Permill::from_parts(parts % 1_000_001) @@ -442,7 +444,7 @@ fn dispatch_op(op: &Op) { Op::SetAssetStatus { asset_idx, level } => { let asset_id = ALL_EXTERNAL_ASSETS[(*asset_idx % ALL_EXTERNAL_ASSETS.len() as u8) as usize]; - if !fh::is_approved_asset(asset_id) { + if !fuzz_helpers::is_approved_asset(asset_id) { return; } let status = match level % 3 { @@ -459,7 +461,7 @@ fn dispatch_op(op: &Op) { Op::AddExternalAsset { asset_idx } => { let candidates: Vec = ALL_EXTERNAL_ASSETS .iter() - .filter(|&&id| !fh::is_approved_asset(id)) + .filter(|&&id| !fuzz_helpers::is_approved_asset(id)) .copied() .collect(); if candidates.is_empty() { @@ -472,8 +474,10 @@ fn dispatch_op(op: &Op) { } }, Op::RemoveExternalAsset { asset_idx } => { - let candidates: Vec = - fh::approved_assets().into_iter().filter(|&id| fh::psm_debt(id) == 0).collect(); + let candidates: Vec = fuzz_helpers::approved_assets() + .into_iter() + .filter(|&id| PsmDebt::::get(id) == 0) + .collect(); if candidates.is_empty() { return; } @@ -515,7 +519,7 @@ fuzz_target!(|input: MultiBlockOps| { for op in &block.0 { dispatch_op(op); } - fh::do_try_state().expect("PSM invariant violated"); + fuzz_helpers::do_try_state().expect("PSM invariant violated"); }); block_number += 1; } diff --git a/substrate/frame/psm/fuzz/fuzz_targets/psm_stateful.rs b/substrate/frame/psm/fuzz/fuzz_targets/psm_stateful.rs index 031da9741abd3..722f392baf21a 100644 --- a/substrate/frame/psm/fuzz/fuzz_targets/psm_stateful.rs +++ b/substrate/frame/psm/fuzz/fuzz_targets/psm_stateful.rs @@ -8,13 +8,14 @@ //! Usage: `psm_stateful [seed] [max_commands]` use frame_support::traits::fungibles::Inspect; -use pallet_psm::mock::fuzz_helpers as fh; +use pallet_psm::mock::fuzz_helpers; use pallet_psm::mock::{ set_mock_maximum_issuance, Assets, MockMaximumIssuance, Psm, RuntimeOrigin, System, Test, ALL_EXTERNAL_ASSETS, DAI_ASSET_ID, FRAX_ASSET_ID, INSURANCE_FUND, PUSD_ASSET_ID, PUSD_UNIT, USDC_ASSET_ID, USDP_ASSET_ID, USDT_ASSET_ID, }; use pallet_psm::CircuitBreakerLevel; +use pallet_psm::PsmDebt; use rand::seq::SliceRandom; use rand::{rngs::StdRng, Rng, SeedableRng}; use sp_io::TestExternalities; @@ -197,19 +198,19 @@ fn build_fuzzer_genesis() -> TestExternalities { // --------------------------------------------------------------------------- fn snapshot_state() -> FuzzState { - let approved = fh::approved_assets(); + let approved = fuzz_helpers::approved_assets(); let max_issuance = MockMaximumIssuance::get(); let total_pusd_issuance = Assets::total_issuance(PUSD_ASSET_ID); let mut assets = Vec::with_capacity(approved.len()); for &asset_id in &approved { - let debt = fh::psm_debt(asset_id); - let ceiling = fh::max_asset_debt(asset_id); + let debt = PsmDebt::::get(asset_id); + let ceiling = fuzz_helpers::max_asset_debt(asset_id); let remaining_ceiling = ceiling.saturating_sub(debt); - let reserve = fh::get_reserve(asset_id); - let minting_fee = fh::minting_fee(asset_id); - let redemption_fee = fh::redemption_fee(asset_id); - let weight = fh::asset_ceiling_weight(asset_id); + let reserve = fuzz_helpers::get_reserve(asset_id); + let minting_fee = fuzz_helpers::minting_fee(asset_id); + let redemption_fee = fuzz_helpers::redemption_fee(asset_id); + let weight = fuzz_helpers::asset_ceiling_weight(asset_id); let mut account_external = Vec::with_capacity(N_ACCOUNTS as usize); let mut account_pusd = Vec::with_capacity(N_ACCOUNTS as usize); @@ -234,7 +235,7 @@ fn snapshot_state() -> FuzzState { let unapproved: Vec = ALL_EXTERNAL_ASSETS .iter() - .filter(|&&id| !fh::is_approved_asset(id)) + .filter(|&&id| !fuzz_helpers::is_approved_asset(id)) .copied() .collect(); @@ -244,9 +245,9 @@ fn snapshot_state() -> FuzzState { FuzzState { assets, unapproved, - max_psm_debt: fh::max_psm_debt(), - total_psm_debt: fh::total_psm_debt(), - max_psm_debt_ratio: fh::max_psm_debt_ratio(), + max_psm_debt: fuzz_helpers::max_psm_debt(), + total_psm_debt: fuzz_helpers::total_psm_debt(), + max_psm_debt_ratio: fuzz_helpers::max_psm_debt_ratio(), max_issuance, total_pusd_issuance, block_number, @@ -575,12 +576,12 @@ fn gen_command(rng: &mut StdRng, state: &FuzzState) -> Command { fn execute_command(cmd: &Command) { match cmd { Command::Mint { account, asset_id, amount } => { - if fh::is_approved_asset(*asset_id) { + if fuzz_helpers::is_approved_asset(*asset_id) { let _ = Psm::mint(RuntimeOrigin::signed(*account), *asset_id, *amount); } }, Command::Redeem { account, asset_id, amount } => { - if fh::is_approved_asset(*asset_id) { + if fuzz_helpers::is_approved_asset(*asset_id) { let _ = Psm::redeem(RuntimeOrigin::signed(*account), *asset_id, *amount); } }, @@ -588,22 +589,22 @@ fn execute_command(cmd: &Command) { let _ = Psm::set_max_psm_debt(RuntimeOrigin::root(), *ratio); }, Command::SetAssetCeilingWeight { asset_id, weight } => { - if fh::is_approved_asset(*asset_id) { + if fuzz_helpers::is_approved_asset(*asset_id) { let _ = Psm::set_asset_ceiling_weight(RuntimeOrigin::root(), *asset_id, *weight); } }, Command::SetMintingFee { asset_id, fee } => { - if fh::is_approved_asset(*asset_id) { + if fuzz_helpers::is_approved_asset(*asset_id) { let _ = Psm::set_minting_fee(RuntimeOrigin::root(), *asset_id, *fee); } }, Command::SetRedemptionFee { asset_id, fee } => { - if fh::is_approved_asset(*asset_id) { + if fuzz_helpers::is_approved_asset(*asset_id) { let _ = Psm::set_redemption_fee(RuntimeOrigin::root(), *asset_id, *fee); } }, Command::SetAssetStatus { asset_id, status } => { - if fh::is_approved_asset(*asset_id) { + if fuzz_helpers::is_approved_asset(*asset_id) { let _ = Psm::set_asset_status(RuntimeOrigin::root(), *asset_id, *status); } }, @@ -730,7 +731,7 @@ fn run_campaign(seed: u64, max_commands: usize) { let cmd = gen_command(&mut rng, &state); log_command(i, &cmd, &state); execute_command(&cmd); - fh::do_try_state().expect("PSM invariant violated — see command log above"); + fuzz_helpers::do_try_state().expect("PSM invariant violated — see command log above"); } }); } diff --git a/substrate/frame/psm/src/mock.rs b/substrate/frame/psm/src/mock.rs index 1a964d9efff29..b6c481dd2316b 100644 --- a/substrate/frame/psm/src/mock.rs +++ b/substrate/frame/psm/src/mock.rs @@ -35,8 +35,14 @@ pub const INSURANCE_FUND: u64 = 100; pub const PUSD_ASSET_ID: u32 = 1; pub const USDC_ASSET_ID: u32 = 2; pub const USDT_ASSET_ID: u32 = 3; +pub const DAI_ASSET_ID: u32 = 4; +pub const USDP_ASSET_ID: u32 = 5; +pub const FRAX_ASSET_ID: u32 = 6; pub const UNSUPPORTED_ASSET_ID: u32 = 99; +pub const ALL_EXTERNAL_ASSETS: &[u32] = + &[USDC_ASSET_ID, USDT_ASSET_ID, DAI_ASSET_ID, USDP_ASSET_ID, FRAX_ASSET_ID]; + // pUSD unit (6 decimals) pub const PUSD_UNIT: u128 = 1_000_000; @@ -193,11 +199,17 @@ pub fn new_test_ext() -> TestState { (PUSD_ASSET_ID, ALICE, true, 1), (USDC_ASSET_ID, ALICE, true, 1), (USDT_ASSET_ID, ALICE, true, 1), + (DAI_ASSET_ID, ALICE, true, 1), + (USDP_ASSET_ID, ALICE, true, 1), + (FRAX_ASSET_ID, ALICE, true, 1), ], metadata: vec![ (PUSD_ASSET_ID, b"pUSD Stablecoin".to_vec(), b"pUSD".to_vec(), 6), (USDC_ASSET_ID, b"USD Coin".to_vec(), b"USDC".to_vec(), 6), (USDT_ASSET_ID, b"Tether USD".to_vec(), b"USDT".to_vec(), 6), + (DAI_ASSET_ID, b"Dai Stablecoin".to_vec(), b"DAI".to_vec(), 6), + (USDP_ASSET_ID, b"Pax Dollar".to_vec(), b"USDP".to_vec(), 6), + (FRAX_ASSET_ID, b"Frax".to_vec(), b"FRAX".to_vec(), 6), ], accounts: vec![ (USDC_ASSET_ID, ALICE, 10_000 * PUSD_UNIT), @@ -325,3 +337,56 @@ pub fn get_asset_balance(asset_id: u32, account: u64) -> u128 { pub fn psm_account() -> u64 { crate::Pallet::::account_id() } + +#[cfg(feature = "fuzzing")] +pub mod fuzz_helpers { + use super::*; + + // PsmDebt is already pub — import it directly from the crate. + // Everything else below is pub(crate) and cannot be re-exported, so + // it goes through monomorphized wrapper functions. + + pub fn max_psm_debt() -> u128 { + Psm::max_psm_debt() + } + + pub fn max_asset_debt(asset_id: u32) -> u128 { + Psm::max_asset_debt(asset_id) + } + + pub fn total_psm_debt() -> u128 { + Psm::total_psm_debt() + } + + pub fn get_reserve(asset_id: u32) -> u128 { + Psm::get_reserve(asset_id) + } + + pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> { + Psm::do_try_state() + } + + pub fn is_approved_asset(asset_id: u32) -> bool { + Psm::is_approved_asset(&asset_id) + } + + pub fn minting_fee(asset_id: u32) -> Permill { + crate::MintingFee::::get(asset_id) + } + + pub fn redemption_fee(asset_id: u32) -> Permill { + crate::RedemptionFee::::get(asset_id) + } + + pub fn asset_ceiling_weight(asset_id: u32) -> Permill { + crate::AssetCeilingWeight::::get(asset_id) + } + + pub fn max_psm_debt_ratio() -> Permill { + crate::MaxPsmDebtOfTotal::::get() + } + + pub fn approved_assets() -> Vec { + crate::ExternalAssets::::iter().map(|(id, _)| id).collect() + } +}