diff --git a/Cargo.toml b/Cargo.toml
index 3eb8697f..cc3b057d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,6 +8,7 @@ members = [
"frame/collator-selection",
"frame/custom-signatures",
"frame/dapps-staking",
+ "frame/dapp-staking-v3",
"frame/pallet-xcm",
"frame/pallet-xvm",
"frame/xc-asset-config",
diff --git a/frame/dapp-staking-v3/Cargo.toml b/frame/dapp-staking-v3/Cargo.toml
new file mode 100644
index 00000000..27f927fe
--- /dev/null
+++ b/frame/dapp-staking-v3/Cargo.toml
@@ -0,0 +1,42 @@
+[package]
+name = "pallet-dapp-staking-v3"
+version = "0.0.1-alpha"
+description = "Pallet for dApp staking v3 protocol"
+authors.workspace = true
+edition.workspace = true
+homepage.workspace = true
+repository.workspace = true
+
+[dependencies]
+frame-support = { workspace = true }
+frame-system = { workspace = true }
+num-traits = { workspace = true }
+parity-scale-codec = { workspace = true }
+
+scale-info = { workspace = true }
+serde = { workspace = true, optional = true }
+sp-arithmetic = { workspace = true }
+sp-core = { workspace = true }
+sp-io = { workspace = true }
+sp-runtime = { workspace = true }
+sp-std = { workspace = true }
+
+[dev-dependencies]
+pallet-balances = { workspace = true }
+
+[features]
+default = ["std"]
+std = [
+ "serde",
+ "parity-scale-codec/std",
+ "scale-info/std",
+ "num-traits/std",
+ "sp-core/std",
+ "sp-runtime/std",
+ "sp-arithmetic/std",
+ "sp-io/std",
+ "sp-std/std",
+ "frame-support/std",
+ "frame-system/std",
+ "pallet-balances/std",
+]
diff --git a/frame/dapp-staking-v3/src/lib.rs b/frame/dapp-staking-v3/src/lib.rs
new file mode 100644
index 00000000..d2057651
--- /dev/null
+++ b/frame/dapp-staking-v3/src/lib.rs
@@ -0,0 +1,460 @@
+// This file is part of Astar.
+
+// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// Astar 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.
+
+// Astar 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 Astar. If not, see .
+
+//! # dApp Staking v3 Pallet
+//!
+//! - [`Config`]
+//!
+//! ## Overview
+//!
+//! Pallet that implements dapps staking protocol.
+//!
+//! <>
+//!
+//! ## Interface
+//!
+//! ### Dispatchable Function
+//!
+//! <>
+//!
+//! ### Other
+//!
+//! <>
+//!
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use frame_support::{
+ pallet_prelude::*,
+ traits::{Currency, LockIdentifier, LockableCurrency, StorageVersion, WithdrawReasons},
+ weights::Weight,
+};
+use frame_system::pallet_prelude::*;
+use sp_runtime::traits::{BadOrigin, Saturating, Zero};
+
+use crate::types::*;
+pub use pallet::*;
+
+#[cfg(test)]
+mod test;
+
+mod types;
+
+const STAKING_ID: LockIdentifier = *b"dapstake";
+
+#[frame_support::pallet]
+pub mod pallet {
+ use super::*;
+
+ /// The current storage version.
+ const STORAGE_VERSION: StorageVersion = StorageVersion::new(5);
+
+ #[pallet::pallet]
+ #[pallet::generate_store(pub(crate) trait Store)]
+ #[pallet::storage_version(STORAGE_VERSION)]
+ pub struct Pallet(_);
+
+ #[pallet::config]
+ pub trait Config: frame_system::Config {
+ /// The overarching event type.
+ type RuntimeEvent: From> + IsType<::RuntimeEvent>;
+
+ /// Currency used for staking.
+ type Currency: LockableCurrency;
+
+ /// Describes smart contract in the context required by dApp staking.
+ type SmartContract: Parameter + Member + MaxEncodedLen;
+
+ /// Privileged origin for managing dApp staking pallet.
+ type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>;
+
+ /// Maximum number of contracts that can be integrated into dApp staking at once.
+ /// TODO: maybe this can be reworded or improved later on - but we want a ceiling!
+ #[pallet::constant]
+ type MaxNumberOfContracts: Get;
+
+ /// Maximum number of locked chunks that can exist per account at a time.
+ #[pallet::constant]
+ type MaxLockedChunks: Get;
+
+ /// Maximum number of unlocking chunks that can exist per account at a time.
+ #[pallet::constant]
+ type MaxUnlockingChunks: Get;
+
+ /// Minimum amount an account has to lock in dApp staking in order to participate.
+ #[pallet::constant]
+ type MinimumLockedAmount: Get>;
+ }
+
+ #[pallet::event]
+ #[pallet::generate_deposit(pub(crate) fn deposit_event)]
+ pub enum Event {
+ /// A smart contract has been registered for dApp staking
+ DAppRegistered {
+ owner: T::AccountId,
+ smart_contract: T::SmartContract,
+ dapp_id: DAppId,
+ },
+ /// dApp reward destination has been updated.
+ DAppRewardDestinationUpdated {
+ smart_contract: T::SmartContract,
+ beneficiary: Option,
+ },
+ /// dApp owner has been changed.
+ DAppOwnerChanged {
+ smart_contract: T::SmartContract,
+ new_owner: T::AccountId,
+ },
+ /// dApp has been unregistered
+ DAppUnregistered {
+ smart_contract: T::SmartContract,
+ era: EraNumber,
+ },
+ /// Account has locked some amount into dApp staking.
+ Locked {
+ account: T::AccountId,
+ amount: BalanceOf,
+ },
+ }
+
+ #[pallet::error]
+ pub enum Error {
+ /// Pallet is disabled/in maintenance mode.
+ Disabled,
+ /// Smart contract already exists within dApp staking protocol.
+ ContractAlreadyExists,
+ /// Maximum number of smart contracts has been reached.
+ ExceededMaxNumberOfContracts,
+ /// Not possible to assign a new dApp Id.
+ /// This should never happen since current type can support up to 65536 - 1 unique dApps.
+ NewDAppIdUnavailable,
+ /// Specified smart contract does not exist in dApp staking.
+ ContractNotFound,
+ /// Call origin is not dApp owner.
+ OriginNotOwner,
+ /// dApp is part of dApp staking but isn't active anymore.
+ NotOperatedDApp,
+ /// Performing locking or staking with 0 amount.
+ ZeroAmount,
+ /// Total locked amount for staker is below minimum threshold.
+ LockedAmountBelowThreshold,
+ /// Cannot add additional locked balance chunks due to size limit.
+ TooManyLockedBalanceChunks,
+ }
+
+ /// General information about dApp staking protocol state.
+ #[pallet::storage]
+ pub type ActiveProtocolState =
+ StorageValue<_, ProtocolState>, ValueQuery>;
+
+ /// Counter for unique dApp identifiers.
+ #[pallet::storage]
+ pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>;
+
+ /// Map of all dApps integrated into dApp staking protocol.
+ #[pallet::storage]
+ pub type IntegratedDApps = CountedStorageMap<
+ _,
+ Blake2_128Concat,
+ T::SmartContract,
+ DAppInfo,
+ OptionQuery,
+ >;
+
+ /// General locked/staked information for each account.
+ #[pallet::storage]
+ pub type Ledger =
+ StorageMap<_, Blake2_128Concat, T::AccountId, AccountLedgerFor, ValueQuery>;
+
+ /// General information about the current era.
+ #[pallet::storage]
+ pub type CurrentEraInfo = StorageValue<_, EraInfo>, ValueQuery>;
+
+ #[pallet::call]
+ impl Pallet {
+ /// Used to enable or disable maintenance mode.
+ /// Can only be called by manager origin.
+ #[pallet::call_index(0)]
+ #[pallet::weight(Weight::zero())]
+ pub fn maintenance_mode(origin: OriginFor, enabled: bool) -> DispatchResult {
+ T::ManagerOrigin::ensure_origin(origin)?;
+ ActiveProtocolState::::mutate(|state| state.maintenance = enabled);
+ Ok(())
+ }
+
+ /// Used to register a new contract for dApp staking.
+ ///
+ /// If successful, smart contract will be assigned a simple, unique numerical identifier.
+ #[pallet::call_index(1)]
+ #[pallet::weight(Weight::zero())]
+ pub fn register(
+ origin: OriginFor,
+ owner: T::AccountId,
+ smart_contract: T::SmartContract,
+ ) -> DispatchResult {
+ Self::ensure_pallet_enabled()?;
+ T::ManagerOrigin::ensure_origin(origin)?;
+
+ ensure!(
+ !IntegratedDApps::::contains_key(&smart_contract),
+ Error::::ContractAlreadyExists,
+ );
+
+ ensure!(
+ IntegratedDApps::::count() < T::MaxNumberOfContracts::get().into(),
+ Error::::ExceededMaxNumberOfContracts
+ );
+
+ let dapp_id = NextDAppId::::get();
+ // MAX value must never be assigned as a dApp Id since it serves as a sentinel value.
+ ensure!(dapp_id < DAppId::MAX, Error::::NewDAppIdUnavailable);
+
+ IntegratedDApps::::insert(
+ &smart_contract,
+ DAppInfo {
+ owner: owner.clone(),
+ id: dapp_id,
+ state: DAppState::Registered,
+ reward_destination: None,
+ },
+ );
+
+ NextDAppId::::put(dapp_id.saturating_add(1));
+
+ Self::deposit_event(Event::::DAppRegistered {
+ owner,
+ smart_contract,
+ dapp_id,
+ });
+
+ Ok(())
+ }
+
+ /// Used to modify the reward destination account for a dApp.
+ ///
+ /// Caller has to be dApp owner.
+ /// If set to `None`, rewards will be deposited to the dApp owner.
+ #[pallet::call_index(2)]
+ #[pallet::weight(Weight::zero())]
+ pub fn set_dapp_reward_destination(
+ origin: OriginFor,
+ smart_contract: T::SmartContract,
+ beneficiary: Option,
+ ) -> DispatchResult {
+ Self::ensure_pallet_enabled()?;
+ let dev_account = ensure_signed(origin)?;
+
+ IntegratedDApps::::try_mutate(
+ &smart_contract,
+ |maybe_dapp_info| -> DispatchResult {
+ let dapp_info = maybe_dapp_info
+ .as_mut()
+ .ok_or(Error::::ContractNotFound)?;
+
+ ensure!(dapp_info.owner == dev_account, Error::::OriginNotOwner);
+
+ dapp_info.reward_destination = beneficiary.clone();
+
+ Ok(())
+ },
+ )?;
+
+ Self::deposit_event(Event::::DAppRewardDestinationUpdated {
+ smart_contract,
+ beneficiary,
+ });
+
+ Ok(())
+ }
+
+ /// Used to change dApp owner.
+ ///
+ /// Can be called by dApp owner or dApp staking manager origin.
+ /// This is useful in two cases:
+ /// 1. when the dApp owner account is compromised, manager can change the owner to a new account
+ /// 2. if project wants to transfer ownership to a new account (DAO, multisig, etc.).
+ #[pallet::call_index(3)]
+ #[pallet::weight(Weight::zero())]
+ pub fn set_dapp_owner(
+ origin: OriginFor,
+ smart_contract: T::SmartContract,
+ new_owner: T::AccountId,
+ ) -> DispatchResult {
+ Self::ensure_pallet_enabled()?;
+ let origin = Self::ensure_signed_or_manager(origin)?;
+
+ IntegratedDApps::::try_mutate(
+ &smart_contract,
+ |maybe_dapp_info| -> DispatchResult {
+ let dapp_info = maybe_dapp_info
+ .as_mut()
+ .ok_or(Error::::ContractNotFound)?;
+
+ // If manager origin, `None`, no need to check if caller is the owner.
+ if let Some(caller) = origin {
+ ensure!(dapp_info.owner == caller, Error::::OriginNotOwner);
+ }
+
+ dapp_info.owner = new_owner.clone();
+
+ Ok(())
+ },
+ )?;
+
+ Self::deposit_event(Event::::DAppOwnerChanged {
+ smart_contract,
+ new_owner,
+ });
+
+ Ok(())
+ }
+
+ /// Unregister dApp from dApp staking protocol, making it ineligible for future rewards.
+ /// This doesn't remove the dApp completely from the system just yet, but it can no longer be used for staking.
+ ///
+ /// Can be called by dApp owner or dApp staking manager origin.
+ #[pallet::call_index(4)]
+ #[pallet::weight(Weight::zero())]
+ pub fn unregister(
+ origin: OriginFor,
+ smart_contract: T::SmartContract,
+ ) -> DispatchResult {
+ Self::ensure_pallet_enabled()?;
+ T::ManagerOrigin::ensure_origin(origin)?;
+
+ let current_era = ActiveProtocolState::::get().era;
+
+ IntegratedDApps::::try_mutate(
+ &smart_contract,
+ |maybe_dapp_info| -> DispatchResult {
+ let dapp_info = maybe_dapp_info
+ .as_mut()
+ .ok_or(Error::::ContractNotFound)?;
+
+ ensure!(
+ dapp_info.state == DAppState::Registered,
+ Error::::NotOperatedDApp
+ );
+
+ dapp_info.state = DAppState::Unregistered(current_era);
+
+ Ok(())
+ },
+ )?;
+
+ // TODO: might require some modification later on, like additional checks to ensure contract can be unregistered.
+
+ Self::deposit_event(Event::::DAppUnregistered {
+ smart_contract,
+ era: current_era,
+ });
+
+ Ok(())
+ }
+
+ /// Locks additional funds into dApp staking.
+ ///
+ /// In case caller account doesn't have sufficient balance to cover the specified amount, everything is locked.
+ /// After adjustment, lock amount must be greater than zero and in total must be equal or greater than the minimum locked amount.
+ ///
+ /// It is possible for call to fail due to caller account already having too many locked balance chunks in storage. To solve this,
+ /// caller should claim pending rewards, before retrying to lock additional funds.
+ #[pallet::call_index(5)]
+ #[pallet::weight(Weight::zero())]
+ pub fn lock(
+ origin: OriginFor,
+ #[pallet::compact] amount: BalanceOf,
+ ) -> DispatchResult {
+ Self::ensure_pallet_enabled()?;
+ let account = ensure_signed(origin)?;
+
+ let state = ActiveProtocolState::::get();
+ let mut ledger = Ledger::::get(&account);
+
+ // Calculate & check amount available for locking
+ let available_balance =
+ T::Currency::free_balance(&account).saturating_sub(ledger.locked_amount());
+ let amount_to_lock = available_balance.min(amount);
+ ensure!(!amount_to_lock.is_zero(), Error::::ZeroAmount);
+
+ // Only lock for the next era onwards.
+ let lock_era = state.era.saturating_add(1);
+ ledger
+ .add_lock_amount(amount_to_lock, lock_era)
+ .map_err(|_| Error::::TooManyLockedBalanceChunks)?;
+ ensure!(
+ ledger.locked_amount() >= T::MinimumLockedAmount::get(),
+ Error::::LockedAmountBelowThreshold
+ );
+
+ Self::update_ledger(&account, ledger);
+ CurrentEraInfo::::mutate(|era_info| {
+ era_info.total_locked.saturating_accrue(amount_to_lock);
+ });
+
+ Self::deposit_event(Event::::Locked {
+ account,
+ amount: amount_to_lock,
+ });
+
+ Ok(())
+ }
+ }
+
+ impl Pallet {
+ /// `Err` if pallet disabled for maintenance, `Ok` otherwise.
+ pub(crate) fn ensure_pallet_enabled() -> Result<(), Error> {
+ if ActiveProtocolState::::get().maintenance {
+ Err(Error::::Disabled)
+ } else {
+ Ok(())
+ }
+ }
+
+ /// Ensure that the origin is either the `ManagerOrigin` or a signed origin.
+ ///
+ /// In case of manager, `Ok(None)` is returned, and if signed origin `Ok(Some(AccountId))` is returned.
+ pub(crate) fn ensure_signed_or_manager(
+ origin: T::RuntimeOrigin,
+ ) -> Result