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>, Runtime::RuntimeCall: From>, BalanceOf: TryFrom + Into + solidity::Codec, - AssetIdOf: TryFrom + Into + From, + AssetIdOf: TryFrom + Into + From, Runtime::AccountId: From, { #[precompile::public("joinOperators(uint256)")] @@ -220,7 +220,7 @@ where let caller = handle.context().caller; let who = Runtime::AddressMapping::into_account_id(caller); - let (deposit_asset, amount) = match (asset_id.as_u32(), token_address.0 .0) { + let (deposit_asset, amount) = match (asset_id.as_u128(), token_address.0 .0) { (0, erc20_token) if erc20_token != [0; 20] => { (Asset::Erc20(erc20_token.into()), amount) }, @@ -254,7 +254,7 @@ where let caller = handle.context().caller; let who = Runtime::AddressMapping::into_account_id(caller); - let (deposit_asset, amount) = match (asset_id.as_u32(), token_address.0 .0) { + let (deposit_asset, amount) = match (asset_id.as_u128(), token_address.0 .0) { (0, erc20_token) if erc20_token != [0; 20] => { (Asset::Erc20(erc20_token.into()), amount) }, @@ -287,7 +287,7 @@ where let caller = handle.context().caller; let who = Runtime::AddressMapping::into_account_id(caller); - let (deposit_asset, amount) = match (asset_id.as_u32(), token_address.0 .0) { + let (deposit_asset, amount) = match (asset_id.as_u128(), token_address.0 .0) { (0, erc20_token) if erc20_token != [0; 20] => { (Asset::Erc20(erc20_token.into()), amount) }, @@ -324,7 +324,7 @@ where let operator = Runtime::AddressMapping::into_account_id(H160::from_slice(&operator.0[12..])); - let (deposit_asset, amount) = match (asset_id.as_u32(), token_address.0 .0) { + let (deposit_asset, amount) = match (asset_id.as_u128(), token_address.0 .0) { (0, erc20_token) if erc20_token != [0; 20] => { (Asset::Erc20(erc20_token.into()), amount) }, @@ -366,7 +366,7 @@ where let operator = Runtime::AddressMapping::into_account_id(H160::from_slice(&operator.0[12..])); - let (deposit_asset, amount) = match (asset_id.as_u32(), token_address.0 .0) { + let (deposit_asset, amount) = match (asset_id.as_u128(), token_address.0 .0) { (0, erc20_token) if erc20_token != [0; 20] => { (Asset::Erc20(erc20_token.into()), amount) }, @@ -414,7 +414,7 @@ where let operator = Runtime::AddressMapping::into_account_id(H160::from_slice(&operator.0[12..])); - let (deposit_asset, amount) = match (asset_id.as_u32(), token_address.0 .0) { + let (deposit_asset, amount) = match (asset_id.as_u128(), token_address.0 .0) { (0, erc20_token) if erc20_token != [0; 20] => { (Asset::Erc20(erc20_token.into()), amount) }, diff --git a/precompiles/multi-asset-delegation/src/mock.rs b/precompiles/multi-asset-delegation/src/mock.rs index aff71710..6d5407aa 100644 --- a/precompiles/multi-asset-delegation/src/mock.rs +++ b/precompiles/multi-asset-delegation/src/mock.rs @@ -45,7 +45,7 @@ pub type AccountId = <::Signer as IdentifyAccount>::Account pub type Balance = u64; type Block = frame_system::mocking::MockBlock; -type AssetId = u32; +type AssetId = u128; const PRECOMPILE_ADDRESS_BYTES: [u8; 32] = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, @@ -218,7 +218,7 @@ impl pallet_assets::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = u64; type AssetId = AssetId; - type AssetIdParameter = u32; + type AssetIdParameter = u128; type Currency = Balances; type CreateOrigin = AsEnsureOriginWithArg>; type ForceOrigin = frame_system::EnsureRoot; @@ -337,14 +337,14 @@ impl pallet_multi_asset_delegation::Config for Runtime { /// Build test externalities, prepopulated with data for testing democracy precompiles #[derive(Default)] -pub(crate) struct ExtBuilder { +pub struct ExtBuilder { /// Endowed accounts with balances balances: Vec<(AccountId, Balance)>, } impl ExtBuilder { /// Build the test externalities for use in tests - pub(crate) fn build(self) -> sp_io::TestExternalities { + pub fn build(self) -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default() .build_storage() .expect("Frame system builds valid default genesis config"); diff --git a/precompiles/multi-asset-delegation/src/tests.rs b/precompiles/multi-asset-delegation/src/tests.rs index b7dfdfbd..a2fdac39 100644 --- a/precompiles/multi-asset-delegation/src/tests.rs +++ b/precompiles/multi-asset-delegation/src/tests.rs @@ -7,7 +7,7 @@ use tangle_primitives::services::Asset; // Helper function for creating and minting tokens pub fn create_and_mint_tokens( - asset_id: u32, + asset_id: u128, recipient: ::AccountId, amount: Balance, ) { diff --git a/taplo.toml b/taplo.toml index 6c2b1c48..cf43a1dd 100644 --- a/taplo.toml +++ b/taplo.toml @@ -6,4 +6,4 @@ column_width = 120 reorder_keys = false indent_tables = false indent_entries = false -indent_string = " " +indent_string = " "