diff --git a/Cargo.lock b/Cargo.lock
index 23804348..bd8038f5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8115,6 +8115,25 @@ dependencies = [
"tangle-primitives",
]
+[[package]]
+name = "pallet-evm-precompile-multi-asset-delegation-fuzzer"
+version = "2.0.0"
+dependencies = [
+ "fp-evm",
+ "frame-support",
+ "frame-system",
+ "honggfuzz",
+ "log",
+ "pallet-evm",
+ "pallet-evm-precompile-multi-asset-delegation",
+ "pallet-multi-asset-delegation",
+ "precompile-utils",
+ "rand",
+ "sp-io",
+ "sp-runtime",
+ "sp-tracing",
+]
+
[[package]]
name = "pallet-evm-precompile-preimage"
version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 45032b9d..c24651af 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,6 +38,7 @@ members = [
"precompiles/verify-schnorr-signatures",
"precompiles/verify-bls381-signature",
"precompiles/multi-asset-delegation",
+ "precompiles/multi-asset-delegation/fuzzer",
"precompiles/services",
"precompiles/tangle-lst",
"precompiles/assets",
@@ -271,17 +272,17 @@ fc-api = { git = "https://github.com/paritytech/frontier.git", branch = "stable2
# Frontier Primitive
fp-account = { git = "https://github.com/paritytech/frontier.git", branch = "stable2407", default-features = false, features = [
- "serde",
+ "serde",
] }
fp-consensus = { git = "https://github.com/paritytech/frontier.git", branch = "stable2407", default-features = false }
fp-dynamic-fee = { git = "https://github.com/paritytech/frontier.git", branch = "stable2407", default-features = false }
fp-ethereum = { git = "https://github.com/paritytech/frontier.git", branch = "stable2407", default-features = false }
fp-evm = { git = "https://github.com/paritytech/frontier.git", branch = "stable2407", default-features = false, features = [
- "serde",
+ "serde",
] }
fp-rpc = { git = "https://github.com/paritytech/frontier.git", branch = "stable2407", default-features = false }
fp-self-contained = { git = "https://github.com/paritytech/frontier.git", branch = "stable2407", default-features = false, features = [
- "serde",
+ "serde",
] }
fp-storage = { git = "https://github.com/paritytech/frontier.git", branch = "stable2407", default-features = false }
diff --git a/precompiles/multi-asset-delegation/Cargo.toml b/precompiles/multi-asset-delegation/Cargo.toml
index 5ad05c85..e83ffd27 100644
--- a/precompiles/multi-asset-delegation/Cargo.toml
+++ b/precompiles/multi-asset-delegation/Cargo.toml
@@ -6,8 +6,6 @@ edition = "2021"
description = "A Precompile to make pallet-multi-asset-delegation calls encoding accessible to pallet-evm"
[dependencies]
-
-
precompile-utils = { workspace = true }
# Substrate
@@ -26,6 +24,46 @@ pallet-evm = { workspace = true, features = ["forbid-evm-reentrancy"] }
tangle-primitives = { workspace = true }
+derive_more = { workspace = true, features = ["full"], optional = true }
+hex-literal = { workspace = true, optional = true }
+serde = { workspace = true, optional = true }
+sha3 = { workspace = true, optional = true }
+ethereum = { workspace = true, features = ["with-codec"], optional = true }
+ethers = { version = "2.0", optional = true }
+hex = { workspace = true, optional = true }
+num_enum = { workspace = true, optional = true }
+libsecp256k1 = { workspace = true, optional = true }
+serde_json = { workspace = true, optional = true }
+smallvec = { workspace = true, optional = true }
+sp-keystore = { workspace = true, optional = true }
+pallet-assets = { workspace = true, features = ["std"], optional = true }
+pallet-timestamp = { workspace = true, features = ["std"], optional = true }
+scale-info = { workspace = true, features = ["derive", "std"], optional = true }
+sp-io = { workspace = true, features = ["std"], optional = true }
+fp-account = { workspace = true, optional = true }
+fp-consensus = { workspace = true, optional = true }
+fp-dynamic-fee = { workspace = true, optional = true }
+fp-ethereum = { workspace = true, optional = true }
+fp-rpc = { workspace = true, optional = true }
+fp-self-contained = { workspace = true, optional = true }
+fp-storage = { workspace = true, optional = true }
+pallet-base-fee = { workspace = true, optional = true }
+pallet-dynamic-fee = { workspace = true, optional = true }
+pallet-ethereum = { workspace = true, optional = true }
+pallet-evm-chain-id = { workspace = true, optional = true }
+pallet-evm-precompile-blake2 = { workspace = true, optional = true }
+pallet-evm-precompile-bn128 = { workspace = true, optional = true }
+pallet-evm-precompile-curve25519 = { workspace = true, optional = true }
+pallet-evm-precompile-ed25519 = { workspace = true, optional = true }
+pallet-evm-precompile-modexp = { workspace = true, optional = true }
+pallet-evm-precompile-sha3fips = { workspace = true, optional = true }
+pallet-evm-precompile-simple = { workspace = true, optional = true }
+pallet-session = { workspace = true, optional = true }
+pallet-staking = { workspace = true, optional = true }
+sp-staking = { workspace = true, optional = true }
+frame-election-provider-support = { workspace = true, optional = true }
+ethabi = { workspace = true, optional = true }
+
[dev-dependencies]
derive_more = { workspace = true, features = ["full"] }
hex-literal = { workspace = true }
@@ -83,56 +121,96 @@ ethabi = { workspace = true }
[features]
default = ["std"]
+fuzzing = [
+ "derive_more",
+ "hex-literal",
+ "serde",
+ "sha3",
+ "ethereum",
+ "ethers",
+ "hex",
+ "num_enum",
+ "libsecp256k1",
+ "serde_json",
+ "smallvec",
+ "sp-keystore",
+ "pallet-assets",
+ "pallet-timestamp",
+ "scale-info",
+ "sp-io",
+ "fp-account",
+ "fp-consensus",
+ "fp-dynamic-fee",
+ "fp-ethereum",
+ "fp-rpc",
+ "fp-self-contained",
+ "fp-storage",
+ "pallet-base-fee",
+ "pallet-dynamic-fee",
+ "pallet-ethereum",
+ "pallet-evm-chain-id",
+ "pallet-evm-precompile-blake2",
+ "pallet-evm-precompile-bn128",
+ "pallet-evm-precompile-curve25519",
+ "pallet-evm-precompile-ed25519",
+ "pallet-evm-precompile-modexp",
+ "pallet-evm-precompile-sha3fips",
+ "pallet-evm-precompile-simple",
+ "pallet-session",
+ "pallet-staking",
+ "sp-staking",
+ "frame-election-provider-support",
+ "ethabi",
+]
std = [
- "fp-evm/std",
- "frame-support/std",
- "frame-system/std",
- "pallet-balances/std",
- "pallet-evm/std",
- "pallet-multi-asset-delegation/std",
- "parity-scale-codec/std",
- "precompile-utils/std",
- "sp-core/std",
- "sp-runtime/std",
- "sp-std/std",
- "tangle-primitives/std",
- "pallet-assets/std",
- "hex/std",
- "scale-info/std",
- "sp-runtime/std",
- "frame-support/std",
- "frame-system/std",
- "sp-core/std",
- "sp-std/std",
- "sp-io/std",
- "tangle-primitives/std",
- "pallet-balances/std",
- "pallet-timestamp/std",
- "fp-account/std",
- "fp-consensus/std",
- "fp-dynamic-fee/std",
- "fp-ethereum/std",
- "fp-evm/std",
- "fp-rpc/std",
- "fp-self-contained/std",
- "fp-storage/std",
- "pallet-base-fee/std",
- "pallet-dynamic-fee/std",
- "pallet-ethereum/std",
- "pallet-evm/std",
- "pallet-evm-chain-id/std",
-
- "pallet-evm-precompile-modexp/std",
- "pallet-evm-precompile-sha3fips/std",
- "pallet-evm-precompile-simple/std",
- "pallet-evm-precompile-blake2/std",
- "pallet-evm-precompile-bn128/std",
- "pallet-evm-precompile-curve25519/std",
- "pallet-evm-precompile-ed25519/std",
- "precompile-utils/std",
- "serde/std",
- "pallet-session/std",
- "pallet-staking/std",
- "sp-staking/std",
- "frame-election-provider-support/std",
-]
\ No newline at end of file
+ "fp-evm/std",
+ "frame-support/std",
+ "frame-system/std",
+ "pallet-balances/std",
+ "pallet-evm/std",
+ "pallet-multi-asset-delegation/std",
+ "parity-scale-codec/std",
+ "precompile-utils/std",
+ "sp-core/std",
+ "sp-runtime/std",
+ "sp-std/std",
+ "tangle-primitives/std",
+ "pallet-assets/std",
+ "hex/std",
+ "scale-info/std",
+ "sp-runtime/std",
+ "frame-support/std",
+ "frame-system/std",
+ "sp-core/std",
+ "sp-std/std",
+ "sp-io/std",
+ "tangle-primitives/std",
+ "pallet-balances/std",
+ "pallet-timestamp/std",
+ "fp-account/std",
+ "fp-consensus/std",
+ "fp-dynamic-fee/std",
+ "fp-ethereum/std",
+ "fp-evm/std",
+ "fp-rpc/std",
+ "fp-self-contained/std",
+ "fp-storage/std",
+ "pallet-base-fee/std",
+ "pallet-dynamic-fee/std",
+ "pallet-ethereum/std",
+ "pallet-evm/std",
+ "pallet-evm-chain-id/std",
+ "pallet-evm-precompile-modexp/std",
+ "pallet-evm-precompile-sha3fips/std",
+ "pallet-evm-precompile-simple/std",
+ "pallet-evm-precompile-blake2/std",
+ "pallet-evm-precompile-bn128/std",
+ "pallet-evm-precompile-curve25519/std",
+ "pallet-evm-precompile-ed25519/std",
+ "precompile-utils/std",
+ "serde/std",
+ "pallet-session/std",
+ "pallet-staking/std",
+ "sp-staking/std",
+ "frame-election-provider-support/std",
+]
diff --git a/precompiles/multi-asset-delegation/fuzzer/Cargo.toml b/precompiles/multi-asset-delegation/fuzzer/Cargo.toml
new file mode 100644
index 00000000..ae78a690
--- /dev/null
+++ b/precompiles/multi-asset-delegation/fuzzer/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "pallet-evm-precompile-multi-asset-delegation-fuzzer"
+version = "2.0.0"
+authors.workspace = true
+edition.workspace = true
+license = "Apache-2.0"
+homepage.workspace = true
+repository.workspace = true
+description = "Fuzzer for pallet-multi-asset-delegation precompile"
+publish = false
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[dependencies]
+honggfuzz = { workspace = true }
+
+
+pallet-multi-asset-delegation = { workspace = true, features = ["fuzzing"], default-features = true }
+pallet-evm-precompile-multi-asset-delegation = { features = ["fuzzing"], workspace = true, default-features = true }
+
+frame-system = { workspace = true, default-features = true }
+frame-support = { workspace = true, default-features = true }
+
+fp-evm = { workspace = true }
+precompile-utils = { workspace = true, features = ["std", "testing"] }
+pallet-evm = { workspace = true, features = ["forbid-evm-reentrancy"] }
+
+sp-runtime = { workspace = true, default-features = true }
+sp-io = { workspace = true, default-features = true }
+sp-tracing = { workspace = true, default-features = true }
+
+rand = { features = ["small_rng"], workspace = true, default-features = true }
+log = { workspace = true, default-features = true }
+
+[[bin]]
+name = "mad-precompile-fuzzer"
+path = "call.rs"
diff --git a/precompiles/multi-asset-delegation/fuzzer/call.rs b/precompiles/multi-asset-delegation/fuzzer/call.rs
new file mode 100644
index 00000000..34fe1cb3
--- /dev/null
+++ b/precompiles/multi-asset-delegation/fuzzer/call.rs
@@ -0,0 +1,539 @@
+// This file is part of Tangle.
+// Copyright (C) 2022-2024 Tangle Foundation.
+//
+// Tangle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Tangle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Tangle. If not, see .
+
+//! # Running
+//! Running this fuzzer can be done with `cargo hfuzz run mad-fuzzer`. `honggfuzz` CLI
+//! options can be used by setting `HFUZZ_RUN_ARGS`, such as `-n 4` to use 4 threads.
+//!
+//! # Debugging a panic
+//! Once a panic is found, it can be debugged with
+//! `cargo hfuzz run-debug mad-fuzzer hfuzz_workspace/mad-fuzzer/*.fuzz`.
+
+use fp_evm::Context;
+use fp_evm::PrecompileSet;
+use frame_support::traits::{Currency, Get};
+use honggfuzz::fuzz;
+use pallet_evm::AddressMapping;
+use pallet_evm_precompile_multi_asset_delegation::{
+ mock::*, mock_evm::PrecompilesValue, MultiAssetDelegationPrecompileCall as MADPrecompileCall,
+};
+use pallet_multi_asset_delegation::{
+ mock::{Asset, AssetId},
+ pallet as mad,
+ types::*,
+};
+use precompile_utils::prelude::*;
+use precompile_utils::testing::*;
+use rand::{seq::SliceRandom, Rng};
+use sp_runtime::traits::{Scale, Zero};
+
+const MAX_ED_MULTIPLE: Balance = 10_000;
+const MIN_ED_MULTIPLE: Balance = 10;
+
+type PCall = MADPrecompileCall;
+
+fn random_address(rng: &mut R) -> Address {
+ Address(rng.gen::<[u8; 20]>().into())
+}
+
+/// Grab random accounts.
+fn random_signed_origin(rng: &mut R) -> (RuntimeOrigin, Address) {
+ let addr = random_address(rng);
+ let signer = >::into_account_id(addr.0);
+ (RuntimeOrigin::signed(signer), addr)
+}
+
+fn random_ed_multiple(rng: &mut R) -> Balance {
+ let multiple = rng.gen_range(MIN_ED_MULTIPLE..MAX_ED_MULTIPLE);
+ ExistentialDeposit::get() * multiple
+}
+
+fn random_asset(rng: &mut R) -> Asset {
+ let asset_id = rng.gen_range(1..u128::MAX);
+ let is_evm = rng.gen_bool(0.5);
+ if is_evm {
+ let evm_address = rng.gen::<[u8; 20]>().into();
+ Asset::Erc20(evm_address)
+ } else {
+ Asset::Custom(asset_id)
+ }
+}
+
+fn fund_account(rng: &mut R, address: &Address) {
+ let target_amount = random_ed_multiple(rng);
+ let signer = >::into_account_id(address.0);
+ if let Some(top_up) = target_amount.checked_sub(Balances::free_balance(signer)) {
+ let _ = Balances::deposit_creating(&signer, top_up);
+ }
+ assert!(Balances::free_balance(signer) >= target_amount);
+}
+
+/// Join operators call.
+fn join_operators_call(rng: &mut R, who: &Address) -> (PCall, Address) {
+ let minimum_bond = <::MinOperatorBondAmount as Get>::get();
+ let multiplier = rng.gen_range(1..50u64);
+ let who_account_id = >::into_account_id(who.0);
+ let _ = Balances::deposit_creating(&who_account_id, minimum_bond.mul(multiplier));
+ let bond_amount = minimum_bond.mul(multiplier).into();
+ (PCall::join_operators { bond_amount }, *who)
+}
+
+fn random_calls(mut rng: &mut R) -> impl IntoIterator- {
+ let op = PCall::selectors().choose(rng).cloned().unwrap();
+ match op {
+ _ if op == PCall::join_operators_selectors()[0] => {
+ // join_operators
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ vec![join_operators_call(&mut rng, &who)]
+ },
+ _ if op == PCall::schedule_leave_operators_selectors()[0] => {
+ // Schedule leave operators
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ vec![join_operators_call(rng, &who), (PCall::schedule_leave_operators {}, who)]
+ },
+ _ if op == PCall::cancel_leave_operators_selectors()[0] => {
+ // Cancel leave operators
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ vec![join_operators_call(rng, &who), (PCall::cancel_leave_operators {}, who)]
+ },
+ _ if op == PCall::execute_leave_operators_selectors()[0] => {
+ // Execute leave operators
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ vec![join_operators_call(rng, &who), (PCall::execute_leave_operators {}, who)]
+ },
+ _ if op == PCall::operator_bond_more_selectors()[0] => {
+ // Operator bond more
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ let additional_bond = random_ed_multiple(&mut rng).into();
+ vec![
+ join_operators_call(rng, &who),
+ (PCall::operator_bond_more { additional_bond }, who),
+ ]
+ },
+ _ if op == PCall::schedule_operator_unstake_selectors()[0] => {
+ // Schedule operator unstake
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ let unstake_amount = random_ed_multiple(&mut rng).into();
+ vec![
+ join_operators_call(rng, &who),
+ (PCall::schedule_operator_unstake { unstake_amount }, who),
+ ]
+ },
+ _ if op == PCall::execute_operator_unstake_selectors()[0] => {
+ // Execute operator unstake
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ vec![join_operators_call(rng, &who), (PCall::execute_operator_unstake {}, who)]
+ },
+ _ if op == PCall::cancel_operator_unstake_selectors()[0] => {
+ // Cancel operator unstake
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ vec![join_operators_call(rng, &who), (PCall::cancel_operator_unstake {}, who)]
+ },
+ _ if op == PCall::go_offline_selectors()[0] => {
+ // Go offline
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ vec![join_operators_call(rng, &who), (PCall::go_offline {}, who)]
+ },
+ _ if op == PCall::go_online_selectors()[0] => {
+ // Go online
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ vec![join_operators_call(rng, &who), (PCall::go_online {}, who)]
+ },
+ _ if op == PCall::deposit_selectors()[0] => {
+ // Deposit
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ let (asset_id, token_address) = match random_asset(&mut rng) {
+ Asset::Custom(id) => (id.into(), Default::default()),
+ Asset::Erc20(token) => (0.into(), token.into()),
+ };
+ let amount = random_ed_multiple(&mut rng).into();
+ vec![(PCall::deposit { asset_id, amount, token_address }, who)]
+ },
+ _ if op == PCall::schedule_withdraw_selectors()[0] => {
+ // Schedule withdraw
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ let (asset_id, token_address) = match random_asset(&mut rng) {
+ Asset::Custom(id) => (id.into(), Default::default()),
+ Asset::Erc20(token) => (0.into(), token.into()),
+ };
+ let amount = random_ed_multiple(&mut rng).into();
+ vec![(PCall::schedule_withdraw { asset_id, token_address, amount }, who)]
+ },
+ _ if op == PCall::execute_withdraw_selectors()[0] => {
+ // Execute withdraw
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ vec![(PCall::execute_withdraw {}, who)]
+ },
+ _ if op == PCall::cancel_withdraw_selectors()[0] => {
+ // Cancel withdraw
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ let (asset_id, token_address) = match random_asset(&mut rng) {
+ Asset::Custom(id) => (id.into(), Default::default()),
+ Asset::Erc20(token) => (0.into(), token.into()),
+ };
+ let amount = random_ed_multiple(&mut rng).into();
+ vec![(PCall::cancel_withdraw { asset_id, amount, token_address }, who)]
+ },
+ _ if op == PCall::delegate_selectors()[0] => {
+ // Delegate
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ let (_, operator) = random_signed_origin(&mut rng);
+ let (asset_id, token_address) = match random_asset(&mut rng) {
+ Asset::Custom(id) => (id.into(), Default::default()),
+ Asset::Erc20(token) => (0.into(), token.into()),
+ };
+ let amount = random_ed_multiple(&mut rng).into();
+ let blueprint_selection = {
+ let count = rng.gen_range(1..MaxDelegatorBlueprints::get());
+ (0..count).map(|_| rng.gen::()).collect::>()
+ };
+ vec![
+ join_operators_call(&mut rng, &operator),
+ (
+ PCall::delegate {
+ operator: operator.0.into(),
+ asset_id,
+ token_address,
+ amount,
+ blueprint_selection,
+ },
+ who,
+ ),
+ ]
+ },
+ _ if op == PCall::schedule_delegator_unstake_selectors()[0] => {
+ // Schedule delegator unstakes
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ let (_, operator) = random_signed_origin(&mut rng);
+ let (asset_id, token_address) = match random_asset(&mut rng) {
+ Asset::Custom(id) => (id.into(), Default::default()),
+ Asset::Erc20(token) => (0.into(), token.into()),
+ };
+ let amount = random_ed_multiple(&mut rng).into();
+ vec![
+ join_operators_call(&mut rng, &operator),
+ (
+ PCall::schedule_delegator_unstake {
+ operator: operator.0.into(),
+ asset_id,
+ token_address,
+ amount,
+ },
+ who,
+ ),
+ ]
+ },
+ _ if op == PCall::execute_delegator_unstake_selectors()[0] => {
+ // Execute delegator unstake
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ vec![(PCall::execute_delegator_unstake {}, who)]
+ },
+ _ if op == PCall::cancel_delegator_unstake_selectors()[0] => {
+ // Cancel delegator unstake
+ let who = random_address(&mut rng);
+ fund_account(&mut rng, &who);
+ let (_, operator) = random_signed_origin(&mut rng);
+ let (asset_id, token_address) = match random_asset(&mut rng) {
+ Asset::Custom(id) => (id.into(), Default::default()),
+ Asset::Erc20(token) => (0.into(), token.into()),
+ };
+ let amount = random_ed_multiple(&mut rng).into();
+ vec![
+ join_operators_call(&mut rng, &operator),
+ (
+ PCall::cancel_delegator_unstake {
+ operator: operator.0.into(),
+ asset_id,
+ token_address,
+ amount,
+ },
+ who,
+ ),
+ ]
+ },
+ _ => {
+ unimplemented!("unknown call name: {}", op)
+ },
+ }
+}
+
+fn main() {
+ sp_tracing::try_init_simple();
+ let mut ext = ExtBuilder::default().build();
+ let mut block_number = 1;
+ let to = Precompile1.into();
+ loop {
+ fuzz!(|seed: [u8; 32]| {
+ use ::rand::{rngs::SmallRng, SeedableRng};
+ let mut rng = SmallRng::from_seed(seed);
+
+ ext.execute_with(|| {
+ System::set_block_number(block_number);
+ for (call, who) in random_calls(&mut rng) {
+ let mut handle = MockHandle::new(
+ to,
+ Context {
+ address: to,
+ caller: who.into(),
+ apparent_value: Default::default(),
+ },
+ );
+ let mut handle_clone = MockHandle::new(
+ to,
+ Context {
+ address: to,
+ caller: who.into(),
+ apparent_value: Default::default(),
+ },
+ );
+ let encoded = call.encode();
+ handle.input = encoded.clone();
+ let call_clone = PCall::parse_call_data(&mut handle).unwrap();
+ handle_clone.input = encoded;
+ let outcome = PrecompilesValue::get().execute(&mut handle).unwrap();
+ sp_tracing::trace!(?who, ?outcome, "fuzzed call");
+
+ // execute sanity checks at a fixed interval, possibly on every block.
+ if let Ok(out) = outcome {
+ sp_tracing::info!("running sanity checks..");
+ do_sanity_checks(call_clone, who, out);
+ }
+ }
+
+ System::reset_events();
+ block_number += 1;
+ });
+ })
+ }
+}
+
+/// Perform sanity checks on the state after a call is executed successfully.
+#[allow(unused)]
+fn do_sanity_checks(call: PCall, origin: Address, outcome: PrecompileOutput) {
+ let caller = >::into_account_id(origin.0);
+ match call {
+ PCall::join_operators { bond_amount } => {
+ assert!(mad::Operators::::contains_key(caller), "operator not found");
+ assert_eq!(
+ MultiAssetDelegation::operator_info(caller).unwrap_or_default().stake,
+ bond_amount.as_u64()
+ );
+ assert!(
+ Balances::reserved_balance(caller).ge(&bond_amount.as_u64()),
+ "bond amount not reserved"
+ );
+ },
+ PCall::schedule_leave_operators {} => {
+ assert!(mad::Operators::::contains_key(caller), "operator not found");
+ let current_round = mad::CurrentRound::::get();
+ let leaving_time =
+ <::LeaveOperatorsDelay as Get>::get() + current_round;
+ assert_eq!(
+ mad::Operators::::get(caller).unwrap_or_default().status,
+ OperatorStatus::Leaving(leaving_time)
+ );
+ },
+ PCall::cancel_leave_operators {} => {
+ assert_eq!(
+ mad::Operators::::get(caller).unwrap_or_default().status,
+ OperatorStatus::Active
+ );
+ },
+ PCall::execute_leave_operators {} => {
+ assert!(!mad::Operators::::contains_key(caller), "operator not removed");
+ assert!(Balances::reserved_balance(caller).is_zero(), "bond amount not unreserved");
+ },
+ PCall::operator_bond_more { additional_bond } => {
+ let info = MultiAssetDelegation::operator_info(caller).unwrap_or_default();
+ assert!(info.stake.ge(&additional_bond.as_u64()), "bond amount not increased");
+ assert!(
+ Balances::reserved_balance(caller).ge(&additional_bond.as_u64()),
+ "bond amount not reserved"
+ );
+ },
+ PCall::schedule_operator_unstake { unstake_amount } => {
+ let info = MultiAssetDelegation::operator_info(caller).unwrap_or_default();
+ let current_round = MultiAssetDelegation::current_round();
+ let unstake_request = OperatorBondLessRequest {
+ amount: unstake_amount.as_u64(),
+ request_time: current_round,
+ };
+ assert_eq!(info.request, Some(unstake_request), "unstake request not set");
+ },
+ PCall::execute_operator_unstake {} => {
+ let info = MultiAssetDelegation::operator_info(caller).unwrap_or_default();
+ assert!(info.request.is_none(), "unstake request not removed");
+ // reserved balance should be reduced and equal to the stake
+ assert!(
+ Balances::reserved_balance(caller).eq(&info.stake),
+ "reserved balance not equal to stake"
+ );
+ },
+ PCall::cancel_operator_unstake {} => {
+ let info = MultiAssetDelegation::operator_info(caller).unwrap_or_default();
+ assert!(info.request.is_none(), "unstake request not removed");
+ },
+ PCall::go_offline {} => {
+ let info = MultiAssetDelegation::operator_info(caller).unwrap_or_default();
+ assert_eq!(info.status, OperatorStatus::Inactive, "status not set to inactive");
+ },
+ PCall::go_online {} => {
+ let info = MultiAssetDelegation::operator_info(caller).unwrap_or_default();
+ assert_eq!(info.status, OperatorStatus::Active, "status not set to active");
+ },
+ PCall::deposit { asset_id, amount, token_address } => {
+ let (deposit_asset, amount) = match (asset_id.as_u32(), token_address.0 .0) {
+ (0, erc20_token) if erc20_token != [0; 20] => {
+ (Asset::Erc20(erc20_token.into()), amount)
+ },
+ (other_asset_id, _) => (Asset::Custom(other_asset_id.into()), amount),
+ };
+ match deposit_asset {
+ Asset::Custom(id) => {
+ let pallet_balance =
+ Assets::balance(id, MultiAssetDelegation::pallet_account());
+ assert!(pallet_balance.ge(&amount.as_u64()), "pallet balance not enough");
+ },
+ Asset::Erc20(token) => {
+ let pallet_balance = MultiAssetDelegation::query_erc20_balance_of(
+ token,
+ MultiAssetDelegation::pallet_evm_account(),
+ )
+ .unwrap_or_default()
+ .0;
+ assert!(pallet_balance.ge(&amount), "pallet balance not enough");
+ },
+ };
+
+ assert_eq!(
+ MultiAssetDelegation::delegators(caller)
+ .unwrap_or_default()
+ .calculate_delegation_by_asset(deposit_asset),
+ amount.as_u64()
+ );
+ },
+ PCall::schedule_withdraw { asset_id, amount, token_address } => {
+ let (deposit_asset, amount) = match (asset_id.as_u32(), token_address.0 .0) {
+ (0, erc20_token) if erc20_token != [0; 20] => {
+ (Asset::Erc20(erc20_token.into()), amount)
+ },
+ (other_asset_id, _) => (Asset::Custom(other_asset_id.into()), amount),
+ };
+ let round = MultiAssetDelegation::current_round();
+ assert!(
+ MultiAssetDelegation::delegators(caller)
+ .unwrap_or_default()
+ .get_withdraw_requests()
+ .contains(&WithdrawRequest {
+ asset_id: deposit_asset,
+ amount: amount.as_u64(),
+ requested_round: round
+ }),
+ "withdraw request not found"
+ );
+ },
+ PCall::execute_withdraw { .. } => {
+ assert!(
+ MultiAssetDelegation::delegators(caller)
+ .unwrap_or_default()
+ .get_withdraw_requests()
+ .is_empty(),
+ "withdraw requests not removed"
+ );
+ },
+ PCall::cancel_withdraw { asset_id, amount, token_address } => {
+ let round = MultiAssetDelegation::current_round();
+
+ let (deposit_asset, amount) = match (asset_id.as_u32(), token_address.0 .0) {
+ (0, erc20_token) if erc20_token != [0; 20] => {
+ (Asset::Erc20(erc20_token.into()), amount)
+ },
+ (other_asset_id, _) => (Asset::Custom(other_asset_id.into()), amount),
+ };
+ assert!(
+ !MultiAssetDelegation::delegators(caller)
+ .unwrap_or_default()
+ .get_withdraw_requests()
+ .contains(&WithdrawRequest {
+ asset_id: deposit_asset,
+ amount: amount.as_u64(),
+ requested_round: round
+ }),
+ "withdraw request not removed"
+ );
+ },
+ PCall::delegate { operator, asset_id, amount, token_address, .. } => {
+ let (deposit_asset, amount) = match (asset_id.as_u32(), token_address.0 .0) {
+ (0, erc20_token) if erc20_token != [0; 20] => {
+ (Asset::Erc20(erc20_token.into()), amount)
+ },
+ (other_asset_id, _) => (Asset::Custom(other_asset_id.into()), amount),
+ };
+ let operator_account = AccountId::from(operator.0);
+ let delegator = MultiAssetDelegation::delegators(caller).unwrap_or_default();
+ let operator_info =
+ MultiAssetDelegation::operator_info(operator_account).unwrap_or_default();
+ assert!(
+ delegator
+ .calculate_delegation_by_operator(operator_account)
+ .iter()
+ .find_map(|x| {
+ if x.asset_id == deposit_asset {
+ Some(x.amount)
+ } else {
+ None
+ }
+ })
+ .ge(&Some(amount.as_u64())),
+ "delegation amount not set"
+ );
+ assert!(
+ operator_info
+ .delegations
+ .iter()
+ .find_map(|x| {
+ if x.delegator == caller && x.asset_id == deposit_asset {
+ Some(x.amount)
+ } else {
+ None
+ }
+ })
+ .ge(&Some(amount.as_u64())),
+ "delegator not added to operator"
+ );
+ },
+ _ => {
+ // ignore other calls
+ },
+ }
+}
diff --git a/precompiles/multi-asset-delegation/src/lib.rs b/precompiles/multi-asset-delegation/src/lib.rs
index 330b4247..2156218f 100644
--- a/precompiles/multi-asset-delegation/src/lib.rs
+++ b/precompiles/multi-asset-delegation/src/lib.rs
@@ -33,10 +33,10 @@
#![cfg_attr(not(feature = "std"), no_std)]
-#[cfg(test)]
-mod mock;
-#[cfg(test)]
-mod mock_evm;
+#[cfg(any(test, feature = "fuzzing"))]
+pub mod mock;
+#[cfg(any(test, feature = "fuzzing"))]
+pub mod mock_evm;
#[cfg(test)]
mod tests;
@@ -70,7 +70,7 @@ where
::RuntimeOrigin: From