diff --git a/node/src/benchmarking.rs b/node/src/benchmarking.rs index 32f99bb46..cd96edf0b 100644 --- a/node/src/benchmarking.rs +++ b/node/src/benchmarking.rs @@ -122,6 +122,7 @@ pub fn create_benchmark_extrinsic( frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(0), frame_metadata_hash_extension::CheckMetadataHash::::new(false), + tangle_runtime::extension::CheckNominatedRestaked::::new(), ); let raw_payload = runtime::SignedPayload::from_raw( @@ -137,6 +138,7 @@ pub fn create_benchmark_extrinsic( (), (), None, + (), ), ); let signature = raw_payload.using_encoded(|e| sender.sign(e)); diff --git a/node/tests/evm_restaking.rs b/node/tests/evm_restaking.rs index 5ef2bf89d..d42cfbd18 100644 --- a/node/tests/evm_restaking.rs +++ b/node/tests/evm_restaking.rs @@ -313,7 +313,7 @@ where new_config: api::runtime_types::pallet_rewards::types::RewardConfigForAssetVault { apy: api::runtime_types::sp_arithmetic::per_things::Perbill( - MOCK_APY * 10000, + MOCK_APY * 1000000, ), // convert percent to perbill deposit_cap: MOCK_DEPOSIT_CAP, incentive_cap: 1, @@ -1281,7 +1281,7 @@ fn mad_rewards() { let bob_new_balance = bob_balance.data.free; assert!(bob_new_balance > bob_original_balance); let change_in_bob_balance = bob_new_balance - bob_original_balance; - assert!(change_in_bob_balance <= original_user_rewards); // account for some fee loss + assert!(change_in_bob_balance >= original_user_rewards); // account for some fee loss // finally lets check that the rewards claimed are not shown again in rpc let user_rewards = t.subxt.runtime_api().at_latest().await?.call(rewards_addr).await?; @@ -1404,7 +1404,7 @@ fn lrt_rewards_erc20() { let bob = TestAccount::Bob; let bob_provider = alloy_provider_with_wallet(&t.provider, bob.evm_wallet()); // Mint WETH for Bob - let weth_amount = U256::from(200 * EIGHTEEN_DECIMALS); + let weth_amount = U256::from(MOCK_DEPOSIT * 2); let weth = MockERC20::new(t.weth, &bob_provider); weth.mint(bob.address(), weth_amount).send().await?.get_receipt().await?; info!("Minted {} WETH for Bob", format_ether(weth_amount)); diff --git a/runtime/mainnet/src/extension.rs b/runtime/mainnet/src/extension.rs new file mode 100644 index 000000000..05181f652 --- /dev/null +++ b/runtime/mainnet/src/extension.rs @@ -0,0 +1,132 @@ +// 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. + +//! Runtime extension implementations for mainnet. + +use frame_support::pallet_prelude::*; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::traits::{DispatchInfoOf, SignedExtension}; + +use crate::Balance; +use crate::Runtime; + +/// Extension that checks for nominated tokens that are being restaked. +/// Prevents unbonding when tokens are delegated through the multi-asset-delegation system. +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct CheckNominatedRestaked(core::marker::PhantomData); + +impl sp_std::fmt::Debug for CheckNominatedRestaked { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "CheckNominatedRestaked") + } + + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + Ok(()) + } +} + +impl CheckNominatedRestaked { + pub fn new() -> Self { + CheckNominatedRestaked(core::marker::PhantomData) + } +} + +impl CheckNominatedRestaked { + /// Checks if unbonding is allowed based on delegated nominations + pub fn can_unbound( + who: &::AccountId, + amount: Balance, + ) -> bool { + pallet_multi_asset_delegation::Pallet::::can_unbound(who, amount) + } +} + +impl Default for CheckNominatedRestaked { + fn default() -> Self { + CheckNominatedRestaked(core::marker::PhantomData) + } +} + +impl SignedExtension for CheckNominatedRestaked { + const IDENTIFIER: &'static str = "CheckNominatedRestaked"; + + type AccountId = ::AccountId; + + type Call = ::RuntimeCall; + + type AdditionalSigned = (); + + type Pre = (); + + fn additional_signed(&self) -> Result { + Ok(()) + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + _info: &DispatchInfoOf, + _len: usize, + ) -> TransactionValidity { + use crate::RuntimeCall; + + match call { + // Match on Staking unbond calls + RuntimeCall::Staking(pallet_staking::Call::unbond { value }) => { + if Self::can_unbound(who, *value) { + Ok(ValidTransaction::default()) + } else { + Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(1))) + } + }, + // Match on Proxy calls + RuntimeCall::Proxy(pallet_proxy::Call::proxy { ref call, real, .. }) => { + // Convert MultiAddress to AccountId + if let sp_runtime::MultiAddress::Id(account_id) = real { + self.validate(account_id, call, _info, _len) + } else { + // If not an Id type, we allow it by default + Ok(ValidTransaction::default()) + } + }, + // Match on various Utility batch calls + RuntimeCall::Utility(pallet_utility::Call::batch { ref calls }) + | RuntimeCall::Utility(pallet_utility::Call::batch_all { ref calls }) + | RuntimeCall::Utility(pallet_utility::Call::force_batch { ref calls }) => { + for call in calls { + self.validate(who, call, _info, _len)?; + } + Ok(ValidTransaction::default()) + }, + // Default case for all other calls + _ => Ok(ValidTransaction::default()), + } + } + + fn pre_dispatch( + self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> Result { + self.validate(who, call, info, len).map(|_| ()) + } +} diff --git a/runtime/mainnet/src/lib.rs b/runtime/mainnet/src/lib.rs index 11dce839f..de41f8687 100644 --- a/runtime/mainnet/src/lib.rs +++ b/runtime/mainnet/src/lib.rs @@ -21,6 +21,7 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +pub mod extension; mod filters; pub mod frontier_evm; pub mod impls; @@ -825,6 +826,7 @@ where frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(tip), frame_metadata_hash_extension::CheckMetadataHash::::new(true), + extension::CheckNominatedRestaked::::new(), ); let raw_payload = SignedPayload::new(call, extra) .map_err(|e| { @@ -1457,6 +1459,7 @@ pub type SignedExtra = ( frame_system::CheckWeight, pallet_transaction_payment::ChargeTransactionPayment, frame_metadata_hash_extension::CheckMetadataHash, + extension::CheckNominatedRestaked, ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = diff --git a/runtime/testnet/src/extension.rs b/runtime/testnet/src/extension.rs new file mode 100644 index 000000000..115a01fce --- /dev/null +++ b/runtime/testnet/src/extension.rs @@ -0,0 +1,132 @@ +// 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. + +//! Runtime extension implementations for testnet. + +use frame_support::pallet_prelude::*; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::traits::{DispatchInfoOf, SignedExtension}; + +use crate::Balance; +use crate::Runtime; + +/// Extension that checks for nominated tokens that are being restaked. +/// Prevents unbonding when tokens are delegated through the multi-asset-delegation system. +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct CheckNominatedRestaked(core::marker::PhantomData); + +impl sp_std::fmt::Debug for CheckNominatedRestaked { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "CheckNominatedRestaked") + } + + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + Ok(()) + } +} + +impl CheckNominatedRestaked { + pub fn new() -> Self { + CheckNominatedRestaked(core::marker::PhantomData) + } +} + +impl CheckNominatedRestaked { + /// Checks if unbonding is allowed based on delegated nominations + pub fn can_unbound( + who: &::AccountId, + amount: Balance, + ) -> bool { + pallet_multi_asset_delegation::Pallet::::can_unbound(who, amount) + } +} + +impl Default for CheckNominatedRestaked { + fn default() -> Self { + CheckNominatedRestaked(core::marker::PhantomData) + } +} + +impl SignedExtension for CheckNominatedRestaked { + const IDENTIFIER: &'static str = "CheckNominatedRestaked"; + + type AccountId = ::AccountId; + + type Call = ::RuntimeCall; + + type AdditionalSigned = (); + + type Pre = (); + + fn additional_signed(&self) -> Result { + Ok(()) + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + _info: &DispatchInfoOf, + _len: usize, + ) -> TransactionValidity { + use crate::RuntimeCall; + + match call { + // Match on Staking unbond calls + RuntimeCall::Staking(pallet_staking::Call::unbond { value }) => { + if Self::can_unbound(who, *value) { + Ok(ValidTransaction::default()) + } else { + Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(1))) + } + }, + // Match on Proxy calls + RuntimeCall::Proxy(pallet_proxy::Call::proxy { ref call, real, .. }) => { + // Convert MultiAddress to AccountId + if let sp_runtime::MultiAddress::Id(account_id) = real { + self.validate(account_id, call, _info, _len) + } else { + // If not an Id type, we allow it by default + Ok(ValidTransaction::default()) + } + }, + // Match on various Utility batch calls + RuntimeCall::Utility(pallet_utility::Call::batch { ref calls }) + | RuntimeCall::Utility(pallet_utility::Call::batch_all { ref calls }) + | RuntimeCall::Utility(pallet_utility::Call::force_batch { ref calls }) => { + for call in calls { + self.validate(who, call, _info, _len)?; + } + Ok(ValidTransaction::default()) + }, + // Default case for all other calls + _ => Ok(ValidTransaction::default()), + } + } + + fn pre_dispatch( + self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> Result { + self.validate(who, call, info, len).map(|_| ()) + } +} diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index 1f0e8cf81..cd6c206aa 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -21,6 +21,7 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +pub mod extension; mod filters; pub mod frontier_evm; pub mod hyperbridge; @@ -835,6 +836,7 @@ where frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(tip), frame_metadata_hash_extension::CheckMetadataHash::::new(true), + extension::CheckNominatedRestaked::::new(), ); let raw_payload = SignedPayload::new(call, extra) .map_err(|e| { @@ -1365,6 +1367,7 @@ pub type SignedExtra = ( frame_system::CheckWeight, pallet_transaction_payment::ChargeTransactionPayment, frame_metadata_hash_extension::CheckMetadataHash, + extension::CheckNominatedRestaked, ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic =