diff --git a/Cargo.lock b/Cargo.lock index ee7a237b..85dd22e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9656,6 +9656,64 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-rewards" +version = "1.2.4" +dependencies = [ + "ethabi", + "ethereum", + "ethers", + "fp-account", + "fp-consensus", + "fp-dynamic-fee", + "fp-ethereum", + "fp-evm", + "fp-rpc", + "fp-self-contained", + "fp-storage", + "frame-benchmarking", + "frame-election-provider-support", + "frame-support", + "frame-system", + "hex", + "hex-literal 0.4.1", + "itertools 0.13.0", + "libsecp256k1", + "log", + "num_enum", + "pallet-assets", + "pallet-balances", + "pallet-base-fee", + "pallet-dynamic-fee", + "pallet-ethereum", + "pallet-evm", + "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", + "pallet-timestamp", + "parity-scale-codec", + "precompile-utils", + "scale-info", + "serde", + "serde_json", + "smallvec", + "sp-core", + "sp-io", + "sp-keyring", + "sp-keystore", + "sp-runtime", + "sp-staking", + "sp-std", + "tangle-primitives", +] + [[package]] name = "pallet-scheduler" version = "38.0.0" @@ -15886,6 +15944,7 @@ dependencies = [ "pallet-offences", "pallet-preimage", "pallet-proxy", + "pallet-rewards", "pallet-scheduler", "pallet-services", "pallet-services-rpc-runtime-api", @@ -16016,6 +16075,7 @@ dependencies = [ "pallet-offences", "pallet-preimage", "pallet-proxy", + "pallet-rewards", "pallet-scheduler", "pallet-services", "pallet-services-rpc-runtime-api", diff --git a/Cargo.toml b/Cargo.toml index 89667d7b..7f72d001 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,6 +126,7 @@ pallet-services-rpc = { path = "pallets/services/rpc" } pallet-multi-asset-delegation = { path = "pallets/multi-asset-delegation", default-features = false } pallet-tangle-lst-benchmarking = { path = "pallets/tangle-lst/benchmarking", default-features = false } pallet-oracle = { path = "pallets/oracle", default-features = false } +pallet-rewards = { path = "pallets/rewards", default-features = false } k256 = { version = "0.13.3", default-features = false } p256 = { version = "0.13.2", default-features = false } diff --git a/node/tests/evm_restaking.rs b/node/tests/evm_restaking.rs index 94e9c364..3619e949 100644 --- a/node/tests/evm_restaking.rs +++ b/node/tests/evm_restaking.rs @@ -225,7 +225,7 @@ fn operator_join_delegator_delegate_erc20() { // Deposit and delegate let deposit_result = precompile - .deposit(U256::ZERO, *usdc.address(), delegate_amount) + .deposit(U256::ZERO, *usdc.address(), delegate_amount, 0) .from(bob.address()) .send() .await? @@ -314,7 +314,7 @@ fn operator_join_delegator_delegate_asset_id() { // Deposit and delegate using asset ID let deposit_result = precompile - .deposit(U256::from(t.usdc_asset_id), Address::ZERO, U256::from(delegate_amount)) + .deposit(U256::from(t.usdc_asset_id), Address::ZERO, U256::from(delegate_amount), 0) .from(bob.address()) .send() .await? diff --git a/pallets/multi-asset-delegation/fuzzer/call.rs b/pallets/multi-asset-delegation/fuzzer/call.rs index ecc81fc0..14e94188 100644 --- a/pallets/multi-asset-delegation/fuzzer/call.rs +++ b/pallets/multi-asset-delegation/fuzzer/call.rs @@ -30,10 +30,7 @@ use frame_system::ensure_signed_or_root; use honggfuzz::fuzz; use pallet_multi_asset_delegation::{mock::*, pallet as mad, types::*}; use rand::{seq::SliceRandom, Rng}; -use sp_runtime::{ - traits::{Scale, Zero}, - Percent, -}; +use sp_runtime::traits::{Scale, Zero}; const MAX_ED_MULTIPLE: Balance = 10_000; const MIN_ED_MULTIPLE: Balance = 10; @@ -197,7 +194,8 @@ fn random_calls( let amount = random_ed_multiple(&mut rng); let evm_address = if rng.gen_bool(0.5) { Some(rng.gen::<[u8; 20]>().into()) } else { None }; - [(mad::Call::deposit { asset_id, amount, evm_address }, origin)].to_vec() + [(mad::Call::deposit { asset_id, amount, evm_address, lock_multiplier: None }, origin)] + .to_vec() }, "schedule_withdraw" => { // Schedule withdraw @@ -283,70 +281,6 @@ fn random_calls( ] .to_vec() }, - "set_incentive_apy_and_cap" => { - // Set incentive APY and cap - let is_root = rng.gen_bool(0.5); - let (origin, who) = if is_root { - (RuntimeOrigin::root(), [0u8; 32].into()) - } else { - random_signed_origin(&mut rng) - }; - fund_account(&mut rng, &who); - let vault_id = rng.gen(); - let apy = Percent::from_percent(rng.gen_range(0..100)); - let cap = rng.gen_range(0..Balance::MAX); - [(mad::Call::set_incentive_apy_and_cap { vault_id, apy, cap }, origin)].to_vec() - }, - "whitelist_blueprint_for_rewards" => { - // Whitelist blueprint for rewards - let is_root = rng.gen_bool(0.5); - let (origin, who) = if is_root { - (RuntimeOrigin::root(), [0u8; 32].into()) - } else { - random_signed_origin(&mut rng) - }; - fund_account(&mut rng, &who); - let blueprint_id = rng.gen::(); - [(mad::Call::whitelist_blueprint_for_rewards { blueprint_id }, origin)].to_vec() - }, - "manage_asset_in_vault" => { - // Manage asset in vault - let is_root = rng.gen_bool(0.5); - let (origin, who) = if is_root { - (RuntimeOrigin::root(), [0u8; 32].into()) - } else { - random_signed_origin(&mut rng) - }; - fund_account(&mut rng, &who); - let asset_id = random_asset(&mut rng); - let vault_id = rng.gen(); - let action = if rng.gen() { AssetAction::Add } else { AssetAction::Remove }; - [(mad::Call::manage_asset_in_vault { asset_id, vault_id, action }, origin)].to_vec() - }, - "add_blueprint_id" => { - // Add blueprint ID - let is_root = rng.gen_bool(0.5); - let (origin, who) = if is_root { - (RuntimeOrigin::root(), [0u8; 32].into()) - } else { - random_signed_origin(&mut rng) - }; - fund_account(&mut rng, &who); - let blueprint_id = rng.gen::(); - [(mad::Call::add_blueprint_id { blueprint_id }, origin)].to_vec() - }, - "remove_blueprint_id" => { - // Remove blueprint ID - let is_root = rng.gen_bool(0.5); - let (origin, who) = if is_root { - (RuntimeOrigin::root(), [0u8; 32].into()) - } else { - random_signed_origin(&mut rng) - }; - fund_account(&mut rng, &who); - let blueprint_id = rng.gen::(); - [(mad::Call::remove_blueprint_id { blueprint_id }, origin)].to_vec() - }, _ => { unimplemented!("unknown call name: {}", op) }, @@ -553,11 +487,6 @@ fn do_sanity_checks(call: mad::Call, origin: RuntimeOrigin, outcome: Po mad::Call::schedule_delegator_unstake { operator, asset_id, amount } => {}, mad::Call::execute_delegator_unstake {} => {}, mad::Call::cancel_delegator_unstake { operator, asset_id, amount } => {}, - mad::Call::set_incentive_apy_and_cap { vault_id, apy, cap } => {}, - mad::Call::whitelist_blueprint_for_rewards { blueprint_id } => {}, - mad::Call::manage_asset_in_vault { vault_id, asset_id, action } => {}, - mad::Call::add_blueprint_id { blueprint_id } => {}, - mad::Call::remove_blueprint_id { blueprint_id } => {}, other => unimplemented!("sanity checks for call: {other:?} not implemented"), } } diff --git a/pallets/multi-asset-delegation/src/functions.rs b/pallets/multi-asset-delegation/src/functions.rs index 9ee9c028..30f79692 100644 --- a/pallets/multi-asset-delegation/src/functions.rs +++ b/pallets/multi-asset-delegation/src/functions.rs @@ -19,5 +19,4 @@ pub mod delegate; pub mod deposit; pub mod evm; pub mod operator; -pub mod rewards; pub mod session_manager; diff --git a/pallets/multi-asset-delegation/src/functions/delegate.rs b/pallets/multi-asset-delegation/src/functions/delegate.rs index 779c6fe0..36c05f8c 100644 --- a/pallets/multi-asset-delegation/src/functions/delegate.rs +++ b/pallets/multi-asset-delegation/src/functions/delegate.rs @@ -56,15 +56,13 @@ impl Pallet { let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; // Ensure enough deposited balance - let balance = + let user_deposit = metadata.deposits.get_mut(&asset_id).ok_or(Error::::InsufficientBalance)?; - ensure!(*balance >= amount, Error::::InsufficientBalance); - // Reduce the balance in deposits - *balance = balance.checked_sub(&amount).ok_or(Error::::InsufficientBalance)?; - if *balance == Zero::zero() { - metadata.deposits.remove(&asset_id); - } + // update the user deposit + user_deposit + .increase_delegated_amount(amount) + .map_err(|_| Error::::InsufficientBalance)?; // Check if the delegation exists and update it, otherwise create a new delegation if let Some(delegation) = metadata @@ -237,27 +235,35 @@ impl Pallet { ensure!(!metadata.delegator_unstake_requests.is_empty(), Error::::NoBondLessRequest); let current_round = Self::current_round(); + let delay = T::DelegationBondLessDelay::get(); - // Process all ready unstake requests - let mut executed_requests = Vec::new(); - metadata.delegator_unstake_requests.retain(|request| { - let delay = T::DelegationBondLessDelay::get(); - if current_round >= delay + request.requested_round { - // Add the amount back to the delegator's deposits - metadata - .deposits - .entry(request.asset_id) - .and_modify(|e| *e += request.amount) - .or_insert(request.amount); - executed_requests.push(request.clone()); - false // Remove this request - } else { - true // Keep this request - } - }); + // First, collect all ready requests and process them + let ready_requests: Vec<_> = metadata + .delegator_unstake_requests + .iter() + .filter(|request| current_round >= delay + request.requested_round) + .cloned() + .collect(); + + // If no requests are ready, return an error + ensure!(!ready_requests.is_empty(), Error::::BondLessNotReady); + + // Process each ready request + for request in ready_requests.iter() { + let deposit_record = metadata + .deposits + .get_mut(&request.asset_id) + .ok_or(Error::::InsufficientBalance)?; + + deposit_record + .decrease_delegated_amount(request.amount) + .map_err(|_| Error::::InsufficientBalance)?; + } - // If no requests were executed, return an error - ensure!(!executed_requests.is_empty(), Error::::BondLessNotReady); + // Remove the processed requests + metadata + .delegator_unstake_requests + .retain(|request| current_round < delay + request.requested_round); Ok(()) }) diff --git a/pallets/multi-asset-delegation/src/functions/deposit.rs b/pallets/multi-asset-delegation/src/functions/deposit.rs index e98056ac..99b6ed54 100644 --- a/pallets/multi-asset-delegation/src/functions/deposit.rs +++ b/pallets/multi-asset-delegation/src/functions/deposit.rs @@ -18,11 +18,12 @@ use crate::{types::*, Pallet}; use frame_support::{ ensure, pallet_prelude::DispatchResult, - sp_runtime::traits::{AccountIdConversion, CheckedAdd, Zero}, + sp_runtime::traits::AccountIdConversion, traits::{fungibles::Mutate, tokens::Preservation, Get}, }; use sp_core::H160; use tangle_primitives::services::{Asset, EvmAddressMapping}; +use tangle_primitives::types::rewards::LockMultiplier; impl Pallet { /// Returns the account ID of the pallet. @@ -89,22 +90,27 @@ impl Pallet { asset_id: Asset, amount: BalanceOf, evm_address: Option, + lock_multiplier: Option, ) -> DispatchResult { ensure!(amount >= T::MinDelegateAmount::get(), Error::::BondTooLow); // Transfer the amount to the pallet account Self::handle_transfer_to_pallet(&who, asset_id, amount, evm_address)?; + let now = >::block_number(); + // Update storage Delegators::::try_mutate(&who, |maybe_metadata| -> DispatchResult { let metadata = maybe_metadata.get_or_insert_with(Default::default); - // Handle checked addition first to avoid ? operator in closure - if let Some(existing) = metadata.deposits.get(&asset_id) { - let new_amount = - existing.checked_add(&amount).ok_or(Error::::DepositOverflow)?; - metadata.deposits.insert(asset_id, new_amount); + // If there's an existing deposit, increase it + if let Some(existing) = metadata.deposits.get_mut(&asset_id) { + existing + .increase_deposited_amount(amount, lock_multiplier, now) + .map_err(|_| Error::::InsufficientBalance)?; } else { - metadata.deposits.insert(asset_id, amount); + // Create a new deposit if none exists + let new_deposit = Deposit::new(amount, lock_multiplier, now); + metadata.deposits.insert(asset_id, new_deposit); } Ok(()) })?; @@ -132,16 +138,14 @@ impl Pallet { Delegators::::try_mutate(&who, |maybe_metadata| { let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; + let now = >::block_number(); + // Ensure there is enough deposited balance - let balance = + let deposit = metadata.deposits.get_mut(&asset_id).ok_or(Error::::InsufficientBalance)?; - ensure!(*balance >= amount, Error::::InsufficientBalance); - - // Reduce the balance in deposits - *balance -= amount; - if *balance == Zero::zero() { - metadata.deposits.remove(&asset_id); - } + deposit + .decrease_deposited_amount(amount, now) + .map_err(|_| Error::::InsufficientBalance)?; // Create the unstake request let current_round = Self::current_round(); @@ -244,6 +248,7 @@ impl Pallet { ) -> DispatchResult { Delegators::::try_mutate(&who, |maybe_metadata| { let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; + let now = >::block_number(); // Find and remove the matching withdraw request let request_index = metadata @@ -255,11 +260,16 @@ impl Pallet { let withdraw_request = metadata.withdraw_requests.remove(request_index); // Add the amount back to the delegator's deposits - metadata - .deposits - .entry(asset_id) - .and_modify(|e| *e += withdraw_request.amount) - .or_insert(withdraw_request.amount); + if let Some(deposit) = metadata.deposits.get_mut(&withdraw_request.asset_id) { + deposit + .increase_deposited_amount(withdraw_request.amount, None, now) + .map_err(|_| Error::::InsufficientBalance)?; + } else { + // we are only able to withdraw from existing deposits without any locks + // so when we add back, add it without any locks + let new_deposit = Deposit::new(withdraw_request.amount, None, now); + metadata.deposits.insert(withdraw_request.asset_id, new_deposit); + } // Update the status if no more delegations exist if metadata.delegations.is_empty() { diff --git a/pallets/multi-asset-delegation/src/functions/operator.rs b/pallets/multi-asset-delegation/src/functions/operator.rs index 68a22b8f..ef603e77 100644 --- a/pallets/multi-asset-delegation/src/functions/operator.rs +++ b/pallets/multi-asset-delegation/src/functions/operator.rs @@ -27,7 +27,8 @@ use sp_runtime::{ traits::{CheckedAdd, CheckedSub}, DispatchError, Percent, }; -use tangle_primitives::{BlueprintId, ServiceManager}; +use tangle_primitives::traits::ServiceManager; +use tangle_primitives::BlueprintId; impl Pallet { /// Handles the deposit of stake amount and creation of an operator. diff --git a/pallets/multi-asset-delegation/src/functions/rewards.rs b/pallets/multi-asset-delegation/src/functions/rewards.rs deleted file mode 100644 index 23288fce..00000000 --- a/pallets/multi-asset-delegation/src/functions/rewards.rs +++ /dev/null @@ -1,144 +0,0 @@ -// 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 . -use super::*; -use crate::{types::*, Pallet}; -use frame_support::{ensure, pallet_prelude::DispatchResult, traits::Currency}; -use sp_runtime::DispatchError; -use sp_std::vec::Vec; -use tangle_primitives::{services::Asset, RoundIndex}; - -impl Pallet { - #[allow(clippy::type_complexity)] - pub fn distribute_rewards(_round: RoundIndex) -> DispatchResult { - // let mut delegation_info: BTreeMap< - // T::AssetId, - // Vec, T::AssetId>>, - // > = BTreeMap::new(); - - // // Iterate through all operator snapshots for the given round - // // TODO: Could be dangerous with many operators - // for (_, operator_snapshot) in AtStake::::iter_prefix(round) { - // for delegation in &operator_snapshot.delegations { - // delegation_info.entry(delegation.asset_id).or_default().push(delegation.clone()); - // } - // } - - // // Get the reward configuration - // if let Some(reward_config) = RewardConfigStorage::::get() { - // // Distribute rewards for each asset - // for (asset_id, delegations) in delegation_info.iter() { - // // We only reward asset in a reward vault - // if let Some(vault_id) = AssetLookupRewardVaults::::get(asset_id) { - // if let Some(config) = reward_config.configs.get(&vault_id) { - // // Calculate total amount and distribute rewards - // let total_amount: BalanceOf = - // delegations.iter().fold(Zero::zero(), |acc, d| acc + d.amount); - // let cap: BalanceOf = config.cap; - - // if total_amount >= cap { - // // Calculate the total reward based on the APY - // let total_reward = - // Self::calculate_total_reward(config.apy, total_amount)?; - - // for delegation in delegations { - // // Calculate the percentage of the cap that the user is staking - // let staking_percentage = - // delegation.amount.saturating_mul(100u32.into()) / cap; - // // Calculate the reward based on the staking percentage - // let reward = - // total_reward.saturating_mul(staking_percentage) / 100u32.into(); - // // Distribute the reward to the delegator - // Self::distribute_reward_to_delegator( - // &delegation.delegator, - // reward, - // )?; - // } - // } - // } - // } - // } - // } - - Ok(()) - } - - fn _calculate_total_reward( - apy: sp_runtime::Percent, - total_amount: BalanceOf, - ) -> Result, DispatchError> { - let total_reward = apy.mul_floor(total_amount); - Ok(total_reward) - } - - fn _distribute_reward_to_delegator( - delegator: &T::AccountId, - reward: BalanceOf, - ) -> DispatchResult { - // mint rewards to delegator - let _ = T::Currency::deposit_creating(delegator, reward); - Ok(()) - } - - pub fn add_asset_to_vault( - vault_id: &T::VaultId, - asset_id: &Asset, - ) -> DispatchResult { - // Ensure the asset is not already associated with any vault - ensure!( - !AssetLookupRewardVaults::::contains_key(asset_id), - Error::::AssetAlreadyInVault - ); - - // Update RewardVaults storage - RewardVaults::::try_mutate(vault_id, |maybe_assets| -> DispatchResult { - let assets = maybe_assets.get_or_insert_with(Vec::new); - - // Ensure the asset is not already in the vault - ensure!(!assets.contains(asset_id), Error::::AssetAlreadyInVault); - - assets.push(*asset_id); - - Ok(()) - })?; - - // Update AssetLookupRewardVaults storage - AssetLookupRewardVaults::::insert(asset_id, vault_id); - - Ok(()) - } - - pub fn remove_asset_from_vault( - vault_id: &T::VaultId, - asset_id: &Asset, - ) -> DispatchResult { - // Update RewardVaults storage - RewardVaults::::try_mutate(vault_id, |maybe_assets| -> DispatchResult { - let assets = maybe_assets.as_mut().ok_or(Error::::VaultNotFound)?; - - // Ensure the asset is in the vault - ensure!(assets.contains(asset_id), Error::::AssetNotInVault); - - assets.retain(|id| id != asset_id); - - Ok(()) - })?; - - // Update AssetLookupRewardVaults storage - AssetLookupRewardVaults::::remove(asset_id); - - Ok(()) - } -} diff --git a/pallets/multi-asset-delegation/src/lib.rs b/pallets/multi-asset-delegation/src/lib.rs index bd831fe4..b2c278ba 100644 --- a/pallets/multi-asset-delegation/src/lib.rs +++ b/pallets/multi-asset-delegation/src/lib.rs @@ -79,7 +79,7 @@ pub use functions::*; #[frame_support::pallet] pub mod pallet { - use crate::types::{delegator::DelegatorBlueprintSelection, AssetAction, *}; + use crate::types::{delegator::DelegatorBlueprintSelection, *}; use frame_support::{ pallet_prelude::*, traits::{tokens::fungibles, Currency, Get, LockableCurrency, ReservableCurrency}, @@ -88,8 +88,10 @@ pub mod pallet { use frame_system::pallet_prelude::*; use scale_info::TypeInfo; use sp_core::H160; - use sp_runtime::traits::{MaybeSerializeDeserialize, Member, Zero}; - use sp_std::{collections::btree_map::BTreeMap, fmt::Debug, prelude::*, vec::Vec}; + use sp_runtime::traits::{MaybeSerializeDeserialize, Member}; + use sp_std::{fmt::Debug, prelude::*, vec::Vec}; + use tangle_primitives::traits::RewardsManager; + use tangle_primitives::types::rewards::LockMultiplier; use tangle_primitives::{services::Asset, traits::ServiceManager, BlueprintId, RoundIndex}; /// Configure the pallet by specifying the parameters and types on which it depends. @@ -115,16 +117,6 @@ pub mod pallet { + Decode + TypeInfo; - /// Type representing the unique ID of a vault. - type VaultId: Parameter - + Member - + Copy - + MaybeSerializeDeserialize - + Ord - + Default - + MaxEncodedLen - + TypeInfo; - /// The maximum number of blueprints a delegator can have in Fixed mode. #[pallet::constant] type MaxDelegatorBlueprints: Get + TypeInfo + MaxEncodedLen + Clone + Debug + PartialEq; @@ -200,6 +192,14 @@ pub mod pallet { /// A type that implements the `EvmAddressMapping` trait for the conversion of EVM address type EvmAddressMapping: tangle_primitives::services::EvmAddressMapping; + /// Type that implements the reward manager trait + type RewardsManager: tangle_primitives::traits::RewardsManager< + Self::AccountId, + Self::AssetId, + BalanceOf, + BlockNumberFor, + >; + /// A type representing the weights required by the dispatchables of this pallet. type WeightInfo: crate::weights::WeightInfo; } @@ -239,25 +239,6 @@ pub mod pallet { pub type Delegators = StorageMap<_, Blake2_128Concat, T::AccountId, DelegatorMetadataOf>; - #[pallet::storage] - #[pallet::getter(fn reward_vaults)] - /// Storage for the reward vaults - pub type RewardVaults = - StorageMap<_, Blake2_128Concat, T::VaultId, Vec>, OptionQuery>; - - #[pallet::storage] - #[pallet::getter(fn asset_reward_vault_lookup)] - /// Storage for the reward vaults - pub type AssetLookupRewardVaults = - StorageMap<_, Blake2_128Concat, Asset, T::VaultId, OptionQuery>; - - #[pallet::storage] - #[pallet::getter(fn reward_config)] - /// Storage for the reward configuration, which includes APY, cap for assets, and whitelisted - /// blueprints. - pub type RewardConfigStorage = - StorageValue<_, RewardConfig>, OptionQuery>; - /// Events emitted by the pallet. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -308,17 +289,6 @@ pub mod pallet { ExecutedDelegatorBondLess { who: T::AccountId }, /// A delegator unstake request has been cancelled. CancelledDelegatorBondLess { who: T::AccountId }, - /// Event emitted when an incentive APY and cap are set for a reward vault - IncentiveAPYAndCapSet { vault_id: T::VaultId, apy: sp_runtime::Percent, cap: BalanceOf }, - /// Event emitted when a blueprint is whitelisted for rewards - BlueprintWhitelisted { blueprint_id: BlueprintId }, - /// Asset has been updated to reward vault - AssetUpdatedInVault { - who: T::AccountId, - vault_id: T::VaultId, - asset_id: Asset, - action: AssetAction, - }, /// Operator has been slashed OperatorSlashed { who: T::AccountId, amount: BalanceOf }, /// Delegator has been slashed @@ -430,6 +400,10 @@ pub mod pallet { EVMAbiEncode, /// EVM decode error EVMAbiDecode, + /// Cannot unstake with locks + LockViolation, + /// Above deposit caps setup + DepositExceedsCapForAsset, } /// Hooks for the pallet. @@ -705,9 +679,14 @@ pub mod pallet { asset_id: Asset, amount: BalanceOf, evm_address: Option, + lock_multiplier: Option, ) -> DispatchResult { let who = ensure_signed(origin)?; - Self::process_deposit(who.clone(), asset_id, amount, evm_address)?; + // ensure the caps have not been exceeded + let remaning = T::RewardsManager::get_asset_deposit_cap_remaining(asset_id) + .map_err(|_| Error::::DepositExceedsCapForAsset)?; + ensure!(amount <= remaning, Error::::DepositExceedsCapForAsset); + Self::process_deposit(who.clone(), asset_id, amount, evm_address, lock_multiplier)?; Self::deposit_event(Event::Deposited { who, amount, asset_id }); Ok(()) } @@ -930,125 +909,6 @@ pub mod pallet { Ok(()) } - /// Sets the APY and cap for a specific asset. - /// - /// # Permissions - /// - /// * Must be called by the force origin - /// - /// # Arguments - /// - /// * `origin` - Origin of the call - /// * `vault_id` - ID of the vault - /// * `apy` - Annual percentage yield (max 10%) - /// * `cap` - Required deposit amount for full APY - /// - /// # Errors - /// - /// * [`Error::APYExceedsMaximum`] - APY exceeds 10% maximum - /// * [`Error::CapCannotBeZero`] - Cap amount cannot be zero - #[pallet::call_index(18)] - #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] - pub fn set_incentive_apy_and_cap( - origin: OriginFor, - vault_id: T::VaultId, - apy: sp_runtime::Percent, - cap: BalanceOf, - ) -> DispatchResult { - T::ForceOrigin::ensure_origin(origin)?; - - ensure!(apy <= sp_runtime::Percent::from_percent(10), Error::::APYExceedsMaximum); - ensure!(!cap.is_zero(), Error::::CapCannotBeZero); - - RewardConfigStorage::::mutate(|maybe_config| { - let mut config = maybe_config.take().unwrap_or_else(|| RewardConfig { - configs: BTreeMap::new(), - whitelisted_blueprint_ids: Vec::new(), - }); - - config.configs.insert(vault_id, RewardConfigForAssetVault { apy, cap }); - - *maybe_config = Some(config); - }); - - Self::deposit_event(Event::IncentiveAPYAndCapSet { vault_id, apy, cap }); - - Ok(()) - } - - /// Whitelists a blueprint for rewards. - /// - /// # Permissions - /// - /// * Must be called by the force origin - /// - /// # Arguments - /// - /// * `origin` - Origin of the call - /// * `blueprint_id` - ID of blueprint to whitelist - #[pallet::call_index(19)] - #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] - pub fn whitelist_blueprint_for_rewards( - origin: OriginFor, - blueprint_id: BlueprintId, - ) -> DispatchResult { - T::ForceOrigin::ensure_origin(origin)?; - - RewardConfigStorage::::mutate(|maybe_config| { - let mut config = maybe_config.take().unwrap_or_else(|| RewardConfig { - configs: BTreeMap::new(), - whitelisted_blueprint_ids: Vec::new(), - }); - - if !config.whitelisted_blueprint_ids.contains(&blueprint_id) { - config.whitelisted_blueprint_ids.push(blueprint_id); - } - - *maybe_config = Some(config); - }); - - Self::deposit_event(Event::BlueprintWhitelisted { blueprint_id }); - - Ok(()) - } - - /// Manage asset id to vault rewards. - /// - /// # Permissions - /// - /// * Must be signed by an authorized account - /// - /// # Arguments - /// - /// * `origin` - Origin of the call - /// * `vault_id` - ID of the vault - /// * `asset_id` - ID of the asset - /// * `action` - Action to perform (Add/Remove) - /// - /// # Errors - /// - /// * [`Error::AssetAlreadyInVault`] - Asset already exists in vault - /// * [`Error::AssetNotInVault`] - Asset does not exist in vault - #[pallet::call_index(20)] - #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] - pub fn manage_asset_in_vault( - origin: OriginFor, - vault_id: T::VaultId, - asset_id: Asset, - action: AssetAction, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - match action { - AssetAction::Add => Self::add_asset_to_vault(&vault_id, &asset_id)?, - AssetAction::Remove => Self::remove_asset_from_vault(&vault_id, &asset_id)?, - } - - Self::deposit_event(Event::AssetUpdatedInVault { who, vault_id, asset_id, action }); - - Ok(()) - } - /// Adds a blueprint ID to a delegator's selection. /// /// # Permissions diff --git a/pallets/multi-asset-delegation/src/mock.rs b/pallets/multi-asset-delegation/src/mock.rs index 0618f553..f005288b 100644 --- a/pallets/multi-asset-delegation/src/mock.rs +++ b/pallets/multi-asset-delegation/src/mock.rs @@ -38,12 +38,15 @@ use serde_json::json; use sp_core::{sr25519, H160}; use sp_keyring::AccountKeyring; use sp_keystore::{testing::MemoryKeystore, KeystoreExt, KeystorePtr}; +use sp_runtime::DispatchError; use sp_runtime::{ testing::UintAuthorityId, traits::{ConvertInto, IdentityLookup}, AccountId32, BuildStorage, Perbill, }; use tangle_primitives::services::{EvmAddressMapping, EvmGasWeightMapping, EvmRunner}; +use tangle_primitives::traits::RewardsManager; +use tangle_primitives::types::rewards::LockMultiplier; use core::ops::Mul; use std::{collections::BTreeMap, sync::Arc}; @@ -273,7 +276,7 @@ impl pallet_assets::Config for Runtime { pub struct MockServiceManager; -impl tangle_primitives::ServiceManager for MockServiceManager { +impl tangle_primitives::traits::ServiceManager for MockServiceManager { fn get_active_blueprints_count(_account: &AccountId) -> usize { // we dont care Default::default() @@ -313,6 +316,45 @@ parameter_types! { pub const MaxDelegations: u32 = 50; } +pub struct MockRewardsManager; + +impl RewardsManager for MockRewardsManager { + type Error = DispatchError; + + fn record_deposit( + _account_id: &AccountId, + _asset: Asset, + _amount: Balance, + _lock_multiplier: Option, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn record_withdrawal( + _account_id: &AccountId, + _asset: Asset, + _amount: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn record_service_reward( + _account_id: &AccountId, + _asset: Asset, + _amount: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn get_asset_deposit_cap_remaining(_asset: Asset) -> Result { + Ok(100_000_u32.into()) + } + + fn get_asset_incentive_cap(_asset: Asset) -> Result { + Ok(0_u32.into()) + } +} + impl pallet_multi_asset_delegation::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = Balances; @@ -326,7 +368,6 @@ impl pallet_multi_asset_delegation::Config for Runtime { type MinDelegateAmount = ConstU128<100>; type Fungibles = Assets; type AssetId = AssetId; - type VaultId = AssetId; type ForceOrigin = frame_system::EnsureRoot; type PalletId = PID; type MaxDelegatorBlueprints = MaxDelegatorBlueprints; @@ -338,6 +379,7 @@ impl pallet_multi_asset_delegation::Config for Runtime { type EvmRunner = MockedEvmRunner; type EvmGasWeightMapping = PalletEVMGasWeightMapping; type EvmAddressMapping = PalletEVMAddressMapping; + type RewardsManager = MockRewardsManager; type WeightInfo = (); } @@ -449,40 +491,6 @@ pub fn new_test_ext_raw_authorities() -> sp_io::TestExternalities { evm_config.assimilate_storage(&mut t).unwrap(); - // let assets_config = pallet_assets::GenesisConfig:: { - // assets: vec![ - // (USDC, authorities[0].clone(), true, 100_000), // 1 cent. - // (WETH, authorities[1].clone(), true, 100), // 100 wei. - // (WBTC, authorities[2].clone(), true, 100), // 100 satoshi. - // (VDOT, authorities[0].clone(), true, 100), - // ], - // metadata: vec![ - // (USDC, Vec::from(b"USD Coin"), Vec::from(b"USDC"), 6), - // (WETH, Vec::from(b"Wrapped Ether"), Vec::from(b"WETH"), 18), - // (WBTC, Vec::from(b"Wrapped Bitcoin"), Vec::from(b"WBTC"), 18), - // (VDOT, Vec::from(b"VeChain"), Vec::from(b"VDOT"), 18), - // ], - // accounts: vec![ - // (USDC, authorities[0].clone(), 1_000_000 * 10u128.pow(6)), - // (WETH, authorities[0].clone(), 100 * 10u128.pow(18)), - // (WBTC, authorities[0].clone(), 50 * 10u128.pow(18)), - // // - // (USDC, authorities[1].clone(), 1_000_000 * 10u128.pow(6)), - // (WETH, authorities[1].clone(), 100 * 10u128.pow(18)), - // (WBTC, authorities[1].clone(), 50 * 10u128.pow(18)), - // // - // (USDC, authorities[2].clone(), 1_000_000 * 10u128.pow(6)), - // (WETH, authorities[2].clone(), 100 * 10u128.pow(18)), - // (WBTC, authorities[2].clone(), 50 * 10u128.pow(18)), - - // // - // (VDOT, authorities[0].clone(), 1_000_000 * 10u128.pow(6)), - // (VDOT, authorities[1].clone(), 1_000_000 * 10u128.pow(6)), - // (VDOT, authorities[2].clone(), 1_000_000 * 10u128.pow(6)), - // ], - // next_asset_id: Some(4), - // }; - // assets_config.assimilate_storage(&mut t).unwrap(); let mut ext = sp_io::TestExternalities::new(t); ext.register_extension(KeystoreExt(Arc::new(MemoryKeystore::new()) as KeystorePtr)); diff --git a/pallets/multi-asset-delegation/src/tests/delegate.rs b/pallets/multi-asset-delegation/src/tests/delegate.rs index 92a7316d..ed2e6004 100644 --- a/pallets/multi-asset-delegation/src/tests/delegate.rs +++ b/pallets/multi-asset-delegation/src/tests/delegate.rs @@ -41,7 +41,8 @@ fn delegate_should_work() { RuntimeOrigin::signed(who.clone()), asset_id.clone(), amount, - None + None, + None, )); assert_ok!(MultiAssetDelegation::delegate( @@ -49,12 +50,11 @@ fn delegate_should_work() { operator.clone(), asset_id.clone(), amount, - Default::default() + Default::default(), )); // Assert let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); - assert!(metadata.deposits.get(&asset_id).is_none()); assert_eq!(metadata.delegations.len(), 1); let delegation = &metadata.delegations[0]; assert_eq!(delegation.operator, operator.clone()); @@ -93,6 +93,7 @@ fn schedule_delegator_unstake_should_work() { RuntimeOrigin::signed(who.clone()), asset_id.clone(), amount, + None, None )); assert_ok!(MultiAssetDelegation::delegate( @@ -100,7 +101,7 @@ fn schedule_delegator_unstake_should_work() { operator.clone(), asset_id.clone(), amount, - Default::default() + Default::default(), )); assert_ok!(MultiAssetDelegation::schedule_delegator_unstake( @@ -146,6 +147,7 @@ fn execute_delegator_unstake_should_work() { RuntimeOrigin::signed(who.clone()), asset_id.clone(), amount, + None, None )); assert_ok!(MultiAssetDelegation::delegate( @@ -153,8 +155,9 @@ fn execute_delegator_unstake_should_work() { operator.clone(), asset_id.clone(), amount, - Default::default() + Default::default(), )); + assert_ok!(MultiAssetDelegation::schedule_delegator_unstake( RuntimeOrigin::signed(who.clone()), operator.clone(), @@ -173,7 +176,9 @@ fn execute_delegator_unstake_should_work() { let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); assert!(metadata.delegator_unstake_requests.is_empty()); assert!(metadata.deposits.get(&asset_id).is_some()); - assert_eq!(metadata.deposits.get(&asset_id).unwrap(), &amount); + let deposit = metadata.deposits.get(&asset_id).unwrap(); + assert_eq!(deposit.amount, amount); + assert_eq!(deposit.delegated_amount, 0); }); } @@ -198,6 +203,7 @@ fn cancel_delegator_unstake_should_work() { RuntimeOrigin::signed(who.clone()), asset_id.clone(), amount, + None, None )); assert_ok!(MultiAssetDelegation::delegate( @@ -205,7 +211,7 @@ fn cancel_delegator_unstake_should_work() { operator.clone(), asset_id.clone(), amount, - Default::default() + Default::default(), )); assert_ok!(MultiAssetDelegation::schedule_delegator_unstake( @@ -272,6 +278,7 @@ fn cancel_delegator_unstake_should_update_already_existing() { RuntimeOrigin::signed(who.clone()), asset_id.clone(), amount, + None, None )); assert_ok!(MultiAssetDelegation::delegate( @@ -279,7 +286,7 @@ fn cancel_delegator_unstake_should_update_already_existing() { operator.clone(), asset_id.clone(), amount, - Default::default() + Default::default(), )); assert_ok!(MultiAssetDelegation::schedule_delegator_unstake( @@ -349,6 +356,7 @@ fn delegate_should_fail_if_not_enough_balance() { RuntimeOrigin::signed(who.clone()), asset_id.clone(), amount - 20, + None, None )); @@ -358,7 +366,7 @@ fn delegate_should_fail_if_not_enough_balance() { operator.clone(), asset_id.clone(), amount, - Default::default() + Default::default(), ), Error::::InsufficientBalance ); @@ -386,6 +394,7 @@ fn schedule_delegator_unstake_should_fail_if_no_delegation() { RuntimeOrigin::signed(who.clone()), asset_id.clone(), amount, + None, None )); @@ -422,6 +431,7 @@ fn execute_delegator_unstake_should_fail_if_not_ready() { RuntimeOrigin::signed(who.clone()), asset_id.clone(), amount, + None, None )); assert_ok!(MultiAssetDelegation::delegate( @@ -429,7 +439,7 @@ fn execute_delegator_unstake_should_fail_if_not_ready() { operator.clone(), asset_id.clone(), amount, - Default::default() + Default::default(), )); assert_noop!( @@ -478,6 +488,7 @@ fn delegate_should_not_create_multiple_on_repeat_delegation() { RuntimeOrigin::signed(who.clone()), asset_id.clone(), amount + additional_amount, + None, None )); @@ -487,7 +498,7 @@ fn delegate_should_not_create_multiple_on_repeat_delegation() { operator.clone(), asset_id.clone(), amount, - Default::default() + Default::default(), )); // Assert first delegation @@ -514,18 +525,9 @@ fn delegate_should_not_create_multiple_on_repeat_delegation() { operator.clone(), asset_id.clone(), additional_amount, - Default::default() + Default::default(), )); - // Assert updated delegation - let updated_metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); - assert!(updated_metadata.deposits.get(&asset_id).is_none()); - assert_eq!(updated_metadata.delegations.len(), 1); - let updated_delegation = &updated_metadata.delegations[0]; - assert_eq!(updated_delegation.operator, operator.clone()); - assert_eq!(updated_delegation.amount, amount + additional_amount); - assert_eq!(updated_delegation.asset_id, asset_id); - // Check the updated operator metadata let updated_operator_metadata = MultiAssetDelegation::operator_info(operator.clone()).unwrap(); diff --git a/pallets/multi-asset-delegation/src/tests/deposit.rs b/pallets/multi-asset-delegation/src/tests/deposit.rs index d72c669e..df062bc7 100644 --- a/pallets/multi-asset-delegation/src/tests/deposit.rs +++ b/pallets/multi-asset-delegation/src/tests/deposit.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . use super::*; -use crate::{types::DelegatorStatus, CurrentRound, Error}; +use crate::{CurrentRound, Error}; use frame_support::{assert_noop, assert_ok}; use sp_keyring::AccountKeyring::Bob; use sp_runtime::ArithmeticError; @@ -52,12 +52,15 @@ fn deposit_should_work_for_fungible_asset() { RuntimeOrigin::signed(who.clone()), Asset::Custom(VDOT), amount, + None, None )); // Assert let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); - assert_eq!(metadata.deposits.get(&Asset::Custom(VDOT),), Some(&amount)); + let deposit = metadata.deposits.get(&Asset::Custom(VDOT)).unwrap(); + assert_eq!(deposit.amount, amount); + assert_eq!( System::events().last().unwrap().event, RuntimeEvent::MultiAssetDelegation(crate::Event::Deposited { @@ -82,12 +85,15 @@ fn deposit_should_work_for_evm_asset() { RuntimeOrigin::signed(who.clone()), Asset::Custom(VDOT), amount, + None, None )); // Assert let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); - assert_eq!(metadata.deposits.get(&Asset::Custom(VDOT),), Some(&amount)); + let deposit = metadata.deposits.get(&Asset::Custom(VDOT)).unwrap(); + assert_eq!(deposit.amount, amount); + assert_eq!( System::events().last().unwrap().event, RuntimeEvent::MultiAssetDelegation(crate::Event::Deposited { @@ -112,12 +118,15 @@ fn multiple_deposit_should_work() { RuntimeOrigin::signed(who.clone()), Asset::Custom(VDOT), amount, - None + None, + None, )); // Assert let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); - assert_eq!(metadata.deposits.get(&Asset::Custom(VDOT),), Some(&amount)); + let deposit = metadata.deposits.get(&Asset::Custom(VDOT)).unwrap(); + assert_eq!(deposit.amount, amount); + assert_eq!( System::events().last().unwrap().event, RuntimeEvent::MultiAssetDelegation(crate::Event::Deposited { @@ -131,12 +140,15 @@ fn multiple_deposit_should_work() { RuntimeOrigin::signed(who.clone()), Asset::Custom(VDOT), amount, + None, None )); // Assert let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); - assert_eq!(metadata.deposits.get(&Asset::Custom(VDOT),), Some(&(amount * 2))); + let deposit = metadata.deposits.get(&Asset::Custom(VDOT)).unwrap(); + assert_eq!(deposit.amount, amount * 2); + assert_eq!( System::events().last().unwrap().event, RuntimeEvent::MultiAssetDelegation(crate::Event::Deposited { @@ -162,6 +174,7 @@ fn deposit_should_fail_for_insufficient_balance() { RuntimeOrigin::signed(who.clone()), Asset::Custom(VDOT), amount, + None, None ), ArithmeticError::Underflow @@ -183,6 +196,7 @@ fn deposit_should_fail_for_bond_too_low() { RuntimeOrigin::signed(who.clone()), Asset::Custom(VDOT), amount, + None, None ), Error::::BondTooLow @@ -205,6 +219,7 @@ fn schedule_withdraw_should_work() { RuntimeOrigin::signed(who.clone()), asset_id, amount, + None, None )); @@ -216,11 +231,9 @@ fn schedule_withdraw_should_work() { // Assert let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); - assert_eq!(metadata.deposits.get(&asset_id), None); + let deposit = metadata.deposits.get(&asset_id).unwrap(); + assert_eq!(deposit.amount, 0_u32.into()); assert!(!metadata.withdraw_requests.is_empty()); - let request = metadata.withdraw_requests.first().unwrap(); - assert_eq!(request.asset_id, asset_id); - assert_eq!(request.amount, amount); }); } @@ -260,6 +273,7 @@ fn schedule_withdraw_should_fail_for_insufficient_balance() { RuntimeOrigin::signed(who.clone()), asset_id, 100, + None, None )); @@ -289,6 +303,7 @@ fn schedule_withdraw_should_fail_if_withdraw_request_exists() { RuntimeOrigin::signed(who.clone()), asset_id, amount, + None, None )); @@ -316,6 +331,7 @@ fn execute_withdraw_should_work() { RuntimeOrigin::signed(who.clone()), asset_id, amount, + None, None )); assert_ok!(MultiAssetDelegation::schedule_withdraw( @@ -372,6 +388,7 @@ fn execute_withdraw_should_fail_if_no_withdraw_request() { RuntimeOrigin::signed(who.clone()), asset_id, amount, + None, None )); @@ -397,8 +414,10 @@ fn execute_withdraw_should_fail_if_withdraw_not_ready() { RuntimeOrigin::signed(who.clone()), asset_id, amount, + None, None )); + assert_ok!(MultiAssetDelegation::schedule_withdraw( RuntimeOrigin::signed(who.clone()), asset_id, @@ -435,8 +454,10 @@ fn cancel_withdraw_should_work() { RuntimeOrigin::signed(who.clone()), asset_id, amount, + None, None )); + assert_ok!(MultiAssetDelegation::schedule_withdraw( RuntimeOrigin::signed(who.clone()), asset_id, @@ -451,9 +472,9 @@ fn cancel_withdraw_should_work() { // Assert let metadata = MultiAssetDelegation::delegators(who.clone()).unwrap(); + let deposit = metadata.deposits.get(&asset_id).unwrap(); + assert_eq!(deposit.amount, amount); assert!(metadata.withdraw_requests.is_empty()); - assert_eq!(metadata.deposits.get(&asset_id), Some(&amount)); - assert_eq!(metadata.status, DelegatorStatus::Active); // Check event System::assert_last_event(RuntimeEvent::MultiAssetDelegation( @@ -494,6 +515,7 @@ fn cancel_withdraw_should_fail_if_no_withdraw_request() { RuntimeOrigin::signed(who.clone()), asset_id, amount, + None, None )); diff --git a/pallets/multi-asset-delegation/src/tests/operator.rs b/pallets/multi-asset-delegation/src/tests/operator.rs index cbe18c5f..08cba275 100644 --- a/pallets/multi-asset-delegation/src/tests/operator.rs +++ b/pallets/multi-asset-delegation/src/tests/operator.rs @@ -638,6 +638,7 @@ fn slash_operator_success() { RuntimeOrigin::signed(Bob.to_account_id()), asset_id, delegator_stake, + None, None )); @@ -734,6 +735,7 @@ fn slash_delegator_fixed_blueprint_not_selected() { RuntimeOrigin::signed(Bob.to_account_id()), Asset::Custom(1), 5_000, + None, None )); diff --git a/pallets/multi-asset-delegation/src/tests/session_manager.rs b/pallets/multi-asset-delegation/src/tests/session_manager.rs index 7174d5f5..411c505c 100644 --- a/pallets/multi-asset-delegation/src/tests/session_manager.rs +++ b/pallets/multi-asset-delegation/src/tests/session_manager.rs @@ -15,6 +15,7 @@ // along with Tangle. If not, see . use super::*; use crate::CurrentRound; +use frame_support::assert_noop; use frame_support::assert_ok; use sp_keyring::AccountKeyring::{Alice, Bob, Charlie, Dave}; use tangle_primitives::services::Asset; @@ -42,7 +43,8 @@ fn handle_round_change_should_work() { RuntimeOrigin::signed(who.clone()), asset_id, amount, - None + None, + None, )); assert_ok!(MultiAssetDelegation::delegate( @@ -50,7 +52,7 @@ fn handle_round_change_should_work() { operator.clone(), asset_id, amount, - Default::default() + Default::default(), )); assert_ok!(Pallet::::handle_round_change()); @@ -76,8 +78,8 @@ fn handle_round_change_with_unstake_should_work() { let operator1 = Charlie.to_account_id(); let operator2 = Dave.to_account_id(); let asset_id = Asset::Custom(VDOT); - let amount1 = 1_000_000_000_000; - let amount2 = 1_000_000_000_000; + let amount1 = 100_000; + let amount2 = 100_000; let unstake_amount = 50; CurrentRound::::put(1); @@ -94,33 +96,49 @@ fn handle_round_change_with_unstake_should_work() { create_and_mint_tokens(VDOT, delegator1.clone(), amount1); mint_tokens(delegator1.clone(), VDOT, delegator2.clone(), amount2); + // Deposit with larger than cap should fail + assert_noop!( + MultiAssetDelegation::deposit( + RuntimeOrigin::signed(delegator1.clone()), + asset_id, + 100_000_000_u32.into(), + None, + None, + ), + Error::::DepositExceedsCapForAsset + ); + // Deposit and delegate first assert_ok!(MultiAssetDelegation::deposit( RuntimeOrigin::signed(delegator1.clone()), asset_id, amount1, None, + None, )); + assert_ok!(MultiAssetDelegation::delegate( RuntimeOrigin::signed(delegator1.clone()), operator1.clone(), asset_id, amount1, - Default::default() + Default::default(), )); assert_ok!(MultiAssetDelegation::deposit( RuntimeOrigin::signed(delegator2.clone()), asset_id, amount2, + None, None )); + assert_ok!(MultiAssetDelegation::delegate( RuntimeOrigin::signed(delegator2.clone()), operator2.clone(), asset_id, amount2, - Default::default() + Default::default(), )); // Delegator1 schedules unstake diff --git a/pallets/multi-asset-delegation/src/traits.rs b/pallets/multi-asset-delegation/src/traits.rs index 653ad9ef..5ed18234 100644 --- a/pallets/multi-asset-delegation/src/traits.rs +++ b/pallets/multi-asset-delegation/src/traits.rs @@ -15,13 +15,17 @@ // along with Tangle. If not, see . use super::*; use crate::types::{BalanceOf, OperatorStatus}; +use frame_system::pallet_prelude::BlockNumberFor; use sp_runtime::{traits::Zero, Percent}; use sp_std::prelude::*; +use tangle_primitives::types::rewards::UserDepositWithLocks; use tangle_primitives::{ services::Asset, traits::MultiAssetDelegationInfo, BlueprintId, RoundIndex, }; -impl MultiAssetDelegationInfo> for crate::Pallet { +impl MultiAssetDelegationInfo, BlockNumberFor> + for crate::Pallet +{ type AssetId = T::AssetId; fn get_current_round() -> RoundIndex { @@ -69,4 +73,16 @@ impl MultiAssetDelegationInfo> for fn slash_operator(operator: &T::AccountId, blueprint_id: BlueprintId, percentage: Percent) { let _ = Pallet::::slash_operator(operator, blueprint_id, percentage); } + + fn get_user_deposit_with_locks( + who: &T::AccountId, + asset_id: Asset, + ) -> Option, BlockNumberFor>> { + Delegators::::get(who).and_then(|metadata| { + metadata.deposits.get(&asset_id).map(|deposit| UserDepositWithLocks { + unlocked_amount: deposit.amount, + amount_with_locks: deposit.locks.as_ref().map(|locks| locks.to_vec()), + }) + }) + } } diff --git a/pallets/multi-asset-delegation/src/types.rs b/pallets/multi-asset-delegation/src/types.rs index a1b0cb4d..e5d740b9 100644 --- a/pallets/multi-asset-delegation/src/types.rs +++ b/pallets/multi-asset-delegation/src/types.rs @@ -16,6 +16,7 @@ use crate::Config; use frame_support::traits::Currency; +use frame_system::pallet_prelude::BlockNumberFor; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; use sp_runtime::RuntimeDebug; @@ -56,4 +57,6 @@ pub type DelegatorMetadataOf = DelegatorMetadata< ::MaxDelegations, ::MaxUnstakeRequests, ::MaxDelegatorBlueprints, + BlockNumberFor, + ::MaxDelegations, >; diff --git a/pallets/multi-asset-delegation/src/types/delegator.rs b/pallets/multi-asset-delegation/src/types/delegator.rs index 774b8f46..23ef2bc4 100644 --- a/pallets/multi-asset-delegation/src/types/delegator.rs +++ b/pallets/multi-asset-delegation/src/types/delegator.rs @@ -15,7 +15,12 @@ // along with Tangle. If not, see . use super::*; +use frame_support::ensure; use frame_support::{pallet_prelude::Get, BoundedVec}; +use sp_std::fmt::Debug; +use sp_std::vec; +use tangle_primitives::types::rewards::LockInfo; +use tangle_primitives::types::rewards::LockMultiplier; use tangle_primitives::{services::Asset, BlueprintId}; /// Represents how a delegator selects which blueprints to work with. @@ -79,9 +84,11 @@ pub struct DelegatorMetadata< MaxDelegations: Get, MaxUnstakeRequests: Get, MaxBlueprints: Get, + BlockNumber, + MaxLocks: Get, > { /// A map of deposited assets and their respective amounts. - pub deposits: BTreeMap, Balance>, + pub deposits: BTreeMap, Deposit>, /// A vector of withdraw requests. pub withdraw_requests: BoundedVec, MaxWithdrawRequests>, /// A list of all current delegations. @@ -102,6 +109,8 @@ impl< MaxDelegations: Get, MaxUnstakeRequests: Get, MaxBlueprints: Get, + BlockNumber, + MaxLocks: Get, > Default for DelegatorMetadata< AccountId, @@ -111,6 +120,8 @@ impl< MaxDelegations, MaxUnstakeRequests, MaxBlueprints, + BlockNumber, + MaxLocks, > { fn default() -> Self { @@ -132,6 +143,8 @@ impl< MaxDelegations: Get, MaxUnstakeRequests: Get, MaxBlueprints: Get, + BlockNumber, + MaxLocks: Get, > DelegatorMetadata< AccountId, @@ -141,6 +154,8 @@ impl< MaxDelegations, MaxUnstakeRequests, MaxBlueprints, + BlockNumber, + MaxLocks, > { /// Returns a reference to the vector of withdraw requests. @@ -196,12 +211,126 @@ impl< } /// Represents a deposit of a specific asset. -#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] -pub struct Deposit { - /// The amount of the asset deposited. +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, Eq, PartialEq)] +pub struct Deposit> { + /// The total amount deposited by the user (includes both delegated and non-delegated). pub amount: Balance, - /// The ID of the deposited asset. - pub asset_id: Asset, + /// The total delegated amount by the user (this can never be greater than `amount`). + pub delegated_amount: Balance, + /// The locks associated with this deposit. + pub locks: Option, MaxLocks>>, +} + +impl< + Balance: Debug + Default + Clone + sp_runtime::Saturating + sp_std::cmp::PartialOrd + From, + BlockNumber: Debug + sp_runtime::Saturating + sp_std::convert::From + sp_std::cmp::PartialOrd, + MaxLocks: Get, + > Deposit +{ + pub fn new( + amount: Balance, + lock_multiplier: Option, + current_block_number: BlockNumber, + ) -> Self { + let locks = lock_multiplier.map(|multiplier| { + let expiry_block = current_block_number.saturating_add(multiplier.get_blocks().into()); + BoundedVec::try_from(vec![LockInfo { + amount: amount.clone(), + expiry_block, + lock_multiplier: multiplier, + }]) + .expect("This should not happen since only one lock exists!") + }); + + Deposit { amount, delegated_amount: Balance::default(), locks } + } + + pub fn get_total_amount(&self) -> Balance { + self.amount.clone() + } + + pub fn increase_delegated_amount( + &mut self, + amount_to_increase: Balance, + ) -> Result<(), &'static str> { + // sanity check that the proposed amount when added to the current delegated amount is not greater than the total amount + let new_delegated_amount = + self.delegated_amount.clone().saturating_add(amount_to_increase.clone()); + ensure!( + new_delegated_amount <= self.amount, + "delegated amount cannot be greater than total amount" + ); + self.delegated_amount = new_delegated_amount; + Ok(()) + } + + pub fn decrease_delegated_amount( + &mut self, + amount_to_decrease: Balance, + ) -> Result<(), &'static str> { + self.delegated_amount = self.delegated_amount.clone().saturating_sub(amount_to_decrease); + Ok(()) + } + + pub fn increase_deposited_amount( + &mut self, + amount_to_increase: Balance, + lock_multiplier: Option, + current_block_number: BlockNumber, + ) -> Result<(), &'static str> { + // Update the total amount first + self.amount = self.amount.clone().saturating_add(amount_to_increase.clone()); + + // If there's a lock multiplier, add a new lock + if let Some(multiplier) = lock_multiplier { + let lock_blocks = multiplier.get_blocks(); + let expiry_block = current_block_number.saturating_add(lock_blocks.into()); + + let new_lock = + LockInfo { amount: amount_to_increase, expiry_block, lock_multiplier: multiplier }; + + // Initialize locks if None or push to existing locks + if let Some(locks) = &mut self.locks { + locks + .try_push(new_lock) + .map_err(|_| "Failed to push new lock - exceeded MaxLocks bound")?; + } else { + self.locks = Some( + BoundedVec::try_from(vec![new_lock]) + .expect("This should not happen since only one lock exists!"), + ); + } + } + + Ok(()) + } + + pub fn decrease_deposited_amount( + &mut self, + amount_to_decrease: Balance, + current_block_number: BlockNumber, + ) -> Result<(), &'static str> { + let total_locked = self.locks.as_ref().map_or(Balance::from(0_u32), |locks| { + locks + .iter() + .filter(|lock| lock.expiry_block > current_block_number) + .fold(Balance::from(0_u32), |acc, lock| acc.saturating_add(lock.amount.clone())) + }); + + let free_amount = self.amount.clone().saturating_sub(total_locked); + ensure!( + free_amount >= amount_to_decrease, + "total free amount cannot be lesser than amount to decrease" + ); + + self.amount = self.amount.clone().saturating_sub(amount_to_decrease); + ensure!( + self.amount >= self.delegated_amount, + "delegated amount cannot be greater than total amount" + ); + + Ok(()) + } } /// Represents a stake between a delegator and an operator. diff --git a/pallets/oracle/src/default_combine_data.rs b/pallets/oracle/src/default_combine_data.rs index f6e29c49..a42f2e10 100644 --- a/pallets/oracle/src/default_combine_data.rs +++ b/pallets/oracle/src/default_combine_data.rs @@ -2,7 +2,7 @@ use crate::{Config, MomentOf, TimestampedValueOf}; use frame_support::traits::{Get, Time}; use sp_runtime::traits::Saturating; use sp_std::{marker, prelude::*}; -use tangle_primitives::CombineData; +use tangle_primitives::traits::CombineData; /// Sort by value and returns median timestamped value. /// Returns prev_value if not enough valid values. diff --git a/pallets/oracle/src/lib.rs b/pallets/oracle/src/lib.rs index daff0802..acf9457a 100644 --- a/pallets/oracle/src/lib.rs +++ b/pallets/oracle/src/lib.rs @@ -37,7 +37,7 @@ use scale_info::TypeInfo; use sp_runtime::{traits::Member, DispatchResult, RuntimeDebug}; use sp_std::{prelude::*, vec}; use tangle_primitives::ordered_set::OrderedSet; -pub use tangle_primitives::{ +pub use tangle_primitives::traits::{ CombineData, DataFeeder, DataProvider, DataProviderExtended, OnNewData, }; diff --git a/pallets/rewards/Cargo.toml b/pallets/rewards/Cargo.toml new file mode 100644 index 00000000..9acdd01a --- /dev/null +++ b/pallets/rewards/Cargo.toml @@ -0,0 +1,123 @@ +[package] +name = "pallet-rewards" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +[dependencies] +frame-benchmarking = { workspace = true, optional = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } +log = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } +ethabi = { workspace = true } +pallet-balances = { workspace = true } +tangle-primitives = { workspace = true } +pallet-assets = { workspace = true, default-features = false } +fp-evm = { workspace = true } +itertools = { workspace = true, features = ["use_alloc"] } +serde = { workspace = true, features = ["derive"], optional = true } +hex = { workspace = true, features = ["alloc"] } + +[dev-dependencies] +ethereum = { workspace = true, features = ["with-codec"] } +ethers = "2.0" +num_enum = { workspace = true } +hex-literal = { workspace = true } +libsecp256k1 = { workspace = true } +pallet-assets = { workspace = true } +pallet-balances = { workspace = true } +pallet-timestamp = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +smallvec = { workspace = true } +sp-io = { workspace = true } +sp-keystore = { workspace = true } + +# Frontier Primitive +fp-account = { workspace = true } +fp-consensus = { workspace = true } +fp-dynamic-fee = { workspace = true } +fp-ethereum = { workspace = true } +fp-rpc = { workspace = true } +fp-self-contained = { workspace = true } +fp-storage = { workspace = true } + +# Frontier FRAME +pallet-base-fee = { workspace = true } +pallet-dynamic-fee = { workspace = true } +pallet-ethereum = { workspace = true } +pallet-evm = { workspace = true } +pallet-evm-chain-id = { workspace = true } + +pallet-evm-precompile-blake2 = { workspace = true } +pallet-evm-precompile-bn128 = { workspace = true } +pallet-evm-precompile-curve25519 = { workspace = true } +pallet-evm-precompile-ed25519 = { workspace = true } +pallet-evm-precompile-modexp = { workspace = true } +pallet-evm-precompile-sha3fips = { workspace = true } +pallet-evm-precompile-simple = { workspace = true } + +precompile-utils = { workspace = true } +sp-keyring ={ workspace = true} +pallet-session = { workspace = true } +pallet-staking = { workspace = true } +sp-staking = { workspace = true } +frame-election-provider-support = { workspace = true } + +[features] +default = ["std"] +std = [ + "scale-info/std", + "sp-runtime/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "sp-core/std", + "sp-std/std", + "pallet-balances/std", + "pallet-assets/std", + "tangle-primitives/std", + "ethabi/std", + "log/std", + "fp-evm/std", + "serde/std", + "hex/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", + "pallet-staking/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", + "ethabi/std", + "sp-keyring/std", + "pallet-ethereum/std" +] +try-runtime = ["frame-support/try-runtime"] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", +] diff --git a/pallets/rewards/fuzzer/Cargo.toml b/pallets/rewards/fuzzer/Cargo.toml new file mode 100644 index 00000000..e6dcedc9 --- /dev/null +++ b/pallets/rewards/fuzzer/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "pallet-rewards-fuzzer" +version = "2.0.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage.workspace = true +repository.workspace = true +description = "Fuzzer for fixed point arithmetic primitives." +documentation = "https://docs.rs/sp-arithmetic-fuzzer" +publish = false + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +honggfuzz = { workspace = true } + +pallet-rewards = { features = ["fuzzing"], workspace = true, default-features = true } + +frame-system = { workspace = true, default-features = true } +frame-support = { workspace = true, default-features = true } + +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-fuzzer" +path = "call.rs" diff --git a/pallets/rewards/fuzzer/call.rs b/pallets/rewards/fuzzer/call.rs new file mode 100644 index 00000000..121d1b1c --- /dev/null +++ b/pallets/rewards/fuzzer/call.rs @@ -0,0 +1,228 @@ +// 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 rewards-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 rewards-fuzzer hfuzz_workspace/rewards-fuzzer/*.fuzz`. + +use crate::runtime::*; +use frame_support::pallet_prelude::*; +use pallet_rewards::{Call as RewardsCall, Config, Error, RewardConfigForAssetVault}; +use sp_runtime::{traits::Zero, Percent}; +use tangle_primitives::types::rewards::LockMultiplier; + +#[derive(Debug)] +pub enum RewardsFuzzCall { + ClaimRewards(u32), + ForceClaimRewards(AccountId, u32), + UpdateVaultRewardConfig(u32, u8, u128, u128, Option), +} + +impl RewardsFuzzCall { + pub fn generate(data: &[u8]) -> Option { + if data.is_empty() { + return None; + } + + // Use first byte to determine call type + match data[0] % 3 { + 0 => Some(RewardsFuzzCall::ClaimRewards( + u32::from_le_bytes(data.get(1..5)?.try_into().ok()?), + )), + 1 => Some(RewardsFuzzCall::ForceClaimRewards( + AccountId::new(data.get(1..33)?.try_into().ok()?), + u32::from_le_bytes(data.get(33..37)?.try_into().ok()?), + )), + 2 => Some(RewardsFuzzCall::UpdateVaultRewardConfig( + u32::from_le_bytes(data.get(1..5)?.try_into().ok()?), + data.get(5)?.clone(), + u128::from_le_bytes(data.get(6..22)?.try_into().ok()?), + u128::from_le_bytes(data.get(22..38)?.try_into().ok()?), + if data.get(38)? % 2 == 0 { + Some(u32::from_le_bytes(data.get(39..43)?.try_into().ok()?)) + } else { + None + }, + )), + _ => None, + } + } + + pub fn execute(&self) -> DispatchResultWithPostInfo { + match self { + RewardsFuzzCall::ClaimRewards(vault_id) => { + RewardsCallExecutor::execute_claim_rewards(*vault_id) + } + RewardsFuzzCall::ForceClaimRewards(account, vault_id) => { + RewardsCallExecutor::execute_force_claim_rewards(account.clone(), *vault_id) + } + RewardsFuzzCall::UpdateVaultRewardConfig(vault_id, apy, deposit_cap, incentive_cap, boost_multiplier) => { + RewardsCallExecutor::execute_update_vault_reward_config( + *vault_id, + *apy, + *deposit_cap, + *incentive_cap, + *boost_multiplier, + ) + } + } + } + + pub fn verify(&self) -> bool { + match self { + RewardsFuzzCall::ClaimRewards(vault_id) => { + RewardsCallVerifier::verify_claim_rewards(*vault_id) + } + RewardsFuzzCall::ForceClaimRewards(account, vault_id) => { + RewardsCallVerifier::verify_force_claim_rewards(account.clone(), *vault_id) + } + RewardsFuzzCall::UpdateVaultRewardConfig(vault_id, apy, deposit_cap, incentive_cap, boost_multiplier) => { + RewardsCallVerifier::verify_update_vault_reward_config( + *vault_id, + *apy, + *deposit_cap, + *incentive_cap, + *boost_multiplier, + ) + } + } + } +} + +#[derive(Debug)] +pub struct RewardsCallGenerator; + +impl RewardsCallGenerator { + pub fn claim_rewards(vault_id: u32) -> RewardsCall { + RewardsCall::claim_rewards { vault_id } + } + + pub fn force_claim_rewards(account: AccountId, vault_id: u32) -> RewardsCall { + RewardsCall::force_claim_rewards { account, vault_id } + } + + pub fn update_vault_reward_config( + vault_id: u32, + apy: u8, + deposit_cap: u128, + incentive_cap: u128, + boost_multiplier: Option, + ) -> RewardsCall { + let config = RewardConfigForAssetVault { + apy: Percent::from_percent(apy.min(100)), + deposit_cap, + incentive_cap, + boost_multiplier: boost_multiplier.map(|m| m.min(500)), // Cap at 5x + }; + RewardsCall::update_vault_reward_config { vault_id, new_config: config } + } +} + +#[derive(Debug)] +pub struct RewardsCallExecutor; + +impl RewardsCallExecutor { + pub fn execute_claim_rewards(vault_id: u32) -> DispatchResultWithPostInfo { + Rewards::claim_rewards(RuntimeOrigin::signed(ALICE), vault_id) + } + + pub fn execute_force_claim_rewards(account: AccountId, vault_id: u32) -> DispatchResultWithPostInfo { + Rewards::force_claim_rewards(RuntimeOrigin::root(), account, vault_id) + } + + pub fn execute_update_vault_reward_config( + vault_id: u32, + apy: u8, + deposit_cap: u128, + incentive_cap: u128, + boost_multiplier: Option, + ) -> DispatchResultWithPostInfo { + let config = RewardConfigForAssetVault { + apy: Percent::from_percent(apy.min(100)), + deposit_cap, + incentive_cap, + boost_multiplier: boost_multiplier.map(|m| m.min(500)), // Cap at 5x + }; + Rewards::update_vault_reward_config(RuntimeOrigin::root(), vault_id, config) + } +} + +#[derive(Debug)] +pub struct RewardsCallVerifier; + +impl RewardsCallVerifier { + pub fn verify_claim_rewards(vault_id: u32) -> bool { + if let Ok(_) = RewardsCallExecutor::execute_claim_rewards(vault_id) { + // Verify that rewards were claimed by checking storage + UserClaimedReward::::contains_key(&ALICE, vault_id) + } else { + false + } + } + + pub fn verify_force_claim_rewards(account: AccountId, vault_id: u32) -> bool { + if let Ok(_) = RewardsCallExecutor::execute_force_claim_rewards(account.clone(), vault_id) { + // Verify that rewards were claimed by checking storage + UserClaimedReward::::contains_key(&account, vault_id) + } else { + false + } + } + + pub fn verify_update_vault_reward_config( + vault_id: u32, + apy: u8, + deposit_cap: u128, + incentive_cap: u128, + boost_multiplier: Option, + ) -> bool { + if let Ok(_) = RewardsCallExecutor::execute_update_vault_reward_config( + vault_id, + apy, + deposit_cap, + incentive_cap, + boost_multiplier, + ) { + // Verify that config was updated by checking storage + if let Some(config) = RewardConfigStorage::::get(vault_id) { + config.apy == Percent::from_percent(apy.min(100)) + && config.deposit_cap == deposit_cap + && config.incentive_cap == incentive_cap + && config.boost_multiplier == boost_multiplier.map(|m| m.min(500)) + } else { + false + } + } else { + false + } + } +} + +fn main() { + loop { + fuzz!(|data: &[u8]| { + if let Some(call) = RewardsFuzzCall::generate(data) { + // Execute the call and verify its effects + let _ = call.execute(); + let _ = call.verify(); + } + }); + } +} diff --git a/pallets/rewards/src/benchmarking.rs b/pallets/rewards/src/benchmarking.rs new file mode 100644 index 00000000..641292fb --- /dev/null +++ b/pallets/rewards/src/benchmarking.rs @@ -0,0 +1,88 @@ +// 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 . +use super::*; +use crate::{types::*, Pallet as MultiAssetDelegation}; +use frame_benchmarking::{account, benchmarks, whitelisted_caller}; +use frame_support::{ + ensure, + pallet_prelude::DispatchResult, + traits::{Currency, Get, ReservableCurrency}, +}; +use frame_system::RawOrigin; +use sp_runtime::{traits::Zero, DispatchError}; + +const SEED: u32 = 0; + +fn setup_vault() -> (T::VaultId, T::AccountId) { + let vault_id = T::VaultId::zero(); + let caller: T::AccountId = account("caller", 0, SEED); + let balance = BalanceOf::::from(1000u32); + T::Currency::make_free_balance_be(&caller, balance); + + // Setup reward config + let reward_config = RewardConfigForAssetVault { + apy: Percent::from_percent(10), + deposit_cap: balance, + incentive_cap: balance, + boost_multiplier: Some(150), + }; + RewardConfigStorage::::insert(vault_id, reward_config); + + (vault_id, caller) +} + +benchmarks! { + claim_rewards { + let (vault_id, caller) = setup_vault::(); + let deposit = BalanceOf::::from(100u32); + let deposit_info = UserDepositWithLocks { + unlocked_amount: deposit, + amount_with_locks: None, + }; + }: _(RawOrigin::Signed(caller.clone()), vault_id) + verify { + assert!(UserClaimedReward::::contains_key(&caller, vault_id)); + } + + force_claim_rewards { + let (vault_id, caller) = setup_vault::(); + let deposit = BalanceOf::::from(100u32); + let deposit_info = UserDepositWithLocks { + unlocked_amount: deposit, + amount_with_locks: None, + }; + let origin = T::ForceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + }: _(origin, caller.clone(), vault_id) + verify { + assert!(UserClaimedReward::::contains_key(&caller, vault_id)); + } + + update_vault_reward_config { + let (vault_id, _) = setup_vault::(); + let new_config = RewardConfigForAssetVault { + apy: Percent::from_percent(20), + deposit_cap: BalanceOf::::from(2000u32), + incentive_cap: BalanceOf::::from(2000u32), + boost_multiplier: Some(200), + }; + let origin = T::ForceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + }: _(origin, vault_id, new_config.clone()) + verify { + assert_eq!(RewardConfigStorage::::get(vault_id), Some(new_config)); + } +} + +impl_benchmark_test_suite!(RewardsPallet, crate::mock::new_test_ext(), crate::mock::Runtime); diff --git a/pallets/rewards/src/functions.rs b/pallets/rewards/src/functions.rs new file mode 100644 index 00000000..3a83b1b0 --- /dev/null +++ b/pallets/rewards/src/functions.rs @@ -0,0 +1,264 @@ +// 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 . +use crate::AssetLookupRewardVaults; +use crate::Error; +use crate::Event; +use crate::RewardConfigForAssetVault; +use crate::RewardConfigStorage; +use crate::RewardVaults; +use crate::TotalRewardVaultScore; +use crate::UserClaimedReward; +use crate::{BalanceOf, Config, Pallet}; +use frame_support::ensure; +use frame_support::traits::Currency; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_runtime::traits::{CheckedDiv, CheckedMul}; +use sp_runtime::traits::{Saturating, Zero}; +use sp_runtime::DispatchError; +use sp_runtime::DispatchResult; +use sp_std::vec::Vec; +use tangle_primitives::services::Asset; +use tangle_primitives::traits::MultiAssetDelegationInfo; +use tangle_primitives::types::rewards::UserDepositWithLocks; + +impl Pallet { + pub fn remove_asset_from_vault( + vault_id: &T::VaultId, + asset_id: &Asset, + ) -> DispatchResult { + // Update RewardVaults storage + RewardVaults::::try_mutate(vault_id, |maybe_assets| -> DispatchResult { + let assets = maybe_assets.as_mut().ok_or(Error::::VaultNotFound)?; + + // Ensure the asset is in the vault + ensure!(assets.contains(asset_id), Error::::AssetNotInVault); + + assets.retain(|id| id != asset_id); + + Ok(()) + })?; + + // Update AssetLookupRewardVaults storage + AssetLookupRewardVaults::::remove(asset_id); + + Ok(()) + } + + pub fn add_asset_to_vault( + vault_id: &T::VaultId, + asset_id: &Asset, + ) -> DispatchResult { + // Ensure the asset is not already associated with any vault + ensure!( + !AssetLookupRewardVaults::::contains_key(asset_id), + Error::::AssetAlreadyInVault + ); + + // Update RewardVaults storage + RewardVaults::::try_mutate(vault_id, |maybe_assets| -> DispatchResult { + let assets = maybe_assets.get_or_insert_with(Vec::new); + + // Ensure the asset is not already in the vault + ensure!(!assets.contains(asset_id), Error::::AssetAlreadyInVault); + + assets.push(*asset_id); + + Ok(()) + })?; + + // Update AssetLookupRewardVaults storage + AssetLookupRewardVaults::::insert(asset_id, vault_id); + + Ok(()) + } + + /// Calculates and pays out rewards for a given account and asset. + /// + /// This function orchestrates the reward calculation and payout process by: + /// 1. Finding the vault associated with the asset + /// 2. Retrieving user deposit information including any locked amounts + /// 3. Calculating rewards based on deposit amounts, lock periods, and APY + /// + /// # Arguments + /// * `account_id` - The account to calculate rewards for + /// * `asset` - The asset to calculate rewards for + /// + /// # Returns + /// * `Ok(BalanceOf)` - The total rewards calculated + /// * `Err(DispatchError)` - If any of the following conditions are met: + /// - Asset is not in a reward vault + /// - No rewards are available for the account + /// - Reward configuration is not found for the vault + /// - Arithmetic overflow occurs during calculation + /// + /// # Assumptions + /// * The asset must be registered in a reward vault + /// * The reward configuration must exist for the vault + pub fn calculate_and_payout_rewards( + account_id: &T::AccountId, + asset: Asset, + ) -> Result, DispatchError> { + // find the vault for the asset id + // if the asset is not in a reward vault, do nothing + let vault_id = + AssetLookupRewardVaults::::get(asset).ok_or(Error::::AssetNotInVault)?; + + // lets read the user deposits from the delegation manager + let deposit_info = + T::DelegationManager::get_user_deposit_with_locks(&account_id.clone(), asset) + .ok_or(Error::::NoRewardsAvailable)?; + + // read the asset reward config + let reward_config = RewardConfigStorage::::get(vault_id); + + // find the total vault score + let total_score = TotalRewardVaultScore::::get(vault_id); + + // get the users last claim + let last_claim = UserClaimedReward::::get(account_id, vault_id); + + let (total_rewards, rewards_to_be_paid) = + Self::calculate_deposit_rewards_with_lock_multiplier( + total_score, + deposit_info, + reward_config.ok_or(Error::::RewardConfigNotFound)?, + last_claim, + )?; + + // mint new TNT rewards and trasnfer to the user + let _ = T::Currency::deposit_creating(account_id, rewards_to_be_paid); + + // update the last claim + UserClaimedReward::::insert( + account_id, + vault_id, + (frame_system::Pallet::::block_number(), total_rewards), + ); + + Self::deposit_event(Event::RewardsClaimed { + account: account_id.clone(), + asset, + amount: rewards_to_be_paid, + }); + + Ok(total_rewards) + } + + /// Calculates rewards for deposits considering both unlocked amounts and locked amounts with their respective multipliers. + /// + /// The reward calculation follows these formulas: + /// 1. For unlocked amounts: + /// ```text + /// base_reward = APY * (user_deposit / total_deposits) * (total_deposits / deposit_capacity) + /// ``` + /// + /// 2. For locked amounts: + /// ```text + /// lock_reward = amount * APY * lock_multiplier * (remaining_lock_time / total_lock_time) + /// ``` + /// + /// # Arguments + /// * `total_asset_score` - Total score for the asset across all deposits + /// * `deposit` - User's deposit information including locked amounts + /// * `reward` - Reward configuration for the asset vault + /// * `last_claim` - Timestamp and amount of user's last reward claim + /// + /// # Returns + /// * `Ok(BalanceOf)` - The calculated rewards + /// * `Err(DispatchError)` - If any arithmetic operation overflows + /// + /// # Assumptions and Constraints + /// * Lock multipliers are fixed at: 1x (1 month), 2x (2 months), 3x (3 months), 6x (6 months) + /// * APY is applied proportionally to the lock period remaining + /// * Rewards scale with: + /// - The proportion of user's deposit to total deposits + /// - The proportion of total deposits to deposit capacity + /// - The lock multiplier (if applicable) + /// - The remaining time in the lock period + /// + pub fn calculate_deposit_rewards_with_lock_multiplier( + total_asset_score: BalanceOf, + deposit: UserDepositWithLocks, BlockNumberFor>, + reward: RewardConfigForAssetVault>, + last_claim: Option<(BlockNumberFor, BalanceOf)>, + ) -> Result<(BalanceOf, BalanceOf), DispatchError> { + // The formula for rewards: + // Base Reward = APY * (user_deposit / total_deposits) * (total_deposits / deposit_capacity) + // For locked amounts: Base Reward * lock_multiplier * (remaining_lock_time / total_lock_time) + + let asset_apy = reward.apy; + let deposit_capacity = reward.deposit_cap; + + // Start with unlocked amount as base score + let mut total_rewards = deposit.unlocked_amount; + + // Get the current block and last claim block + let current_block = frame_system::Pallet::::block_number(); + let last_claim_block = last_claim.map(|(block, _)| block).unwrap_or(current_block); + + // Calculate base reward rate + // APY * (deposit / total_deposits) * (total_deposits / capacity) + let base_reward_rate = if !total_asset_score.is_zero() { + let deposit_ratio = total_rewards + .checked_mul(&total_rewards) + .and_then(|v| v.checked_div(&total_asset_score)) + .ok_or(Error::::ArithmeticError)?; + + let capacity_ratio = total_asset_score + .checked_div(&deposit_capacity) + .ok_or(Error::::ArithmeticError)?; + + asset_apy.mul_floor(deposit_ratio.saturating_mul(capacity_ratio)) + } else { + Zero::zero() + }; + + total_rewards = total_rewards.saturating_add(base_reward_rate); + + // Add rewards for locked amounts if any exist + if let Some(locks) = deposit.amount_with_locks { + for lock in locks { + if lock.expiry_block > last_claim_block { + // Calculate remaining lock time as a ratio + let blocks_remaining: u32 = + TryInto::::try_into(lock.expiry_block.saturating_sub(current_block)) + .map_err(|_| Error::::ArithmeticError)?; + + let total_lock_blocks = lock.lock_multiplier.get_blocks(); + let time_ratio = BalanceOf::::from(blocks_remaining) + .checked_div(&BalanceOf::::from(total_lock_blocks)) + .ok_or(Error::::ArithmeticError)?; + + // Calculate lock reward: + // amount * APY * multiplier * time_ratio + let multiplier = BalanceOf::::from(lock.lock_multiplier.value()); + let lock_reward = asset_apy + .mul_floor(lock.amount) + .saturating_mul(multiplier) + .saturating_mul(time_ratio); + + total_rewards = total_rewards.saturating_add(lock_reward); + } + } + } + + // lets remove any already claimed rewards + let rewards_to_be_paid = total_rewards + .saturating_sub(last_claim.map(|(_, amount)| amount).unwrap_or(Zero::zero())); + + Ok((total_rewards, rewards_to_be_paid)) + } +} diff --git a/pallets/rewards/src/impls.rs b/pallets/rewards/src/impls.rs new file mode 100644 index 00000000..7bd3a1aa --- /dev/null +++ b/pallets/rewards/src/impls.rs @@ -0,0 +1,108 @@ +// 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 . + +use crate::AssetLookupRewardVaults; +use crate::BalanceOf; +use crate::Error; +use crate::RewardConfigStorage; +use crate::TotalRewardVaultScore; +use crate::UserServiceReward; +use crate::{Config, Pallet}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_runtime::traits::Saturating; +use sp_runtime::DispatchError; +use tangle_primitives::types::rewards::LockMultiplier; +use tangle_primitives::{services::Asset, traits::rewards::RewardsManager}; + +impl RewardsManager, BlockNumberFor> + for Pallet +{ + type Error = DispatchError; + + fn record_deposit( + _account_id: &T::AccountId, + asset: Asset, + amount: BalanceOf, + _lock_multiplier: Option, + ) -> Result<(), Self::Error> { + // find the vault for the asset id + // if the asset is not in a reward vault, do nothing + if let Some(vault_id) = AssetLookupRewardVaults::::get(asset) { + // Update the reward vault score + let score = TotalRewardVaultScore::::get(vault_id).saturating_add(amount); + TotalRewardVaultScore::::insert(vault_id, score); + } + Ok(()) + } + + fn record_withdrawal( + _account_id: &T::AccountId, + asset: Asset, + amount: BalanceOf, + ) -> Result<(), Self::Error> { + // find the vault for the asset id + // if the asset is not in a reward vault, do nothing + if let Some(vault_id) = AssetLookupRewardVaults::::get(asset) { + // Update the reward vault score + let score = TotalRewardVaultScore::::get(vault_id).saturating_sub(amount); + TotalRewardVaultScore::::insert(vault_id, score); + } + Ok(()) + } + + fn record_service_reward( + account_id: &T::AccountId, + asset: Asset, + amount: BalanceOf, + ) -> Result<(), Self::Error> { + // update the amount in the user service reward storage + UserServiceReward::::try_mutate(account_id, asset, |reward| { + *reward = reward.saturating_add(amount); + Ok(()) + }) + } + + fn get_asset_deposit_cap_remaining( + asset: Asset, + ) -> Result, Self::Error> { + // find the vault for the asset id + // if the asset is not in a reward vault, do nothing + if let Some(vault_id) = AssetLookupRewardVaults::::get(asset) { + if let Some(config) = RewardConfigStorage::::get(vault_id) { + let current_score = TotalRewardVaultScore::::get(vault_id); + Ok(config.deposit_cap.saturating_sub(current_score)) + } else { + Err(Error::::RewardConfigNotFound.into()) + } + } else { + Err(Error::::AssetNotInVault.into()) + } + } + + fn get_asset_incentive_cap(asset: Asset) -> Result, Self::Error> { + // find the vault for the asset id + // if the asset is not in a reward vault, do nothing + if let Some(vault_id) = AssetLookupRewardVaults::::get(asset) { + if let Some(config) = RewardConfigStorage::::get(vault_id) { + Ok(config.incentive_cap) + } else { + Err(Error::::RewardConfigNotFound.into()) + } + } else { + Err(Error::::AssetNotInVault.into()) + } + } +} diff --git a/pallets/rewards/src/lib.rs b/pallets/rewards/src/lib.rs new file mode 100644 index 00000000..46cf5236 --- /dev/null +++ b/pallets/rewards/src/lib.rs @@ -0,0 +1,340 @@ +// 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 . + +//! # Rewards Pallet +//! +//! A flexible reward distribution system that supports multiple vaults with configurable reward parameters. +//! +//! ## Overview +//! +//! The Rewards pallet provides a mechanism for distributing rewards to users who deposit assets into +//! various vaults. Each vault can have its own reward configuration, including APY rates and deposit caps. +//! The system supports both unlocked deposits and locked deposits with multipliers for longer lock periods. +//! +//! ## Reward Vaults +//! +//! Each vault is identified by a unique `VaultId` and has its own reward configuration: +//! - `apy`: Annual Percentage Yield for the vault +//! - `deposit_cap`: Maximum amount that can be deposited +//! - `incentive_cap`: Maximum amount of incentives that can be distributed +//! - `boost_multiplier`: Optional multiplier to boost rewards +//! +//! ## Reward Calculation +//! +//! Rewards are calculated based on several factors: +//! +//! 1. Base Rewards: +//! ```text +//! Base Reward = APY * (user_deposit / total_deposits) * (total_deposits / deposit_capacity) +//! ``` +//! +//! 2. Locked Deposits: +//! For locked deposits, additional rewards are calculated using: +//! ```text +//! Lock Reward = Base Reward * lock_multiplier * (remaining_lock_time / total_lock_time) +//! ``` +//! +//! Lock multipliers increase rewards based on lock duration: +//! - One Month: 1.1x +//! - Two Months: 1.2x +//! - Three Months: 1.3x +//! - Six Months: 1.6x +//! +//! ## Notes +//! +//! - The reward vaults will consider all assets in parity, so only add the same type of asset in the same vault. +//! +//! +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod mock_evm; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +use scale_info::TypeInfo; +use tangle_primitives::services::Asset; +pub mod types; +pub use types::*; +pub mod functions; +pub mod impls; +use sp_std::vec::Vec; +use tangle_primitives::BlueprintId; + +/// The pallet's account ID. +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{ + pallet_prelude::*, + traits::{Currency, LockableCurrency, ReservableCurrency}, + PalletId, + }; + + use frame_system::pallet_prelude::*; + use sp_runtime::traits::AccountIdConversion; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Because this pallet emits events, it depends on the runtime's definition of an event. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The currency type used for managing balances. + type Currency: Currency + + ReservableCurrency + + LockableCurrency; + + /// Type representing the unique ID of an asset. + type AssetId: Parameter + Member + Copy + Ord + Default + MaxEncodedLen + TypeInfo; + + /// The pallet's account ID. + type PalletId: Get; + + /// Type representing the unique ID of a vault. + type VaultId: Parameter + + Member + + Copy + + MaybeSerializeDeserialize + + Ord + + Default + + MaxEncodedLen + + TypeInfo; + + /// Manager for getting operator stake and delegation info + type DelegationManager: tangle_primitives::traits::MultiAssetDelegationInfo< + Self::AccountId, + BalanceOf, + BlockNumberFor, + AssetId = Self::AssetId, + >; + + /// The origin that can manage reward assets + type ForceOrigin: EnsureOrigin; + } + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + /// Stores the total score for each asset + #[pallet::storage] + #[pallet::getter(fn total_reward_vault_score)] + pub type TotalRewardVaultScore = + StorageMap<_, Blake2_128Concat, T::VaultId, BalanceOf, ValueQuery>; + + /// Stores the service reward for a given user + #[pallet::storage] + #[pallet::getter(fn user_reward_score)] + pub type UserServiceReward = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Blake2_128Concat, + Asset, + BalanceOf, + ValueQuery, + >; + + /// Stores the service reward for a given user + #[pallet::storage] + #[pallet::getter(fn user_claimed_reward)] + pub type UserClaimedReward = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Blake2_128Concat, + T::VaultId, + (BlockNumberFor, BalanceOf), + >; + + #[pallet::storage] + #[pallet::getter(fn reward_vaults)] + /// Storage for the reward vaults + pub type RewardVaults = + StorageMap<_, Blake2_128Concat, T::VaultId, Vec>, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn asset_reward_vault_lookup)] + /// Storage for the reward vaults + pub type AssetLookupRewardVaults = + StorageMap<_, Blake2_128Concat, Asset, T::VaultId, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn reward_config)] + /// Storage for the reward configuration, which includes APY, cap for assets + pub type RewardConfigStorage = StorageMap< + _, + Blake2_128Concat, + T::VaultId, + RewardConfigForAssetVault>, + OptionQuery, + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Rewards have been claimed by an account + RewardsClaimed { + account: T::AccountId, + asset: Asset, + amount: BalanceOf, + }, + /// Event emitted when an incentive APY and cap are set for a reward vault + IncentiveAPYAndCapSet { + vault_id: T::VaultId, + apy: sp_runtime::Percent, + cap: BalanceOf, + }, + /// Event emitted when a blueprint is whitelisted for rewards + BlueprintWhitelisted { + blueprint_id: BlueprintId, + }, + /// Asset has been updated to reward vault + AssetUpdatedInVault { + vault_id: T::VaultId, + asset_id: Asset, + action: AssetAction, + }, + VaultRewardConfigUpdated { + vault_id: T::VaultId, + }, + } + + #[pallet::error] + pub enum Error { + /// No rewards available to claim + NoRewardsAvailable, + /// Insufficient rewards balance in pallet account + InsufficientRewardsBalance, + /// Asset is not whitelisted for rewards + AssetNotWhitelisted, + /// Asset is already whitelisted + AssetAlreadyWhitelisted, + /// Invalid APY value + InvalidAPY, + /// Asset already exists in a reward vault + AssetAlreadyInVault, + /// Asset not found in reward vault + AssetNotInVault, + /// The reward vault does not exist + VaultNotFound, + /// Error returned when trying to add a blueprint ID that already exists. + DuplicateBlueprintId, + /// Error returned when trying to remove a blueprint ID that doesn't exist. + BlueprintIdNotFound, + /// Error returned when the reward configuration for the vault is not found. + RewardConfigNotFound, + /// Arithmetic operation caused an overflow + ArithmeticError, + } + + #[pallet::call] + impl Pallet { + /// Claim rewards for a specific asset and reward type + #[pallet::call_index(1)] + #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] + pub fn claim_rewards(origin: OriginFor, asset: Asset) -> DispatchResult { + let who = ensure_signed(origin)?; + + // calculate and payout rewards + Self::calculate_and_payout_rewards(&who, asset)?; + + Ok(()) + } + + /// Manage asset id to vault rewards. + /// + /// # Permissions + /// + /// * Must be signed by an authorized account + /// + /// # Arguments + /// + /// * `origin` - Origin of the call + /// * `vault_id` - ID of the vault + /// * `asset_id` - ID of the asset + /// * `action` - Action to perform (Add/Remove) + /// + /// # Errors + /// + /// * [`Error::AssetAlreadyInVault`] - Asset already exists in vault + /// * [`Error::AssetNotInVault`] - Asset does not exist in vault + #[pallet::call_index(2)] + #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] + pub fn manage_asset_reward_vault( + origin: OriginFor, + vault_id: T::VaultId, + asset_id: Asset, + action: AssetAction, + ) -> DispatchResult { + let _who = T::ForceOrigin::ensure_origin(origin)?; + + match action { + AssetAction::Add => Self::add_asset_to_vault(&vault_id, &asset_id)?, + AssetAction::Remove => Self::remove_asset_from_vault(&vault_id, &asset_id)?, + } + + Self::deposit_event(Event::AssetUpdatedInVault { vault_id, asset_id, action }); + + Ok(()) + } + + /// Updates the reward configuration for a specific vault. + /// + /// # Arguments + /// * `origin` - Origin of the call, must pass `ForceOrigin` check + /// * `vault_id` - The ID of the vault to update + /// * `new_config` - The new reward configuration containing: + /// * `apy` - Annual Percentage Yield for the vault + /// * `deposit_cap` - Maximum amount that can be deposited + /// * `incentive_cap` - Maximum amount of incentives that can be distributed + /// * `boost_multiplier` - Optional multiplier to boost rewards + /// + /// # Events + /// * `VaultRewardConfigUpdated` - Emitted when vault reward config is updated + /// + /// # Errors + /// * `BadOrigin` - If caller is not authorized through `ForceOrigin` + #[pallet::call_index(3)] + #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] + pub fn update_vault_reward_config( + origin: OriginFor, + vault_id: T::VaultId, + new_config: RewardConfigForAssetVault>, + ) -> DispatchResult { + let _who = T::ForceOrigin::ensure_origin(origin)?; + RewardConfigStorage::::insert(vault_id, new_config); + Self::deposit_event(Event::VaultRewardConfigUpdated { vault_id }); + Ok(()) + } + } + + impl Pallet { + /// The account ID of the rewards pot. + pub fn account_id() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + } +} diff --git a/pallets/rewards/src/mock.rs b/pallets/rewards/src/mock.rs new file mode 100644 index 00000000..206e77e1 --- /dev/null +++ b/pallets/rewards/src/mock.rs @@ -0,0 +1,457 @@ +// 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 . +#![allow(clippy::all)] +use crate::{self as pallet_rewards}; +use ethabi::Uint; +use frame_election_provider_support::{ + bounds::{ElectionBounds, ElectionBoundsBuilder}, + onchain, SequentialPhragmen, +}; +use frame_support::{ + construct_runtime, derive_impl, parameter_types, + traits::{AsEnsureOriginWithArg, ConstU128, ConstU32, OneSessionHandler}, + PalletId, +}; +use pallet_session::historical as pallet_session_historical; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_core::{sr25519, H160}; +use sp_keyring::AccountKeyring; +use sp_keystore::{testing::MemoryKeystore, KeystoreExt, KeystorePtr}; +use sp_runtime::{ + testing::UintAuthorityId, + traits::{ConvertInto, IdentityLookup}, + AccountId32, BuildStorage, Perbill, +}; +use tangle_primitives::services::Asset; +use tangle_primitives::types::rewards::UserDepositWithLocks; + +use core::ops::Mul; +use std::{cell::RefCell, collections::BTreeMap, sync::Arc}; + +pub type AccountId = AccountId32; +pub type Balance = u128; +type Nonce = u32; +pub type AssetId = u128; +pub type BlockNumber = u64; + +#[frame_support::derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Runtime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Nonce = Nonce; + type RuntimeCall = RuntimeCall; + type Hash = sp_core::H256; + type Hashing = sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for Runtime { + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type MaxLocks = (); + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = (); + type WeightInfo = (); + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = (); + type FreezeIdentifier = (); + type MaxFreezes = (); +} + +parameter_types! { + pub ElectionBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default() + .voters_count(5_000.into()).targets_count(1_250.into()).build(); + pub ElectionBoundsMultiPhase: ElectionBounds = ElectionBoundsBuilder::default() + .voters_count(10_000.into()).targets_count(1_500.into()).build(); +} + +impl pallet_session::historical::Config for Runtime { + type FullIdentification = AccountId; + type FullIdentificationOf = ConvertInto; +} + +sp_runtime::impl_opaque_keys! { + pub struct MockSessionKeys { + pub other: MockSessionHandler, + } +} + +pub struct MockSessionHandler; +impl OneSessionHandler for MockSessionHandler { + type Key = UintAuthorityId; + + fn on_genesis_session<'a, I: 'a>(_: I) + where + I: Iterator, + AccountId: 'a, + { + } + + fn on_new_session<'a, I: 'a>(_: bool, _: I, _: I) + where + I: Iterator, + AccountId: 'a, + { + } + + fn on_disabled(_validator_index: u32) {} +} + +impl sp_runtime::BoundToRuntimeAppPublic for MockSessionHandler { + type Public = UintAuthorityId; +} + +pub struct MockSessionManager; + +impl pallet_session::SessionManager for MockSessionManager { + fn end_session(_: sp_staking::SessionIndex) {} + fn start_session(_: sp_staking::SessionIndex) {} + fn new_session(idx: sp_staking::SessionIndex) -> Option> { + if idx == 0 || idx == 1 || idx == 2 { + Some(vec![mock_pub_key(1), mock_pub_key(2), mock_pub_key(3), mock_pub_key(4)]) + } else { + None + } + } +} + +parameter_types! { + pub const Period: u64 = 1; + pub const Offset: u64 = 0; +} + +impl pallet_session::Config for Runtime { + type SessionManager = MockSessionManager; + type Keys = MockSessionKeys; + type ShouldEndSession = pallet_session::PeriodicSessions; + type NextSessionRotation = pallet_session::PeriodicSessions; + type SessionHandler = (MockSessionHandler,); + type RuntimeEvent = RuntimeEvent; + type ValidatorId = AccountId; + type ValidatorIdOf = pallet_staking::StashOf; + type WeightInfo = (); +} + +pub struct OnChainSeqPhragmen; +impl onchain::Config for OnChainSeqPhragmen { + type System = Runtime; + type Solver = SequentialPhragmen; + type DataProvider = Staking; + type WeightInfo = (); + type MaxWinners = ConstU32<100>; + type Bounds = ElectionBoundsOnChain; +} + +/// Upper limit on the number of NPOS nominations. +const MAX_QUOTA_NOMINATIONS: u32 = 16; + +impl pallet_staking::Config for Runtime { + type Currency = Balances; + type CurrencyBalance = ::Balance; + type UnixTime = pallet_timestamp::Pallet; + type CurrencyToVote = (); + type RewardRemainder = (); + type RuntimeEvent = RuntimeEvent; + type Slash = (); + type Reward = (); + type SessionsPerEra = (); + type SlashDeferDuration = (); + type AdminOrigin = frame_system::EnsureRoot; + type BondingDuration = (); + type SessionInterface = (); + type EraPayout = (); + type NextNewSession = Session; + type MaxExposurePageSize = ConstU32<64>; + type MaxControllersInDeprecationBatch = ConstU32<100>; + type ElectionProvider = onchain::OnChainExecution; + type GenesisElectionProvider = Self::ElectionProvider; + type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; + type TargetList = pallet_staking::UseValidatorsMap; + type MaxUnlockingChunks = ConstU32<32>; + type HistoryDepth = ConstU32<84>; + type EventListeners = (); + type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; + type NominationsQuota = pallet_staking::FixedNominationsQuota; + type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; +} + +parameter_types! { + pub const ServicesEVMAddress: H160 = H160([0x11; 20]); +} + +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = u128; + type AssetId = AssetId; + type AssetIdParameter = AssetId; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU128<1>; + type AssetAccountDeposit = ConstU128<10>; + type MetadataDepositBase = ConstU128<1>; + type MetadataDepositPerByte = ConstU128<1>; + type ApprovalDeposit = ConstU128<1>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type WeightInfo = (); + type CallbackHandle = (); + type Extra = (); + type RemoveItemsLimit = ConstU32<5>; +} + +parameter_types! { + pub RewardsPID: PalletId = PalletId(*b"PotStake"); +} + +impl pallet_rewards::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AssetId = AssetId; + type Currency = Balances; + type PalletId = RewardsPID; + type VaultId = u32; + type DelegationManager = MockDelegationManager; + type ForceOrigin = frame_system::EnsureRoot; +} + +thread_local! { + pub static MOCK_DELEGATION_INFO: RefCell = RefCell::new(MockDelegationData::default()); +} + +#[derive(Default)] +pub struct MockDelegationData { + pub deposits: BTreeMap<(AccountId, Asset), UserDepositWithLocks>, +} + +pub struct MockDelegationManager; +impl tangle_primitives::traits::MultiAssetDelegationInfo + for MockDelegationManager +{ + type AssetId = AssetId; + + fn get_current_round() -> tangle_primitives::types::RoundIndex { + Default::default() + } + + fn is_operator(_operator: &AccountId) -> bool { + // dont care + true + } + + fn is_operator_active(operator: &AccountId) -> bool { + if operator == &mock_pub_key(10) { + return false; + } + true + } + + fn get_operator_stake(operator: &AccountId) -> Balance { + if operator == &mock_pub_key(10) { + Default::default() + } else { + 1000 + } + } + + fn get_total_delegation_by_asset_id( + _operator: &AccountId, + _asset_id: &Asset, + ) -> Balance { + Default::default() + } + + fn get_delegators_for_operator( + _operator: &AccountId, + ) -> Vec<(AccountId, Balance, Asset)> { + Default::default() + } + + fn slash_operator( + _operator: &AccountId, + _blueprint_id: tangle_primitives::BlueprintId, + _percentage: sp_runtime::Percent, + ) { + } + + fn get_user_deposit_with_locks( + who: &AccountId, + asset_id: Asset, + ) -> Option> { + MOCK_DELEGATION_INFO.with(|delegation_info| { + delegation_info.borrow().deposits.get(&(who.clone(), asset_id)).cloned() + }) + } +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaxLocks: u32 = 50; + pub const MinOperatorBondAmount: u64 = 10_000; + pub const BondDuration: u32 = 10; + pub PID: PalletId = PalletId(*b"PotStake"); + pub SlashedAmountRecipient : AccountId = AccountKeyring::Alice.into(); + #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub const MaxDelegatorBlueprints : u32 = 50; + #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub const MaxOperatorBlueprints : u32 = 50; + #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub const MaxWithdrawRequests: u32 = 50; + #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub const MaxUnstakeRequests: u32 = 50; + #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub const MaxDelegations: u32 = 50; +} + +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Runtime + { + System: frame_system, + Timestamp: pallet_timestamp, + Balances: pallet_balances, + Assets: pallet_assets, + EVM: pallet_evm, + Ethereum: pallet_ethereum, + Session: pallet_session, + Staking: pallet_staking, + Historical: pallet_session_historical, + RewardsPallet: pallet_rewards, + } +); + +pub struct ExtBuilder; + +impl Default for ExtBuilder { + fn default() -> Self { + ExtBuilder + } +} + +pub fn mock_pub_key(id: u8) -> AccountId { + sr25519::Public::from_raw([id; 32]).into() +} + +pub fn mock_address(id: u8) -> H160 { + H160::from_slice(&[id; 20]) +} + +pub fn account_id_to_address(account_id: AccountId) -> H160 { + H160::from_slice(&AsRef::<[u8; 32]>::as_ref(&account_id)[0..20]) +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + new_test_ext_raw_authorities() +} + +// This function basically just builds a genesis storage key/value store according to +// our desired mockup. +pub fn new_test_ext_raw_authorities() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + // We use default for brevity, but you can configure as desired if needed. + let authorities: Vec = vec![ + AccountKeyring::Alice.into(), + AccountKeyring::Bob.into(), + AccountKeyring::Charlie.into(), + ]; + let mut balances: Vec<_> = authorities.iter().map(|i| (i.clone(), 200_000_u128)).collect(); + + // Add test accounts with enough balance + let test_accounts = vec![AccountKeyring::Dave.into(), AccountKeyring::Eve.into()]; + + balances.extend(test_accounts.iter().map(|i: &AccountId| (i.clone(), 1_000_000_u128))); + + pallet_balances::GenesisConfig:: { balances } + .assimilate_storage(&mut t) + .unwrap(); + + let mut evm_accounts = BTreeMap::new(); + + for i in 1..=authorities.len() { + evm_accounts.insert( + mock_address(i as u8), + fp_evm::GenesisAccount { + code: vec![], + storage: Default::default(), + nonce: Default::default(), + balance: Uint::from(1_000).mul(Uint::from(10).pow(Uint::from(18))), + }, + ); + } + + for a in &authorities { + evm_accounts.insert( + account_id_to_address(a.clone()), + fp_evm::GenesisAccount { + code: vec![], + storage: Default::default(), + nonce: Default::default(), + balance: Uint::from(1_000).mul(Uint::from(10).pow(Uint::from(18))), + }, + ); + } + + let evm_config = + pallet_evm::GenesisConfig:: { accounts: evm_accounts, ..Default::default() }; + + evm_config.assimilate_storage(&mut t).unwrap(); + + // assets_config.assimilate_storage(&mut t).unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.register_extension(KeystoreExt(Arc::new(MemoryKeystore::new()) as KeystorePtr)); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +#[macro_export] +macro_rules! evm_log { + () => { + fp_evm::Log { address: H160::zero(), topics: vec![], data: vec![] } + }; + + ($contract:expr) => { + fp_evm::Log { address: $contract, topics: vec![], data: vec![] } + }; + + ($contract:expr, $topic:expr) => { + fp_evm::Log { + address: $contract, + topics: vec![sp_core::keccak_256($topic).into()], + data: vec![], + } + }; +} diff --git a/pallets/rewards/src/mock_evm.rs b/pallets/rewards/src/mock_evm.rs new file mode 100644 index 00000000..93538421 --- /dev/null +++ b/pallets/rewards/src/mock_evm.rs @@ -0,0 +1,279 @@ +// 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 . +#![allow(clippy::all)] +use crate::mock::{ + AccountId, Balances, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, Timestamp, +}; +use fp_evm::FeeCalculator; +use frame_support::{parameter_types, traits::FindAuthor, weights::Weight, PalletId}; +use pallet_ethereum::{EthereumBlockHashMapping, IntermediateStateRoot, PostLogContent, RawOrigin}; +use pallet_evm::{ + EnsureAddressNever, EnsureAddressRoot, HashedAddressMapping, OnChargeEVMTransaction, +}; +use sp_core::{keccak_256, ConstU32, H160, H256, U256}; +use sp_runtime::{ + traits::{BlakeTwo256, DispatchInfoOf, Dispatchable}, + transaction_validity::{TransactionValidity, TransactionValidityError}, + ConsensusEngineId, +}; + +use pallet_evm_precompile_blake2::Blake2F; +use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; +use pallet_evm_precompile_modexp::Modexp; +use pallet_evm_precompile_sha3fips::Sha3FIPS256; +use pallet_evm_precompile_simple::{ECRecover, ECRecoverPublicKey, Identity, Ripemd160, Sha256}; + +use precompile_utils::precompile_set::{ + AcceptDelegateCall, AddressU64, CallableByContract, CallableByPrecompile, PrecompileAt, + PrecompileSetBuilder, PrecompilesInRangeInclusive, +}; + +type EthereumPrecompilesChecks = (AcceptDelegateCall, CallableByContract, CallableByPrecompile); + +#[precompile_utils::precompile_name_from_address] +pub type DefaultPrecompiles = ( + // Ethereum precompiles: + PrecompileAt, ECRecover, EthereumPrecompilesChecks>, + PrecompileAt, Sha256, EthereumPrecompilesChecks>, + PrecompileAt, Ripemd160, EthereumPrecompilesChecks>, + PrecompileAt, Identity, EthereumPrecompilesChecks>, + PrecompileAt, Modexp, EthereumPrecompilesChecks>, + PrecompileAt, Bn128Add, EthereumPrecompilesChecks>, + PrecompileAt, Bn128Mul, EthereumPrecompilesChecks>, + PrecompileAt, Bn128Pairing, EthereumPrecompilesChecks>, + PrecompileAt, Blake2F, EthereumPrecompilesChecks>, + PrecompileAt, Sha3FIPS256, (CallableByContract, CallableByPrecompile)>, + PrecompileAt, ECRecoverPublicKey, (CallableByContract, CallableByPrecompile)>, +); + +pub type TanglePrecompiles = PrecompileSetBuilder< + R, + (PrecompilesInRangeInclusive<(AddressU64<1>, AddressU64<2095>), DefaultPrecompiles>,), +>; + +parameter_types! { + pub const MinimumPeriod: u64 = 6000 / 2; + + pub PrecompilesValue: TanglePrecompiles = TanglePrecompiles::<_>::new(); +} + +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +pub struct FixedGasPrice; +impl FeeCalculator for FixedGasPrice { + fn min_gas_price() -> (U256, Weight) { + (1.into(), Weight::zero()) + } +} + +pub struct FindAuthorTruncated; +impl FindAuthor for FindAuthorTruncated { + fn find_author<'a, I>(_digests: I) -> Option + where + I: 'a + IntoIterator, + { + Some(address_build(0).address) + } +} + +const BLOCK_GAS_LIMIT: u64 = 150_000_000; +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; + +parameter_types! { + pub const TransactionByteFee: u64 = 1; + pub const ChainId: u64 = 42; + pub const EVMModuleId: PalletId = PalletId(*b"py/evmpa"); + pub BlockGasLimit: U256 = U256::from(BLOCK_GAS_LIMIT); + pub const GasLimitPovSizeRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_POV_SIZE); + pub const WeightPerGas: Weight = Weight::from_parts(20_000, 0); +} + +parameter_types! { + pub SuicideQuickClearLimit: u32 = 0; +} + +pub struct FreeEVMExecution; + +impl OnChargeEVMTransaction for FreeEVMExecution { + type LiquidityInfo = (); + + fn withdraw_fee( + _who: &H160, + _fee: U256, + ) -> Result> { + Ok(()) + } + + fn correct_and_deposit_fee( + _who: &H160, + _corrected_fee: U256, + _base_fee: U256, + already_withdrawn: Self::LiquidityInfo, + ) -> Self::LiquidityInfo { + already_withdrawn + } + + fn pay_priority_fee(_tip: Self::LiquidityInfo) {} +} + +impl pallet_evm::Config for Runtime { + type FeeCalculator = FixedGasPrice; + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type BlockHashMapping = EthereumBlockHashMapping; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = HashedAddressMapping; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type PrecompilesType = TanglePrecompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = ChainId; + type BlockGasLimit = BlockGasLimit; + type Runner = pallet_evm::runner::stack::Runner; + type OnChargeTransaction = (); + type OnCreate = (); + type SuicideQuickClearLimit = SuicideQuickClearLimit; + type FindAuthor = FindAuthorTruncated; + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type Timestamp = Timestamp; + type WeightInfo = (); +} + +parameter_types! { + pub const PostBlockAndTxnHashes: PostLogContent = PostLogContent::BlockAndTxnHashes; +} + +impl pallet_ethereum::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type StateRoot = IntermediateStateRoot; + type PostLogContent = PostBlockAndTxnHashes; + type ExtraDataLength = ConstU32<30>; +} + +impl fp_self_contained::SelfContainedCall for RuntimeCall { + type SignedInfo = H160; + + fn is_self_contained(&self) -> bool { + match self { + RuntimeCall::Ethereum(call) => call.is_self_contained(), + _ => false, + } + } + + fn check_self_contained(&self) -> Option> { + match self { + RuntimeCall::Ethereum(call) => call.check_self_contained(), + _ => None, + } + } + + fn validate_self_contained( + &self, + info: &Self::SignedInfo, + dispatch_info: &DispatchInfoOf, + len: usize, + ) -> Option { + match self { + RuntimeCall::Ethereum(call) => call.validate_self_contained(info, dispatch_info, len), + _ => None, + } + } + + fn pre_dispatch_self_contained( + &self, + info: &Self::SignedInfo, + dispatch_info: &DispatchInfoOf, + len: usize, + ) -> Option> { + match self { + RuntimeCall::Ethereum(call) => { + call.pre_dispatch_self_contained(info, dispatch_info, len) + }, + _ => None, + } + } + + fn apply_self_contained( + self, + info: Self::SignedInfo, + ) -> Option>> { + match self { + call @ RuntimeCall::Ethereum(pallet_ethereum::Call::transact { .. }) => { + Some(call.dispatch(RuntimeOrigin::from(RawOrigin::EthereumTransaction(info)))) + }, + _ => None, + } + } +} + +pub struct MockedEvmRunner; + +impl tangle_primitives::services::EvmRunner for MockedEvmRunner { + type Error = pallet_evm::Error; + + fn call( + source: sp_core::H160, + target: sp_core::H160, + input: Vec, + value: sp_core::U256, + gas_limit: u64, + is_transactional: bool, + validate: bool, + ) -> Result> { + let max_fee_per_gas = FixedGasPrice::min_gas_price().0; + let max_priority_fee_per_gas = max_fee_per_gas.saturating_mul(U256::from(2)); + let nonce = None; + let access_list = Default::default(); + let weight_limit = None; + let proof_size_base_cost = None; + <::Runner as pallet_evm::Runner>::call( + source, + target, + input, + value, + gas_limit, + Some(max_fee_per_gas), + Some(max_priority_fee_per_gas), + nonce, + access_list, + is_transactional, + validate, + weight_limit, + proof_size_base_cost, + ::config(), + ) + .map_err(|o| tangle_primitives::services::RunnerError { error: o.error, weight: o.weight }) + } +} + +pub struct AccountInfo { + pub address: H160, +} + +pub fn address_build(seed: u8) -> AccountInfo { + let private_key = H256::from_slice(&[(seed + 1); 32]); //H256::from_low_u64_be((i + 1) as u64); + let secret_key = libsecp256k1::SecretKey::parse_slice(&private_key[..]).unwrap(); + let public_key = &libsecp256k1::PublicKey::from_secret_key(&secret_key).serialize()[1..65]; + let address = H160::from(H256::from(keccak_256(public_key))); + + AccountInfo { address } +} diff --git a/pallets/rewards/src/tests.rs b/pallets/rewards/src/tests.rs new file mode 100644 index 00000000..34c62f38 --- /dev/null +++ b/pallets/rewards/src/tests.rs @@ -0,0 +1,383 @@ +// 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 . +use crate::{ + mock::*, types::*, AssetAction, Error, Event, Pallet as RewardsPallet, UserClaimedReward, +}; +use frame_support::{assert_noop, assert_ok}; +use sp_runtime::{AccountId32, Percent}; +use tangle_primitives::types::rewards::LockInfo; +use tangle_primitives::types::rewards::LockMultiplier; +use tangle_primitives::{services::Asset, types::rewards::UserDepositWithLocks}; + +fn run_to_block(n: u64) { + while System::block_number() < n { + System::set_block_number(System::block_number() + 1); + } +} + +#[test] +fn test_claim_rewards() { + new_test_ext().execute_with(|| { + let account: AccountId32 = AccountId32::new([1u8; 32]); + let vault_id = 1u32; + let asset = Asset::Custom(vault_id as u128); + let deposit = 100; + let apy = Percent::from_percent(10); + let deposit_cap = 1000; + let boost_multiplier = Some(150); + let incentive_cap = 1000; + + // Configure the reward vault + assert_ok!(RewardsPallet::::update_vault_reward_config( + RuntimeOrigin::root(), + vault_id, + RewardConfigForAssetVault { apy, deposit_cap, incentive_cap, boost_multiplier } + )); + + // Add asset to vault + assert_ok!(RewardsPallet::::manage_asset_reward_vault( + RuntimeOrigin::root(), + vault_id, + asset, + AssetAction::Add, + )); + + // Mock deposit with UserDepositWithLocks + MOCK_DELEGATION_INFO.with(|m| { + m.borrow_mut().deposits.insert( + (account.clone(), asset), + UserDepositWithLocks { unlocked_amount: deposit, amount_with_locks: None }, + ); + }); + + // Initial balance should be 0 + assert_eq!(Balances::free_balance(&account), 0); + + // Claim rewards + assert_ok!(RewardsPallet::::claim_rewards( + RuntimeOrigin::signed(account.clone()), + asset + )); + + // Balance should be greater than 0 after claiming rewards + assert!(Balances::free_balance(&account) > 0); + + // Check events + System::assert_has_event( + Event::RewardsClaimed { + account: account.clone(), + asset, + amount: Balances::free_balance(&account), + } + .into(), + ); + + // Check storage + assert!(UserClaimedReward::::contains_key(&account, vault_id)); + }); +} + +#[test] +fn test_claim_rewards_with_invalid_asset() { + new_test_ext().execute_with(|| { + let account: AccountId32 = AccountId32::new([1u8; 32]); + let vault_id = 1u32; + let asset = Asset::Custom(vault_id as u128); + + // Try to claim rewards for an asset that doesn't exist in the vault + assert_noop!( + RewardsPallet::::claim_rewards(RuntimeOrigin::signed(account.clone()), asset), + Error::::AssetNotInVault + ); + + // Configure the reward vault + assert_ok!(RewardsPallet::::update_vault_reward_config( + RuntimeOrigin::root(), + vault_id, + RewardConfigForAssetVault { + apy: Percent::from_percent(10), + deposit_cap: 1000, + incentive_cap: 1000, + boost_multiplier: Some(150), + } + )); + + // Add asset to vault + assert_ok!(RewardsPallet::::manage_asset_reward_vault( + RuntimeOrigin::root(), + vault_id, + asset, + AssetAction::Add, + )); + + // Try to claim rewards without any deposit + assert_noop!( + RewardsPallet::::claim_rewards(RuntimeOrigin::signed(account.clone()), asset), + Error::::NoRewardsAvailable + ); + }); +} + +#[test] +fn test_claim_rewards_with_no_deposit() { + new_test_ext().execute_with(|| { + let account: AccountId32 = AccountId32::new([1u8; 32]); + let vault_id = 1u32; + let asset = Asset::Custom(vault_id as u128); + + // Configure the reward vault + assert_ok!(RewardsPallet::::update_vault_reward_config( + RuntimeOrigin::root(), + vault_id, + RewardConfigForAssetVault { + apy: Percent::from_percent(10), + deposit_cap: 1000, + incentive_cap: 1000, + boost_multiplier: Some(150), + } + )); + + // Add asset to vault + assert_ok!(RewardsPallet::::manage_asset_reward_vault( + RuntimeOrigin::root(), + vault_id, + asset, + AssetAction::Add, + )); + + // Try to claim rewards without any deposit + assert_noop!( + RewardsPallet::::claim_rewards(RuntimeOrigin::signed(account.clone()), asset), + Error::::NoRewardsAvailable + ); + }); +} + +#[test] +fn test_claim_rewards_multiple_times() { + new_test_ext().execute_with(|| { + let account: AccountId32 = AccountId32::new([1u8; 32]); + let vault_id = 1u32; + let asset = Asset::Custom(vault_id as u128); + let deposit = 100; + + // Configure the reward vault + assert_ok!(RewardsPallet::::update_vault_reward_config( + RuntimeOrigin::root(), + vault_id, + RewardConfigForAssetVault { + apy: Percent::from_percent(10), + deposit_cap: 1000, + incentive_cap: 1000, + boost_multiplier: Some(150), + } + )); + + // Add asset to vault + assert_ok!(RewardsPallet::::manage_asset_reward_vault( + RuntimeOrigin::root(), + vault_id, + asset, + AssetAction::Add, + )); + + // Mock deposit + MOCK_DELEGATION_INFO.with(|m| { + m.borrow_mut().deposits.insert( + (account.clone(), asset), + UserDepositWithLocks { + unlocked_amount: deposit, + amount_with_locks: Some(vec![LockInfo { + amount: deposit, + expiry_block: 3000_u64, + lock_multiplier: LockMultiplier::SixMonths, + }]), + }, + ); + }); + + // Initial balance should be 0 + assert_eq!(Balances::free_balance(&account), 0); + + // Run some blocks to accumulate initial rewards + run_to_block(100); + + // Claim rewards first time + assert_ok!(RewardsPallet::::claim_rewards( + RuntimeOrigin::signed(account.clone()), + asset + )); + + let first_claim_balance = Balances::free_balance(&account); + assert!(first_claim_balance > 0); + + // Run more blocks to accumulate more rewards + run_to_block(1000); + + // Claim rewards second time + assert_ok!(RewardsPallet::::claim_rewards( + RuntimeOrigin::signed(account.clone()), + asset + )); + }); +} + +#[test] +fn test_calculate_deposit_rewards_with_lock_multiplier() { + new_test_ext().execute_with(|| { + let account: AccountId32 = AccountId32::new([1u8; 32]); + let vault_id = 1u32; + let asset = Asset::Custom(vault_id as u128); + let deposit = 100; + let apy = Percent::from_percent(10); + let deposit_cap = 1000; + let boost_multiplier = Some(150); + let incentive_cap = 1000; + + // Configure the reward vault + assert_ok!(RewardsPallet::::update_vault_reward_config( + RuntimeOrigin::root(), + vault_id, + RewardConfigForAssetVault { apy, deposit_cap, incentive_cap, boost_multiplier } + )); + + // Add asset to vault + assert_ok!(RewardsPallet::::manage_asset_reward_vault( + RuntimeOrigin::root(), + vault_id, + asset, + AssetAction::Add, + )); + + // Mock deposit with locked amounts + let lock_expiry = 3000_u64; + let lock_info = LockInfo { + amount: deposit, + expiry_block: lock_expiry, + lock_multiplier: LockMultiplier::SixMonths, + }; + + MOCK_DELEGATION_INFO.with(|m| { + m.borrow_mut().deposits.insert( + (account.clone(), asset), + UserDepositWithLocks { + unlocked_amount: deposit, + amount_with_locks: Some(vec![lock_info.clone()]), + }, + ); + }); + + // Calculate rewards with no previous claim + let total_score = BalanceOf::::from(200u32); // Total deposits of 200 + let deposit_info = UserDepositWithLocks { + unlocked_amount: deposit, + amount_with_locks: Some(vec![lock_info]), + }; + let reward_config = + RewardConfigForAssetVault { apy, deposit_cap, incentive_cap, boost_multiplier }; + let last_claim = None; + + let (total_rewards, rewards_to_be_paid) = + RewardsPallet::::calculate_deposit_rewards_with_lock_multiplier( + total_score, + deposit_info.clone(), + reward_config.clone(), + last_claim, + ) + .unwrap(); + + // Verify rewards are greater than 0 + assert!(total_rewards > 0); + assert_eq!(total_rewards, rewards_to_be_paid); + + // Test with previous claim + let previous_claim_amount = total_rewards / 2; + let last_claim = Some((1u64, previous_claim_amount)); + + let (total_rewards_2, rewards_to_be_paid_2) = + RewardsPallet::::calculate_deposit_rewards_with_lock_multiplier( + total_score, + deposit_info, + reward_config, + last_claim, + ) + .unwrap(); + + // Verify rewards calculation with previous claim + assert_eq!(total_rewards, total_rewards_2); + assert_eq!(rewards_to_be_paid_2, total_rewards.saturating_sub(previous_claim_amount)); + }); +} + +#[test] +fn test_calculate_deposit_rewards_with_expired_locks() { + new_test_ext().execute_with(|| { + let vault_id = 1u32; + let asset = Asset::Custom(vault_id as u128); + let deposit = 100; + let apy = Percent::from_percent(10); + let deposit_cap = 1000; + let boost_multiplier = Some(150); + let incentive_cap = 1000; + + // Configure the reward vault + assert_ok!(RewardsPallet::::update_vault_reward_config( + RuntimeOrigin::root(), + vault_id, + RewardConfigForAssetVault { apy, deposit_cap, incentive_cap, boost_multiplier } + )); + + // Add asset to vault + assert_ok!(RewardsPallet::::manage_asset_reward_vault( + RuntimeOrigin::root(), + vault_id, + asset, + AssetAction::Add, + )); + + let total_score = BalanceOf::::from(200u32); + let reward_config = + RewardConfigForAssetVault { apy, deposit_cap, incentive_cap, boost_multiplier }; + + // Test with expired lock + let expired_lock = LockInfo { + amount: deposit, + expiry_block: 50_u64, // Expired block + lock_multiplier: LockMultiplier::SixMonths, + }; + + let deposit_info = UserDepositWithLocks { + unlocked_amount: deposit, + amount_with_locks: Some(vec![expired_lock]), + }; + + // Run to block after expiry + run_to_block(100); + + let (total_rewards, rewards_to_be_paid) = + RewardsPallet::::calculate_deposit_rewards_with_lock_multiplier( + total_score, + deposit_info, + reward_config, + None, + ) + .unwrap(); + + // Verify only base rewards are calculated (no lock multiplier) + assert_eq!(total_rewards, rewards_to_be_paid); + assert!(total_rewards > 0); + }); +} diff --git a/pallets/rewards/src/types.rs b/pallets/rewards/src/types.rs new file mode 100644 index 00000000..29cdeb31 --- /dev/null +++ b/pallets/rewards/src/types.rs @@ -0,0 +1,54 @@ +// 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 . +use crate::Config; +use frame_support::traits::Currency; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::Percent; +use sp_runtime::RuntimeDebug; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; + +pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +/// Configuration for rewards associated with a specific asset. +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, Eq, PartialEq)] +pub struct RewardConfigForAssetVault { + // The annual percentage yield (APY) for the asset, represented as a Percent + pub apy: Percent, + // The minimum amount required before the asset can be rewarded. + pub incentive_cap: Balance, + // The maximum amount of asset that can be deposited. + pub deposit_cap: Balance, + // Boost multiplier for this asset, if None boost multiplier is not enabled + pub boost_multiplier: Option, +} + +/// Configuration for rewards in the system. +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct RewardConfig { + // A map of asset IDs to their respective reward configurations. + pub configs: BTreeMap>, + // A list of blueprint IDs that are whitelisted for rewards. + pub whitelisted_blueprint_ids: Vec, +} + +/// Asset action for vaults +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)] +pub enum AssetAction { + Add, + Remove, +} diff --git a/pallets/rewards/src/weights.rs b/pallets/rewards/src/weights.rs new file mode 100644 index 00000000..665c6195 --- /dev/null +++ b/pallets/rewards/src/weights.rs @@ -0,0 +1,55 @@ +// 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 . + +//! Autogenerated weights for pallet_dkg +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 +//! DATE: 2021-08-16, STEPS: `[50, ]`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 + +// Executed Command: +// target/release/tangle +// benchmark +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_dkg +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_dkg. +pub trait WeightInfo { + fn set_fee() -> Weight; +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn set_fee() -> Weight { + Weight::from_parts(32_778_000, 0) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} \ No newline at end of file diff --git a/pallets/services/src/lib.rs b/pallets/services/src/lib.rs index f3a7c771..77cab221 100644 --- a/pallets/services/src/lib.rs +++ b/pallets/services/src/lib.rs @@ -20,13 +20,13 @@ #[cfg(not(feature = "std"))] extern crate alloc; - use frame_support::{ pallet_prelude::*, traits::{Currency, ExistenceRequirement, ReservableCurrency}, }; use frame_system::pallet_prelude::*; use sp_runtime::{traits::Get, DispatchResult}; +use tangle_primitives::traits::MultiAssetDelegationInfo; mod functions; mod impls; @@ -71,7 +71,7 @@ pub mod module { use sp_std::vec::Vec; use tangle_primitives::{ services::{MasterBlueprintServiceManagerRevision, *}, - Account, MultiAssetDelegationInfo, + Account, }; use types::*; @@ -187,6 +187,7 @@ pub mod module { type OperatorDelegationManager: tangle_primitives::traits::MultiAssetDelegationInfo< Self::AccountId, BalanceOf, + BlockNumberFor, >; /// Number of eras that slashes are deferred by, after computation. diff --git a/pallets/services/src/mock.rs b/pallets/services/src/mock.rs index 8e99f7b2..c6df6998 100644 --- a/pallets/services/src/mock.rs +++ b/pallets/services/src/mock.rs @@ -37,6 +37,7 @@ use sp_runtime::{ traits::{ConvertInto, IdentityLookup}, AccountId32, BuildStorage, Perbill, }; +use tangle_primitives::rewards::UserDepositWithLocks; use tangle_primitives::services::{Asset, EvmAddressMapping, EvmGasWeightMapping, EvmRunner}; use core::ops::Mul; @@ -260,7 +261,7 @@ impl pallet_assets::Config for Runtime { pub type AssetId = u32; pub struct MockDelegationManager; -impl tangle_primitives::traits::MultiAssetDelegationInfo +impl tangle_primitives::traits::MultiAssetDelegationInfo for MockDelegationManager { type AssetId = AssetId; @@ -308,6 +309,13 @@ impl tangle_primitives::traits::MultiAssetDelegationInfo _percentage: sp_runtime::Percent, ) { } + + fn get_user_deposit_with_locks( + _who: &AccountId, + _asset_id: Asset, + ) -> Option> { + None + } } parameter_types! { diff --git a/pallets/services/src/tests.rs b/pallets/services/src/tests.rs index f071038c..384f8fec 100644 --- a/pallets/services/src/tests.rs +++ b/pallets/services/src/tests.rs @@ -21,7 +21,7 @@ use k256::ecdsa::{SigningKey, VerifyingKey}; use mock::*; use sp_core::{bounded_vec, ecdsa, ByteArray, Pair, U256}; use sp_runtime::{KeyTypeId, Percent}; -use tangle_primitives::{services::*, MultiAssetDelegationInfo}; +use tangle_primitives::{services::*, traits::MultiAssetDelegationInfo}; const ALICE: u8 = 1; const BOB: u8 = 2; diff --git a/precompiles/assets/src/mock.rs b/precompiles/assets/src/mock.rs index 799fdee7..17a8213a 100644 --- a/precompiles/assets/src/mock.rs +++ b/precompiles/assets/src/mock.rs @@ -278,7 +278,7 @@ impl pallet_timestamp::Config for Runtime { type WeightInfo = (); } -impl tangle_primitives::NextAssetId for Runtime { +impl tangle_primitives::traits::NextAssetId for Runtime { fn next_asset_id() -> Option { None } diff --git a/precompiles/multi-asset-delegation/MultiAssetDelegation.sol b/precompiles/multi-asset-delegation/MultiAssetDelegation.sol index 7bdd2f9e..3029340c 100644 --- a/precompiles/multi-asset-delegation/MultiAssetDelegation.sol +++ b/precompiles/multi-asset-delegation/MultiAssetDelegation.sol @@ -14,57 +14,58 @@ MultiAssetDelegation constant MULTI_ASSET_DELEGATION_CONTRACT = MultiAssetDelega interface MultiAssetDelegation { /// @dev Join as an operator with a bond amount. /// @param bondAmount The amount to bond as an operator. - function joinOperators(uint256 bondAmount) external returns (uint8); + function joinOperators(uint256 bondAmount) external; /// @dev Schedule to leave as an operator. - function scheduleLeaveOperators() external returns (uint8); + function scheduleLeaveOperators() external; /// @dev Cancel the scheduled leave as an operator. - function cancelLeaveOperators() external returns (uint8); + function cancelLeaveOperators() external; /// @dev Execute the leave as an operator. - function executeLeaveOperators() external returns (uint8); + function executeLeaveOperators() external; /// @dev Bond more as an operator. /// @param additionalBond The additional amount to bond. - function operatorBondMore(uint256 additionalBond) external returns (uint8); + function operatorBondMore(uint256 additionalBond) external; /// @dev Schedule to unstake as an operator. /// @param unstakeAmount The amount to unstake. - function scheduleOperatorUnstake(uint256 unstakeAmount) external returns (uint8); + function scheduleOperatorUnstake(uint256 unstakeAmount) external; /// @dev Execute the unstake as an operator. - function executeOperatorUnstake() external returns (uint8); + function executeOperatorUnstake() external; /// @dev Cancel the scheduled unstake as an operator. - function cancelOperatorUnstake() external returns (uint8); + function cancelOperatorUnstake() external; /// @dev Go offline as an operator. - function goOffline() external returns (uint8); + function goOffline() external; /// @dev Go online as an operator. - function goOnline() external returns (uint8); + function goOnline() external; /// @dev Deposit an amount of an asset. /// @param assetId The ID of the asset (0 for ERC20). /// @param tokenAddress The address of the ERC20 token (if assetId is 0). /// @param amount The amount to deposit. - function deposit(uint256 assetId, address tokenAddress, uint256 amount) external returns (uint8); + /// @param lockMultiplier The lock multiplier. + function deposit(uint256 assetId, address tokenAddress, uint256 amount, uint8 lockMultiplier) external; /// @dev Schedule a withdrawal of an amount of an asset. /// @param assetId The ID of the asset (0 for ERC20). /// @param tokenAddress The address of the ERC20 token (if assetId is 0). /// @param amount The amount to withdraw. - function scheduleWithdraw(uint256 assetId, address tokenAddress, uint256 amount) external returns (uint8); + function scheduleWithdraw(uint256 assetId, address tokenAddress, uint256 amount) external; /// @dev Execute the scheduled withdrawal. - function executeWithdraw() external returns (uint8); + function executeWithdraw() external; /// @dev Cancel the scheduled withdrawal. /// @param assetId The ID of the asset (0 for ERC20). /// @param tokenAddress The address of the ERC20 token (if assetId is 0). /// @param amount The amount to cancel withdrawal. - function cancelWithdraw(uint256 assetId, address tokenAddress, uint256 amount) external returns (uint8); + function cancelWithdraw(uint256 assetId, address tokenAddress, uint256 amount) external; /// @dev Delegate an amount of an asset to an operator. /// @param operator The address of the operator. @@ -72,22 +73,22 @@ interface MultiAssetDelegation { /// @param tokenAddress The address of the ERC20 token (if assetId is 0). /// @param amount The amount to delegate. /// @param blueprintSelection The blueprint selection. - function delegate(bytes32 operator, uint256 assetId, address tokenAddress, uint256 amount, uint64[] memory blueprintSelection) external returns (uint8); + function delegate(bytes32 operator, uint256 assetId, address tokenAddress, uint256 amount, uint64[] memory blueprintSelection) external; /// @dev Schedule an unstake of an amount of an asset as a delegator. /// @param operator The address of the operator. /// @param assetId The ID of the asset (0 for ERC20). /// @param tokenAddress The address of the ERC20 token (if assetId is 0). /// @param amount The amount to unstake. - function scheduleDelegatorUnstake(bytes32 operator, uint256 assetId, address tokenAddress, uint256 amount) external returns (uint8); + function scheduleDelegatorUnstake(bytes32 operator, uint256 assetId, address tokenAddress, uint256 amount) external; /// @dev Execute the scheduled unstake as a delegator. - function executeDelegatorUnstake() external returns (uint8); + function executeDelegatorUnstake() external; /// @dev Cancel the scheduled unstake as a delegator. /// @param operator The address of the operator. /// @param assetId The ID of the asset (0 for ERC20). /// @param tokenAddress The address of the ERC20 token (if assetId is 0). /// @param amount The amount to cancel unstake. - function cancelDelegatorUnstake(bytes32 operator, uint256 assetId, address tokenAddress, uint256 amount) external returns (uint8); + function cancelDelegatorUnstake(bytes32 operator, uint256 assetId, address tokenAddress, uint256 amount) external; } \ No newline at end of file diff --git a/precompiles/multi-asset-delegation/fuzzer/call.rs b/precompiles/multi-asset-delegation/fuzzer/call.rs index 34fe1cb3..963d1614 100644 --- a/precompiles/multi-asset-delegation/fuzzer/call.rs +++ b/precompiles/multi-asset-delegation/fuzzer/call.rs @@ -171,7 +171,7 @@ fn random_calls(mut rng: &mut R) -> impl IntoIterator (0.into(), token.into()), }; let amount = random_ed_multiple(&mut rng).into(); - vec![(PCall::deposit { asset_id, amount, token_address }, who)] + vec![(PCall::deposit { asset_id, amount, token_address, lock_multiplier: 0 }, who)] }, _ if op == PCall::schedule_withdraw_selectors()[0] => { // Schedule withdraw @@ -411,7 +411,7 @@ fn do_sanity_checks(call: PCall, origin: Address, outcome: PrecompileOutput) { 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 } => { + PCall::deposit { asset_id, amount, token_address, lock_multiplier: 0 } => { 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) diff --git a/precompiles/multi-asset-delegation/src/lib.rs b/precompiles/multi-asset-delegation/src/lib.rs index abeed514..8add5db3 100644 --- a/precompiles/multi-asset-delegation/src/lib.rs +++ b/precompiles/multi-asset-delegation/src/lib.rs @@ -39,6 +39,7 @@ pub mod mock; pub mod mock_evm; #[cfg(test)] mod tests; +use tangle_primitives::types::rewards::LockMultiplier; use ethabi::Function; use fp_evm::{PrecompileFailure, PrecompileHandle}; @@ -211,12 +212,13 @@ where Ok(()) } - #[precompile::public("deposit(uint256,address,uint256)")] + #[precompile::public("deposit(uint256,address,uint256,uint8)")] fn deposit( handle: &mut impl PrecompileHandle, asset_id: U256, token_address: Address, amount: U256, + lock_multiplier: u8, ) -> EvmResult { handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; @@ -232,6 +234,15 @@ where (other_asset_id, _) => (Asset::Custom(other_asset_id.into()), amount), }; + let lock_multiplier = match lock_multiplier { + 0 => None, + 1 => Some(LockMultiplier::OneMonth), + 2 => Some(LockMultiplier::TwoMonths), + 3 => Some(LockMultiplier::ThreeMonths), + 4 => Some(LockMultiplier::SixMonths), + _ => return Err(RevertReason::custom("Invalid lock multiplier").into()), + }; + RuntimeHelper::::try_dispatch( handle, Some(who).into(), @@ -241,6 +252,7 @@ where .try_into() .map_err(|_| RevertReason::value_is_too_large("amount"))?, evm_address: Some(caller), + lock_multiplier, }, )?; diff --git a/precompiles/multi-asset-delegation/src/mock.rs b/precompiles/multi-asset-delegation/src/mock.rs index 6d5407aa..b6c79999 100644 --- a/precompiles/multi-asset-delegation/src/mock.rs +++ b/precompiles/multi-asset-delegation/src/mock.rs @@ -32,17 +32,17 @@ use sp_core::{ sr25519::{Public as sr25519Public, Signature}, ConstU32, H160, }; +use sp_runtime::DispatchError; use sp_runtime::{ traits::{IdentifyAccount, Verify}, AccountId32, BuildStorage, }; -use tangle_primitives::{ - services::{EvmAddressMapping, EvmGasWeightMapping}, - ServiceManager, -}; +use tangle_primitives::services::{EvmAddressMapping, EvmGasWeightMapping}; +use tangle_primitives::traits::{RewardsManager, ServiceManager}; pub type AccountId = <::Signer as IdentifyAccount>::AccountId; pub type Balance = u64; +pub type BlockNumber = u64; type Block = frame_system::mocking::MockBlock; type AssetId = u128; @@ -307,6 +307,45 @@ parameter_types! { pub const MaxDelegations: u32 = 50; } +pub struct MockRewardsManager; + +impl RewardsManager for MockRewardsManager { + type Error = DispatchError; + + fn record_deposit( + _account_id: &AccountId, + _asset: Asset, + _amount: Balance, + _lock_multiplier: Option, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn record_withdrawal( + _account_id: &AccountId, + _asset: Asset, + _amount: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn record_service_reward( + _account_id: &AccountId, + _asset: Asset, + _amount: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn get_asset_deposit_cap_remaining(_asset: Asset) -> Result { + Ok(100_000_u32.into()) + } + + fn get_asset_incentive_cap(_asset: Asset) -> Result { + Ok(0_u32.into()) + } +} + impl pallet_multi_asset_delegation::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = Balances; @@ -323,7 +362,6 @@ impl pallet_multi_asset_delegation::Config for Runtime { type MinDelegateAmount = ConstU64<100>; type Fungibles = Assets; type AssetId = AssetId; - type VaultId = AssetId; type ForceOrigin = frame_system::EnsureRoot; type MaxDelegatorBlueprints = MaxDelegatorBlueprints; type MaxOperatorBlueprints = MaxOperatorBlueprints; @@ -332,6 +370,7 @@ impl pallet_multi_asset_delegation::Config for Runtime { type MaxDelegations = MaxDelegations; type SlashedAmountRecipient = SlashedAmountRecipient; type PalletId = PID; + type RewardsManager = MockRewardsManager; type WeightInfo = (); } diff --git a/precompiles/multi-asset-delegation/src/tests.rs b/precompiles/multi-asset-delegation/src/tests.rs index a2fdac39..e3aaf323 100644 --- a/precompiles/multi-asset-delegation/src/tests.rs +++ b/precompiles/multi-asset-delegation/src/tests.rs @@ -80,7 +80,7 @@ fn test_delegate_assets_invalid_operator() { Balances::make_free_balance_be(&delegator_account, 500); create_and_mint_tokens(1, delegator_account, 500); - assert_ok!(MultiAssetDelegation::deposit(RuntimeOrigin::signed(delegator_account), Asset::Custom(1), 200, Some(TestAccount::Alex.into()))); + assert_ok!(MultiAssetDelegation::deposit(RuntimeOrigin::signed(delegator_account), Asset::Custom(1), 200, Some(TestAccount::Alex.into()), None)); PrecompilesValue::get() .prepare_test( @@ -119,7 +119,8 @@ fn test_delegate_assets() { RuntimeOrigin::signed(delegator_account), Asset::Custom(1), 200, - Some(TestAccount::Alex.into()) + Some(TestAccount::Alex.into()), + None )); assert_eq!(Assets::balance(1, delegator_account), 500 - 200); // should lose deposit @@ -157,7 +158,7 @@ fn test_delegate_assets_insufficient_balance() { create_and_mint_tokens(1, delegator_account, 500); - assert_ok!(MultiAssetDelegation::deposit(RuntimeOrigin::signed(delegator_account), Asset::Custom(1), 200, Some(TestAccount::Alex.into()))); + assert_ok!(MultiAssetDelegation::deposit(RuntimeOrigin::signed(delegator_account), Asset::Custom(1), 200, Some(TestAccount::Alex.into()), None)); PrecompilesValue::get() .prepare_test( @@ -201,6 +202,7 @@ fn test_schedule_withdraw() { asset_id: U256::from(1), amount: U256::from(200), token_address: Default::default(), + lock_multiplier: 0, }, ) .execute_returns(()); @@ -235,10 +237,6 @@ fn test_schedule_withdraw() { ) .execute_returns(()); - let metadata = MultiAssetDelegation::delegators(delegator_account).unwrap(); - assert_eq!(metadata.deposits.get(&Asset::Custom(1)), None); - assert!(!metadata.withdraw_requests.is_empty()); - assert_eq!(Assets::balance(1, delegator_account), 500 - 200); // no change }); } @@ -265,6 +263,7 @@ fn test_execute_withdraw() { asset_id: U256::from(1), amount: U256::from(200), token_address: Default::default(), + lock_multiplier: 0, }, ) .execute_returns(()); @@ -298,20 +297,12 @@ fn test_execute_withdraw() { ) .execute_returns(()); - let metadata = MultiAssetDelegation::delegators(delegator_account).unwrap(); - assert_eq!(metadata.deposits.get(&Asset::Custom(1)), None); - assert!(!metadata.withdraw_requests.is_empty()); - >::put(3); PrecompilesValue::get() .prepare_test(TestAccount::Alex, H160::from_low_u64_be(1), PCall::execute_withdraw {}) .execute_returns(()); - let metadata = MultiAssetDelegation::delegators(delegator_account).unwrap(); - assert_eq!(metadata.deposits.get(&Asset::Custom(1)), None); - assert!(metadata.withdraw_requests.is_empty()); - assert_eq!(Assets::balance(1, delegator_account), 500 - 100); // deposited 200, withdrew 100 }); } @@ -339,6 +330,7 @@ fn test_execute_withdraw_before_due() { asset_id: U256::from(1), amount: U256::from(200), token_address: Default::default(), + lock_multiplier: 0, }, ) .execute_returns(()); @@ -373,10 +365,6 @@ fn test_execute_withdraw_before_due() { ) .execute_returns(()); - let metadata = MultiAssetDelegation::delegators(delegator_account).unwrap(); - assert_eq!(metadata.deposits.get(&Asset::Custom(1)), None); - assert!(!metadata.withdraw_requests.is_empty()); - PrecompilesValue::get() .prepare_test(TestAccount::Alex, H160::from_low_u64_be(1), PCall::execute_withdraw {}) .execute_returns(()); // should not fail @@ -408,6 +396,7 @@ fn test_cancel_withdraw() { asset_id: U256::from(1), amount: U256::from(200), token_address: Default::default(), + lock_multiplier: 0, }, ) .execute_returns(()); @@ -441,10 +430,6 @@ fn test_cancel_withdraw() { ) .execute_returns(()); - let metadata = MultiAssetDelegation::delegators(delegator_account).unwrap(); - assert_eq!(metadata.deposits.get(&Asset::Custom(1)), None); - assert!(!metadata.withdraw_requests.is_empty()); - PrecompilesValue::get() .prepare_test( TestAccount::Alex, diff --git a/precompiles/rewards/Cargo.toml b/precompiles/rewards/Cargo.toml new file mode 100644 index 00000000..582aa066 --- /dev/null +++ b/precompiles/rewards/Cargo.toml @@ -0,0 +1,216 @@ +[package] +name = "pallet-evm-precompile-rewards" +version = "0.1.0" +authors = { workspace = true } +edition = "2021" +description = "A Precompile to make pallet-rewards calls encoding accessible to pallet-evm" + +[dependencies] +precompile-utils = { workspace = true } + +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-balances = { workspace = true } +pallet-multi-asset-delegation = { workspace = true } +parity-scale-codec = { workspace = true, features = ["derive"] } +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# Frontier +fp-evm = { workspace = true } +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 } +serde = { workspace = true } +sha3 = { workspace = true } +ethereum = { workspace = true, features = ["with-codec"] } +ethers = "2.0" +hex = { workspace = true } +num_enum = { workspace = true } +libsecp256k1 = { workspace = true } +serde_json = { workspace = true } +smallvec = { workspace = true } +sp-keystore = { workspace = true } + + +precompile-utils = { workspace = true, features = ["std", "testing"] } + +# Substrate +pallet-balances = { workspace = true, features = ["std"] } +pallet-assets = { workspace = true, features = ["std"] } +pallet-timestamp = { workspace = true, features = ["std"] } +scale-info = { workspace = true, features = ["derive", "std"] } +sp-io = { workspace = true, features = ["std"] } + +# Frontier Primitive +fp-account = { workspace = true } +fp-consensus = { workspace = true } +fp-dynamic-fee = { workspace = true } +fp-ethereum = { workspace = true } +fp-rpc = { workspace = true } +fp-self-contained = { workspace = true } +fp-storage = { workspace = true } + +# Frontier FRAME +pallet-base-fee = { workspace = true } +pallet-dynamic-fee = { workspace = true } +pallet-ethereum = { workspace = true } +pallet-evm = { workspace = true } +pallet-evm-chain-id = { workspace = true } + +pallet-evm-precompile-blake2 = { workspace = true } +pallet-evm-precompile-bn128 = { workspace = true } +pallet-evm-precompile-curve25519 = { workspace = true } +pallet-evm-precompile-ed25519 = { workspace = true } +pallet-evm-precompile-modexp = { workspace = true } +pallet-evm-precompile-sha3fips = { workspace = true } +pallet-evm-precompile-simple = { workspace = true } + +pallet-session = { workspace = true } +pallet-staking = { workspace = true } +sp-staking = { workspace = true } +frame-election-provider-support = { workspace = true } + +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", +] diff --git a/precompiles/rewards/Rewards.sol b/precompiles/rewards/Rewards.sol new file mode 100644 index 00000000..4e8c09c9 --- /dev/null +++ b/precompiles/rewards/Rewards.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @dev The Rewards contract's address. +address constant REWARDS = 0x0000000000000000000000000000000000000823; + +/// @dev The Rewards contract's instance. +Rewards constant REWARDS_CONTRACT = Rewards(REWARDS); + +/// @author The Tangle Team +/// @title Pallet Rewards Interface +/// @title The interface through which solidity contracts will interact with the Rewards pallet +/// @custom:address 0x0000000000000000000000000000000000000823 +interface Rewards { + /// @notice Claims rewards for a specific asset + /// @param assetId The ID of the asset + /// @param tokenAddress The EVM address of the token (zero for native assets) + function claimRewards(uint256 assetId, address tokenAddress) external; +} \ No newline at end of file diff --git a/precompiles/rewards/src/lib.rs b/precompiles/rewards/src/lib.rs new file mode 100644 index 00000000..857c6778 --- /dev/null +++ b/precompiles/rewards/src/lib.rs @@ -0,0 +1,74 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +use core::marker::PhantomData; +use frame_support::traits::Currency; +use fp_evm::{PrecompileHandle, PrecompileOutput}; +use pallet_evm::Precompile; +use pallet_rewards::Config; +use precompile_utils::{ + prelude::*, + solidity::{ + codec::{Address, BoundedVec}, + modifier::FunctionModifier, + revert::InjectBacktrace, + }, +}; +use sp_core::{H160, U256}; +use sp_runtime::traits::StaticLookup; +use sp_std::{marker::PhantomData, prelude::*}; +use tangle_primitives::services::Asset; + +/// Solidity selector of the Transfer log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_REWARDS_CLAIMED: [u8; 32] = keccak256!("RewardsClaimed(address,uint256)"); + +/// A precompile to wrap the functionality from pallet-rewards. +pub struct RewardsPrecompile(PhantomData); + +#[precompile_utils::precompile] +impl RewardsPrecompile +where + Runtime: Config + pallet_evm::Config, + Runtime::AccountId: From + Into, +{ + #[precompile::public("claimRewards(uint256,address)")] + fn claim_rewards( + handle: &mut impl PrecompileHandle, + asset_id: U256, + token_address: Address, + ) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + + let caller = handle.context().caller; + let who = Runtime::AddressMapping::into_account_id(caller); + + let (asset, _) = match (asset_id.as_u128(), token_address.0 .0) { + (0, erc20_token) if erc20_token != [0; 20] => { + (Asset::Erc20(erc20_token.into()), U256::zero()) + }, + (other_asset_id, _) => (Asset::Custom(other_asset_id.into()), U256::zero()), + }; + + RuntimeHelper::::try_dispatch( + handle, + Some(who).into(), + pallet_rewards::Call::::claim_rewards { asset }, + )?; + + Ok(()) + } +} diff --git a/precompiles/rewards/src/mock.rs b/precompiles/rewards/src/mock.rs new file mode 100644 index 00000000..6d5407aa --- /dev/null +++ b/precompiles/rewards/src/mock.rs @@ -0,0 +1,378 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Tangle Foundation. +// +// This file is part of pallet-evm-precompile-multi-asset-delegation package. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test utilities +use super::*; +use crate::mock_evm::*; +use frame_support::{ + construct_runtime, derive_impl, parameter_types, + traits::{AsEnsureOriginWithArg, ConstU64}, + weights::Weight, + PalletId, +}; +use pallet_evm::GasWeightMapping; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_core::{ + self, + sr25519::{Public as sr25519Public, Signature}, + ConstU32, H160, +}; +use sp_runtime::{ + traits::{IdentifyAccount, Verify}, + AccountId32, BuildStorage, +}; +use tangle_primitives::{ + services::{EvmAddressMapping, EvmGasWeightMapping}, + ServiceManager, +}; + +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; +pub type Balance = u64; + +type Block = frame_system::mocking::MockBlock; +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, +]; + +#[derive( + Eq, + PartialEq, + Ord, + PartialOrd, + Clone, + Encode, + Decode, + Debug, + MaxEncodedLen, + Serialize, + Deserialize, + derive_more::Display, + scale_info::TypeInfo, +)] +pub enum TestAccount { + Empty, + Alex, + Bobo, + Dave, + Charlie, + Eve, + PrecompileAddress, +} + +impl Default for TestAccount { + fn default() -> Self { + Self::Empty + } +} + +// needed for associated type in pallet_evm +impl AddressMapping for TestAccount { + fn into_account_id(h160_account: H160) -> AccountId32 { + match h160_account { + a if a == H160::repeat_byte(0x01) => TestAccount::Alex.into(), + a if a == H160::repeat_byte(0x02) => TestAccount::Bobo.into(), + a if a == H160::repeat_byte(0x03) => TestAccount::Charlie.into(), + a if a == H160::repeat_byte(0x04) => TestAccount::Dave.into(), + a if a == H160::repeat_byte(0x05) => TestAccount::Eve.into(), + a if a == H160::from_low_u64_be(6) => TestAccount::PrecompileAddress.into(), + _ => TestAccount::Empty.into(), + } + } +} + +impl AddressMapping for TestAccount { + fn into_account_id(h160_account: H160) -> sp_core::sr25519::Public { + match h160_account { + a if a == H160::repeat_byte(0x01) => sr25519Public::from_raw([1u8; 32]), + a if a == H160::repeat_byte(0x02) => sr25519Public::from_raw([2u8; 32]), + a if a == H160::repeat_byte(0x03) => sr25519Public::from_raw([3u8; 32]), + a if a == H160::repeat_byte(0x04) => sr25519Public::from_raw([4u8; 32]), + a if a == H160::repeat_byte(0x05) => sr25519Public::from_raw([5u8; 32]), + a if a == H160::from_low_u64_be(6) => sr25519Public::from_raw(PRECOMPILE_ADDRESS_BYTES), + _ => sr25519Public::from_raw([0u8; 32]), + } + } +} + +impl From for H160 { + fn from(x: TestAccount) -> H160 { + match x { + TestAccount::Alex => H160::repeat_byte(0x01), + TestAccount::Bobo => H160::repeat_byte(0x02), + TestAccount::Charlie => H160::repeat_byte(0x03), + TestAccount::Dave => H160::repeat_byte(0x04), + TestAccount::Eve => H160::repeat_byte(0x05), + TestAccount::PrecompileAddress => H160::from_low_u64_be(6), + _ => Default::default(), + } + } +} + +impl From for AccountId32 { + fn from(x: TestAccount) -> Self { + match x { + TestAccount::Alex => AccountId32::from([1u8; 32]), + TestAccount::Bobo => AccountId32::from([2u8; 32]), + TestAccount::Charlie => AccountId32::from([3u8; 32]), + TestAccount::Dave => AccountId32::from([4u8; 32]), + TestAccount::Eve => AccountId32::from([5u8; 32]), + TestAccount::PrecompileAddress => AccountId32::from(PRECOMPILE_ADDRESS_BYTES), + _ => AccountId32::from([0u8; 32]), + } + } +} + +impl From for sp_core::sr25519::Public { + fn from(x: TestAccount) -> Self { + match x { + TestAccount::Alex => sr25519Public::from_raw([1u8; 32]), + TestAccount::Bobo => sr25519Public::from_raw([2u8; 32]), + TestAccount::Charlie => sr25519Public::from_raw([3u8; 32]), + TestAccount::Dave => sr25519Public::from_raw([4u8; 32]), + TestAccount::Eve => sr25519Public::from_raw([5u8; 32]), + TestAccount::PrecompileAddress => sr25519Public::from_raw(PRECOMPILE_ADDRESS_BYTES), + _ => sr25519Public::from_raw([0u8; 32]), + } + } +} + +construct_runtime!( + pub enum Runtime + { + System: frame_system, + Balances: pallet_balances, + Evm: pallet_evm, + Ethereum: pallet_ethereum, + Timestamp: pallet_timestamp, + Assets: pallet_assets, + MultiAssetDelegation: pallet_multi_asset_delegation, + } +); + +parameter_types! { + pub const SS58Prefix: u8 = 42; + pub static ExistentialDeposit: Balance = 1; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Runtime { + type SS58Prefix = (); + type BaseCallFilter = frame_support::traits::Everything; + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type RuntimeCall = RuntimeCall; + type Hash = sp_core::H256; + type Hashing = sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = sp_runtime::traits::IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = (); + type DbWeight = (); + type BlockLength = (); + type BlockWeights = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = [u8; 4]; + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = (); + type FreezeIdentifier = (); + type MaxFreezes = (); +} + +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = u64; + type AssetId = AssetId; + type AssetIdParameter = u128; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU64<1>; + type AssetAccountDeposit = ConstU64<10>; + type MetadataDepositBase = ConstU64<1>; + type MetadataDepositPerByte = ConstU64<1>; + type ApprovalDeposit = ConstU64<1>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type WeightInfo = (); + type CallbackHandle = (); + type Extra = (); + type RemoveItemsLimit = ConstU32<5>; +} + +pub struct MockServiceManager; + +impl ServiceManager for MockServiceManager { + fn get_active_blueprints_count(_account: &AccountId) -> usize { + // we dont care + Default::default() + } + + fn get_active_services_count(_account: &AccountId) -> usize { + // we dont care + Default::default() + } + + fn can_exit(_account: &AccountId) -> bool { + // Mock logic to determine if the given account can exit + true + } + + fn get_blueprints_by_operator(_account: &AccountId) -> Vec { + // we dont care + Default::default() + } +} + +pub struct PalletEVMGasWeightMapping; + +impl EvmGasWeightMapping for PalletEVMGasWeightMapping { + fn gas_to_weight(gas: u64, without_base_weight: bool) -> Weight { + pallet_evm::FixedGasWeightMapping::::gas_to_weight(gas, without_base_weight) + } + + fn weight_to_gas(weight: Weight) -> u64 { + pallet_evm::FixedGasWeightMapping::::weight_to_gas(weight) + } +} + +pub struct PalletEVMAddressMapping; + +impl EvmAddressMapping for PalletEVMAddressMapping { + fn into_account_id(address: H160) -> AccountId { + use pallet_evm::AddressMapping; + ::AddressMapping::into_account_id(address) + } + + fn into_address(account_id: AccountId) -> H160 { + account_id.using_encoded(|b| { + let mut addr = [0u8; 20]; + addr.copy_from_slice(&b[0..20]); + H160(addr) + }) + } +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaxLocks: u32 = 50; + pub const MinOperatorBondAmount: u64 = 10_000; + pub const BondDuration: u32 = 10; + pub PID: PalletId = PalletId(*b"PotStake"); + pub SlashedAmountRecipient : AccountId = TestAccount::Alex.into(); + #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub const MaxDelegatorBlueprints : u32 = 50; + #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub const MaxOperatorBlueprints : u32 = 50; + #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub const MaxWithdrawRequests: u32 = 5; + #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub const MaxUnstakeRequests: u32 = 5; + #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] + pub const MaxDelegations: u32 = 50; +} + +impl pallet_multi_asset_delegation::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type MinOperatorBondAmount = MinOperatorBondAmount; + type BondDuration = BondDuration; + type ServiceManager = MockServiceManager; + type LeaveOperatorsDelay = ConstU32<10>; + type EvmRunner = MockedEvmRunner; + type EvmAddressMapping = PalletEVMAddressMapping; + type EvmGasWeightMapping = PalletEVMGasWeightMapping; + type OperatorBondLessDelay = ConstU32<1>; + type LeaveDelegatorsDelay = ConstU32<1>; + type DelegationBondLessDelay = ConstU32<5>; + type MinDelegateAmount = ConstU64<100>; + type Fungibles = Assets; + type AssetId = AssetId; + type VaultId = AssetId; + type ForceOrigin = frame_system::EnsureRoot; + type MaxDelegatorBlueprints = MaxDelegatorBlueprints; + type MaxOperatorBlueprints = MaxOperatorBlueprints; + type MaxWithdrawRequests = MaxWithdrawRequests; + type MaxUnstakeRequests = MaxUnstakeRequests; + type MaxDelegations = MaxDelegations; + type SlashedAmountRecipient = SlashedAmountRecipient; + type PalletId = PID; + type WeightInfo = (); +} + +/// Build test externalities, prepopulated with data for testing democracy precompiles +#[derive(Default)] +pub struct ExtBuilder { + /// Endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl ExtBuilder { + /// Build the test externalities for use in tests + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + balances: self + .balances + .iter() + .chain( + [ + (TestAccount::Alex.into(), 1_000_000), + (TestAccount::Bobo.into(), 1_000_000), + (TestAccount::Charlie.into(), 1_000_000), + (MultiAssetDelegation::pallet_account(), 100), /* give pallet some ED so + * it can receive tokens */ + ] + .iter(), + ) + .cloned() + .collect(), + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + System::set_block_number(1); + }); + ext + } +} diff --git a/precompiles/rewards/src/mock_evm.rs b/precompiles/rewards/src/mock_evm.rs new file mode 100644 index 00000000..958342ea --- /dev/null +++ b/precompiles/rewards/src/mock_evm.rs @@ -0,0 +1,331 @@ +// 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 . +#![allow(clippy::all)] +use crate::{ + mock::{AccountId, Balances, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, Timestamp}, + MultiAssetDelegationPrecompile, MultiAssetDelegationPrecompileCall, +}; +use fp_evm::FeeCalculator; +use frame_support::{ + parameter_types, + traits::{Currency, FindAuthor, OnUnbalanced}, + weights::Weight, + PalletId, +}; +use pallet_ethereum::{EthereumBlockHashMapping, IntermediateStateRoot, PostLogContent, RawOrigin}; +use pallet_evm::{EnsureAddressNever, EnsureAddressOrigin, OnChargeEVMTransaction}; +use precompile_utils::precompile_set::{AddressU64, PrecompileAt, PrecompileSetBuilder}; +use sp_core::{keccak_256, ConstU32, H160, H256, U256}; +use sp_runtime::{ + traits::{DispatchInfoOf, Dispatchable}, + transaction_validity::{TransactionValidity, TransactionValidityError}, + ConsensusEngineId, +}; +use tangle_primitives::services::EvmRunner; + +pub type Precompiles = + PrecompileSetBuilder, MultiAssetDelegationPrecompile>,)>; + +pub type PCall = MultiAssetDelegationPrecompileCall; + +parameter_types! { + pub const MinimumPeriod: u64 = 6000 / 2; +} + +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +pub struct FixedGasPrice; +impl FeeCalculator for FixedGasPrice { + fn min_gas_price() -> (U256, Weight) { + (1.into(), Weight::zero()) + } +} + +pub struct EnsureAddressAlways; +impl EnsureAddressOrigin for EnsureAddressAlways { + type Success = (); + + fn try_address_origin( + _address: &H160, + _origin: OuterOrigin, + ) -> Result { + Ok(()) + } + + fn ensure_address_origin( + _address: &H160, + _origin: OuterOrigin, + ) -> Result { + Ok(()) + } +} + +pub struct FindAuthorTruncated; +impl FindAuthor for FindAuthorTruncated { + fn find_author<'a, I>(_digests: I) -> Option + where + I: 'a + IntoIterator, + { + Some(address_build(0).address) + } +} + +const BLOCK_GAS_LIMIT: u64 = 150_000_000; +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; + +parameter_types! { + pub const TransactionByteFee: u64 = 1; + pub const ChainId: u64 = 42; + pub const EVMModuleId: PalletId = PalletId(*b"py/evmpa"); + pub PrecompilesValue: Precompiles = Precompiles::new(); + pub BlockGasLimit: U256 = U256::from(BLOCK_GAS_LIMIT); + pub const GasLimitPovSizeRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_POV_SIZE); + pub const WeightPerGas: Weight = Weight::from_parts(20_000, 0); +} + +parameter_types! { + pub SuicideQuickClearLimit: u32 = 0; +} + +pub struct DealWithFees; +impl OnUnbalanced for DealWithFees { + fn on_unbalanceds(_fees_then_tips: impl Iterator) { + // whatever + } +} +pub struct FreeEVMExecution; + +impl OnChargeEVMTransaction for FreeEVMExecution { + type LiquidityInfo = (); + + fn withdraw_fee( + _who: &H160, + _fee: U256, + ) -> Result> { + Ok(()) + } + + fn correct_and_deposit_fee( + _who: &H160, + _corrected_fee: U256, + _base_fee: U256, + already_withdrawn: Self::LiquidityInfo, + ) -> Self::LiquidityInfo { + already_withdrawn + } + + fn pay_priority_fee(_tip: Self::LiquidityInfo) {} +} + +/// Type alias for negative imbalance during fees +type RuntimeNegativeImbalance = + ::AccountId>>::NegativeImbalance; + +/// See: [`pallet_evm::EVMCurrencyAdapter`] +pub struct CustomEVMCurrencyAdapter; + +impl OnChargeEVMTransaction for CustomEVMCurrencyAdapter { + type LiquidityInfo = Option; + + fn withdraw_fee( + who: &H160, + fee: U256, + ) -> Result> { + let pallet_multi_asset_delegation_address = + pallet_multi_asset_delegation::Pallet::::pallet_evm_account(); + // Make pallet services account free to use + if who == &pallet_multi_asset_delegation_address { + return Ok(None); + } + // fallback to the default implementation + as OnChargeEVMTransaction< + Runtime, + >>::withdraw_fee(who, fee) + } + + fn correct_and_deposit_fee( + who: &H160, + corrected_fee: U256, + base_fee: U256, + already_withdrawn: Self::LiquidityInfo, + ) -> Self::LiquidityInfo { + let pallet_multi_asset_delegation_address = + pallet_multi_asset_delegation::Pallet::::pallet_evm_account(); + // Make pallet services account free to use + if who == &pallet_multi_asset_delegation_address { + return already_withdrawn; + } + // fallback to the default implementation + as OnChargeEVMTransaction< + Runtime, + >>::correct_and_deposit_fee(who, corrected_fee, base_fee, already_withdrawn) + } + + fn pay_priority_fee(tip: Self::LiquidityInfo) { + as OnChargeEVMTransaction< + Runtime, + >>::pay_priority_fee(tip) + } +} + +impl pallet_evm::Config for Runtime { + type FeeCalculator = FixedGasPrice; + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type BlockHashMapping = EthereumBlockHashMapping; + type CallOrigin = EnsureAddressAlways; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = crate::mock::TestAccount; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type PrecompilesType = Precompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = ChainId; + type BlockGasLimit = BlockGasLimit; + type Runner = pallet_evm::runner::stack::Runner; + type OnChargeTransaction = CustomEVMCurrencyAdapter; + type OnCreate = (); + type SuicideQuickClearLimit = SuicideQuickClearLimit; + type FindAuthor = FindAuthorTruncated; + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type Timestamp = Timestamp; + type WeightInfo = (); +} + +parameter_types! { + pub const PostBlockAndTxnHashes: PostLogContent = PostLogContent::BlockAndTxnHashes; +} + +impl pallet_ethereum::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type StateRoot = IntermediateStateRoot; + type PostLogContent = PostBlockAndTxnHashes; + type ExtraDataLength = ConstU32<30>; +} + +impl fp_self_contained::SelfContainedCall for RuntimeCall { + type SignedInfo = H160; + + fn is_self_contained(&self) -> bool { + match self { + RuntimeCall::Ethereum(call) => call.is_self_contained(), + _ => false, + } + } + + fn check_self_contained(&self) -> Option> { + match self { + RuntimeCall::Ethereum(call) => call.check_self_contained(), + _ => None, + } + } + + fn validate_self_contained( + &self, + info: &Self::SignedInfo, + dispatch_info: &DispatchInfoOf, + len: usize, + ) -> Option { + match self { + RuntimeCall::Ethereum(call) => call.validate_self_contained(info, dispatch_info, len), + _ => None, + } + } + + fn pre_dispatch_self_contained( + &self, + info: &Self::SignedInfo, + dispatch_info: &DispatchInfoOf, + len: usize, + ) -> Option> { + match self { + RuntimeCall::Ethereum(call) => { + call.pre_dispatch_self_contained(info, dispatch_info, len) + }, + _ => None, + } + } + + fn apply_self_contained( + self, + info: Self::SignedInfo, + ) -> Option>> { + match self { + call @ RuntimeCall::Ethereum(pallet_ethereum::Call::transact { .. }) => { + Some(call.dispatch(RuntimeOrigin::from(RawOrigin::EthereumTransaction(info)))) + }, + _ => None, + } + } +} + +pub struct MockedEvmRunner; + +impl EvmRunner for MockedEvmRunner { + type Error = pallet_evm::Error; + + fn call( + source: sp_core::H160, + target: sp_core::H160, + input: Vec, + value: sp_core::U256, + gas_limit: u64, + is_transactional: bool, + validate: bool, + ) -> Result> { + let max_fee_per_gas = FixedGasPrice::min_gas_price().0; + let max_priority_fee_per_gas = max_fee_per_gas.saturating_mul(U256::from(2)); + let nonce = None; + let access_list = Default::default(); + let weight_limit = None; + let proof_size_base_cost = None; + <::Runner as pallet_evm::Runner>::call( + source, + target, + input, + value, + gas_limit, + Some(max_fee_per_gas), + Some(max_priority_fee_per_gas), + nonce, + access_list, + is_transactional, + validate, + weight_limit, + proof_size_base_cost, + ::config(), + ) + .map_err(|o| tangle_primitives::services::RunnerError { error: o.error, weight: o.weight }) + } +} + +pub struct AccountInfo { + pub address: H160, +} + +pub fn address_build(seed: u8) -> AccountInfo { + let private_key = H256::from_slice(&[(seed + 1); 32]); //H256::from_low_u64_be((i + 1) as u64); + let secret_key = libsecp256k1::SecretKey::parse_slice(&private_key[..]).unwrap(); + let public_key = &libsecp256k1::PublicKey::from_secret_key(&secret_key).serialize()[1..65]; + let address = H160::from(H256::from(keccak_256(public_key))); + + AccountInfo { address } +} diff --git a/precompiles/rewards/src/tests.rs b/precompiles/rewards/src/tests.rs new file mode 100644 index 00000000..79852f32 --- /dev/null +++ b/precompiles/rewards/src/tests.rs @@ -0,0 +1,77 @@ +use super::*; +use mock::*; +use precompile_utils::testing::*; +use sp_core::H160; +use sp_runtime::Percent; +use frame_support::assert_ok; + +fn precompiles() -> TestPrecompileSet { + PrecompilesValue::get() +} + +#[test] +fn test_solidity_interface_has_all_function_selectors_documented() { + for file in ["Rewards.sol"] { + precompiles() + .process_selectors(file, |fn_selector, fn_signature| { + assert!( + DOCUMENTED_FUNCTIONS.contains(&fn_selector), + "documented_functions must contain {fn_selector:?} ({fn_signature})", + ); + }); + } +} + +#[test] +fn test_claim_rewards() { + ExtBuilder::default().build().execute_with(|| { + let vault_id = 1u32; + let asset_id = Asset::Custom(1); + let alice: AccountId = TestAccount::Alice.into(); + let deposit_amount = 1_000u128; + + // Setup vault and add asset + assert_ok!(Rewards::manage_asset_reward_vault( + RuntimeOrigin::root(), + vault_id, + asset_id, + AssetAction::Add, + )); + + // Setup reward config + let config = RewardConfigForAssetVault { + apy: Percent::from_percent(10), + deposit_cap: 1_000_000, + incentive_cap: 100_000, + boost_multiplier: Some(200), + }; + assert_ok!(Rewards::update_vault_reward_config( + RuntimeOrigin::root(), + vault_id, + config, + )); + + // Setup mock deposit + MOCK_DELEGATION_INFO.with(|m| { + m.borrow_mut().deposits.insert( + (alice.clone(), asset_id), + UserDepositWithLocks { unlocked_amount: deposit_amount, amount_with_locks: None }, + ); + }); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alice, + H160::from_low_u64_be(1), + PrecompileCall::claim_rewards { + asset_id: U256::from(1), + token_address: Address::zero(), + }, + ) + .execute_returns(()); + + // Check that rewards were claimed + let claimed = UserClaimedReward::::get(alice, vault_id); + assert!(claimed.is_some()); + }); +} diff --git a/precompiles/services/src/mock.rs b/precompiles/services/src/mock.rs index 7ba2ddc8..ef41b084 100644 --- a/precompiles/services/src/mock.rs +++ b/precompiles/services/src/mock.rs @@ -41,6 +41,7 @@ use sp_runtime::{ testing::UintAuthorityId, traits::ConvertInto, AccountId32, BuildStorage, Perbill, }; use std::{collections::BTreeMap, sync::Arc}; +use tangle_primitives::rewards::UserDepositWithLocks; use tangle_primitives::services::{EvmAddressMapping, EvmGasWeightMapping, EvmRunner}; pub type AccountId = AccountId32; @@ -391,7 +392,7 @@ impl From for sp_core::sr25519::Public { pub type AssetId = u32; pub struct MockDelegationManager; -impl tangle_primitives::traits::MultiAssetDelegationInfo +impl tangle_primitives::traits::MultiAssetDelegationInfo for MockDelegationManager { type AssetId = AssetId; @@ -435,6 +436,13 @@ impl tangle_primitives::traits::MultiAssetDelegationInfo _percentage: sp_runtime::Percent, ) { } + + fn get_user_deposit_with_locks( + _who: &AccountId, + _asset_id: Asset, + ) -> Option> { + None + } } parameter_types! { diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index e8f95d30..8f44f6e3 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -30,13 +30,12 @@ use sp_runtime::{ #[cfg(not(feature = "std"))] extern crate alloc; +pub mod chain_identifier; +pub mod impls; pub mod services; pub mod types; pub use types::*; -pub mod chain_identifier; -pub mod impls; pub mod traits; -pub use traits::*; #[cfg(feature = "verifying")] pub mod verifier; diff --git a/primitives/src/traits/data_provider.rs b/primitives/src/traits/data_provider.rs index 485bc4fa..236ccd08 100644 --- a/primitives/src/traits/data_provider.rs +++ b/primitives/src/traits/data_provider.rs @@ -56,31 +56,31 @@ pub fn median(mut items: Vec) -> Option { macro_rules! create_median_value_data_provider { ($name:ident, $key:ty, $value:ty, $timestamped_value:ty, [$( $provider:ty ),*]) => { pub struct $name; - impl $crate::DataProvider<$key, $value> for $name { + impl $crate::traits::DataProvider<$key, $value> for $name { fn get(key: &$key) -> Option<$value> { let mut values = vec![]; $( - if let Some(v) = <$provider as $crate::DataProvider<$key, $value>>::get(&key) { + if let Some(v) = <$provider as $crate::traits::DataProvider<$key, $value>>::get(&key) { values.push(v); } )* - $crate::data_provider::median(values) + $crate::traits::data_provider::median(values) } } - impl $crate::DataProviderExtended<$key, $timestamped_value> for $name { + impl $crate::traits::DataProviderExtended<$key, $timestamped_value> for $name { fn get_no_op(key: &$key) -> Option<$timestamped_value> { let mut values = vec![]; $( - if let Some(v) = <$provider as $crate::DataProviderExtended<$key, $timestamped_value>>::get_no_op(&key) { + if let Some(v) = <$provider as $crate::traits::DataProviderExtended<$key, $timestamped_value>>::get_no_op(&key) { values.push(v); } )* - $crate::data_provider::median(values) + $crate::traits::data_provider::median(values) } fn get_all_values() -> Vec<($key, Option<$timestamped_value>)> { let mut keys = sp_std::collections::btree_set::BTreeSet::new(); $( - <$provider as $crate::DataProviderExtended<$key, $timestamped_value>>::get_all_values() + <$provider as $crate::traits::DataProviderExtended<$key, $timestamped_value>>::get_all_values() .into_iter() .for_each(|(k, _)| { keys.insert(k); }); )* diff --git a/primitives/src/traits/mod.rs b/primitives/src/traits/mod.rs index fef97c98..0fb9c6b9 100644 --- a/primitives/src/traits/mod.rs +++ b/primitives/src/traits/mod.rs @@ -1,9 +1,11 @@ pub mod assets; pub mod data_provider; pub mod multi_asset_delegation; +pub mod rewards; pub mod services; pub use assets::*; pub use data_provider::*; pub use multi_asset_delegation::*; +pub use rewards::*; pub use services::*; diff --git a/primitives/src/traits/multi_asset_delegation.rs b/primitives/src/traits/multi_asset_delegation.rs index ebd5bc95..f9639e53 100644 --- a/primitives/src/traits/multi_asset_delegation.rs +++ b/primitives/src/traits/multi_asset_delegation.rs @@ -1,3 +1,4 @@ +use crate::types::rewards::UserDepositWithLocks; use crate::{services::Asset, types::RoundIndex}; use sp_std::prelude::*; @@ -12,7 +13,8 @@ use sp_std::prelude::*; /// * `AccountId`: The type representing an account identifier. /// * `AssetId`: The type representing an asset identifier. /// * `Balance`: The type representing a balance or amount. -pub trait MultiAssetDelegationInfo { +/// * `BlockNumber`: The type representing a block number. +pub trait MultiAssetDelegationInfo { type AssetId; /// Get the current round index. @@ -106,4 +108,9 @@ pub trait MultiAssetDelegationInfo { blueprint_id: crate::BlueprintId, percentage: sp_runtime::Percent, ); + + fn get_user_deposit_with_locks( + who: &AccountId, + asset_id: Asset, + ) -> Option>; } diff --git a/primitives/src/traits/rewards.rs b/primitives/src/traits/rewards.rs new file mode 100644 index 00000000..dff0a1fd --- /dev/null +++ b/primitives/src/traits/rewards.rs @@ -0,0 +1,127 @@ +// 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 . + +use crate::services::Asset; +use crate::types::rewards::LockMultiplier; +use sp_runtime::traits::Zero; + +/// Trait for managing rewards in the Tangle network. +/// This trait provides functionality to record deposits, withdrawals, and service rewards, +/// as well as query total deposits for accounts. +pub trait RewardsManager { + type Error; + + /// Records a deposit for an account with an optional lock multiplier. + /// + /// # Parameters + /// * `account_id` - The account making the deposit + /// * `asset` - The asset being deposited + /// * `amount` - The amount being deposited + /// * `lock_multiplier` - Optional multiplier for locked deposits + fn record_deposit( + account_id: &AccountId, + asset: Asset, + amount: Balance, + lock_multiplier: Option, + ) -> Result<(), Self::Error>; + + /// Records a withdrawal for an account. + /// + /// # Parameters + /// * `account_id` - The account making the withdrawal + /// * `asset` - The asset being withdrawn + /// * `amount` - The amount being withdrawn + fn record_withdrawal( + account_id: &AccountId, + asset: Asset, + amount: Balance, + ) -> Result<(), Self::Error>; + + /// Records a service reward for an account. + /// + /// # Parameters + /// * `account_id` - The account receiving the reward + /// * `asset` - The asset being rewarded + /// * `amount` - The reward amount + fn record_service_reward( + account_id: &AccountId, + asset: Asset, + amount: Balance, + ) -> Result<(), Self::Error>; + + /// Gets the maximum deposit cap for an asset at a given block number. + /// This represents the maximum amount that can be deposited for this asset. + /// + /// # Parameters + /// * `asset` - The asset to query the deposit cap for + /// + /// # Returns + /// * `Ok(Balance)` - The maximum deposit cap for the asset + /// * `Err(Self::Error)` - If there was an error retrieving the cap + fn get_asset_deposit_cap_remaining(asset: Asset) -> Result; + + /// Gets the incentive cap for an asset at a given block number. + /// This represents the minimum amount required to receive full incentives. + /// + /// # Parameters + /// * `asset` - The asset to query the incentive cap for + /// + /// # Returns + /// * `Ok(Balance)` - The incentive cap for the asset + /// * `Err(Self::Error)` - If there was an error retrieving the cap + fn get_asset_incentive_cap(asset: Asset) -> Result; +} + +impl + RewardsManager for () +where + Balance: Zero, +{ + type Error = &'static str; + + fn record_deposit( + _account_id: &AccountId, + _asset: Asset, + _amount: Balance, + _lock_multiplier: Option, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn record_withdrawal( + _account_id: &AccountId, + _asset: Asset, + _amount: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn record_service_reward( + _account_id: &AccountId, + _asset: Asset, + _amount: Balance, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn get_asset_deposit_cap_remaining(_asset: Asset) -> Result { + Ok(Balance::zero()) + } + + fn get_asset_incentive_cap(_asset: Asset) -> Result { + Ok(Balance::zero()) + } +} diff --git a/primitives/src/types.rs b/primitives/src/types.rs index 50cf21ac..a78d88ea 100644 --- a/primitives/src/types.rs +++ b/primitives/src/types.rs @@ -15,6 +15,7 @@ // use super::*; pub mod ordered_set; +pub mod rewards; use frame_support::pallet_prelude::*; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; diff --git a/primitives/src/types/rewards.rs b/primitives/src/types/rewards.rs new file mode 100644 index 00000000..19912cb7 --- /dev/null +++ b/primitives/src/types/rewards.rs @@ -0,0 +1,117 @@ +use super::*; +use crate::services::Asset; +use frame_system::Config; +use sp_std::vec::Vec; + +/// Represents different types of rewards a user can earn +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)] +pub struct UserRewards> { + /// Rewards earned from restaking (in TNT) + pub restaking_rewards: Balance, + /// Boost rewards information + pub boost_rewards: BoostInfo, + /// Service rewards in their respective assets + pub service_rewards: BoundedVec, MaxServiceRewards>, +} + +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)] +pub struct UserRestakeUpdate { + pub asset: Asset, + pub amount: Balance, + pub multiplier: LockMultiplier, +} + +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)] +pub struct ServiceRewards { + asset_id: Asset, + amount: Balance, +} + +/// Information about a user's boost rewards +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)] +pub struct BoostInfo { + /// Amount of boost rewards + pub amount: Balance, + /// Multiplier for the boost (e.g. OneMonth = 1x, TwoMonths = 2x) + pub multiplier: LockMultiplier, + /// Block number when the boost expires + pub expiry: BlockNumber, +} + +impl Default for BoostInfo { + fn default() -> Self { + Self { + amount: Balance::default(), + multiplier: LockMultiplier::OneMonth, + expiry: BlockNumber::default(), + } + } +} + +impl> Default + for UserRewards +{ + fn default() -> Self { + Self { + restaking_rewards: Balance::default(), + boost_rewards: BoostInfo::default(), + service_rewards: BoundedVec::default(), + } + } +} + +/// Lock multiplier for rewards, representing months of lock period +#[derive(Clone, Copy, Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)] +pub enum LockMultiplier { + /// One month lock period (1x multiplier) + OneMonth = 1, + /// Two months lock period (2x multiplier) + TwoMonths = 2, + /// Three months lock period (3x multiplier) + ThreeMonths = 3, + /// Six months lock period (6x multiplier) + SixMonths = 6, +} + +impl Default for LockMultiplier { + fn default() -> Self { + Self::OneMonth + } +} + +impl LockMultiplier { + /// Get the multiplier value + pub fn value(&self) -> u32 { + *self as u32 + } + + /// Get the block number for each multiplier + pub fn get_blocks(&self) -> u32 { + // assuming block time of 6 seconds + match self { + LockMultiplier::OneMonth => 432000, + LockMultiplier::TwoMonths => 864000, + LockMultiplier::ThreeMonths => 1296000, + LockMultiplier::SixMonths => 2592000, + } + } + + /// Calculate the expiry block number based on the current block number and multiplier + pub fn expiry_block_number(&self, current_block: u32) -> u32 { + current_block.saturating_add(self.get_blocks()) + } +} + +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)] +pub struct UserDepositWithLocks { + pub unlocked_amount: Balance, + pub amount_with_locks: Option>>, +} + +/// Struct to store the lock info +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, Eq, PartialEq)] +pub struct LockInfo { + pub amount: Balance, + pub lock_multiplier: LockMultiplier, + pub expiry_block: BlockNumber, +} diff --git a/runtime/mainnet/Cargo.toml b/runtime/mainnet/Cargo.toml index a1b2fbc6..6aadcbbc 100644 --- a/runtime/mainnet/Cargo.toml +++ b/runtime/mainnet/Cargo.toml @@ -91,6 +91,7 @@ pallet-services-rpc-runtime-api = { workspace = true } tangle-primitives = { workspace = true, features = ["verifying"] } tangle-crypto-primitives = { workspace = true } pallet-multi-asset-delegation = { workspace = true } +pallet-rewards = { workspace = true } # Frontier dependencies fp-rpc = { workspace = true } @@ -230,6 +231,7 @@ std = [ "pallet-services/std", "pallet-multi-asset-delegation/std", "pallet-services-rpc-runtime-api/std", + "pallet-rewards/std", # Frontier "fp-rpc/std", diff --git a/runtime/mainnet/src/lib.rs b/runtime/mainnet/src/lib.rs index 630f1961..ee398f9b 100644 --- a/runtime/mainnet/src/lib.rs +++ b/runtime/mainnet/src/lib.rs @@ -1204,7 +1204,7 @@ pub type AssetId = u128; #[cfg(feature = "runtime-benchmarks")] pub type AssetId = u32; -impl tangle_primitives::NextAssetId for Runtime { +impl tangle_primitives::traits::NextAssetId for Runtime { fn next_asset_id() -> Option { pallet_assets::NextAssetId::::get() } @@ -1265,7 +1265,6 @@ impl pallet_multi_asset_delegation::Config for Runtime { type AssetId = AssetId; type ForceOrigin = frame_system::EnsureRoot; type PalletId = PID; - type VaultId = AssetId; type SlashedAmountRecipient = TreasuryAccount; type MaxDelegatorBlueprints = MaxDelegatorBlueprints; type MaxOperatorBlueprints = MaxOperatorBlueprints; @@ -1273,6 +1272,7 @@ impl pallet_multi_asset_delegation::Config for Runtime { type MaxUnstakeRequests = MaxUnstakeRequests; type MaxDelegations = MaxDelegations; type EvmRunner = crate::tangle_services::PalletEvmRunner; + type RewardsManager = Rewards; type EvmGasWeightMapping = crate::tangle_services::PalletEVMGasWeightMapping; type EvmAddressMapping = crate::tangle_services::PalletEVMAddressMapping; type WeightInfo = (); @@ -1308,6 +1308,20 @@ impl pallet_tangle_lst::Config for Runtime { type MaxPointsToBalance = frame_support::traits::ConstU8<10>; } +parameter_types! { + pub const RewardsPID: PalletId = PalletId(*b"py/tnrew"); +} + +impl pallet_rewards::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AssetId = AssetId; + type Currency = Balances; + type PalletId = RewardsPID; + type VaultId = u32; + type DelegationManager = MultiAssetDelegation; + type ForceOrigin = frame_system::EnsureRoot; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime { @@ -1368,6 +1382,7 @@ construct_runtime!( Assets: pallet_assets = 44, MultiAssetDelegation: pallet_multi_asset_delegation = 45, Services: pallet_services = 46, + Rewards: pallet_rewards = 47, } ); diff --git a/runtime/testnet/Cargo.toml b/runtime/testnet/Cargo.toml index ed132d86..48aef3de 100644 --- a/runtime/testnet/Cargo.toml +++ b/runtime/testnet/Cargo.toml @@ -89,6 +89,7 @@ pallet-utility = { workspace = true } pallet-multisig = { workspace = true } pallet-vesting = { workspace = true } pallet-tangle-lst = { workspace = true } +pallet-rewards = { workspace = true } # Tangle dependencies pallet-airdrop-claims = { workspace = true } @@ -256,6 +257,7 @@ std = [ "pallet-multi-asset-delegation/std", "pallet-tangle-lst/std", "pallet-services-rpc-runtime-api/std", + "pallet-rewards/std", # Frontier "fp-evm/std", diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index 51c048b3..711944af 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -1216,6 +1216,20 @@ impl pallet_tangle_lst::Config for Runtime { type MaxPointsToBalance = frame_support::traits::ConstU8<10>; } +parameter_types! { + pub const RewardsPID: PalletId = PalletId(*b"py/tnrew"); +} + +impl pallet_rewards::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AssetId = AssetId; + type Currency = Balances; + type PalletId = RewardsPID; + type VaultId = u32; + type DelegationManager = MultiAssetDelegation; + type ForceOrigin = frame_system::EnsureRoot; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime { @@ -1278,6 +1292,7 @@ construct_runtime!( MultiAssetDelegation: pallet_multi_asset_delegation = 45, Services: pallet_services = 51, Lst: pallet_tangle_lst = 52, + Rewards: pallet_rewards = 53, } ); @@ -1416,7 +1431,7 @@ pub type AssetId = u128; #[cfg(feature = "runtime-benchmarks")] pub type AssetId = u32; -impl tangle_primitives::NextAssetId for Runtime { +impl tangle_primitives::traits::NextAssetId for Runtime { fn next_asset_id() -> Option { pallet_assets::NextAssetId::::get() } @@ -1478,7 +1493,7 @@ impl pallet_multi_asset_delegation::Config for Runtime { type SlashedAmountRecipient = TreasuryAccount; type ForceOrigin = frame_system::EnsureRoot; type PalletId = PID; - type VaultId = AssetId; + type RewardsManager = Rewards; type MaxDelegatorBlueprints = MaxDelegatorBlueprints; type MaxOperatorBlueprints = MaxOperatorBlueprints; type MaxWithdrawRequests = MaxWithdrawRequests;