From be2404cccd9923c41e2f16bfe655f19574f1ae0e Mon Sep 17 00:00:00 2001
From: liamaharon <liam.aharon@hotmail.com>
Date: Thu, 16 Jan 2025 10:26:59 +0400
Subject: [PATCH] Implement `pallet-asset-rewards` (#3926)

Closes #3149

## Description

This PR introduces `pallet-asset-rewards`, which allows accounts to be
rewarded for freezing `fungible` tokens. The motivation for creating
this pallet is to allow incentivising LPs.

See the pallet docs for more info about the pallet.

## Runtime changes

The pallet has been added to
- `asset-hub-rococo`
- `asset-hub-westend`

The `NativeAndAssets` `fungibles` Union did not contain `PoolAssets`, so
it has been renamed `NativeAndNonPoolAssets`

A new `fungibles` Union `NativeAndAllAssets` was created to encompass
all assets and the native token.

## TODO
- [x] Emulation tests
- [x] Fill in Freeze logic (blocked
https://github.com/paritytech/polkadot-sdk/issues/3342) and re-run
benchmarks

---------

Co-authored-by: command-bot <>
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: muharem <ismailov.m.h@gmail.com>
Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>
---
 Cargo.lock                                    |   27 +
 Cargo.toml                                    |    2 +
 .../assets/asset-hub-rococo/Cargo.toml        |    1 +
 .../emulated/common/src/lib.rs                |    2 +
 .../tests/assets/asset-hub-rococo/Cargo.toml  |    1 +
 .../tests/assets/asset-hub-rococo/src/lib.rs  |    3 +-
 .../assets/asset-hub-rococo/src/tests/mod.rs  |    1 +
 .../asset-hub-rococo/src/tests/reward_pool.rs |  114 ++
 .../tests/assets/asset-hub-westend/Cargo.toml |    1 +
 .../tests/assets/asset-hub-westend/src/lib.rs |    8 +-
 .../assets/asset-hub-westend/src/tests/mod.rs |    1 +
 .../src/tests/reward_pool.rs                  |  113 ++
 .../assets/asset-hub-rococo/Cargo.toml        |    5 +
 .../assets/asset-hub-rococo/src/lib.rs        |  142 +-
 .../asset-hub-rococo/src/weights/mod.rs       |    1 +
 .../src/weights/pallet_asset_rewards.rs       |  217 +++
 .../assets/asset-hub-rococo/src/xcm_config.rs |   10 +-
 .../assets/asset-hub-westend/Cargo.toml       |    5 +
 .../assets/asset-hub-westend/src/lib.rs       |  144 +-
 .../asset-hub-westend/src/weights/mod.rs      |    1 +
 .../src/weights/pallet_asset_rewards.rs       |  217 +++
 .../asset-hub-westend/src/xcm_config.rs       |    7 +-
 .../runtimes/assets/common/src/lib.rs         |    7 +-
 polkadot/runtime/rococo/src/xcm_config.rs     |   20 +-
 polkadot/runtime/westend/src/xcm_config.rs    |    5 +-
 prdoc/pr_3926.prdoc                           |   30 +
 substrate/bin/node/runtime/src/lib.rs         |   77 +-
 substrate/frame/asset-rewards/Cargo.toml      |   71 +
 .../frame/asset-rewards/src/benchmarking.rs   |  355 ++++
 substrate/frame/asset-rewards/src/lib.rs      |  905 ++++++++++
 substrate/frame/asset-rewards/src/mock.rs     |  221 +++
 substrate/frame/asset-rewards/src/tests.rs    | 1457 +++++++++++++++++
 substrate/frame/asset-rewards/src/weights.rs  |  368 +++++
 substrate/frame/support/src/traits.rs         |    5 +-
 substrate/frame/support/src/traits/storage.rs |   12 +
 umbrella/Cargo.toml                           |   10 +-
 umbrella/src/lib.rs                           |    4 +
 37 files changed, 4517 insertions(+), 53 deletions(-)
 create mode 100644 cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/reward_pool.rs
 create mode 100644 cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/reward_pool.rs
 create mode 100644 cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_asset_rewards.rs
 create mode 100644 cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_asset_rewards.rs
 create mode 100644 prdoc/pr_3926.prdoc
 create mode 100644 substrate/frame/asset-rewards/Cargo.toml
 create mode 100644 substrate/frame/asset-rewards/src/benchmarking.rs
 create mode 100644 substrate/frame/asset-rewards/src/lib.rs
 create mode 100644 substrate/frame/asset-rewards/src/mock.rs
 create mode 100644 substrate/frame/asset-rewards/src/tests.rs
 create mode 100644 substrate/frame/asset-rewards/src/weights.rs

diff --git a/Cargo.lock b/Cargo.lock
index 0d71a770d38..6eba7e65109 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -910,6 +910,7 @@ dependencies = [
  "cumulus-primitives-core 0.7.0",
  "emulated-integration-tests-common",
  "frame-support 28.0.0",
+ "pallet-asset-rewards",
  "parachains-common 7.0.0",
  "rococo-emulated-chain",
  "sp-core 28.0.0",
@@ -928,6 +929,7 @@ dependencies = [
  "emulated-integration-tests-common",
  "frame-support 28.0.0",
  "pallet-asset-conversion 10.0.0",
+ "pallet-asset-rewards",
  "pallet-assets 29.1.0",
  "pallet-balances 28.0.0",
  "pallet-message-queue 31.0.0",
@@ -978,6 +980,7 @@ dependencies = [
  "pallet-asset-conversion 10.0.0",
  "pallet-asset-conversion-ops 0.1.0",
  "pallet-asset-conversion-tx-payment 10.0.0",
+ "pallet-asset-rewards",
  "pallet-assets 29.1.0",
  "pallet-assets-freezer 0.1.0",
  "pallet-aura 27.0.0",
@@ -1063,6 +1066,7 @@ dependencies = [
  "frame-support 28.0.0",
  "frame-system 28.0.0",
  "pallet-asset-conversion 10.0.0",
+ "pallet-asset-rewards",
  "pallet-asset-tx-payment 28.0.0",
  "pallet-assets 29.1.0",
  "pallet-balances 28.0.0",
@@ -1114,6 +1118,7 @@ dependencies = [
  "pallet-asset-conversion 10.0.0",
  "pallet-asset-conversion-ops 0.1.0",
  "pallet-asset-conversion-tx-payment 10.0.0",
+ "pallet-asset-rewards",
  "pallet-assets 29.1.0",
  "pallet-assets-freezer 0.1.0",
  "pallet-aura 27.0.0",
@@ -12036,6 +12041,27 @@ dependencies = [
  "sp-runtime 39.0.2",
 ]
 
+[[package]]
+name = "pallet-asset-rewards"
+version = "0.1.0"
+dependencies = [
+ "frame-benchmarking 28.0.0",
+ "frame-support 28.0.0",
+ "frame-system 28.0.0",
+ "pallet-assets 29.1.0",
+ "pallet-assets-freezer 0.1.0",
+ "pallet-balances 28.0.0",
+ "parity-scale-codec",
+ "primitive-types 0.13.1",
+ "scale-info",
+ "sp-api 26.0.0",
+ "sp-arithmetic 23.0.0",
+ "sp-core 28.0.0",
+ "sp-io 30.0.0",
+ "sp-runtime 31.0.1",
+ "sp-std 14.0.0",
+]
+
 [[package]]
 name = "pallet-asset-tx-payment"
 version = "28.0.0"
@@ -18715,6 +18741,7 @@ dependencies = [
  "pallet-asset-conversion-ops 0.1.0",
  "pallet-asset-conversion-tx-payment 10.0.0",
  "pallet-asset-rate 7.0.0",
+ "pallet-asset-rewards",
  "pallet-asset-tx-payment 28.0.0",
  "pallet-assets 29.1.0",
  "pallet-assets-freezer 0.1.0",
diff --git a/Cargo.toml b/Cargo.toml
index eb99b80e16f..509775fe99e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -315,6 +315,7 @@ members = [
 	"substrate/frame/asset-conversion",
 	"substrate/frame/asset-conversion/ops",
 	"substrate/frame/asset-rate",
+	"substrate/frame/asset-rewards",
 	"substrate/frame/assets",
 	"substrate/frame/assets-freezer",
 	"substrate/frame/atomic-swap",
@@ -893,6 +894,7 @@ pallet-asset-conversion = { path = "substrate/frame/asset-conversion", default-f
 pallet-asset-conversion-ops = { path = "substrate/frame/asset-conversion/ops", default-features = false }
 pallet-asset-conversion-tx-payment = { path = "substrate/frame/transaction-payment/asset-conversion-tx-payment", default-features = false }
 pallet-asset-rate = { path = "substrate/frame/asset-rate", default-features = false }
+pallet-asset-rewards = { path = "substrate/frame/asset-rewards", default-features = false }
 pallet-asset-tx-payment = { path = "substrate/frame/transaction-payment/asset-tx-payment", default-features = false }
 pallet-assets = { path = "substrate/frame/assets", default-features = false }
 pallet-assets-freezer = { path = "substrate/frame/assets-freezer", default-features = false }
diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-rococo/Cargo.toml b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-rococo/Cargo.toml
index a164a8197f7..c6a8baeff3b 100644
--- a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-rococo/Cargo.toml
+++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-rococo/Cargo.toml
@@ -14,6 +14,7 @@ workspace = true
 
 # Substrate
 frame-support = { workspace = true }
+pallet-asset-rewards = { workspace = true }
 sp-core = { workspace = true }
 sp-keyring = { workspace = true }
 
diff --git a/cumulus/parachains/integration-tests/emulated/common/src/lib.rs b/cumulus/parachains/integration-tests/emulated/common/src/lib.rs
index e2757f8b9a3..f5466a63f1f 100644
--- a/cumulus/parachains/integration-tests/emulated/common/src/lib.rs
+++ b/cumulus/parachains/integration-tests/emulated/common/src/lib.rs
@@ -58,6 +58,8 @@ pub const USDT_ID: u32 = 1984;
 
 pub const PENPAL_A_ID: u32 = 2000;
 pub const PENPAL_B_ID: u32 = 2001;
+pub const ASSET_HUB_ROCOCO_ID: u32 = 1000;
+pub const ASSET_HUB_WESTEND_ID: u32 = 1000;
 pub const ASSETS_PALLET_ID: u8 = 50;
 
 parameter_types! {
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/Cargo.toml b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/Cargo.toml
index 9e8b8f2a52d..b53edb39c73 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/Cargo.toml
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/Cargo.toml
@@ -17,6 +17,7 @@ codec = { workspace = true }
 # Substrate
 frame-support = { workspace = true }
 pallet-asset-conversion = { workspace = true }
+pallet-asset-rewards = { workspace = true }
 pallet-assets = { workspace = true }
 pallet-balances = { workspace = true }
 pallet-message-queue = { workspace = true }
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/lib.rs
index f3a1b3f5bfa..513ca278a31 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/lib.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/lib.rs
@@ -76,10 +76,11 @@ mod imports {
 			genesis::ED as ROCOCO_ED,
 			rococo_runtime::{
 				governance as rococo_governance,
+				governance::pallet_custom_origins::Origin::Treasurer,
 				xcm_config::{
 					UniversalLocation as RococoUniversalLocation, XcmConfig as RococoXcmConfig,
 				},
-				OriginCaller as RococoOriginCaller,
+				Dmp, OriginCaller as RococoOriginCaller,
 			},
 			RococoRelayPallet as RococoPallet,
 		},
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/mod.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/mod.rs
index 88fa379c407..75714acb07c 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/mod.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/mod.rs
@@ -16,6 +16,7 @@
 mod claim_assets;
 mod hybrid_transfers;
 mod reserve_transfer;
+mod reward_pool;
 mod send;
 mod set_xcm_versions;
 mod swap;
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/reward_pool.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/reward_pool.rs
new file mode 100644
index 00000000000..2f3ee536a7b
--- /dev/null
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/reward_pool.rs
@@ -0,0 +1,114 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// 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.
+
+use crate::imports::*;
+use codec::Encode;
+use frame_support::{assert_ok, sp_runtime::traits::Dispatchable, traits::schedule::DispatchTime};
+use xcm_executor::traits::ConvertLocation;
+
+#[test]
+fn treasury_creates_asset_reward_pool() {
+	AssetHubRococo::execute_with(|| {
+		type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
+		type Balances = <AssetHubRococo as AssetHubRococoPallet>::Balances;
+
+		let treasurer =
+			Location::new(1, [Plurality { id: BodyId::Treasury, part: BodyPart::Voice }]);
+		let treasurer_account =
+			ahr_xcm_config::LocationToAccountId::convert_location(&treasurer).unwrap();
+
+		assert_ok!(Balances::force_set_balance(
+			<AssetHubRococo as Chain>::RuntimeOrigin::root(),
+			treasurer_account.clone().into(),
+			ASSET_HUB_ROCOCO_ED * 100_000,
+		));
+
+		let events = AssetHubRococo::events();
+		match events.iter().last() {
+			Some(RuntimeEvent::Balances(pallet_balances::Event::BalanceSet { who, .. })) =>
+				assert_eq!(*who, treasurer_account),
+			_ => panic!("Expected Balances::BalanceSet event"),
+		}
+	});
+
+	Rococo::execute_with(|| {
+		type AssetHubRococoRuntimeCall = <AssetHubRococo as Chain>::RuntimeCall;
+		type AssetHubRococoRuntime = <AssetHubRococo as Chain>::Runtime;
+		type RococoRuntimeCall = <Rococo as Chain>::RuntimeCall;
+		type RococoRuntime = <Rococo as Chain>::Runtime;
+		type RococoRuntimeEvent = <Rococo as Chain>::RuntimeEvent;
+		type RococoRuntimeOrigin = <Rococo as Chain>::RuntimeOrigin;
+
+		Dmp::make_parachain_reachable(AssetHubRococo::para_id());
+
+		let staked_asset_id = bx!(RelayLocation::get());
+		let reward_asset_id = bx!(RelayLocation::get());
+
+		let reward_rate_per_block = 1_000_000_000;
+		let lifetime = 1_000_000_000;
+		let admin = None;
+
+		let create_pool_call =
+			RococoRuntimeCall::XcmPallet(pallet_xcm::Call::<RococoRuntime>::send {
+				dest: bx!(VersionedLocation::V4(
+					xcm::v4::Junction::Parachain(AssetHubRococo::para_id().into()).into()
+				)),
+				message: bx!(VersionedXcm::V5(Xcm(vec![
+					UnpaidExecution { weight_limit: Unlimited, check_origin: None },
+					Transact {
+						origin_kind: OriginKind::SovereignAccount,
+						fallback_max_weight: None,
+						call: AssetHubRococoRuntimeCall::AssetRewards(
+							pallet_asset_rewards::Call::<AssetHubRococoRuntime>::create_pool {
+								staked_asset_id,
+								reward_asset_id,
+								reward_rate_per_block,
+								expiry: DispatchTime::After(lifetime),
+								admin
+							}
+						)
+						.encode()
+						.into(),
+					}
+				]))),
+			});
+
+		let treasury_origin: RococoRuntimeOrigin = Treasurer.into();
+		assert_ok!(create_pool_call.dispatch(treasury_origin));
+
+		assert_expected_events!(
+			Rococo,
+			vec![
+				RococoRuntimeEvent::XcmPallet(pallet_xcm::Event::Sent { .. }) => {},
+			]
+		);
+	});
+
+	AssetHubRococo::execute_with(|| {
+		type Runtime = <AssetHubRococo as Chain>::Runtime;
+		type RuntimeEvent = <AssetHubRococo as Chain>::RuntimeEvent;
+
+		assert_eq!(1, pallet_asset_rewards::Pools::<Runtime>::iter().count());
+
+		let events = AssetHubRococo::events();
+		match events.iter().last() {
+			Some(RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed {
+				success: true,
+				..
+			})) => (),
+			_ => panic!("Expected MessageQueue::Processed event"),
+		}
+	});
+}
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml
index 5cd00c239e6..ef68a53c3b1 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml
@@ -19,6 +19,7 @@ frame-metadata-hash-extension = { workspace = true, default-features = true }
 frame-support = { workspace = true }
 frame-system = { workspace = true }
 pallet-asset-conversion = { workspace = true }
+pallet-asset-rewards = { workspace = true }
 pallet-asset-tx-payment = { workspace = true }
 pallet-assets = { workspace = true }
 pallet-balances = { workspace = true }
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs
index 36630e2d222..68dc87250f7 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs
@@ -79,8 +79,12 @@ mod imports {
 		},
 		westend_emulated_chain::{
 			genesis::ED as WESTEND_ED,
-			westend_runtime::xcm_config::{
-				UniversalLocation as WestendUniversalLocation, XcmConfig as WestendXcmConfig,
+			westend_runtime::{
+				governance::pallet_custom_origins::Origin::Treasurer,
+				xcm_config::{
+					UniversalLocation as WestendUniversalLocation, XcmConfig as WestendXcmConfig,
+				},
+				Dmp,
 			},
 			WestendRelayPallet as WestendPallet,
 		},
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs
index 0dfe7a85f4c..576c44fc542 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs
@@ -17,6 +17,7 @@ mod claim_assets;
 mod fellowship_treasury;
 mod hybrid_transfers;
 mod reserve_transfer;
+mod reward_pool;
 mod send;
 mod set_asset_claimer;
 mod set_xcm_versions;
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/reward_pool.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/reward_pool.rs
new file mode 100644
index 00000000000..4df51abcace
--- /dev/null
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/reward_pool.rs
@@ -0,0 +1,113 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// 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.
+
+use crate::imports::*;
+use codec::Encode;
+use frame_support::{assert_ok, sp_runtime::traits::Dispatchable, traits::schedule::DispatchTime};
+use xcm_executor::traits::ConvertLocation;
+
+#[test]
+fn treasury_creates_asset_reward_pool() {
+	AssetHubWestend::execute_with(|| {
+		type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
+		type Balances = <AssetHubWestend as AssetHubWestendPallet>::Balances;
+
+		let treasurer =
+			Location::new(1, [Plurality { id: BodyId::Treasury, part: BodyPart::Voice }]);
+		let treasurer_account =
+			ahw_xcm_config::LocationToAccountId::convert_location(&treasurer).unwrap();
+
+		assert_ok!(Balances::force_set_balance(
+			<AssetHubWestend as Chain>::RuntimeOrigin::root(),
+			treasurer_account.clone().into(),
+			ASSET_HUB_WESTEND_ED * 100_000,
+		));
+
+		let events = AssetHubWestend::events();
+		match events.iter().last() {
+			Some(RuntimeEvent::Balances(pallet_balances::Event::BalanceSet { who, .. })) =>
+				assert_eq!(*who, treasurer_account),
+			_ => panic!("Expected Balances::BalanceSet event"),
+		}
+	});
+	Westend::execute_with(|| {
+		type AssetHubWestendRuntimeCall = <AssetHubWestend as Chain>::RuntimeCall;
+		type AssetHubWestendRuntime = <AssetHubWestend as Chain>::Runtime;
+		type WestendRuntimeCall = <Westend as Chain>::RuntimeCall;
+		type WestendRuntime = <Westend as Chain>::Runtime;
+		type WestendRuntimeEvent = <Westend as Chain>::RuntimeEvent;
+		type WestendRuntimeOrigin = <Westend as Chain>::RuntimeOrigin;
+
+		Dmp::make_parachain_reachable(AssetHubWestend::para_id());
+
+		let staked_asset_id = bx!(RelayLocation::get());
+		let reward_asset_id = bx!(RelayLocation::get());
+
+		let reward_rate_per_block = 1_000_000_000;
+		let lifetime = 1_000_000_000;
+		let admin = None;
+
+		let create_pool_call =
+			WestendRuntimeCall::XcmPallet(pallet_xcm::Call::<WestendRuntime>::send {
+				dest: bx!(VersionedLocation::V4(
+					xcm::v4::Junction::Parachain(AssetHubWestend::para_id().into()).into()
+				)),
+				message: bx!(VersionedXcm::V5(Xcm(vec![
+					UnpaidExecution { weight_limit: Unlimited, check_origin: None },
+					Transact {
+						origin_kind: OriginKind::SovereignAccount,
+						fallback_max_weight: None,
+						call: AssetHubWestendRuntimeCall::AssetRewards(
+							pallet_asset_rewards::Call::<AssetHubWestendRuntime>::create_pool {
+								staked_asset_id,
+								reward_asset_id,
+								reward_rate_per_block,
+								expiry: DispatchTime::After(lifetime),
+								admin
+							}
+						)
+						.encode()
+						.into(),
+					}
+				]))),
+			});
+
+		let treasury_origin: WestendRuntimeOrigin = Treasurer.into();
+		assert_ok!(create_pool_call.dispatch(treasury_origin));
+
+		assert_expected_events!(
+			Westend,
+			vec![
+				WestendRuntimeEvent::XcmPallet(pallet_xcm::Event::Sent { .. }) => {},
+			]
+		);
+	});
+
+	AssetHubWestend::execute_with(|| {
+		type Runtime = <AssetHubWestend as Chain>::Runtime;
+		type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
+
+		assert_eq!(1, pallet_asset_rewards::Pools::<Runtime>::iter().count());
+
+		let events = AssetHubWestend::events();
+		match events.iter().last() {
+			Some(RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed {
+				success: true,
+				..
+			})) => (),
+			_ => panic!("Expected MessageQueue::Processed event"),
+		}
+	});
+}
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml
index abe59a8439a..d612dd03c24 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml
@@ -30,6 +30,7 @@ frame-try-runtime = { optional = true, workspace = true }
 pallet-asset-conversion = { workspace = true }
 pallet-asset-conversion-ops = { workspace = true }
 pallet-asset-conversion-tx-payment = { workspace = true }
+pallet-asset-rewards = { workspace = true }
 pallet-assets = { workspace = true }
 pallet-assets-freezer = { workspace = true }
 pallet-aura = { workspace = true }
@@ -61,6 +62,7 @@ sp-storage = { workspace = true }
 sp-transaction-pool = { workspace = true }
 sp-version = { workspace = true }
 sp-weights = { workspace = true }
+
 # num-traits feature needed for dex integer sq root:
 primitive-types = { features = ["codec", "num-traits", "scale-info"], workspace = true }
 
@@ -123,6 +125,7 @@ runtime-benchmarks = [
 	"pallet-asset-conversion-ops/runtime-benchmarks",
 	"pallet-asset-conversion-tx-payment/runtime-benchmarks",
 	"pallet-asset-conversion/runtime-benchmarks",
+	"pallet-asset-rewards/runtime-benchmarks",
 	"pallet-assets-freezer/runtime-benchmarks",
 	"pallet-assets/runtime-benchmarks",
 	"pallet-balances/runtime-benchmarks",
@@ -162,6 +165,7 @@ try-runtime = [
 	"pallet-asset-conversion-ops/try-runtime",
 	"pallet-asset-conversion-tx-payment/try-runtime",
 	"pallet-asset-conversion/try-runtime",
+	"pallet-asset-rewards/try-runtime",
 	"pallet-assets-freezer/try-runtime",
 	"pallet-assets/try-runtime",
 	"pallet-aura/try-runtime",
@@ -212,6 +216,7 @@ std = [
 	"pallet-asset-conversion-ops/std",
 	"pallet-asset-conversion-tx-payment/std",
 	"pallet-asset-conversion/std",
+	"pallet-asset-rewards/std",
 	"pallet-assets-freezer/std",
 	"pallet-assets/std",
 	"pallet-aura/std",
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
index db9a8201ebb..43b7bf0ba11 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
@@ -35,7 +35,7 @@ use assets_common::{
 	foreign_creators::ForeignCreators,
 	local_and_foreign_assets::{LocalFromLeft, TargetFromLeft},
 	matching::{FromNetwork, FromSiblingParachain},
-	AssetIdForTrustBackedAssetsConvert,
+	AssetIdForPoolAssets, AssetIdForPoolAssetsConvert, AssetIdForTrustBackedAssetsConvert,
 };
 use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases;
 use cumulus_primitives_core::{AggregateMessageOrigin, ClaimQueueOffset, CoreSelector};
@@ -61,9 +61,9 @@ use frame_support::{
 	genesis_builder_helper::{build_state, get_preset},
 	ord_parameter_types, parameter_types,
 	traits::{
-		fungible, fungibles, tokens::imbalance::ResolveAssetTo, AsEnsureOriginWithArg, ConstBool,
-		ConstU128, ConstU32, ConstU64, ConstU8, EitherOfDiverse, Equals, InstanceFilter,
-		TransformOrigin,
+		fungible, fungible::HoldConsideration, fungibles, tokens::imbalance::ResolveAssetTo,
+		AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, ConstU64, ConstU8,
+		ConstantStoragePrice, EitherOfDiverse, Equals, InstanceFilter, TransformOrigin,
 	},
 	weights::{ConstantMultiplier, Weight, WeightToFee as _},
 	BoundedVec, PalletId,
@@ -84,8 +84,8 @@ use sp_runtime::{Perbill, RuntimeDebug};
 use testnet_parachains_constants::rococo::{consensus::*, currency::*, fee::WeightToFee, time::*};
 use xcm_config::{
 	ForeignAssetsConvertedConcreteId, GovernanceLocation, LocationToAccountId,
-	PoolAssetsConvertedConcreteId, TokenLocation, TrustBackedAssetsConvertedConcreteId,
-	TrustBackedAssetsPalletLocation,
+	PoolAssetsConvertedConcreteId, PoolAssetsPalletLocation, TokenLocation,
+	TrustBackedAssetsConvertedConcreteId, TrustBackedAssetsPalletLocation,
 };
 
 #[cfg(test)]
@@ -111,6 +111,9 @@ use xcm_runtime_apis::{
 	fees::Error as XcmPaymentApiError,
 };
 
+#[cfg(feature = "runtime-benchmarks")]
+use frame_support::traits::PalletInfoAccess;
+
 use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight};
 
 impl_opaque_keys! {
@@ -217,8 +220,8 @@ impl pallet_balances::Config for Runtime {
 	type ReserveIdentifier = [u8; 8];
 	type RuntimeHoldReason = RuntimeHoldReason;
 	type RuntimeFreezeReason = RuntimeFreezeReason;
-	type FreezeIdentifier = ();
-	type MaxFreezes = ConstU32<0>;
+	type FreezeIdentifier = RuntimeFreezeReason;
+	type MaxFreezes = ConstU32<50>;
 	type DoneSlashHandler = ();
 }
 
@@ -302,7 +305,7 @@ impl pallet_assets::Config<PoolAssetsInstance> for Runtime {
 	type RuntimeEvent = RuntimeEvent;
 	type Balance = Balance;
 	type RemoveItemsLimit = ConstU32<1000>;
-	type AssetId = u32;
+	type AssetId = AssetIdForPoolAssets;
 	type AssetIdParameter = u32;
 	type Currency = Balances;
 	type CreateOrigin =
@@ -343,8 +346,21 @@ pub type LocalAndForeignAssets = fungibles::UnionOf<
 	AccountId,
 >;
 
-/// Union fungibles implementation for [`LocalAndForeignAssets`] and `Balances`.
-pub type NativeAndAssets = fungible::UnionOf<
+/// Union fungibles implementation for `AssetsFreezer` and `ForeignAssetsFreezer`.
+pub type LocalAndForeignAssetsFreezer = fungibles::UnionOf<
+	AssetsFreezer,
+	ForeignAssetsFreezer,
+	LocalFromLeft<
+		AssetIdForTrustBackedAssetsConvert<TrustBackedAssetsPalletLocation, xcm::v5::Location>,
+		AssetIdForTrustBackedAssets,
+		xcm::v5::Location,
+	>,
+	xcm::v5::Location,
+	AccountId,
+>;
+
+/// Union fungibles implementation for [`LocalAndForeignAssets`] and [`Balances`].
+pub type NativeAndNonPoolAssets = fungible::UnionOf<
 	Balances,
 	LocalAndForeignAssets,
 	TargetFromLeft<TokenLocation, xcm::v5::Location>,
@@ -352,6 +368,45 @@ pub type NativeAndAssets = fungible::UnionOf<
 	AccountId,
 >;
 
+/// Union fungibles implementation for [`LocalAndForeignAssetsFreezer`] and [`Balances`].
+pub type NativeAndNonPoolAssetsFreezer = fungible::UnionOf<
+	Balances,
+	LocalAndForeignAssetsFreezer,
+	TargetFromLeft<TokenLocation, xcm::v5::Location>,
+	xcm::v5::Location,
+	AccountId,
+>;
+
+/// Union fungibles implementation for [`PoolAssets`] and [`NativeAndNonPoolAssets`].
+///
+/// NOTE: Should be kept updated to include ALL balances and assets in the runtime.
+pub type NativeAndAllAssets = fungibles::UnionOf<
+	PoolAssets,
+	NativeAndNonPoolAssets,
+	LocalFromLeft<
+		AssetIdForPoolAssetsConvert<PoolAssetsPalletLocation, xcm::v5::Location>,
+		AssetIdForPoolAssets,
+		xcm::v5::Location,
+	>,
+	xcm::v5::Location,
+	AccountId,
+>;
+
+/// Union fungibles implementation for [`PoolAssetsFreezer`] and [`NativeAndNonPoolAssetsFreezer`].
+///
+/// NOTE: Should be kept updated to include ALL balances and assets in the runtime.
+pub type NativeAndAllAssetsFreezer = fungibles::UnionOf<
+	PoolAssetsFreezer,
+	NativeAndNonPoolAssetsFreezer,
+	LocalFromLeft<
+		AssetIdForPoolAssetsConvert<PoolAssetsPalletLocation, xcm::v5::Location>,
+		AssetIdForPoolAssets,
+		xcm::v5::Location,
+	>,
+	xcm::v5::Location,
+	AccountId,
+>;
+
 pub type PoolIdToAccountId = pallet_asset_conversion::AccountIdConverter<
 	AssetConversionPalletId,
 	(xcm::v5::Location, xcm::v5::Location),
@@ -362,7 +417,7 @@ impl pallet_asset_conversion::Config for Runtime {
 	type Balance = Balance;
 	type HigherPrecisionBalance = sp_core::U256;
 	type AssetKind = xcm::v5::Location;
-	type Assets = NativeAndAssets;
+	type Assets = NativeAndNonPoolAssets;
 	type PoolId = (Self::AssetKind, Self::AssetKind);
 	type PoolLocator = pallet_asset_conversion::WithFirstAsset<
 		TokenLocation,
@@ -823,9 +878,9 @@ impl pallet_asset_conversion_tx_payment::Config for Runtime {
 	type AssetId = xcm::v5::Location;
 	type OnChargeAssetTransaction = SwapAssetAdapter<
 		TokenLocation,
-		NativeAndAssets,
+		NativeAndNonPoolAssets,
 		AssetConversion,
-		ResolveAssetTo<StakingPot, NativeAndAssets>,
+		ResolveAssetTo<StakingPot, NativeAndNonPoolAssets>,
 	>;
 	type WeightInfo = weights::pallet_asset_conversion_tx_payment::WeightInfo<Runtime>;
 	#[cfg(feature = "runtime-benchmarks")]
@@ -953,6 +1008,55 @@ impl pallet_xcm_bridge_hub_router::Config<ToWestendXcmRouterInstance> for Runtim
 	type FeeAsset = xcm_config::bridging::XcmBridgeHubRouterFeeAssetId;
 }
 
+#[cfg(feature = "runtime-benchmarks")]
+pub struct PalletAssetRewardsBenchmarkHelper;
+
+#[cfg(feature = "runtime-benchmarks")]
+impl pallet_asset_rewards::benchmarking::BenchmarkHelper<xcm::v5::Location>
+	for PalletAssetRewardsBenchmarkHelper
+{
+	fn staked_asset() -> Location {
+		Location::new(
+			0,
+			[PalletInstance(<Assets as PalletInfoAccess>::index() as u8), GeneralIndex(100)],
+		)
+	}
+	fn reward_asset() -> Location {
+		Location::new(
+			0,
+			[PalletInstance(<Assets as PalletInfoAccess>::index() as u8), GeneralIndex(101)],
+		)
+	}
+}
+
+parameter_types! {
+	pub const AssetRewardsPalletId: PalletId = PalletId(*b"py/astrd");
+	pub const RewardsPoolCreationHoldReason: RuntimeHoldReason =
+		RuntimeHoldReason::AssetRewards(pallet_asset_rewards::HoldReason::PoolCreation);
+	// 1 item, 135 bytes into the storage on pool creation.
+	pub const StakePoolCreationDeposit: Balance = deposit(1, 135);
+}
+
+impl pallet_asset_rewards::Config for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type PalletId = AssetRewardsPalletId;
+	type Balance = Balance;
+	type Assets = NativeAndAllAssets;
+	type AssetsFreezer = NativeAndAllAssetsFreezer;
+	type AssetId = xcm::v5::Location;
+	type CreatePoolOrigin = EnsureSigned<AccountId>;
+	type RuntimeFreezeReason = RuntimeFreezeReason;
+	type Consideration = HoldConsideration<
+		AccountId,
+		Balances,
+		RewardsPoolCreationHoldReason,
+		ConstantStoragePrice<StakePoolCreationDeposit, Balance>,
+	>;
+	type WeightInfo = weights::pallet_asset_rewards::WeightInfo<Runtime>;
+	#[cfg(feature = "runtime-benchmarks")]
+	type BenchmarkHelper = PalletAssetRewardsBenchmarkHelper;
+}
+
 // Create the runtime by composing the FRAME pallets that were previously configured.
 construct_runtime!(
 	pub enum Runtime
@@ -998,10 +1102,13 @@ construct_runtime!(
 		NftFractionalization: pallet_nft_fractionalization = 54,
 		PoolAssets: pallet_assets::<Instance3> = 55,
 		AssetConversion: pallet_asset_conversion = 56,
+
 		AssetsFreezer: pallet_assets_freezer::<Instance1> = 57,
 		ForeignAssetsFreezer: pallet_assets_freezer::<Instance2> = 58,
 		PoolAssetsFreezer: pallet_assets_freezer::<Instance3> = 59,
 
+		AssetRewards: pallet_asset_rewards = 60,
+
 		// TODO: the pallet instance should be removed once all pools have migrated
 		// to the new account IDs.
 		AssetConversionMigration: pallet_asset_conversion_ops = 200,
@@ -1193,6 +1300,7 @@ mod benches {
 		[pallet_assets, Foreign]
 		[pallet_assets, Pool]
 		[pallet_asset_conversion, AssetConversion]
+		[pallet_asset_rewards, AssetRewards]
 		[pallet_asset_conversion_tx_payment, AssetTxPayment]
 		[pallet_balances, Balances]
 		[pallet_message_queue, MessageQueue]
@@ -1503,6 +1611,12 @@ impl_runtime_apis! {
 		}
 	}
 
+	impl pallet_asset_rewards::AssetRewards<Block, Balance> for Runtime {
+		fn pool_creation_cost() -> Balance {
+			StakePoolCreationDeposit::get()
+		}
+	}
+
 	impl cumulus_primitives_core::GetCoreSelectorApi<Block> for Runtime {
 		fn core_selector() -> (CoreSelector, ClaimQueueOffset) {
 			ParachainSystem::core_selector()
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/mod.rs
index ae78a56d8b3..6893766ac72 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/mod.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/mod.rs
@@ -24,6 +24,7 @@ pub mod frame_system_extensions;
 pub mod pallet_asset_conversion;
 pub mod pallet_asset_conversion_ops;
 pub mod pallet_asset_conversion_tx_payment;
+pub mod pallet_asset_rewards;
 pub mod pallet_assets_foreign;
 pub mod pallet_assets_local;
 pub mod pallet_assets_pool;
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_asset_rewards.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_asset_rewards.rs
new file mode 100644
index 00000000000..218c93c5103
--- /dev/null
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_asset_rewards.rs
@@ -0,0 +1,217 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Cumulus.
+
+// Cumulus 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.
+
+// Cumulus 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 Cumulus.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Autogenerated weights for `pallet_asset_rewards`
+//!
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0
+//! DATE: 2025-01-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! WORST CASE MAP SIZE: `1000000`
+//! HOSTNAME: `runner-ys-ssygq-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
+//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("asset-hub-rococo-dev")`, DB CACHE: 1024
+
+// Executed Command:
+// target/production/polkadot-parachain
+// benchmark
+// pallet
+// --steps=50
+// --repeat=20
+// --extrinsic=*
+// --wasm-execution=compiled
+// --heap-pages=4096
+// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json
+// --pallet=pallet_asset_rewards
+// --chain=asset-hub-rococo-dev
+// --header=./cumulus/file_header.txt
+// --output=./cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/
+
+#![cfg_attr(rustfmt, rustfmt_skip)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+#![allow(missing_docs)]
+
+use frame_support::{traits::Get, weights::Weight};
+use core::marker::PhantomData;
+
+/// Weight functions for `pallet_asset_rewards`.
+pub struct WeightInfo<T>(PhantomData<T>);
+impl<T: frame_system::Config> pallet_asset_rewards::WeightInfo for WeightInfo<T> {
+	/// Storage: `Assets::Asset` (r:2 w:0)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::NextPoolId` (r:1 w:1)
+	/// Proof: `AssetRewards::NextPoolId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `Balances::Holds` (r:1 w:1)
+	/// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolCost` (r:0 w:1)
+	/// Proof: `AssetRewards::PoolCost` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::Pools` (r:0 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	fn create_pool() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `358`
+		//  Estimated: `6360`
+		// Minimum execution time: 65_882_000 picoseconds.
+		Weight::from_parts(67_073_000, 0)
+			.saturating_add(Weight::from_parts(0, 6360))
+			.saturating_add(T::DbWeight::get().reads(5))
+			.saturating_add(T::DbWeight::get().writes(5))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
+	/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(87), added: 2562, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:1 w:0)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	fn stake() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `872`
+		//  Estimated: `4809`
+		// Minimum execution time: 56_950_000 picoseconds.
+		Weight::from_parts(58_088_000, 0)
+			.saturating_add(Weight::from_parts(0, 4809))
+			.saturating_add(T::DbWeight::get().reads(5))
+			.saturating_add(T::DbWeight::get().writes(4))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
+	/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(87), added: 2562, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:1 w:0)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	fn unstake() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `872`
+		//  Estimated: `4809`
+		// Minimum execution time: 59_509_000 picoseconds.
+		Weight::from_parts(61_064_000, 0)
+			.saturating_add(Weight::from_parts(0, 4809))
+			.saturating_add(T::DbWeight::get().reads(5))
+			.saturating_add(T::DbWeight::get().writes(4))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:0)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	fn harvest_rewards() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `1072`
+		//  Estimated: `6208`
+		// Minimum execution time: 80_685_000 picoseconds.
+		Weight::from_parts(83_505_000, 0)
+			.saturating_add(Weight::from_parts(0, 6208))
+			.saturating_add(T::DbWeight::get().reads(6))
+			.saturating_add(T::DbWeight::get().writes(4))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	fn set_pool_reward_rate_per_block() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `318`
+		//  Estimated: `4809`
+		// Minimum execution time: 17_032_000 picoseconds.
+		Weight::from_parts(17_628_000, 0)
+			.saturating_add(Weight::from_parts(0, 4809))
+			.saturating_add(T::DbWeight::get().reads(1))
+			.saturating_add(T::DbWeight::get().writes(1))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	fn set_pool_admin() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `318`
+		//  Estimated: `4809`
+		// Minimum execution time: 15_290_000 picoseconds.
+		Weight::from_parts(16_212_000, 0)
+			.saturating_add(Weight::from_parts(0, 4809))
+			.saturating_add(T::DbWeight::get().reads(1))
+			.saturating_add(T::DbWeight::get().writes(1))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	fn set_pool_expiry_block() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `318`
+		//  Estimated: `4809`
+		// Minimum execution time: 17_721_000 picoseconds.
+		Weight::from_parts(18_603_000, 0)
+			.saturating_add(Weight::from_parts(0, 4809))
+			.saturating_add(T::DbWeight::get().reads(1))
+			.saturating_add(T::DbWeight::get().writes(1))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:0)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	fn deposit_reward_tokens() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `747`
+		//  Estimated: `6208`
+		// Minimum execution time: 67_754_000 picoseconds.
+		Weight::from_parts(69_428_000, 0)
+			.saturating_add(Weight::from_parts(0, 6208))
+			.saturating_add(T::DbWeight::get().reads(6))
+			.saturating_add(T::DbWeight::get().writes(4))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:0)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:2 w:2)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolCost` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolCost` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`)
+	/// Storage: `Balances::Holds` (r:1 w:1)
+	/// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::Freezes` (r:0 w:1)
+	/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(87), added: 2562, mode: `MaxEncodedLen`)
+	fn cleanup_pool() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `1105`
+		//  Estimated: `6208`
+		// Minimum execution time: 127_524_000 picoseconds.
+		Weight::from_parts(130_238_000, 0)
+			.saturating_add(Weight::from_parts(0, 6208))
+			.saturating_add(T::DbWeight::get().reads(10))
+			.saturating_add(T::DbWeight::get().writes(10))
+	}
+}
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs
index 08b2f520c4b..0c6ff5e4bfd 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs
@@ -76,6 +76,10 @@ parameter_types! {
 	pub TrustBackedAssetsPalletLocation: Location =
 		PalletInstance(TrustBackedAssetsPalletIndex::get()).into();
 	pub TrustBackedAssetsPalletIndex: u8 = <Assets as PalletInfoAccess>::index() as u8;
+	pub TrustBackedAssetsPalletLocationV3: xcm::v3::Location =
+		xcm::v3::Junction::PalletInstance(<Assets as PalletInfoAccess>::index() as u8).into();
+	pub PoolAssetsPalletLocationV3: xcm::v3::Location =
+		xcm::v3::Junction::PalletInstance(<PoolAssets as PalletInfoAccess>::index() as u8).into();
 	pub ForeignAssetsPalletLocation: Location =
 		PalletInstance(<ForeignAssets as PalletInfoAccess>::index() as u8).into();
 	pub PoolAssetsPalletLocation: Location =
@@ -336,7 +340,7 @@ pub type TrustedTeleporters = (
 /// asset and the asset required for fee payment.
 pub type PoolAssetsExchanger = SingleAssetExchangeAdapter<
 	crate::AssetConversion,
-	crate::NativeAndAssets,
+	crate::NativeAndNonPoolAssets,
 	(
 		TrustBackedAssetsAsLocation<TrustBackedAssetsPalletLocation, Balance, xcm::v5::Location>,
 		ForeignAssetsConvertedConcreteId,
@@ -387,7 +391,7 @@ impl xcm_executor::Config for XcmConfig {
 			TokenLocation,
 			crate::AssetConversion,
 			WeightToFee,
-			crate::NativeAndAssets,
+			crate::NativeAndNonPoolAssets,
 			(
 				TrustBackedAssetsAsLocation<
 					TrustBackedAssetsPalletLocation,
@@ -396,7 +400,7 @@ impl xcm_executor::Config for XcmConfig {
 				>,
 				ForeignAssetsConvertedConcreteId,
 			),
-			ResolveAssetTo<StakingPot, crate::NativeAndAssets>,
+			ResolveAssetTo<StakingPot, crate::NativeAndNonPoolAssets>,
 			AccountId,
 		>,
 		// This trader allows to pay with `is_sufficient=true` "Trust Backed" assets from dedicated
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml
index cb10ae9a480..65ef63a7fb3 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml
@@ -30,6 +30,7 @@ frame-try-runtime = { optional = true, workspace = true }
 pallet-asset-conversion = { workspace = true }
 pallet-asset-conversion-ops = { workspace = true }
 pallet-asset-conversion-tx-payment = { workspace = true }
+pallet-asset-rewards = { workspace = true }
 pallet-assets = { workspace = true }
 pallet-assets-freezer = { workspace = true }
 pallet-aura = { workspace = true }
@@ -62,6 +63,7 @@ sp-std = { workspace = true }
 sp-storage = { workspace = true }
 sp-transaction-pool = { workspace = true }
 sp-version = { workspace = true }
+
 # num-traits feature needed for dex integer sq root:
 primitive-types = { features = ["codec", "num-traits", "scale-info"], workspace = true }
 
@@ -125,6 +127,7 @@ runtime-benchmarks = [
 	"pallet-asset-conversion-ops/runtime-benchmarks",
 	"pallet-asset-conversion-tx-payment/runtime-benchmarks",
 	"pallet-asset-conversion/runtime-benchmarks",
+	"pallet-asset-rewards/runtime-benchmarks",
 	"pallet-assets-freezer/runtime-benchmarks",
 	"pallet-assets/runtime-benchmarks",
 	"pallet-balances/runtime-benchmarks",
@@ -166,6 +169,7 @@ try-runtime = [
 	"pallet-asset-conversion-ops/try-runtime",
 	"pallet-asset-conversion-tx-payment/try-runtime",
 	"pallet-asset-conversion/try-runtime",
+	"pallet-asset-rewards/try-runtime",
 	"pallet-assets-freezer/try-runtime",
 	"pallet-assets/try-runtime",
 	"pallet-aura/try-runtime",
@@ -218,6 +222,7 @@ std = [
 	"pallet-asset-conversion-ops/std",
 	"pallet-asset-conversion-tx-payment/std",
 	"pallet-asset-conversion/std",
+	"pallet-asset-rewards/std",
 	"pallet-assets-freezer/std",
 	"pallet-assets/std",
 	"pallet-aura/std",
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
index 5966dd01f18..3ef5e87f24c 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
@@ -33,7 +33,7 @@ extern crate alloc;
 use alloc::{vec, vec::Vec};
 use assets_common::{
 	local_and_foreign_assets::{LocalFromLeft, TargetFromLeft},
-	AssetIdForTrustBackedAssetsConvert,
+	AssetIdForPoolAssets, AssetIdForPoolAssetsConvert, AssetIdForTrustBackedAssetsConvert,
 };
 use codec::{Decode, Encode, MaxEncodedLen};
 use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases;
@@ -44,10 +44,12 @@ use frame_support::{
 	genesis_builder_helper::{build_state, get_preset},
 	ord_parameter_types, parameter_types,
 	traits::{
-		fungible, fungibles,
+		fungible,
+		fungible::HoldConsideration,
+		fungibles,
 		tokens::{imbalance::ResolveAssetTo, nonfungibles_v2::Inspect},
-		AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, Equals,
-		InstanceFilter, Nothing, TransformOrigin,
+		AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, ConstU64, ConstU8,
+		ConstantStoragePrice, Equals, InstanceFilter, Nothing, TransformOrigin,
 	},
 	weights::{ConstantMultiplier, Weight, WeightToFee as _},
 	BoundedVec, PalletId,
@@ -81,8 +83,8 @@ use testnet_parachains_constants::westend::{
 };
 use xcm_config::{
 	ForeignAssetsConvertedConcreteId, LocationToAccountId, PoolAssetsConvertedConcreteId,
-	TrustBackedAssetsConvertedConcreteId, TrustBackedAssetsPalletLocation, WestendLocation,
-	XcmOriginToTransactDispatchOrigin,
+	PoolAssetsPalletLocation, TrustBackedAssetsConvertedConcreteId,
+	TrustBackedAssetsPalletLocation, WestendLocation, XcmOriginToTransactDispatchOrigin,
 };
 
 #[cfg(any(feature = "std", test))]
@@ -93,11 +95,15 @@ use assets_common::{
 	matching::{FromNetwork, FromSiblingParachain},
 };
 use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate};
+use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight};
 use xcm::{
 	latest::prelude::AssetId,
 	prelude::{VersionedAsset, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm},
 };
 
+#[cfg(feature = "runtime-benchmarks")]
+use frame_support::traits::PalletInfoAccess;
+
 #[cfg(feature = "runtime-benchmarks")]
 use xcm::latest::prelude::{
 	Asset, Assets as XcmAssets, Fungible, Here, InteriorLocation, Junction, Junction::*, Location,
@@ -109,8 +115,6 @@ use xcm_runtime_apis::{
 	fees::Error as XcmPaymentApiError,
 };
 
-use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight};
-
 impl_opaque_keys! {
 	pub struct SessionKeys {
 		pub aura: Aura,
@@ -218,8 +222,8 @@ impl pallet_balances::Config for Runtime {
 	type ReserveIdentifier = [u8; 8];
 	type RuntimeHoldReason = RuntimeHoldReason;
 	type RuntimeFreezeReason = RuntimeFreezeReason;
-	type FreezeIdentifier = ();
-	type MaxFreezes = ConstU32<0>;
+	type FreezeIdentifier = RuntimeFreezeReason;
+	type MaxFreezes = ConstU32<50>;
 	type DoneSlashHandler = ();
 }
 
@@ -341,8 +345,22 @@ pub type LocalAndForeignAssets = fungibles::UnionOf<
 	xcm::v5::Location,
 	AccountId,
 >;
+
+/// Union fungibles implementation for `AssetsFreezer` and `ForeignAssetsFreezer`.
+pub type LocalAndForeignAssetsFreezer = fungibles::UnionOf<
+	AssetsFreezer,
+	ForeignAssetsFreezer,
+	LocalFromLeft<
+		AssetIdForTrustBackedAssetsConvert<TrustBackedAssetsPalletLocation, xcm::v5::Location>,
+		AssetIdForTrustBackedAssets,
+		xcm::v5::Location,
+	>,
+	xcm::v5::Location,
+	AccountId,
+>;
+
 /// Union fungibles implementation for [`LocalAndForeignAssets`] and `Balances`.
-pub type NativeAndAssets = fungible::UnionOf<
+pub type NativeAndNonPoolAssets = fungible::UnionOf<
 	Balances,
 	LocalAndForeignAssets,
 	TargetFromLeft<WestendLocation, xcm::v5::Location>,
@@ -350,6 +368,45 @@ pub type NativeAndAssets = fungible::UnionOf<
 	AccountId,
 >;
 
+/// Union fungibles implementation for [`LocalAndForeignAssetsFreezer`] and [`Balances`].
+pub type NativeAndNonPoolAssetsFreezer = fungible::UnionOf<
+	Balances,
+	LocalAndForeignAssetsFreezer,
+	TargetFromLeft<WestendLocation, xcm::v5::Location>,
+	xcm::v5::Location,
+	AccountId,
+>;
+
+/// Union fungibles implementation for [`PoolAssets`] and [`NativeAndNonPoolAssets`].
+///
+/// NOTE: Should be kept updated to include ALL balances and assets in the runtime.
+pub type NativeAndAllAssets = fungibles::UnionOf<
+	PoolAssets,
+	NativeAndNonPoolAssets,
+	LocalFromLeft<
+		AssetIdForPoolAssetsConvert<PoolAssetsPalletLocation, xcm::v5::Location>,
+		AssetIdForPoolAssets,
+		xcm::v5::Location,
+	>,
+	xcm::v5::Location,
+	AccountId,
+>;
+
+/// Union fungibles implementation for [`PoolAssetsFreezer`] and [`NativeAndNonPoolAssetsFreezer`].
+///
+/// NOTE: Should be kept updated to include ALL balances and assets in the runtime.
+pub type NativeAndAllAssetsFreezer = fungibles::UnionOf<
+	PoolAssetsFreezer,
+	NativeAndNonPoolAssetsFreezer,
+	LocalFromLeft<
+		AssetIdForPoolAssetsConvert<PoolAssetsPalletLocation, xcm::v5::Location>,
+		AssetIdForPoolAssets,
+		xcm::v5::Location,
+	>,
+	xcm::v5::Location,
+	AccountId,
+>;
+
 pub type PoolIdToAccountId = pallet_asset_conversion::AccountIdConverter<
 	AssetConversionPalletId,
 	(xcm::v5::Location, xcm::v5::Location),
@@ -360,7 +417,7 @@ impl pallet_asset_conversion::Config for Runtime {
 	type Balance = Balance;
 	type HigherPrecisionBalance = sp_core::U256;
 	type AssetKind = xcm::v5::Location;
-	type Assets = NativeAndAssets;
+	type Assets = NativeAndNonPoolAssets;
 	type PoolId = (Self::AssetKind, Self::AssetKind);
 	type PoolLocator = pallet_asset_conversion::WithFirstAsset<
 		WestendLocation,
@@ -388,6 +445,55 @@ impl pallet_asset_conversion::Config for Runtime {
 	>;
 }
 
+#[cfg(feature = "runtime-benchmarks")]
+pub struct PalletAssetRewardsBenchmarkHelper;
+
+#[cfg(feature = "runtime-benchmarks")]
+impl pallet_asset_rewards::benchmarking::BenchmarkHelper<xcm::v5::Location>
+	for PalletAssetRewardsBenchmarkHelper
+{
+	fn staked_asset() -> Location {
+		Location::new(
+			0,
+			[PalletInstance(<Assets as PalletInfoAccess>::index() as u8), GeneralIndex(100)],
+		)
+	}
+	fn reward_asset() -> Location {
+		Location::new(
+			0,
+			[PalletInstance(<Assets as PalletInfoAccess>::index() as u8), GeneralIndex(101)],
+		)
+	}
+}
+
+parameter_types! {
+	pub const AssetRewardsPalletId: PalletId = PalletId(*b"py/astrd");
+	pub const RewardsPoolCreationHoldReason: RuntimeHoldReason =
+		RuntimeHoldReason::AssetRewards(pallet_asset_rewards::HoldReason::PoolCreation);
+	// 1 item, 135 bytes into the storage on pool creation.
+	pub const StakePoolCreationDeposit: Balance = deposit(1, 135);
+}
+
+impl pallet_asset_rewards::Config for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type PalletId = AssetRewardsPalletId;
+	type Balance = Balance;
+	type Assets = NativeAndAllAssets;
+	type AssetsFreezer = NativeAndAllAssetsFreezer;
+	type AssetId = xcm::v5::Location;
+	type CreatePoolOrigin = EnsureSigned<AccountId>;
+	type RuntimeFreezeReason = RuntimeFreezeReason;
+	type Consideration = HoldConsideration<
+		AccountId,
+		Balances,
+		RewardsPoolCreationHoldReason,
+		ConstantStoragePrice<StakePoolCreationDeposit, Balance>,
+	>;
+	type WeightInfo = weights::pallet_asset_rewards::WeightInfo<Runtime>;
+	#[cfg(feature = "runtime-benchmarks")]
+	type BenchmarkHelper = PalletAssetRewardsBenchmarkHelper;
+}
+
 impl pallet_asset_conversion_ops::Config for Runtime {
 	type RuntimeEvent = RuntimeEvent;
 	type PriorAccountIdConverter = pallet_asset_conversion::AccountIdConverterNoSeed<
@@ -816,9 +922,9 @@ impl pallet_asset_conversion_tx_payment::Config for Runtime {
 	type AssetId = xcm::v5::Location;
 	type OnChargeAssetTransaction = SwapAssetAdapter<
 		WestendLocation,
-		NativeAndAssets,
+		NativeAndNonPoolAssets,
 		AssetConversion,
-		ResolveAssetTo<StakingPot, NativeAndAssets>,
+		ResolveAssetTo<StakingPot, NativeAndNonPoolAssets>,
 	>;
 	type WeightInfo = weights::pallet_asset_conversion_tx_payment::WeightInfo<Runtime>;
 	#[cfg(feature = "runtime-benchmarks")]
@@ -1035,11 +1141,14 @@ construct_runtime!(
 		NftFractionalization: pallet_nft_fractionalization = 54,
 		PoolAssets: pallet_assets::<Instance3> = 55,
 		AssetConversion: pallet_asset_conversion = 56,
+
 		AssetsFreezer: pallet_assets_freezer::<Instance1> = 57,
 		ForeignAssetsFreezer: pallet_assets_freezer::<Instance2> = 58,
 		PoolAssetsFreezer: pallet_assets_freezer::<Instance3> = 59,
 		Revive: pallet_revive = 60,
 
+		AssetRewards: pallet_asset_rewards = 61,
+
 		StateTrieMigration: pallet_state_trie_migration = 70,
 
 		// TODO: the pallet instance should be removed once all pools have migrated
@@ -1317,6 +1426,7 @@ mod benches {
 		[pallet_assets, Foreign]
 		[pallet_assets, Pool]
 		[pallet_asset_conversion, AssetConversion]
+		[pallet_asset_rewards, AssetRewards]
 		[pallet_asset_conversion_tx_payment, AssetTxPayment]
 		[pallet_balances, Balances]
 		[pallet_message_queue, MessageQueue]
@@ -1674,6 +1784,12 @@ impl_runtime_apis! {
 		}
 	}
 
+	impl pallet_asset_rewards::AssetRewards<Block, Balance> for Runtime {
+		fn pool_creation_cost() -> Balance {
+			StakePoolCreationDeposit::get()
+		}
+	}
+
 	impl cumulus_primitives_core::GetCoreSelectorApi<Block> for Runtime {
 		fn core_selector() -> (CoreSelector, ClaimQueueOffset) {
 			ParachainSystem::core_selector()
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/mod.rs
index 442b58635f4..d653838ad80 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/mod.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/mod.rs
@@ -23,6 +23,7 @@ pub mod frame_system_extensions;
 pub mod pallet_asset_conversion;
 pub mod pallet_asset_conversion_ops;
 pub mod pallet_asset_conversion_tx_payment;
+pub mod pallet_asset_rewards;
 pub mod pallet_assets_foreign;
 pub mod pallet_assets_local;
 pub mod pallet_assets_pool;
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_asset_rewards.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_asset_rewards.rs
new file mode 100644
index 00000000000..3bbc289fec7
--- /dev/null
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_asset_rewards.rs
@@ -0,0 +1,217 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Cumulus.
+
+// Cumulus 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.
+
+// Cumulus 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 Cumulus.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Autogenerated weights for `pallet_asset_rewards`
+//!
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0
+//! DATE: 2025-01-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! WORST CASE MAP SIZE: `1000000`
+//! HOSTNAME: `runner-ys-ssygq-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
+//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("asset-hub-westend-dev")`, DB CACHE: 1024
+
+// Executed Command:
+// target/production/polkadot-parachain
+// benchmark
+// pallet
+// --steps=50
+// --repeat=20
+// --extrinsic=*
+// --wasm-execution=compiled
+// --heap-pages=4096
+// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json
+// --pallet=pallet_asset_rewards
+// --chain=asset-hub-westend-dev
+// --header=./cumulus/file_header.txt
+// --output=./cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/
+
+#![cfg_attr(rustfmt, rustfmt_skip)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+#![allow(missing_docs)]
+
+use frame_support::{traits::Get, weights::Weight};
+use core::marker::PhantomData;
+
+/// Weight functions for `pallet_asset_rewards`.
+pub struct WeightInfo<T>(PhantomData<T>);
+impl<T: frame_system::Config> pallet_asset_rewards::WeightInfo for WeightInfo<T> {
+	/// Storage: `Assets::Asset` (r:2 w:0)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::NextPoolId` (r:1 w:1)
+	/// Proof: `AssetRewards::NextPoolId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `Balances::Holds` (r:1 w:1)
+	/// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolCost` (r:0 w:1)
+	/// Proof: `AssetRewards::PoolCost` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::Pools` (r:0 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	fn create_pool() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `392`
+		//  Estimated: `6360`
+		// Minimum execution time: 60_734_000 picoseconds.
+		Weight::from_parts(61_828_000, 0)
+			.saturating_add(Weight::from_parts(0, 6360))
+			.saturating_add(T::DbWeight::get().reads(5))
+			.saturating_add(T::DbWeight::get().writes(5))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
+	/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(87), added: 2562, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:1 w:0)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	fn stake() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `906`
+		//  Estimated: `4809`
+		// Minimum execution time: 56_014_000 picoseconds.
+		Weight::from_parts(58_487_000, 0)
+			.saturating_add(Weight::from_parts(0, 4809))
+			.saturating_add(T::DbWeight::get().reads(5))
+			.saturating_add(T::DbWeight::get().writes(4))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
+	/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(87), added: 2562, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:1 w:0)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	fn unstake() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `906`
+		//  Estimated: `4809`
+		// Minimum execution time: 59_071_000 picoseconds.
+		Weight::from_parts(60_631_000, 0)
+			.saturating_add(Weight::from_parts(0, 4809))
+			.saturating_add(T::DbWeight::get().reads(5))
+			.saturating_add(T::DbWeight::get().writes(4))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:0)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	fn harvest_rewards() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `1106`
+		//  Estimated: `6208`
+		// Minimum execution time: 80_585_000 picoseconds.
+		Weight::from_parts(82_186_000, 0)
+			.saturating_add(Weight::from_parts(0, 6208))
+			.saturating_add(T::DbWeight::get().reads(6))
+			.saturating_add(T::DbWeight::get().writes(4))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	fn set_pool_reward_rate_per_block() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `318`
+		//  Estimated: `4809`
+		// Minimum execution time: 17_083_000 picoseconds.
+		Weight::from_parts(17_816_000, 0)
+			.saturating_add(Weight::from_parts(0, 4809))
+			.saturating_add(T::DbWeight::get().reads(1))
+			.saturating_add(T::DbWeight::get().writes(1))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	fn set_pool_admin() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `318`
+		//  Estimated: `4809`
+		// Minimum execution time: 15_269_000 picoseconds.
+		Weight::from_parts(15_881_000, 0)
+			.saturating_add(Weight::from_parts(0, 4809))
+			.saturating_add(T::DbWeight::get().reads(1))
+			.saturating_add(T::DbWeight::get().writes(1))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	fn set_pool_expiry_block() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `318`
+		//  Estimated: `4809`
+		// Minimum execution time: 17_482_000 picoseconds.
+		Weight::from_parts(18_124_000, 0)
+			.saturating_add(Weight::from_parts(0, 4809))
+			.saturating_add(T::DbWeight::get().reads(1))
+			.saturating_add(T::DbWeight::get().writes(1))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:0)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	fn deposit_reward_tokens() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `781`
+		//  Estimated: `6208`
+		// Minimum execution time: 66_644_000 picoseconds.
+		Weight::from_parts(67_950_000, 0)
+			.saturating_add(Weight::from_parts(0, 6208))
+			.saturating_add(T::DbWeight::get().reads(6))
+			.saturating_add(T::DbWeight::get().writes(4))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(1344), added: 3819, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:0)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:2 w:2)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolCost` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolCost` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`)
+	/// Storage: `Balances::Holds` (r:1 w:1)
+	/// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::Freezes` (r:0 w:1)
+	/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(87), added: 2562, mode: `MaxEncodedLen`)
+	fn cleanup_pool() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `1139`
+		//  Estimated: `6208`
+		// Minimum execution time: 124_136_000 picoseconds.
+		Weight::from_parts(128_642_000, 0)
+			.saturating_add(Weight::from_parts(0, 6208))
+			.saturating_add(T::DbWeight::get().reads(10))
+			.saturating_add(T::DbWeight::get().writes(10))
+	}
+}
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs
index b4e938f1f8b..1ea2ce5136a 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs
@@ -65,6 +65,7 @@ use xcm_executor::XcmExecutor;
 parameter_types! {
 	pub const RootLocation: Location = Location::here();
 	pub const WestendLocation: Location = Location::parent();
+	pub const GovernanceLocation: Location = Location::parent();
 	pub const RelayNetwork: Option<NetworkId> = Some(NetworkId::ByGenesis(WESTEND_GENESIS_HASH));
 	pub RelayChainOrigin: RuntimeOrigin = cumulus_pallet_xcm::Origin::Relay.into();
 	pub UniversalLocation: InteriorLocation =
@@ -359,7 +360,7 @@ pub type TrustedTeleporters = (
 /// asset and the asset required for fee payment.
 pub type PoolAssetsExchanger = SingleAssetExchangeAdapter<
 	crate::AssetConversion,
-	crate::NativeAndAssets,
+	crate::NativeAndNonPoolAssets,
 	(
 		TrustBackedAssetsAsLocation<TrustBackedAssetsPalletLocation, Balance, xcm::v5::Location>,
 		ForeignAssetsConvertedConcreteId,
@@ -409,7 +410,7 @@ impl xcm_executor::Config for XcmConfig {
 			WestendLocation,
 			crate::AssetConversion,
 			WeightToFee,
-			crate::NativeAndAssets,
+			crate::NativeAndNonPoolAssets,
 			(
 				TrustBackedAssetsAsLocation<
 					TrustBackedAssetsPalletLocation,
@@ -418,7 +419,7 @@ impl xcm_executor::Config for XcmConfig {
 				>,
 				ForeignAssetsConvertedConcreteId,
 			),
-			ResolveAssetTo<StakingPot, crate::NativeAndAssets>,
+			ResolveAssetTo<StakingPot, crate::NativeAndNonPoolAssets>,
 			AccountId,
 		>,
 		// This trader allows to pay with `is_sufficient=true` "Trust Backed" assets from dedicated
diff --git a/cumulus/parachains/runtimes/assets/common/src/lib.rs b/cumulus/parachains/runtimes/assets/common/src/lib.rs
index 25c2df6b68d..50b1b63146b 100644
--- a/cumulus/parachains/runtimes/assets/common/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/common/src/lib.rs
@@ -123,10 +123,11 @@ pub type ForeignAssetsConvertedConcreteId<
 	BalanceConverter,
 >;
 
-type AssetIdForPoolAssets = u32;
+pub type AssetIdForPoolAssets = u32;
+
 /// `Location` vs `AssetIdForPoolAssets` converter for `PoolAssets`.
-pub type AssetIdForPoolAssetsConvert<PoolAssetsPalletLocation> =
-	AsPrefixedGeneralIndex<PoolAssetsPalletLocation, AssetIdForPoolAssets, TryConvertInto>;
+pub type AssetIdForPoolAssetsConvert<PoolAssetsPalletLocation, L = Location> =
+	AsPrefixedGeneralIndex<PoolAssetsPalletLocation, AssetIdForPoolAssets, TryConvertInto, L>;
 /// [`MatchedConvertedConcreteId`] converter dedicated for `PoolAssets`
 pub type PoolAssetsConvertedConcreteId<PoolAssetsPalletLocation, Balance> =
 	MatchedConvertedConcreteId<
diff --git a/polkadot/runtime/rococo/src/xcm_config.rs b/polkadot/runtime/rococo/src/xcm_config.rs
index bb77ec0000e..10c3f6c0cbf 100644
--- a/polkadot/runtime/rococo/src/xcm_config.rs
+++ b/polkadot/runtime/rococo/src/xcm_config.rs
@@ -18,7 +18,8 @@
 
 use super::{
 	parachains_origin, AccountId, AllPalletsWithSystem, Balances, Dmp, Fellows, ParaId, Runtime,
-	RuntimeCall, RuntimeEvent, RuntimeOrigin, TransactionByteFee, Treasury, WeightToFee, XcmPallet,
+	RuntimeCall, RuntimeEvent, RuntimeOrigin, TransactionByteFee, Treasurer, Treasury, WeightToFee,
+	XcmPallet,
 };
 
 use crate::governance::StakingAdmin;
@@ -228,11 +229,14 @@ impl xcm_executor::Config for XcmConfig {
 }
 
 parameter_types! {
+	/// Collective pluralistic body.
 	pub const CollectiveBodyId: BodyId = BodyId::Unit;
-	// StakingAdmin pluralistic body.
+	/// StakingAdmin pluralistic body.
 	pub const StakingAdminBodyId: BodyId = BodyId::Defense;
-	// Fellows pluralistic body.
+	/// Fellows pluralistic body.
 	pub const FellowsBodyId: BodyId = BodyId::Technical;
+	/// Treasury pluralistic body.
+	pub const TreasuryBodyId: BodyId = BodyId::Treasury;
 }
 
 /// Type to convert an `Origin` type value into a `Location` value which represents an interior
@@ -249,6 +253,9 @@ pub type StakingAdminToPlurality =
 /// Type to convert the Fellows origin to a Plurality `Location` value.
 pub type FellowsToPlurality = OriginToPluralityVoice<RuntimeOrigin, Fellows, FellowsBodyId>;
 
+/// Type to convert the Treasury origin to a Plurality `Location` value.
+pub type TreasurerToPlurality = OriginToPluralityVoice<RuntimeOrigin, Treasurer, TreasuryBodyId>;
+
 /// Type to convert a pallet `Origin` type value into a `Location` value which represents an
 /// interior location of this chain for a destination chain.
 pub type LocalPalletOriginToLocation = (
@@ -256,13 +263,18 @@ pub type LocalPalletOriginToLocation = (
 	StakingAdminToPlurality,
 	// Fellows origin to be used in XCM as a corresponding Plurality `Location` value.
 	FellowsToPlurality,
+	// Treasurer origin to be used in XCM as a corresponding Plurality `Location` value.
+	TreasurerToPlurality,
 );
 
 impl pallet_xcm::Config for Runtime {
 	type RuntimeEvent = RuntimeEvent;
 	// Note that this configuration of `SendXcmOrigin` is different from the one present in
 	// production.
-	type SendXcmOrigin = xcm_builder::EnsureXcmOrigin<RuntimeOrigin, LocalOriginToLocation>;
+	type SendXcmOrigin = xcm_builder::EnsureXcmOrigin<
+		RuntimeOrigin,
+		(LocalPalletOriginToLocation, LocalOriginToLocation),
+	>;
 	type XcmRouter = XcmRouter;
 	// Anyone can execute XCM messages locally.
 	type ExecuteXcmOrigin = xcm_builder::EnsureXcmOrigin<RuntimeOrigin, LocalOriginToLocation>;
diff --git a/polkadot/runtime/westend/src/xcm_config.rs b/polkadot/runtime/westend/src/xcm_config.rs
index 3f6a7304c8a..4235edf82b2 100644
--- a/polkadot/runtime/westend/src/xcm_config.rs
+++ b/polkadot/runtime/westend/src/xcm_config.rs
@@ -280,7 +280,10 @@ impl pallet_xcm::Config for Runtime {
 	type RuntimeEvent = RuntimeEvent;
 	// Note that this configuration of `SendXcmOrigin` is different from the one present in
 	// production.
-	type SendXcmOrigin = xcm_builder::EnsureXcmOrigin<RuntimeOrigin, LocalOriginToLocation>;
+	type SendXcmOrigin = xcm_builder::EnsureXcmOrigin<
+		RuntimeOrigin,
+		(LocalPalletOriginToLocation, LocalOriginToLocation),
+	>;
 	type XcmRouter = XcmRouter;
 	// Anyone can execute XCM messages locally.
 	type ExecuteXcmOrigin = xcm_builder::EnsureXcmOrigin<RuntimeOrigin, LocalOriginToLocation>;
diff --git a/prdoc/pr_3926.prdoc b/prdoc/pr_3926.prdoc
new file mode 100644
index 00000000000..7f352f7a45f
--- /dev/null
+++ b/prdoc/pr_3926.prdoc
@@ -0,0 +1,30 @@
+title: Introduce pallet-asset-rewards
+
+doc:
+  - audience: Runtime Dev
+    description: |
+        Introduce pallet-asset-rewards, which allows accounts to be rewarded for freezing fungible 
+        tokens. The motivation for creating this pallet is to allow incentivising LPs.
+        See the pallet docs for more info about the pallet.
+
+crates:
+  - name: pallet-asset-rewards
+    bump: major
+  - name: polkadot-sdk
+    bump: minor
+  - name: kitchensink-runtime
+    bump: major
+  - name: asset-hub-rococo-runtime
+    bump: major
+  - name: asset-hub-westend-runtime
+    bump: major
+  - name: assets-common
+    bump: minor
+  - name: rococo-runtime
+    bump: minor
+  - name: westend-runtime
+    bump: patch
+  - name: frame-support
+    bump: minor
+  - name: emulated-integration-tests-common
+    bump: minor
diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs
index 97728f12f5f..de377a55bc8 100644
--- a/substrate/bin/node/runtime/src/lib.rs
+++ b/substrate/bin/node/runtime/src/lib.rs
@@ -56,10 +56,10 @@ use frame_support::{
 			imbalance::ResolveAssetTo, nonfungibles_v2::Inspect, pay::PayAssetFromAccount,
 			GetSalary, PayFromAccount,
 		},
-		AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU16, ConstU32, ConstU64, Contains,
-		Currency, EitherOfDiverse, EnsureOriginWithArg, EqualPrivilegeOnly, Imbalance, InsideBoth,
-		InstanceFilter, KeyOwnerProofSystem, LinearStoragePrice, LockIdentifier, Nothing,
-		OnUnbalanced, VariantCountOf, WithdrawReasons,
+		AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU16, ConstU32, ConstU64,
+		ConstantStoragePrice, Contains, Currency, EitherOfDiverse, EnsureOriginWithArg,
+		EqualPrivilegeOnly, Imbalance, InsideBoth, InstanceFilter, KeyOwnerProofSystem,
+		LinearStoragePrice, LockIdentifier, Nothing, OnUnbalanced, VariantCountOf, WithdrawReasons,
 	},
 	weights::{
 		constants::{
@@ -511,7 +511,8 @@ impl pallet_glutton::Config for Runtime {
 }
 
 parameter_types! {
-	pub const PreimageHoldReason: RuntimeHoldReason = RuntimeHoldReason::Preimage(pallet_preimage::HoldReason::Preimage);
+	pub const PreimageHoldReason: RuntimeHoldReason =
+		RuntimeHoldReason::Preimage(pallet_preimage::HoldReason::Preimage);
 }
 
 impl pallet_preimage::Config for Runtime {
@@ -618,6 +619,12 @@ impl pallet_transaction_payment::Config for Runtime {
 	type WeightInfo = pallet_transaction_payment::weights::SubstrateWeight<Runtime>;
 }
 
+pub type AssetsFreezerInstance = pallet_assets_freezer::Instance1;
+impl pallet_assets_freezer::Config<AssetsFreezerInstance> for Runtime {
+	type RuntimeFreezeReason = RuntimeFreezeReason;
+	type RuntimeEvent = RuntimeEvent;
+}
+
 impl pallet_asset_conversion_tx_payment::Config for Runtime {
 	type RuntimeEvent = RuntimeEvent;
 	type AssetId = NativeOrWithId<u32>;
@@ -1858,6 +1865,53 @@ impl pallet_asset_conversion::Config for Runtime {
 	type BenchmarkHelper = ();
 }
 
+pub type NativeAndAssetsFreezer =
+	UnionOf<Balances, AssetsFreezer, NativeFromLeft, NativeOrWithId<u32>, AccountId>;
+
+/// Benchmark Helper
+#[cfg(feature = "runtime-benchmarks")]
+pub struct AssetRewardsBenchmarkHelper;
+
+#[cfg(feature = "runtime-benchmarks")]
+impl pallet_asset_rewards::benchmarking::BenchmarkHelper<NativeOrWithId<u32>>
+	for AssetRewardsBenchmarkHelper
+{
+	fn staked_asset() -> NativeOrWithId<u32> {
+		NativeOrWithId::<u32>::WithId(100)
+	}
+	fn reward_asset() -> NativeOrWithId<u32> {
+		NativeOrWithId::<u32>::WithId(101)
+	}
+}
+
+parameter_types! {
+	pub const StakingRewardsPalletId: PalletId = PalletId(*b"py/stkrd");
+	pub const CreationHoldReason: RuntimeHoldReason =
+		RuntimeHoldReason::AssetRewards(pallet_asset_rewards::HoldReason::PoolCreation);
+	// 1 item, 135 bytes into the storage on pool creation.
+	pub const StakePoolCreationDeposit: Balance = deposit(1, 135);
+}
+
+impl pallet_asset_rewards::Config for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type RuntimeFreezeReason = RuntimeFreezeReason;
+	type AssetId = NativeOrWithId<u32>;
+	type Balance = Balance;
+	type Assets = NativeAndAssets;
+	type PalletId = StakingRewardsPalletId;
+	type CreatePoolOrigin = EnsureSigned<AccountId>;
+	type WeightInfo = ();
+	type AssetsFreezer = NativeAndAssetsFreezer;
+	type Consideration = HoldConsideration<
+		AccountId,
+		Balances,
+		CreationHoldReason,
+		ConstantStoragePrice<StakePoolCreationDeposit, Balance>,
+	>;
+	#[cfg(feature = "runtime-benchmarks")]
+	type BenchmarkHelper = AssetRewardsBenchmarkHelper;
+}
+
 impl pallet_asset_conversion_ops::Config for Runtime {
 	type RuntimeEvent = RuntimeEvent;
 	type PriorAccountIdConverter = pallet_asset_conversion::AccountIdConverterNoSeed<(
@@ -2636,6 +2690,12 @@ mod runtime {
 
 	#[runtime::pallet_index(81)]
 	pub type VerifySignature = pallet_verify_signature::Pallet<Runtime>;
+
+	#[runtime::pallet_index(83)]
+	pub type AssetRewards = pallet_asset_rewards::Pallet<Runtime>;
+
+	#[runtime::pallet_index(84)]
+	pub type AssetsFreezer = pallet_assets_freezer::Pallet<Runtime, Instance1>;
 }
 
 impl TryFrom<RuntimeCall> for pallet_revive::Call<Runtime> {
@@ -2846,6 +2906,7 @@ mod benches {
 		[pallet_example_tasks, TasksExample]
 		[pallet_democracy, Democracy]
 		[pallet_asset_conversion, AssetConversion]
+		[pallet_asset_rewards, AssetRewards]
 		[pallet_asset_conversion_tx_payment, AssetConversionTxPayment]
 		[pallet_transaction_payment, TransactionPayment]
 		[pallet_election_provider_multi_phase, ElectionProviderMultiPhase]
@@ -3553,6 +3614,12 @@ impl_runtime_apis! {
 		}
 	}
 
+	impl pallet_asset_rewards::AssetRewards<Block, Balance> for Runtime {
+		fn pool_creation_cost() -> Balance {
+			StakePoolCreationDeposit::get()
+		}
+	}
+
 	#[cfg(feature = "try-runtime")]
 	impl frame_try_runtime::TryRuntime<Block> for Runtime {
 		fn on_runtime_upgrade(checks: frame_try_runtime::UpgradeCheckSelect) -> (Weight, Weight) {
diff --git a/substrate/frame/asset-rewards/Cargo.toml b/substrate/frame/asset-rewards/Cargo.toml
new file mode 100644
index 00000000000..a03fa17cf0d
--- /dev/null
+++ b/substrate/frame/asset-rewards/Cargo.toml
@@ -0,0 +1,71 @@
+[package]
+name = "pallet-asset-rewards"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license = "Apache-2.0"
+homepage.workspace = true
+repository.workspace = true
+description = "FRAME asset rewards pallet"
+
+[lints]
+workspace = true
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[dependencies]
+codec = { workspace = true }
+frame-benchmarking = { workspace = true, optional = true }
+frame-support = { workspace = true, features = ["experimental"] }
+frame-system = { workspace = true }
+scale-info = { workspace = true, features = ["derive"] }
+sp-api = { workspace = true }
+sp-arithmetic = { workspace = true }
+sp-core = { workspace = true }
+sp-io = { workspace = true }
+sp-runtime = { workspace = true }
+sp-std = { workspace = true }
+
+[dev-dependencies]
+pallet-assets = { workspace = true }
+pallet-assets-freezer = { workspace = true }
+pallet-balances = { workspace = true }
+primitive-types = { workspace = true, features = ["codec", "num-traits", "scale-info"] }
+
+[features]
+default = ["std"]
+std = [
+	"codec/std",
+	"frame-benchmarking?/std",
+	"frame-support/std",
+	"frame-system/std",
+	"pallet-assets-freezer/std",
+	"pallet-assets/std",
+	"pallet-balances/std",
+	"primitive-types/std",
+	"scale-info/std",
+	"sp-api/std",
+	"sp-arithmetic/std",
+	"sp-core/std",
+	"sp-io/std",
+	"sp-runtime/std",
+	"sp-std/std",
+]
+runtime-benchmarks = [
+	"frame-benchmarking/runtime-benchmarks",
+	"frame-support/runtime-benchmarks",
+	"frame-system/runtime-benchmarks",
+	"pallet-assets-freezer/runtime-benchmarks",
+	"pallet-assets/runtime-benchmarks",
+	"pallet-balances/runtime-benchmarks",
+	"sp-runtime/runtime-benchmarks",
+]
+try-runtime = [
+	"frame-support/try-runtime",
+	"frame-system/try-runtime",
+	"pallet-assets-freezer/try-runtime",
+	"pallet-assets/try-runtime",
+	"pallet-balances/try-runtime",
+	"sp-runtime/try-runtime",
+]
diff --git a/substrate/frame/asset-rewards/src/benchmarking.rs b/substrate/frame/asset-rewards/src/benchmarking.rs
new file mode 100644
index 00000000000..5605804dd20
--- /dev/null
+++ b/substrate/frame/asset-rewards/src/benchmarking.rs
@@ -0,0 +1,355 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// 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.
+
+//! Asset Rewards pallet benchmarking.
+
+use super::*;
+use crate::Pallet as AssetRewards;
+use frame_benchmarking::{v2::*, whitelisted_caller, BenchmarkError};
+use frame_support::{
+	assert_ok,
+	traits::{
+		fungibles::{Create, Inspect, Mutate},
+		Consideration, EnsureOrigin, Footprint,
+	},
+};
+use frame_system::{pallet_prelude::BlockNumberFor, Pallet as System, RawOrigin};
+use sp_runtime::{traits::One, Saturating};
+use sp_std::prelude::*;
+
+/// Benchmark Helper
+pub trait BenchmarkHelper<AssetId> {
+	/// Returns the staked asset id.
+	///
+	/// If the asset does not exist, it will be created by the benchmark.
+	fn staked_asset() -> AssetId;
+	/// Returns the reward asset id.
+	///
+	/// If the asset does not exist, it will be created by the benchmark.
+	fn reward_asset() -> AssetId;
+}
+
+fn pool_expire<T: Config>() -> DispatchTime<BlockNumberFor<T>> {
+	DispatchTime::At(BlockNumberFor::<T>::from(100u32))
+}
+
+fn create_reward_pool<T: Config>() -> Result<T::RuntimeOrigin, BenchmarkError>
+where
+	T::Assets: Create<T::AccountId> + Mutate<T::AccountId>,
+{
+	let caller_origin =
+		T::CreatePoolOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
+	let caller = T::CreatePoolOrigin::ensure_origin(caller_origin.clone()).unwrap();
+
+	let footprint = Footprint::from_mel::<(PoolId, PoolInfoFor<T>)>();
+	T::Consideration::ensure_successful(&caller, footprint);
+
+	let staked_asset = T::BenchmarkHelper::staked_asset();
+	let reward_asset = T::BenchmarkHelper::reward_asset();
+
+	let min_staked_balance =
+		T::Assets::minimum_balance(staked_asset.clone()).max(T::Balance::one());
+	if !T::Assets::asset_exists(staked_asset.clone()) {
+		assert_ok!(T::Assets::create(
+			staked_asset.clone(),
+			caller.clone(),
+			true,
+			min_staked_balance
+		));
+	}
+	let min_reward_balance =
+		T::Assets::minimum_balance(reward_asset.clone()).max(T::Balance::one());
+	if !T::Assets::asset_exists(reward_asset.clone()) {
+		assert_ok!(T::Assets::create(
+			reward_asset.clone(),
+			caller.clone(),
+			true,
+			min_reward_balance
+		));
+	}
+
+	assert_ok!(AssetRewards::<T>::create_pool(
+		caller_origin.clone(),
+		Box::new(staked_asset),
+		Box::new(reward_asset),
+		// reward rate per block
+		min_reward_balance,
+		pool_expire::<T>(),
+		Some(caller),
+	));
+
+	Ok(caller_origin)
+}
+
+fn mint_into<T: Config>(caller: &T::AccountId, asset: &T::AssetId) -> T::Balance
+where
+	T::Assets: Mutate<T::AccountId>,
+{
+	let min_balance = T::Assets::minimum_balance(asset.clone());
+	assert_ok!(T::Assets::mint_into(
+		asset.clone(),
+		&caller,
+		min_balance.saturating_mul(10u32.into())
+	));
+	min_balance
+}
+
+fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
+	System::<T>::assert_last_event(generic_event.into());
+}
+
+#[benchmarks(where T::Assets: Create<T::AccountId> + Mutate<T::AccountId>)]
+mod benchmarks {
+	use super::*;
+
+	#[benchmark]
+	fn create_pool() -> Result<(), BenchmarkError> {
+		let caller_origin =
+			T::CreatePoolOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
+		let caller = T::CreatePoolOrigin::ensure_origin(caller_origin.clone()).unwrap();
+
+		let footprint = Footprint::from_mel::<(PoolId, PoolInfoFor<T>)>();
+		T::Consideration::ensure_successful(&caller, footprint);
+
+		let staked_asset = T::BenchmarkHelper::staked_asset();
+		let reward_asset = T::BenchmarkHelper::reward_asset();
+
+		let min_balance = T::Assets::minimum_balance(staked_asset.clone()).max(T::Balance::one());
+		if !T::Assets::asset_exists(staked_asset.clone()) {
+			assert_ok!(T::Assets::create(staked_asset.clone(), caller.clone(), true, min_balance));
+		}
+		let min_balance = T::Assets::minimum_balance(reward_asset.clone()).max(T::Balance::one());
+		if !T::Assets::asset_exists(reward_asset.clone()) {
+			assert_ok!(T::Assets::create(reward_asset.clone(), caller.clone(), true, min_balance));
+		}
+
+		#[extrinsic_call]
+		_(
+			caller_origin as T::RuntimeOrigin,
+			Box::new(staked_asset.clone()),
+			Box::new(reward_asset.clone()),
+			min_balance,
+			pool_expire::<T>(),
+			Some(caller.clone()),
+		);
+
+		assert_last_event::<T>(
+			Event::PoolCreated {
+				creator: caller.clone(),
+				admin: caller,
+				staked_asset_id: staked_asset,
+				reward_asset_id: reward_asset,
+				reward_rate_per_block: min_balance,
+				expiry_block: pool_expire::<T>().evaluate(System::<T>::block_number()),
+				pool_id: 0,
+			}
+			.into(),
+		);
+
+		Ok(())
+	}
+
+	#[benchmark]
+	fn stake() -> Result<(), BenchmarkError> {
+		create_reward_pool::<T>()?;
+
+		let staker: T::AccountId = whitelisted_caller();
+		let min_balance = mint_into::<T>(&staker, &T::BenchmarkHelper::staked_asset());
+
+		// stake first to get worth case benchmark.
+		assert_ok!(AssetRewards::<T>::stake(
+			RawOrigin::Signed(staker.clone()).into(),
+			0,
+			min_balance
+		));
+
+		#[extrinsic_call]
+		_(RawOrigin::Signed(staker.clone()), 0, min_balance);
+
+		assert_last_event::<T>(Event::Staked { staker, pool_id: 0, amount: min_balance }.into());
+
+		Ok(())
+	}
+
+	#[benchmark]
+	fn unstake() -> Result<(), BenchmarkError> {
+		create_reward_pool::<T>()?;
+
+		let staker: T::AccountId = whitelisted_caller();
+		let min_balance = mint_into::<T>(&staker, &T::BenchmarkHelper::staked_asset());
+
+		assert_ok!(AssetRewards::<T>::stake(
+			RawOrigin::Signed(staker.clone()).into(),
+			0,
+			min_balance,
+		));
+
+		#[extrinsic_call]
+		_(RawOrigin::Signed(staker.clone()), 0, min_balance, None);
+
+		assert_last_event::<T>(
+			Event::Unstaked { caller: staker.clone(), staker, pool_id: 0, amount: min_balance }
+				.into(),
+		);
+
+		Ok(())
+	}
+
+	#[benchmark]
+	fn harvest_rewards() -> Result<(), BenchmarkError> {
+		create_reward_pool::<T>()?;
+
+		let pool_acc = AssetRewards::<T>::pool_account_id(&0u32);
+		let min_reward_balance = mint_into::<T>(&pool_acc, &T::BenchmarkHelper::reward_asset());
+
+		let staker = whitelisted_caller();
+		let _ = mint_into::<T>(&staker, &T::BenchmarkHelper::staked_asset());
+		assert_ok!(AssetRewards::<T>::stake(
+			RawOrigin::Signed(staker.clone()).into(),
+			0,
+			T::Balance::one(),
+		));
+
+		System::<T>::set_block_number(System::<T>::block_number() + BlockNumberFor::<T>::one());
+
+		#[extrinsic_call]
+		_(RawOrigin::Signed(staker.clone()), 0, None);
+
+		assert_last_event::<T>(
+			Event::RewardsHarvested {
+				caller: staker.clone(),
+				staker,
+				pool_id: 0,
+				amount: min_reward_balance,
+			}
+			.into(),
+		);
+
+		Ok(())
+	}
+
+	#[benchmark]
+	fn set_pool_reward_rate_per_block() -> Result<(), BenchmarkError> {
+		let caller_origin = create_reward_pool::<T>()?;
+
+		// stake first to get worth case benchmark.
+		{
+			let staker: T::AccountId = whitelisted_caller();
+			let min_balance = mint_into::<T>(&staker, &T::BenchmarkHelper::staked_asset());
+
+			assert_ok!(AssetRewards::<T>::stake(RawOrigin::Signed(staker).into(), 0, min_balance));
+		}
+
+		let new_reward_rate_per_block =
+			T::Assets::minimum_balance(T::BenchmarkHelper::reward_asset()).max(T::Balance::one()) +
+				T::Balance::one();
+
+		#[extrinsic_call]
+		_(caller_origin as T::RuntimeOrigin, 0, new_reward_rate_per_block);
+
+		assert_last_event::<T>(
+			Event::PoolRewardRateModified { pool_id: 0, new_reward_rate_per_block }.into(),
+		);
+		Ok(())
+	}
+
+	#[benchmark]
+	fn set_pool_admin() -> Result<(), BenchmarkError> {
+		let caller_origin = create_reward_pool::<T>()?;
+		let new_admin: T::AccountId = whitelisted_caller();
+
+		#[extrinsic_call]
+		_(caller_origin as T::RuntimeOrigin, 0, new_admin.clone());
+
+		assert_last_event::<T>(Event::PoolAdminModified { pool_id: 0, new_admin }.into());
+
+		Ok(())
+	}
+
+	#[benchmark]
+	fn set_pool_expiry_block() -> Result<(), BenchmarkError> {
+		let create_origin = create_reward_pool::<T>()?;
+
+		// stake first to get worth case benchmark.
+		{
+			let staker: T::AccountId = whitelisted_caller();
+			let min_balance = mint_into::<T>(&staker, &T::BenchmarkHelper::staked_asset());
+
+			assert_ok!(AssetRewards::<T>::stake(RawOrigin::Signed(staker).into(), 0, min_balance));
+		}
+
+		let new_expiry_block =
+			pool_expire::<T>().evaluate(System::<T>::block_number()) + BlockNumberFor::<T>::one();
+
+		#[extrinsic_call]
+		_(create_origin as T::RuntimeOrigin, 0, DispatchTime::At(new_expiry_block));
+
+		assert_last_event::<T>(
+			Event::PoolExpiryBlockModified { pool_id: 0, new_expiry_block }.into(),
+		);
+
+		Ok(())
+	}
+
+	#[benchmark]
+	fn deposit_reward_tokens() -> Result<(), BenchmarkError> {
+		create_reward_pool::<T>()?;
+		let caller = whitelisted_caller();
+
+		let reward_asset = T::BenchmarkHelper::reward_asset();
+		let pool_acc = AssetRewards::<T>::pool_account_id(&0u32);
+		let min_balance = mint_into::<T>(&caller, &reward_asset);
+
+		let balance_before = T::Assets::balance(reward_asset.clone(), &pool_acc);
+
+		#[extrinsic_call]
+		_(RawOrigin::Signed(caller), 0, min_balance);
+
+		let balance_after = T::Assets::balance(reward_asset, &pool_acc);
+
+		assert_eq!(balance_after, balance_before + min_balance);
+
+		Ok(())
+	}
+
+	#[benchmark]
+	fn cleanup_pool() -> Result<(), BenchmarkError> {
+		let create_origin = create_reward_pool::<T>()?;
+		let caller = T::CreatePoolOrigin::ensure_origin(create_origin.clone()).unwrap();
+
+		// deposit rewards tokens to get worth case benchmark.
+		{
+			let caller = whitelisted_caller();
+			let reward_asset = T::BenchmarkHelper::reward_asset();
+			let min_balance = mint_into::<T>(&caller, &reward_asset);
+			assert_ok!(AssetRewards::<T>::deposit_reward_tokens(
+				RawOrigin::Signed(caller).into(),
+				0,
+				min_balance
+			));
+		}
+
+		#[extrinsic_call]
+		_(RawOrigin::Signed(caller), 0);
+
+		assert_last_event::<T>(Event::PoolCleanedUp { pool_id: 0 }.into());
+
+		Ok(())
+	}
+
+	impl_benchmark_test_suite!(AssetRewards, crate::mock::new_test_ext(), crate::mock::MockRuntime);
+}
diff --git a/substrate/frame/asset-rewards/src/lib.rs b/substrate/frame/asset-rewards/src/lib.rs
new file mode 100644
index 00000000000..4ce73e9febf
--- /dev/null
+++ b/substrate/frame/asset-rewards/src/lib.rs
@@ -0,0 +1,905 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// 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.
+
+//! # FRAME Staking Rewards Pallet
+//!
+//! Allows accounts to be rewarded for holding `fungible` asset/s, for example LP tokens.
+//!
+//! ## Overview
+//!
+//! Initiate an incentive program for a fungible asset by creating a new pool.
+//!
+//! During pool creation, a 'staking asset', 'reward asset', 'reward rate per block', 'expiry
+//! block', and 'admin' are specified.
+//!
+//! Once created, holders of the 'staking asset' can 'stake' them in a corresponding pool, which
+//! creates a Freeze on the asset.
+//!
+//! Once staked, rewards denominated in 'reward asset' begin accumulating to the staker,
+//! proportional to their share of the total staked tokens in the pool.
+//!
+//! Reward assets pending distribution are held in an account unique to each pool.
+//!
+//! Care should be taken by the pool operator to keep pool accounts adequately funded with the
+//! reward asset.
+//!
+//! The pool admin may increase reward rate per block, increase expiry block, and change admin.
+//!
+//! ## Disambiguation
+//!
+//! While this pallet shares some terminology with the `staking-pool` and similar native staking
+//! related pallets, it is distinct and is entirely unrelated to native staking.
+//!
+//! ## Permissioning
+//!
+//! Currently, pool creation and management restricted to a configured Origin.
+//!
+//! Future iterations of this pallet may allow permissionless creation and management of pools.
+//!
+//! Note: The permissioned origin must return an AccountId. This can be achieved for any Origin by
+//! wrapping it with `EnsureSuccess`.
+//!
+//! ## Implementation Notes
+//!
+//! Internal logic functions such as `update_pool_and_staker_rewards` were deliberately written
+//! without side-effects.
+//!
+//! Storage interaction such as reads and writes are instead all performed in the top level
+//! pallet Call method, which while slightly more verbose, makes it easier to understand the
+//! code and reason about how storage reads and writes occur in the pallet.
+//!
+//! ## Rewards Algorithm
+//!
+//! The rewards algorithm is based on the Synthetix [StakingRewards.sol](https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol)
+//! smart contract.
+//!
+//! Rewards are calculated JIT (just-in-time), and all operations are O(1) making the approach
+//! scalable to many pools and stakers.
+//!
+//! ### Resources
+//!
+//! - [This video series](https://www.youtube.com/watch?v=6ZO5aYg1GI8), which walks through the math
+//!   of the algorithm.
+//! - [This dev.to article](https://dev.to/heymarkkop/understanding-sushiswaps-masterchef-staking-rewards-1m6f),
+//!   which explains the algorithm of the SushiSwap MasterChef staking. While not identical to the
+//!   Synthetix approach, they are quite similar.
+#![deny(missing_docs)]
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub use pallet::*;
+
+use codec::{Codec, Decode, Encode, MaxEncodedLen};
+use frame_support::{
+	traits::{
+		fungibles::{Inspect, Mutate},
+		schedule::DispatchTime,
+		tokens::Balance,
+	},
+	PalletId,
+};
+use frame_system::pallet_prelude::BlockNumberFor;
+use scale_info::TypeInfo;
+use sp_core::Get;
+use sp_runtime::{
+	traits::{MaybeDisplay, Zero},
+	DispatchError,
+};
+use sp_std::boxed::Box;
+
+#[cfg(feature = "runtime-benchmarks")]
+pub mod benchmarking;
+#[cfg(test)]
+mod mock;
+#[cfg(test)]
+mod tests;
+mod weights;
+
+pub use weights::WeightInfo;
+
+/// Unique id type for each pool.
+pub type PoolId = u32;
+
+/// Multiplier to maintain precision when calculating rewards.
+pub(crate) const PRECISION_SCALING_FACTOR: u16 = 4096;
+
+/// Convenience alias for `PoolInfo`.
+pub type PoolInfoFor<T> = PoolInfo<
+	<T as frame_system::Config>::AccountId,
+	<T as Config>::AssetId,
+	<T as Config>::Balance,
+	BlockNumberFor<T>,
+>;
+
+/// The state of a staker in a pool.
+#[derive(Debug, Default, Clone, Decode, Encode, MaxEncodedLen, TypeInfo)]
+pub struct PoolStakerInfo<Balance> {
+	/// Amount of tokens staked.
+	amount: Balance,
+	/// Accumulated, unpaid rewards.
+	rewards: Balance,
+	/// Reward per token value at the time of the staker's last interaction with the contract.
+	reward_per_token_paid: Balance,
+}
+
+/// The state and configuration of an incentive pool.
+#[derive(Debug, Clone, Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)]
+pub struct PoolInfo<AccountId, AssetId, Balance, BlockNumber> {
+	/// The asset staked in this pool.
+	staked_asset_id: AssetId,
+	/// The asset distributed as rewards by this pool.
+	reward_asset_id: AssetId,
+	/// The amount of tokens rewarded per block.
+	reward_rate_per_block: Balance,
+	/// The block the pool will cease distributing rewards.
+	expiry_block: BlockNumber,
+	/// The account authorized to manage this pool.
+	admin: AccountId,
+	/// The total amount of tokens staked in this pool.
+	total_tokens_staked: Balance,
+	/// Total rewards accumulated per token, up to the `last_update_block`.
+	reward_per_token_stored: Balance,
+	/// Last block number the pool was updated.
+	last_update_block: BlockNumber,
+	/// The account that holds the pool's rewards.
+	account: AccountId,
+}
+
+sp_api::decl_runtime_apis! {
+	/// The runtime API for the asset rewards pallet.
+	pub trait AssetRewards<Cost: MaybeDisplay + Codec> {
+		/// Get the cost of creating a pool.
+		///
+		/// This is especially useful when the cost is dynamic.
+		fn pool_creation_cost() -> Cost;
+	}
+}
+
+#[frame_support::pallet]
+pub mod pallet {
+	use super::*;
+	use frame_support::{
+		pallet_prelude::*,
+		traits::{
+			fungibles::MutateFreeze,
+			tokens::{AssetId, Fortitude, Preservation},
+			Consideration, Footprint,
+		},
+	};
+	use frame_system::pallet_prelude::*;
+	use sp_runtime::{
+		traits::{
+			AccountIdConversion, BadOrigin, EnsureAdd, EnsureAddAssign, EnsureDiv, EnsureMul,
+			EnsureSub, EnsureSubAssign,
+		},
+		DispatchResult,
+	};
+
+	#[pallet::pallet]
+	pub struct Pallet<T>(_);
+
+	/// A reason for the pallet placing a hold on funds.
+	#[pallet::composite_enum]
+	pub enum FreezeReason {
+		/// Funds are staked in the pallet.
+		#[codec(index = 0)]
+		Staked,
+	}
+
+	/// A reason for the pallet placing a hold on funds.
+	#[pallet::composite_enum]
+	pub enum HoldReason {
+		/// Cost associated with storing pool information on-chain.
+		#[codec(index = 0)]
+		PoolCreation,
+	}
+
+	#[pallet::config]
+	pub trait Config: frame_system::Config {
+		/// Overarching event type.
+		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
+
+		/// The pallet's unique identifier, used to derive the pool's account ID.
+		///
+		/// The account ID is derived once during pool creation and stored in the storage.
+		#[pallet::constant]
+		type PalletId: Get<PalletId>;
+
+		/// Identifier for each type of asset.
+		type AssetId: AssetId + Member + Parameter;
+
+		/// The type in which the assets are measured.
+		type Balance: Balance + TypeInfo;
+
+		/// The origin with permission to create pools.
+		///
+		/// The Origin must return an AccountId.
+		type CreatePoolOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
+
+		/// Registry of assets that can be configured to either stake for rewards, or be offered as
+		/// rewards for staking.
+		type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = Self::Balance>
+			+ Mutate<Self::AccountId>;
+
+		/// Freezer for the Assets.
+		type AssetsFreezer: MutateFreeze<
+			Self::AccountId,
+			Id = Self::RuntimeFreezeReason,
+			AssetId = Self::AssetId,
+			Balance = Self::Balance,
+		>;
+
+		/// The overarching freeze reason.
+		type RuntimeFreezeReason: From<FreezeReason>;
+
+		/// Means for associating a cost with the on-chain storage of pool information, which
+		/// is incurred by the pool creator.
+		///
+		/// The passed `Footprint` specifically accounts for the storage footprint of the pool's
+		/// information itself, excluding any potential storage footprint related to the stakers.
+		type Consideration: Consideration<Self::AccountId, Footprint>;
+
+		/// Weight information for extrinsics in this pallet.
+		type WeightInfo: WeightInfo;
+
+		/// Helper for benchmarking.
+		#[cfg(feature = "runtime-benchmarks")]
+		type BenchmarkHelper: benchmarking::BenchmarkHelper<Self::AssetId>;
+	}
+
+	/// State of pool stakers.
+	#[pallet::storage]
+	pub type PoolStakers<T: Config> = StorageDoubleMap<
+		_,
+		Blake2_128Concat,
+		PoolId,
+		Blake2_128Concat,
+		T::AccountId,
+		PoolStakerInfo<T::Balance>,
+	>;
+
+	/// State and configuration of each staking pool.
+	#[pallet::storage]
+	pub type Pools<T: Config> = StorageMap<_, Blake2_128Concat, PoolId, PoolInfoFor<T>>;
+
+	/// The cost associated with storing pool information on-chain which was incurred by the pool
+	/// creator.
+	///
+	/// This cost may be [`None`], as determined by [`Config::Consideration`].
+	#[pallet::storage]
+	pub type PoolCost<T: Config> =
+		StorageMap<_, Blake2_128Concat, PoolId, (T::AccountId, T::Consideration)>;
+
+	/// Stores the [`PoolId`] to use for the next pool.
+	///
+	/// Incremented when a new pool is created.
+	#[pallet::storage]
+	pub type NextPoolId<T: Config> = StorageValue<_, PoolId, ValueQuery>;
+
+	#[pallet::event]
+	#[pallet::generate_deposit(pub(super) fn deposit_event)]
+	pub enum Event<T: Config> {
+		/// An account staked some tokens in a pool.
+		Staked {
+			/// The account that staked assets.
+			staker: T::AccountId,
+			/// The pool.
+			pool_id: PoolId,
+			/// The staked asset amount.
+			amount: T::Balance,
+		},
+		/// An account unstaked some tokens from a pool.
+		Unstaked {
+			/// The account that signed transaction.
+			caller: T::AccountId,
+			/// The account that unstaked assets.
+			staker: T::AccountId,
+			/// The pool.
+			pool_id: PoolId,
+			/// The unstaked asset amount.
+			amount: T::Balance,
+		},
+		/// An account harvested some rewards.
+		RewardsHarvested {
+			/// The account that signed transaction.
+			caller: T::AccountId,
+			/// The staker whos rewards were harvested.
+			staker: T::AccountId,
+			/// The pool.
+			pool_id: PoolId,
+			/// The amount of harvested tokens.
+			amount: T::Balance,
+		},
+		/// A new reward pool was created.
+		PoolCreated {
+			/// The account that created the pool.
+			creator: T::AccountId,
+			/// The unique ID for the new pool.
+			pool_id: PoolId,
+			/// The staking asset.
+			staked_asset_id: T::AssetId,
+			/// The reward asset.
+			reward_asset_id: T::AssetId,
+			/// The initial reward rate per block.
+			reward_rate_per_block: T::Balance,
+			/// The block the pool will cease to accumulate rewards.
+			expiry_block: BlockNumberFor<T>,
+			/// The account allowed to modify the pool.
+			admin: T::AccountId,
+		},
+		/// A pool reward rate was modified by the admin.
+		PoolRewardRateModified {
+			/// The modified pool.
+			pool_id: PoolId,
+			/// The new reward rate per block.
+			new_reward_rate_per_block: T::Balance,
+		},
+		/// A pool admin was modified.
+		PoolAdminModified {
+			/// The modified pool.
+			pool_id: PoolId,
+			/// The new admin.
+			new_admin: T::AccountId,
+		},
+		/// A pool expiry block was modified by the admin.
+		PoolExpiryBlockModified {
+			/// The modified pool.
+			pool_id: PoolId,
+			/// The new expiry block.
+			new_expiry_block: BlockNumberFor<T>,
+		},
+		/// A pool information was cleared after it's completion.
+		PoolCleanedUp {
+			/// The cleared pool.
+			pool_id: PoolId,
+		},
+	}
+
+	#[pallet::error]
+	pub enum Error<T> {
+		/// The staker does not have enough tokens to perform the operation.
+		NotEnoughTokens,
+		/// An operation was attempted on a non-existent pool.
+		NonExistentPool,
+		/// An operation was attempted for a non-existent staker.
+		NonExistentStaker,
+		/// An operation was attempted with a non-existent asset.
+		NonExistentAsset,
+		/// There was an error converting a block number.
+		BlockNumberConversionError,
+		/// The expiry block must be in the future.
+		ExpiryBlockMustBeInTheFuture,
+		/// Insufficient funds to create the freeze.
+		InsufficientFunds,
+		/// The expiry block can be only extended.
+		ExpiryCut,
+		/// The reward rate per block can be only increased.
+		RewardRateCut,
+		/// The pool still has staked tokens or rewards.
+		NonEmptyPool,
+	}
+
+	#[pallet::hooks]
+	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
+		fn integrity_test() {
+			// The AccountId is at least 16 bytes to contain the unique PalletId.
+			let pool_id: PoolId = 1;
+			assert!(
+				<frame_support::PalletId as AccountIdConversion<T::AccountId>>::try_into_sub_account(
+					&T::PalletId::get(), pool_id,
+				)
+				.is_some()
+			);
+		}
+	}
+
+	/// Pallet's callable functions.
+	#[pallet::call(weight(<T as Config>::WeightInfo))]
+	impl<T: Config> Pallet<T> {
+		/// Create a new reward pool.
+		///
+		/// Parameters:
+		/// - `origin`: must be `Config::CreatePoolOrigin`;
+		/// - `staked_asset_id`: the asset to be staked in the pool;
+		/// - `reward_asset_id`: the asset to be distributed as rewards;
+		/// - `reward_rate_per_block`: the amount of reward tokens distributed per block;
+		/// - `expiry`: the block number at which the pool will cease to accumulate rewards. The
+		///   [`DispatchTime::After`] variant evaluated at the execution time.
+		/// - `admin`: the account allowed to extend the pool expiration, increase the rewards rate
+		///   and receive the unutilized reward tokens back after the pool completion. If `None`,
+		///   the caller is set as an admin.
+		#[pallet::call_index(0)]
+		pub fn create_pool(
+			origin: OriginFor<T>,
+			staked_asset_id: Box<T::AssetId>,
+			reward_asset_id: Box<T::AssetId>,
+			reward_rate_per_block: T::Balance,
+			expiry: DispatchTime<BlockNumberFor<T>>,
+			admin: Option<T::AccountId>,
+		) -> DispatchResult {
+			// Check the origin.
+			let creator = T::CreatePoolOrigin::ensure_origin(origin)?;
+
+			// Ensure the assets exist.
+			ensure!(
+				T::Assets::asset_exists(*staked_asset_id.clone()),
+				Error::<T>::NonExistentAsset
+			);
+			ensure!(
+				T::Assets::asset_exists(*reward_asset_id.clone()),
+				Error::<T>::NonExistentAsset
+			);
+
+			// Check the expiry block.
+			let expiry_block = expiry.evaluate(frame_system::Pallet::<T>::block_number());
+			ensure!(
+				expiry_block > frame_system::Pallet::<T>::block_number(),
+				Error::<T>::ExpiryBlockMustBeInTheFuture
+			);
+
+			let pool_id = NextPoolId::<T>::get();
+
+			let footprint = Self::pool_creation_footprint();
+			let cost = T::Consideration::new(&creator, footprint)?;
+			PoolCost::<T>::insert(pool_id, (creator.clone(), cost));
+
+			let admin = admin.unwrap_or(creator.clone());
+
+			// Create the pool.
+			let pool = PoolInfoFor::<T> {
+				staked_asset_id: *staked_asset_id.clone(),
+				reward_asset_id: *reward_asset_id.clone(),
+				reward_rate_per_block,
+				total_tokens_staked: 0u32.into(),
+				reward_per_token_stored: 0u32.into(),
+				last_update_block: 0u32.into(),
+				expiry_block,
+				admin: admin.clone(),
+				account: Self::pool_account_id(&pool_id),
+			};
+
+			// Insert it into storage.
+			Pools::<T>::insert(pool_id, pool);
+
+			NextPoolId::<T>::put(pool_id.ensure_add(1)?);
+
+			// Emit created event.
+			Self::deposit_event(Event::PoolCreated {
+				creator,
+				pool_id,
+				staked_asset_id: *staked_asset_id,
+				reward_asset_id: *reward_asset_id,
+				reward_rate_per_block,
+				expiry_block,
+				admin,
+			});
+
+			Ok(())
+		}
+
+		/// Stake additional tokens in a pool.
+		///
+		/// A freeze is placed on the staked tokens.
+		#[pallet::call_index(1)]
+		pub fn stake(origin: OriginFor<T>, pool_id: PoolId, amount: T::Balance) -> DispatchResult {
+			let staker = ensure_signed(origin)?;
+
+			// Always start by updating staker and pool rewards.
+			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
+			let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
+			let (mut pool_info, mut staker_info) =
+				Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
+
+			T::AssetsFreezer::increase_frozen(
+				pool_info.staked_asset_id.clone(),
+				&FreezeReason::Staked.into(),
+				&staker,
+				amount,
+			)?;
+
+			// Update Pools.
+			pool_info.total_tokens_staked.ensure_add_assign(amount)?;
+
+			Pools::<T>::insert(pool_id, pool_info);
+
+			// Update PoolStakers.
+			staker_info.amount.ensure_add_assign(amount)?;
+			PoolStakers::<T>::insert(pool_id, &staker, staker_info);
+
+			// Emit event.
+			Self::deposit_event(Event::Staked { staker, pool_id, amount });
+
+			Ok(())
+		}
+
+		/// Unstake tokens from a pool.
+		///
+		/// Removes the freeze on the staked tokens.
+		///
+		/// Parameters:
+		/// - origin: must be the `staker` if the pool is still active. Otherwise, any account.
+		/// - pool_id: the pool to unstake from.
+		/// - amount: the amount of tokens to unstake.
+		/// - staker: the account to unstake from. If `None`, the caller is used.
+		#[pallet::call_index(2)]
+		pub fn unstake(
+			origin: OriginFor<T>,
+			pool_id: PoolId,
+			amount: T::Balance,
+			staker: Option<T::AccountId>,
+		) -> DispatchResult {
+			let caller = ensure_signed(origin)?;
+			let staker = staker.unwrap_or(caller.clone());
+
+			// Always start by updating the pool rewards.
+			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
+			let now = frame_system::Pallet::<T>::block_number();
+			ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
+
+			let staker_info = PoolStakers::<T>::get(pool_id, &staker).unwrap_or_default();
+			let (mut pool_info, mut staker_info) =
+				Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
+
+			// Check the staker has enough staked tokens.
+			ensure!(staker_info.amount >= amount, Error::<T>::NotEnoughTokens);
+
+			// Unfreeze staker assets.
+			T::AssetsFreezer::decrease_frozen(
+				pool_info.staked_asset_id.clone(),
+				&FreezeReason::Staked.into(),
+				&staker,
+				amount,
+			)?;
+
+			// Update Pools.
+			pool_info.total_tokens_staked.ensure_sub_assign(amount)?;
+			Pools::<T>::insert(pool_id, pool_info);
+
+			// Update PoolStakers.
+			staker_info.amount.ensure_sub_assign(amount)?;
+
+			if staker_info.amount.is_zero() && staker_info.rewards.is_zero() {
+				PoolStakers::<T>::remove(&pool_id, &staker);
+			} else {
+				PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
+			}
+
+			// Emit event.
+			Self::deposit_event(Event::Unstaked { caller, staker, pool_id, amount });
+
+			Ok(())
+		}
+
+		/// Harvest unclaimed pool rewards.
+		///
+		/// Parameters:
+		/// - origin: must be the `staker` if the pool is still active. Otherwise, any account.
+		/// - pool_id: the pool to harvest from.
+		/// - staker: the account for which to harvest rewards. If `None`, the caller is used.
+		#[pallet::call_index(3)]
+		pub fn harvest_rewards(
+			origin: OriginFor<T>,
+			pool_id: PoolId,
+			staker: Option<T::AccountId>,
+		) -> DispatchResult {
+			let caller = ensure_signed(origin)?;
+			let staker = staker.unwrap_or(caller.clone());
+
+			// Always start by updating the pool and staker rewards.
+			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
+			let now = frame_system::Pallet::<T>::block_number();
+			ensure!(now > pool_info.expiry_block || caller == staker, BadOrigin);
+
+			let staker_info =
+				PoolStakers::<T>::get(pool_id, &staker).ok_or(Error::<T>::NonExistentStaker)?;
+			let (pool_info, mut staker_info) =
+				Self::update_pool_and_staker_rewards(&pool_info, &staker_info)?;
+
+			// Transfer unclaimed rewards from the pool to the staker.
+			T::Assets::transfer(
+				pool_info.reward_asset_id,
+				&pool_info.account,
+				&staker,
+				staker_info.rewards,
+				// Could kill the account, but only if the pool was already almost empty.
+				Preservation::Expendable,
+			)?;
+
+			// Emit event.
+			Self::deposit_event(Event::RewardsHarvested {
+				caller,
+				staker: staker.clone(),
+				pool_id,
+				amount: staker_info.rewards,
+			});
+
+			// Reset staker rewards.
+			staker_info.rewards = 0u32.into();
+
+			if staker_info.amount.is_zero() {
+				PoolStakers::<T>::remove(&pool_id, &staker);
+			} else {
+				PoolStakers::<T>::insert(&pool_id, &staker, staker_info);
+			}
+
+			Ok(())
+		}
+
+		/// Modify a pool reward rate.
+		///
+		/// Currently the reward rate can only be increased.
+		///
+		/// Only the pool admin may perform this operation.
+		#[pallet::call_index(4)]
+		pub fn set_pool_reward_rate_per_block(
+			origin: OriginFor<T>,
+			pool_id: PoolId,
+			new_reward_rate_per_block: T::Balance,
+		) -> DispatchResult {
+			let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
+				.or_else(|_| ensure_signed(origin))?;
+
+			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
+			ensure!(pool_info.admin == caller, BadOrigin);
+			ensure!(
+				new_reward_rate_per_block > pool_info.reward_rate_per_block,
+				Error::<T>::RewardRateCut
+			);
+
+			// Always start by updating the pool rewards.
+			let rewards_per_token = Self::reward_per_token(&pool_info)?;
+			let mut pool_info = Self::update_pool_rewards(&pool_info, rewards_per_token)?;
+
+			pool_info.reward_rate_per_block = new_reward_rate_per_block;
+			Pools::<T>::insert(pool_id, pool_info);
+
+			Self::deposit_event(Event::PoolRewardRateModified {
+				pool_id,
+				new_reward_rate_per_block,
+			});
+
+			Ok(())
+		}
+
+		/// Modify a pool admin.
+		///
+		/// Only the pool admin may perform this operation.
+		#[pallet::call_index(5)]
+		pub fn set_pool_admin(
+			origin: OriginFor<T>,
+			pool_id: PoolId,
+			new_admin: T::AccountId,
+		) -> DispatchResult {
+			let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
+				.or_else(|_| ensure_signed(origin))?;
+
+			let mut pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
+			ensure!(pool_info.admin == caller, BadOrigin);
+
+			pool_info.admin = new_admin.clone();
+			Pools::<T>::insert(pool_id, pool_info);
+
+			Self::deposit_event(Event::PoolAdminModified { pool_id, new_admin });
+
+			Ok(())
+		}
+
+		/// Set when the pool should expire.
+		///
+		/// Currently the expiry block can only be extended.
+		///
+		/// Only the pool admin may perform this operation.
+		#[pallet::call_index(6)]
+		pub fn set_pool_expiry_block(
+			origin: OriginFor<T>,
+			pool_id: PoolId,
+			new_expiry: DispatchTime<BlockNumberFor<T>>,
+		) -> DispatchResult {
+			let caller = T::CreatePoolOrigin::ensure_origin(origin.clone())
+				.or_else(|_| ensure_signed(origin))?;
+
+			let new_expiry = new_expiry.evaluate(frame_system::Pallet::<T>::block_number());
+			ensure!(
+				new_expiry > frame_system::Pallet::<T>::block_number(),
+				Error::<T>::ExpiryBlockMustBeInTheFuture
+			);
+
+			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
+			ensure!(pool_info.admin == caller, BadOrigin);
+			ensure!(new_expiry > pool_info.expiry_block, Error::<T>::ExpiryCut);
+
+			// Always start by updating the pool rewards.
+			let reward_per_token = Self::reward_per_token(&pool_info)?;
+			let mut pool_info = Self::update_pool_rewards(&pool_info, reward_per_token)?;
+
+			pool_info.expiry_block = new_expiry;
+			Pools::<T>::insert(pool_id, pool_info);
+
+			Self::deposit_event(Event::PoolExpiryBlockModified {
+				pool_id,
+				new_expiry_block: new_expiry,
+			});
+
+			Ok(())
+		}
+
+		/// Convenience method to deposit reward tokens into a pool.
+		///
+		/// This method is not strictly necessary (tokens could be transferred directly to the
+		/// pool pot address), but is provided for convenience so manual derivation of the
+		/// account id is not required.
+		#[pallet::call_index(7)]
+		pub fn deposit_reward_tokens(
+			origin: OriginFor<T>,
+			pool_id: PoolId,
+			amount: T::Balance,
+		) -> DispatchResult {
+			let caller = ensure_signed(origin)?;
+			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
+			T::Assets::transfer(
+				pool_info.reward_asset_id,
+				&caller,
+				&pool_info.account,
+				amount,
+				Preservation::Preserve,
+			)?;
+			Ok(())
+		}
+
+		/// Cleanup a pool.
+		///
+		/// Origin must be the pool admin.
+		///
+		/// Cleanup storage, release any associated storage cost and return the remaining reward
+		/// tokens to the admin.
+		#[pallet::call_index(8)]
+		pub fn cleanup_pool(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
+			let who = ensure_signed(origin)?;
+
+			let pool_info = Pools::<T>::get(pool_id).ok_or(Error::<T>::NonExistentPool)?;
+			ensure!(pool_info.admin == who, BadOrigin);
+
+			let stakers = PoolStakers::<T>::iter_key_prefix(pool_id).next();
+			ensure!(stakers.is_none(), Error::<T>::NonEmptyPool);
+
+			let pool_balance = T::Assets::reducible_balance(
+				pool_info.reward_asset_id.clone(),
+				&pool_info.account,
+				Preservation::Expendable,
+				Fortitude::Polite,
+			);
+			T::Assets::transfer(
+				pool_info.reward_asset_id,
+				&pool_info.account,
+				&pool_info.admin,
+				pool_balance,
+				Preservation::Expendable,
+			)?;
+
+			if let Some((who, cost)) = PoolCost::<T>::take(pool_id) {
+				T::Consideration::drop(cost, &who)?;
+			}
+
+			Pools::<T>::remove(pool_id);
+
+			Self::deposit_event(Event::PoolCleanedUp { pool_id });
+
+			Ok(())
+		}
+	}
+
+	impl<T: Config> Pallet<T> {
+		/// The pool creation footprint.
+		///
+		/// The footprint specifically accounts for the storage footprint of the pool's information
+		/// itself, excluding any potential storage footprint related to the stakers.
+		pub fn pool_creation_footprint() -> Footprint {
+			Footprint::from_mel::<(PoolId, PoolInfoFor<T>)>()
+		}
+
+		/// Derive a pool account ID from the pool's ID.
+		pub fn pool_account_id(id: &PoolId) -> T::AccountId {
+			T::PalletId::get().into_sub_account_truncating(id)
+		}
+
+		/// Computes update pool and staker reward state.
+		///
+		/// Should be called prior to any operation involving a staker.
+		///
+		/// Returns the updated pool and staker info.
+		///
+		/// NOTE: this function has no side-effects. Side-effects such as storage modifications are
+		/// the responsibility of the caller.
+		pub fn update_pool_and_staker_rewards(
+			pool_info: &PoolInfoFor<T>,
+			staker_info: &PoolStakerInfo<T::Balance>,
+		) -> Result<(PoolInfoFor<T>, PoolStakerInfo<T::Balance>), DispatchError> {
+			let reward_per_token = Self::reward_per_token(&pool_info)?;
+			let pool_info = Self::update_pool_rewards(pool_info, reward_per_token)?;
+
+			let mut new_staker_info = staker_info.clone();
+			new_staker_info.rewards = Self::derive_rewards(&staker_info, &reward_per_token)?;
+			new_staker_info.reward_per_token_paid = pool_info.reward_per_token_stored;
+			return Ok((pool_info, new_staker_info));
+		}
+
+		/// Computes update pool reward state.
+		///
+		/// Should be called every time the pool is adjusted, and a staker is not involved.
+		///
+		/// Returns the updated pool and staker info.
+		///
+		/// NOTE: this function has no side-effects. Side-effects such as storage modifications are
+		/// the responsibility of the caller.
+		pub fn update_pool_rewards(
+			pool_info: &PoolInfoFor<T>,
+			reward_per_token: T::Balance,
+		) -> Result<PoolInfoFor<T>, DispatchError> {
+			let mut new_pool_info = pool_info.clone();
+			new_pool_info.last_update_block = frame_system::Pallet::<T>::block_number();
+			new_pool_info.reward_per_token_stored = reward_per_token;
+
+			Ok(new_pool_info)
+		}
+
+		/// Derives the current reward per token for this pool.
+		fn reward_per_token(pool_info: &PoolInfoFor<T>) -> Result<T::Balance, DispatchError> {
+			if pool_info.total_tokens_staked.is_zero() {
+				return Ok(pool_info.reward_per_token_stored)
+			}
+
+			let rewardable_blocks_elapsed: u32 =
+				match Self::last_block_reward_applicable(pool_info.expiry_block)
+					.ensure_sub(pool_info.last_update_block)?
+					.try_into()
+				{
+					Ok(b) => b,
+					Err(_) => return Err(Error::<T>::BlockNumberConversionError.into()),
+				};
+
+			Ok(pool_info.reward_per_token_stored.ensure_add(
+				pool_info
+					.reward_rate_per_block
+					.ensure_mul(rewardable_blocks_elapsed.into())?
+					.ensure_mul(PRECISION_SCALING_FACTOR.into())?
+					.ensure_div(pool_info.total_tokens_staked)?,
+			)?)
+		}
+
+		/// Derives the amount of rewards earned by a staker.
+		///
+		/// This is a helper function for `update_pool_rewards` and should not be called directly.
+		fn derive_rewards(
+			staker_info: &PoolStakerInfo<T::Balance>,
+			reward_per_token: &T::Balance,
+		) -> Result<T::Balance, DispatchError> {
+			Ok(staker_info
+				.amount
+				.ensure_mul(reward_per_token.ensure_sub(staker_info.reward_per_token_paid)?)?
+				.ensure_div(PRECISION_SCALING_FACTOR.into())?
+				.ensure_add(staker_info.rewards)?)
+		}
+
+		fn last_block_reward_applicable(pool_expiry_block: BlockNumberFor<T>) -> BlockNumberFor<T> {
+			let now = frame_system::Pallet::<T>::block_number();
+			if now < pool_expiry_block {
+				now
+			} else {
+				pool_expiry_block
+			}
+		}
+	}
+}
diff --git a/substrate/frame/asset-rewards/src/mock.rs b/substrate/frame/asset-rewards/src/mock.rs
new file mode 100644
index 00000000000..87c8a8a0dea
--- /dev/null
+++ b/substrate/frame/asset-rewards/src/mock.rs
@@ -0,0 +1,221 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// 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 environment for Asset Rewards pallet.
+
+use super::*;
+use crate as pallet_asset_rewards;
+use core::default::Default;
+use frame_support::{
+	construct_runtime, derive_impl,
+	instances::Instance1,
+	parameter_types,
+	traits::{
+		tokens::fungible::{HoldConsideration, NativeFromLeft, NativeOrWithId, UnionOf},
+		AsEnsureOriginWithArg, ConstU128, ConstU32, EnsureOrigin, LinearStoragePrice,
+	},
+	PalletId,
+};
+use frame_system::EnsureSigned;
+use sp_runtime::{traits::IdentityLookup, BuildStorage};
+
+#[cfg(feature = "runtime-benchmarks")]
+use self::benchmarking::BenchmarkHelper;
+
+type Block = frame_system::mocking::MockBlock<MockRuntime>;
+
+construct_runtime!(
+	pub enum MockRuntime
+	{
+		System: frame_system,
+		Balances: pallet_balances,
+		Assets: pallet_assets::<Instance1>,
+		AssetsFreezer: pallet_assets_freezer::<Instance1>,
+		StakingRewards: pallet_asset_rewards,
+	}
+);
+
+#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
+impl frame_system::Config for MockRuntime {
+	type AccountId = u128;
+	type Lookup = IdentityLookup<Self::AccountId>;
+	type Block = Block;
+	type AccountData = pallet_balances::AccountData<u128>;
+}
+
+impl pallet_balances::Config for MockRuntime {
+	type Balance = u128;
+	type DustRemoval = ();
+	type RuntimeEvent = RuntimeEvent;
+	type ExistentialDeposit = ConstU128<100>;
+	type AccountStore = System;
+	type WeightInfo = ();
+	type MaxLocks = ();
+	type MaxReserves = ConstU32<50>;
+	type ReserveIdentifier = [u8; 8];
+	type FreezeIdentifier = RuntimeFreezeReason;
+	type MaxFreezes = ConstU32<50>;
+	type RuntimeHoldReason = RuntimeHoldReason;
+	type RuntimeFreezeReason = RuntimeFreezeReason;
+	type DoneSlashHandler = ();
+}
+
+impl pallet_assets::Config<Instance1> for MockRuntime {
+	type RuntimeEvent = RuntimeEvent;
+	type Balance = u128;
+	type RemoveItemsLimit = ConstU32<1000>;
+	type AssetId = u32;
+	type AssetIdParameter = u32;
+	type Currency = Balances;
+	type CreateOrigin = AsEnsureOriginWithArg<EnsureSigned<Self::AccountId>>;
+	type ForceOrigin = frame_system::EnsureRoot<Self::AccountId>;
+	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 = AssetsFreezer;
+	type Extra = ();
+	type WeightInfo = ();
+	type CallbackHandle = ();
+	pallet_assets::runtime_benchmarks_enabled! {
+		type BenchmarkHelper = ();
+	}
+}
+
+parameter_types! {
+	pub const StakingRewardsPalletId: PalletId = PalletId(*b"py/stkrd");
+	pub const Native: NativeOrWithId<u32> = NativeOrWithId::Native;
+	pub const PermissionedAccountId: u128 = 0;
+}
+
+/// Give Root Origin permission to create pools.
+pub struct MockPermissionedOrigin;
+impl EnsureOrigin<RuntimeOrigin> for MockPermissionedOrigin {
+	type Success = <MockRuntime as frame_system::Config>::AccountId;
+
+	fn try_origin(origin: RuntimeOrigin) -> Result<Self::Success, RuntimeOrigin> {
+		match origin.clone().into() {
+			Ok(frame_system::RawOrigin::Root) => Ok(PermissionedAccountId::get()),
+			_ => Err(origin),
+		}
+	}
+
+	#[cfg(feature = "runtime-benchmarks")]
+	fn try_successful_origin() -> Result<RuntimeOrigin, ()> {
+		Ok(RuntimeOrigin::root())
+	}
+}
+
+/// Allow Freezes for the `Assets` pallet
+impl pallet_assets_freezer::Config<pallet_assets_freezer::Instance1> for MockRuntime {
+	type RuntimeFreezeReason = RuntimeFreezeReason;
+	type RuntimeEvent = RuntimeEvent;
+}
+
+pub type NativeAndAssets = UnionOf<Balances, Assets, NativeFromLeft, NativeOrWithId<u32>, u128>;
+
+pub type NativeAndAssetsFreezer =
+	UnionOf<Balances, AssetsFreezer, NativeFromLeft, NativeOrWithId<u32>, u128>;
+
+#[cfg(feature = "runtime-benchmarks")]
+pub struct AssetRewardsBenchmarkHelper;
+#[cfg(feature = "runtime-benchmarks")]
+impl BenchmarkHelper<NativeOrWithId<u32>> for AssetRewardsBenchmarkHelper {
+	fn staked_asset() -> NativeOrWithId<u32> {
+		NativeOrWithId::<u32>::WithId(101)
+	}
+	fn reward_asset() -> NativeOrWithId<u32> {
+		NativeOrWithId::<u32>::WithId(102)
+	}
+}
+
+parameter_types! {
+	pub const CreationHoldReason: RuntimeHoldReason =
+		RuntimeHoldReason::StakingRewards(pallet_asset_rewards::HoldReason::PoolCreation);
+}
+
+impl Config for MockRuntime {
+	type RuntimeEvent = RuntimeEvent;
+	type AssetId = NativeOrWithId<u32>;
+	type Balance = <Self as pallet_balances::Config>::Balance;
+	type Assets = NativeAndAssets;
+	type AssetsFreezer = NativeAndAssetsFreezer;
+	type PalletId = StakingRewardsPalletId;
+	type CreatePoolOrigin = MockPermissionedOrigin;
+	type WeightInfo = ();
+	type RuntimeFreezeReason = RuntimeFreezeReason;
+	type Consideration = HoldConsideration<
+		u128,
+		Balances,
+		CreationHoldReason,
+		LinearStoragePrice<ConstU128<100>, ConstU128<0>, u128>,
+	>;
+	#[cfg(feature = "runtime-benchmarks")]
+	type BenchmarkHelper = AssetRewardsBenchmarkHelper;
+}
+
+pub(crate) fn new_test_ext() -> sp_io::TestExternalities {
+	let mut t = frame_system::GenesisConfig::<MockRuntime>::default().build_storage().unwrap();
+
+	pallet_assets::GenesisConfig::<MockRuntime, Instance1> {
+		// Genesis assets: id, owner, is_sufficient, min_balance
+		// pub assets: Vec<(T::AssetId, T::AccountId, bool, T::Balance)>,
+		assets: vec![(1, 1, true, 1), (10, 1, true, 1), (20, 1, true, 1)],
+		// Genesis metadata: id, name, symbol, decimals
+		// pub metadata: Vec<(T::AssetId, Vec<u8>, Vec<u8>, u8)>,
+		metadata: vec![
+			(1, b"test".to_vec(), b"TST".to_vec(), 18),
+			(10, b"test10".to_vec(), b"T10".to_vec(), 18),
+			(20, b"test20".to_vec(), b"T20".to_vec(), 18),
+		],
+		// Genesis accounts: id, account_id, balance
+		// pub accounts: Vec<(T::AssetId, T::AccountId, T::Balance)>,
+		accounts: vec![
+			(1, 1, 10000),
+			(1, 2, 20000),
+			(1, 3, 30000),
+			(1, 4, 40000),
+			(1, 10, 40000),
+			(1, 20, 40000),
+		],
+		next_asset_id: None,
+	}
+	.assimilate_storage(&mut t)
+	.unwrap();
+
+	let pool_zero_account_id = 31086825966906540362769395565;
+	pallet_balances::GenesisConfig::<MockRuntime> {
+		balances: vec![
+			(0, 10000),
+			(1, 10000),
+			(2, 20000),
+			(3, 30000),
+			(4, 40000),
+			(10, 40000),
+			(20, 40000),
+			(pool_zero_account_id, 100_000), // Top up the default pool account id
+		],
+	}
+	.assimilate_storage(&mut t)
+	.unwrap();
+
+	let mut ext = sp_io::TestExternalities::new(t);
+	ext.execute_with(|| System::set_block_number(1));
+	ext
+}
diff --git a/substrate/frame/asset-rewards/src/tests.rs b/substrate/frame/asset-rewards/src/tests.rs
new file mode 100644
index 00000000000..399d6a54c93
--- /dev/null
+++ b/substrate/frame/asset-rewards/src/tests.rs
@@ -0,0 +1,1457 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// 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.
+
+use crate::{mock::*, *};
+use frame_support::{
+	assert_err, assert_noop, assert_ok, hypothetically,
+	traits::{
+		fungible,
+		fungible::NativeOrWithId,
+		fungibles,
+		tokens::{Fortitude, Preservation},
+	},
+};
+use sp_runtime::{traits::BadOrigin, ArithmeticError, TokenError};
+
+const DEFAULT_STAKED_ASSET_ID: NativeOrWithId<u32> = NativeOrWithId::<u32>::WithId(1);
+const DEFAULT_REWARD_ASSET_ID: NativeOrWithId<u32> = NativeOrWithId::<u32>::Native;
+const DEFAULT_REWARD_RATE_PER_BLOCK: u128 = 100;
+const DEFAULT_EXPIRE_AFTER: u64 = 200;
+const DEFAULT_ADMIN: u128 = 1;
+
+/// Creates a basic pool with values:
+/// - Staking asset: 1
+/// - Reward asset: Native
+/// - Reward rate per block: 100
+/// - Lifetime: 100
+/// - Admin: 1
+///
+/// Useful to reduce boilerplate in tests when it's not important to customise or reuse pool
+/// params.
+pub fn create_default_pool() {
+	assert_ok!(StakingRewards::create_pool(
+		RuntimeOrigin::root(),
+		Box::new(DEFAULT_STAKED_ASSET_ID.clone()),
+		Box::new(DEFAULT_REWARD_ASSET_ID.clone()),
+		DEFAULT_REWARD_RATE_PER_BLOCK,
+		DispatchTime::After(DEFAULT_EXPIRE_AFTER),
+		Some(DEFAULT_ADMIN)
+	));
+}
+
+/// The same as [`create_default_pool`], but with the admin parameter set to the creator.
+pub fn create_default_pool_permissioned_admin() {
+	assert_ok!(StakingRewards::create_pool(
+		RuntimeOrigin::root(),
+		Box::new(DEFAULT_STAKED_ASSET_ID.clone()),
+		Box::new(DEFAULT_REWARD_ASSET_ID.clone()),
+		DEFAULT_REWARD_RATE_PER_BLOCK,
+		DispatchTime::After(DEFAULT_EXPIRE_AFTER),
+		Some(PermissionedAccountId::get()),
+	));
+}
+
+fn assert_hypothetically_earned(
+	staker: u128,
+	expected_earned: u128,
+	pool_id: u32,
+	reward_asset_id: NativeOrWithId<u32>,
+) {
+	hypothetically!({
+		// Get the pre-harvest balance.
+		let balance_before: <MockRuntime as Config>::Balance =
+			<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker);
+
+		// Harvest the rewards.
+		assert_ok!(StakingRewards::harvest_rewards(RuntimeOrigin::signed(staker), pool_id, None),);
+
+		// Sanity check: staker rewards are reset to 0 if some `amount` is still staked, otherwise
+		// the storage item removed.
+		if let Some(staker_pool) = PoolStakers::<MockRuntime>::get(pool_id, staker) {
+			assert!(staker_pool.rewards == 0);
+			assert!(staker_pool.amount > 0);
+		}
+
+		// Check that the staker has earned the expected amount.
+		let balance_after =
+			<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker);
+		assert_eq!(balance_after - balance_before, expected_earned);
+	});
+}
+
+fn events() -> Vec<Event<MockRuntime>> {
+	let result = System::events()
+		.into_iter()
+		.map(|r| r.event)
+		.filter_map(|e| {
+			if let mock::RuntimeEvent::StakingRewards(inner) = e {
+				Some(inner)
+			} else {
+				None
+			}
+		})
+		.collect();
+
+	System::reset_events();
+
+	result
+}
+
+fn pools() -> Vec<(u32, PoolInfo<u128, NativeOrWithId<u32>, u128, u64>)> {
+	Pools::<MockRuntime>::iter().collect()
+}
+
+mod create_pool {
+	use super::*;
+
+	#[test]
+	fn success() {
+		new_test_ext().execute_with(|| {
+			assert_eq!(NextPoolId::<MockRuntime>::get(), 0);
+
+			System::set_block_number(10);
+			let expected_expiry_block = DEFAULT_EXPIRE_AFTER + 10;
+
+			// Create a pool with default values, and no admin override so [`PermissionedAccountId`]
+			// is admin.
+			assert_ok!(StakingRewards::create_pool(
+				RuntimeOrigin::root(),
+				Box::new(DEFAULT_STAKED_ASSET_ID),
+				Box::new(DEFAULT_REWARD_ASSET_ID),
+				DEFAULT_REWARD_RATE_PER_BLOCK,
+				DispatchTime::After(DEFAULT_EXPIRE_AFTER),
+				Some(PermissionedAccountId::get())
+			));
+
+			// Event is emitted.
+			assert_eq!(
+				events(),
+				[Event::<MockRuntime>::PoolCreated {
+					creator: PermissionedAccountId::get(),
+					pool_id: 0,
+					staked_asset_id: DEFAULT_STAKED_ASSET_ID,
+					reward_asset_id: DEFAULT_REWARD_ASSET_ID,
+					reward_rate_per_block: DEFAULT_REWARD_RATE_PER_BLOCK,
+					expiry_block: expected_expiry_block,
+					admin: PermissionedAccountId::get(),
+				}]
+			);
+
+			// State is updated correctly.
+			assert_eq!(NextPoolId::<MockRuntime>::get(), 1);
+			assert_eq!(
+				pools(),
+				vec![(
+					0,
+					PoolInfo {
+						staked_asset_id: DEFAULT_STAKED_ASSET_ID,
+						reward_asset_id: DEFAULT_REWARD_ASSET_ID,
+						reward_rate_per_block: DEFAULT_REWARD_RATE_PER_BLOCK,
+						expiry_block: expected_expiry_block,
+						admin: PermissionedAccountId::get(),
+						total_tokens_staked: 0,
+						reward_per_token_stored: 0,
+						last_update_block: 0,
+						account: StakingRewards::pool_account_id(&0),
+					}
+				)]
+			);
+
+			// Create another pool with explicit admin and other overrides.
+			let admin = 2;
+			let staked_asset_id = NativeOrWithId::<u32>::WithId(10);
+			let reward_asset_id = NativeOrWithId::<u32>::WithId(20);
+			let reward_rate_per_block = 250;
+			let expiry_block = 500;
+			let expected_expiry_block = expiry_block + 10;
+			assert_ok!(StakingRewards::create_pool(
+				RuntimeOrigin::root(),
+				Box::new(staked_asset_id.clone()),
+				Box::new(reward_asset_id.clone()),
+				reward_rate_per_block,
+				DispatchTime::After(expiry_block),
+				Some(admin)
+			));
+
+			// Event is emitted.
+			assert_eq!(
+				events(),
+				[Event::<MockRuntime>::PoolCreated {
+					creator: PermissionedAccountId::get(),
+					pool_id: 1,
+					staked_asset_id: staked_asset_id.clone(),
+					reward_asset_id: reward_asset_id.clone(),
+					reward_rate_per_block,
+					admin,
+					expiry_block: expected_expiry_block,
+				}]
+			);
+
+			// State is updated correctly.
+			assert_eq!(NextPoolId::<MockRuntime>::get(), 2);
+			assert_eq!(
+				pools(),
+				vec![
+					(
+						0,
+						PoolInfo {
+							staked_asset_id: DEFAULT_STAKED_ASSET_ID,
+							reward_asset_id: DEFAULT_REWARD_ASSET_ID,
+							reward_rate_per_block: DEFAULT_REWARD_RATE_PER_BLOCK,
+							admin: PermissionedAccountId::get(),
+							expiry_block: DEFAULT_EXPIRE_AFTER + 10,
+							total_tokens_staked: 0,
+							reward_per_token_stored: 0,
+							last_update_block: 0,
+							account: StakingRewards::pool_account_id(&0),
+						}
+					),
+					(
+						1,
+						PoolInfo {
+							staked_asset_id,
+							reward_asset_id,
+							reward_rate_per_block,
+							admin,
+							total_tokens_staked: 0,
+							expiry_block: expected_expiry_block,
+							reward_per_token_stored: 0,
+							last_update_block: 0,
+							account: StakingRewards::pool_account_id(&1),
+						}
+					)
+				]
+			);
+		});
+	}
+
+	#[test]
+	fn success_same_assets() {
+		new_test_ext().execute_with(|| {
+			assert_eq!(NextPoolId::<MockRuntime>::get(), 0);
+
+			System::set_block_number(10);
+			let expected_expiry_block = DEFAULT_EXPIRE_AFTER + 10;
+
+			// Create a pool with the same staking and reward asset.
+			let asset = NativeOrWithId::<u32>::Native;
+			assert_ok!(StakingRewards::create_pool(
+				RuntimeOrigin::root(),
+				Box::new(asset.clone()),
+				Box::new(asset.clone()),
+				DEFAULT_REWARD_RATE_PER_BLOCK,
+				DispatchTime::After(DEFAULT_EXPIRE_AFTER),
+				Some(PermissionedAccountId::get())
+			));
+
+			// Event is emitted.
+			assert_eq!(
+				events(),
+				[Event::<MockRuntime>::PoolCreated {
+					creator: PermissionedAccountId::get(),
+					pool_id: 0,
+					staked_asset_id: asset.clone(),
+					reward_asset_id: asset.clone(),
+					reward_rate_per_block: DEFAULT_REWARD_RATE_PER_BLOCK,
+					expiry_block: expected_expiry_block,
+					admin: PermissionedAccountId::get(),
+				}]
+			);
+
+			// State is updated correctly.
+			assert_eq!(NextPoolId::<MockRuntime>::get(), 1);
+			assert_eq!(
+				pools(),
+				vec![(
+					0,
+					PoolInfo {
+						staked_asset_id: asset.clone(),
+						reward_asset_id: asset,
+						reward_rate_per_block: DEFAULT_REWARD_RATE_PER_BLOCK,
+						expiry_block: expected_expiry_block,
+						admin: PermissionedAccountId::get(),
+						total_tokens_staked: 0,
+						reward_per_token_stored: 0,
+						last_update_block: 0,
+						account: StakingRewards::pool_account_id(&0),
+					}
+				)]
+			);
+		})
+	}
+
+	#[test]
+	fn fails_for_non_existent_asset() {
+		new_test_ext().execute_with(|| {
+			let valid_asset = NativeOrWithId::<u32>::WithId(1);
+			let invalid_asset = NativeOrWithId::<u32>::WithId(200);
+
+			assert_err!(
+				StakingRewards::create_pool(
+					RuntimeOrigin::root(),
+					Box::new(valid_asset.clone()),
+					Box::new(invalid_asset.clone()),
+					10,
+					DispatchTime::After(10u64),
+					None
+				),
+				Error::<MockRuntime>::NonExistentAsset
+			);
+
+			assert_err!(
+				StakingRewards::create_pool(
+					RuntimeOrigin::root(),
+					Box::new(invalid_asset.clone()),
+					Box::new(valid_asset.clone()),
+					10,
+					DispatchTime::After(10u64),
+					None
+				),
+				Error::<MockRuntime>::NonExistentAsset
+			);
+
+			assert_err!(
+				StakingRewards::create_pool(
+					RuntimeOrigin::root(),
+					Box::new(invalid_asset.clone()),
+					Box::new(invalid_asset.clone()),
+					10,
+					DispatchTime::After(10u64),
+					None
+				),
+				Error::<MockRuntime>::NonExistentAsset
+			);
+		})
+	}
+
+	#[test]
+	fn fails_for_not_permissioned() {
+		new_test_ext().execute_with(|| {
+			let user = 100;
+			let staked_asset_id = NativeOrWithId::<u32>::Native;
+			let reward_asset_id = NativeOrWithId::<u32>::WithId(1);
+			let reward_rate_per_block = 100;
+			let expiry_block = 100u64;
+			assert_err!(
+				StakingRewards::create_pool(
+					RuntimeOrigin::signed(user),
+					Box::new(staked_asset_id.clone()),
+					Box::new(reward_asset_id.clone()),
+					reward_rate_per_block,
+					DispatchTime::After(expiry_block),
+					None
+				),
+				BadOrigin
+			);
+		});
+	}
+
+	#[test]
+	fn create_pool_with_caller_admin() {
+		new_test_ext().execute_with(|| {
+			assert_eq!(NextPoolId::<MockRuntime>::get(), 0);
+
+			System::set_block_number(10);
+			let expected_expiry_block = DEFAULT_EXPIRE_AFTER + 10;
+
+			assert_ok!(StakingRewards::create_pool(
+				RuntimeOrigin::root(),
+				Box::new(DEFAULT_STAKED_ASSET_ID),
+				Box::new(DEFAULT_REWARD_ASSET_ID),
+				DEFAULT_REWARD_RATE_PER_BLOCK,
+				DispatchTime::After(DEFAULT_EXPIRE_AFTER),
+				None,
+			));
+
+			assert_eq!(
+				events(),
+				[Event::<MockRuntime>::PoolCreated {
+					creator: PermissionedAccountId::get(),
+					pool_id: 0,
+					staked_asset_id: DEFAULT_STAKED_ASSET_ID,
+					reward_asset_id: DEFAULT_REWARD_ASSET_ID,
+					reward_rate_per_block: DEFAULT_REWARD_RATE_PER_BLOCK,
+					expiry_block: expected_expiry_block,
+					admin: PermissionedAccountId::get(),
+				}]
+			);
+
+			assert_eq!(Pools::<MockRuntime>::get(0).unwrap().admin, PermissionedAccountId::get());
+		});
+	}
+}
+
+mod stake {
+	use super::*;
+
+	#[test]
+	fn success() {
+		new_test_ext().execute_with(|| {
+			let user = 1;
+			create_default_pool();
+			let pool_id = 0;
+			let initial_balance = <Assets as fungibles::Inspect<u128>>::reducible_balance(
+				1,
+				&user,
+				Preservation::Expendable,
+				Fortitude::Force,
+			);
+
+			// User stakes tokens
+			assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(user), pool_id, 1000));
+
+			// Check that the user's staked amount is updated
+			assert_eq!(PoolStakers::<MockRuntime>::get(pool_id, user).unwrap().amount, 1000);
+
+			// Event is emitted.
+			assert_eq!(
+				*events().last().unwrap(),
+				Event::<MockRuntime>::Staked { staker: user, amount: 1000, pool_id: 0 }
+			);
+
+			// Check that the pool's total tokens staked is updated
+			assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().total_tokens_staked, 1000);
+
+			// Check user's frozen balance is updated
+			assert_eq!(
+				<Assets as fungibles::Inspect<u128>>::reducible_balance(
+					1,
+					&user,
+					Preservation::Expendable,
+					Fortitude::Force,
+				),
+				// - extra 1 for ed
+				initial_balance - 1000 - 1
+			);
+
+			// User stakes more tokens
+			assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(user), pool_id, 500));
+
+			// Event is emitted.
+			assert_eq!(
+				*events().last().unwrap(),
+				Event::<MockRuntime>::Staked { staker: user, amount: 500, pool_id: 0 }
+			);
+
+			// Check that the user's staked amount is updated
+			assert_eq!(PoolStakers::<MockRuntime>::get(pool_id, user).unwrap().amount, 1000 + 500);
+
+			// Check that the pool's total tokens staked is updated
+			assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().total_tokens_staked, 1000 + 500);
+
+			assert_eq!(
+				<Assets as fungibles::Inspect<u128>>::reducible_balance(
+					1,
+					&user,
+					Preservation::Expendable,
+					Fortitude::Force,
+				),
+				// - extra 1 for ed
+				initial_balance - 1500 - 1
+			);
+
+			// Event is emitted.
+			assert_eq!(events(), []);
+		});
+	}
+
+	#[test]
+	fn fails_for_non_existent_pool() {
+		new_test_ext().execute_with(|| {
+			let user = 1;
+			assert_err!(
+				StakingRewards::stake(RuntimeOrigin::signed(user), 999, 1000),
+				Error::<MockRuntime>::NonExistentPool
+			);
+		});
+	}
+
+	#[test]
+	fn fails_for_insufficient_balance() {
+		new_test_ext().execute_with(|| {
+			let user = 1;
+			create_default_pool();
+			let pool_id = 0;
+			let initial_balance = <Assets as fungibles::Inspect<u128>>::reducible_balance(
+				1,
+				&user,
+				Preservation::Expendable,
+				Fortitude::Force,
+			);
+			assert_err!(
+				StakingRewards::stake(RuntimeOrigin::signed(user), pool_id, initial_balance + 1),
+				TokenError::FundsUnavailable,
+			);
+		})
+	}
+}
+
+mod unstake {
+	use super::*;
+
+	#[test]
+	fn success() {
+		new_test_ext().execute_with(|| {
+			let user = 1;
+			create_default_pool();
+			let pool_id = 0;
+
+			// User stakes tokens
+			assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(user), pool_id, 1000));
+
+			// User unstakes tokens
+			assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(user), pool_id, 500, None));
+
+			// Event is emitted.
+			assert_eq!(
+				*events().last().unwrap(),
+				Event::<MockRuntime>::Unstaked {
+					caller: user,
+					staker: user,
+					amount: 500,
+					pool_id: 0
+				}
+			);
+
+			// Check that the user's staked amount is updated
+			assert_eq!(PoolStakers::<MockRuntime>::get(pool_id, user).unwrap().amount, 500);
+
+			// Check that the pool's total tokens staked is updated
+			assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().total_tokens_staked, 500);
+
+			// User unstakes remaining tokens
+			assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(user), pool_id, 500, None));
+
+			// Check that the storage items is removed since stake amount and rewards are zero.
+			assert!(PoolStakers::<MockRuntime>::get(pool_id, user).is_none());
+
+			// Check that the pool's total tokens staked is zero
+			assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().total_tokens_staked, 0);
+		});
+	}
+
+	#[test]
+	fn unstake_for_other() {
+		new_test_ext().execute_with(|| {
+			let staker = 1;
+			let caller = 2;
+			let pool_id = 0;
+			let init_block = System::block_number();
+
+			create_default_pool();
+
+			// User stakes tokens
+			assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker), pool_id, 1000));
+
+			// Fails to unstake for other since pool is still active
+			assert_noop!(
+				StakingRewards::unstake(RuntimeOrigin::signed(caller), pool_id, 500, Some(staker)),
+				BadOrigin,
+			);
+
+			System::set_block_number(init_block + DEFAULT_EXPIRE_AFTER + 1);
+
+			assert_ok!(StakingRewards::unstake(
+				RuntimeOrigin::signed(caller),
+				pool_id,
+				500,
+				Some(staker)
+			));
+
+			// Event is emitted.
+			assert_eq!(
+				*events().last().unwrap(),
+				Event::<MockRuntime>::Unstaked { caller, staker, amount: 500, pool_id: 0 }
+			);
+		});
+	}
+
+	#[test]
+	fn fails_for_non_existent_pool() {
+		new_test_ext().execute_with(|| {
+			let user = 1;
+			let non_existent_pool_id = 999;
+
+			// User tries to unstake tokens from a non-existent pool
+			assert_err!(
+				StakingRewards::unstake(
+					RuntimeOrigin::signed(user),
+					non_existent_pool_id,
+					500,
+					None
+				),
+				Error::<MockRuntime>::NonExistentPool
+			);
+		});
+	}
+
+	#[test]
+	fn fails_for_insufficient_staked_amount() {
+		new_test_ext().execute_with(|| {
+			let user = 1;
+			create_default_pool();
+			let pool_id = 0;
+
+			// User stakes tokens
+			assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(user), pool_id, 1000));
+
+			// User tries to unstake more tokens than they have staked
+			assert_err!(
+				StakingRewards::unstake(RuntimeOrigin::signed(user), pool_id, 1500, None),
+				Error::<MockRuntime>::NotEnoughTokens
+			);
+		});
+	}
+}
+
+mod harvest_rewards {
+	use super::*;
+
+	#[test]
+	fn success() {
+		new_test_ext().execute_with(|| {
+			let staker = 1;
+			let pool_id = 0;
+			let reward_asset_id = NativeOrWithId::<u32>::Native;
+			create_default_pool();
+
+			// Stake
+			System::set_block_number(10);
+			assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker), pool_id, 1000));
+
+			// Harvest
+			System::set_block_number(20);
+			let balance_before: <MockRuntime as Config>::Balance =
+				<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker);
+			assert_ok!(StakingRewards::harvest_rewards(
+				RuntimeOrigin::signed(staker),
+				pool_id,
+				None
+			));
+			let balance_after =
+				<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker);
+
+			// Assert
+			assert_eq!(
+				balance_after - balance_before,
+				10 * Pools::<MockRuntime>::get(pool_id).unwrap().reward_rate_per_block
+			);
+			assert_eq!(
+				*events().last().unwrap(),
+				Event::<MockRuntime>::RewardsHarvested {
+					caller: staker,
+					staker,
+					pool_id,
+					amount: 10 * Pools::<MockRuntime>::get(pool_id).unwrap().reward_rate_per_block
+				}
+			);
+		});
+	}
+
+	#[test]
+	fn harvest_for_other() {
+		new_test_ext().execute_with(|| {
+			let caller = 2;
+			let staker = 1;
+			let pool_id = 0;
+			let init_block = System::block_number();
+
+			create_default_pool();
+
+			// Stake
+			System::set_block_number(10);
+			assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker), pool_id, 1000));
+
+			System::set_block_number(20);
+
+			// Fails to harvest for staker since pool is still active
+			assert_noop!(
+				StakingRewards::harvest_rewards(
+					RuntimeOrigin::signed(caller),
+					pool_id,
+					Some(staker)
+				),
+				BadOrigin
+			);
+
+			System::set_block_number(init_block + DEFAULT_EXPIRE_AFTER + 1);
+
+			// Harvest for staker
+			assert_ok!(StakingRewards::harvest_rewards(
+				RuntimeOrigin::signed(caller),
+				pool_id,
+				Some(staker),
+			));
+
+			assert!(matches!(
+				events().last().unwrap(),
+				Event::<MockRuntime>::RewardsHarvested {
+					caller,
+					staker,
+					pool_id,
+					..
+				} if caller == caller && staker == staker && pool_id == pool_id
+			));
+		});
+	}
+
+	#[test]
+	fn fails_for_non_existent_staker() {
+		new_test_ext().execute_with(|| {
+			let non_existent_staker = 999;
+
+			create_default_pool();
+			assert_err!(
+				StakingRewards::harvest_rewards(
+					RuntimeOrigin::signed(non_existent_staker),
+					0,
+					None
+				),
+				Error::<MockRuntime>::NonExistentStaker
+			);
+		});
+	}
+
+	#[test]
+	fn fails_for_non_existent_pool() {
+		new_test_ext().execute_with(|| {
+			let staker = 1;
+			let non_existent_pool_id = 999;
+
+			assert_err!(
+				StakingRewards::harvest_rewards(
+					RuntimeOrigin::signed(staker),
+					non_existent_pool_id,
+					None,
+				),
+				Error::<MockRuntime>::NonExistentPool
+			);
+		});
+	}
+}
+
+mod set_pool_admin {
+	use super::*;
+
+	#[test]
+	fn success_signed_admin() {
+		new_test_ext().execute_with(|| {
+			let admin = 1;
+			let new_admin = 2;
+			let pool_id = 0;
+			create_default_pool();
+
+			// Modify the pool admin
+			assert_ok!(StakingRewards::set_pool_admin(
+				RuntimeOrigin::signed(admin),
+				pool_id,
+				new_admin,
+			));
+
+			// Check state
+			assert_eq!(
+				*events().last().unwrap(),
+				Event::<MockRuntime>::PoolAdminModified { pool_id, new_admin }
+			);
+			assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().admin, new_admin);
+		});
+	}
+
+	#[test]
+	fn success_permissioned_admin() {
+		new_test_ext().execute_with(|| {
+			let new_admin = 2;
+			let pool_id = 0;
+			create_default_pool_permissioned_admin();
+
+			// Modify the pool admin
+			assert_ok!(StakingRewards::set_pool_admin(RuntimeOrigin::root(), pool_id, new_admin));
+
+			// Check state
+			assert_eq!(
+				*events().last().unwrap(),
+				Event::<MockRuntime>::PoolAdminModified { pool_id, new_admin }
+			);
+			assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().admin, new_admin);
+		});
+	}
+
+	#[test]
+	fn fails_for_non_existent_pool() {
+		new_test_ext().execute_with(|| {
+			let admin = 1;
+			let new_admin = 2;
+			let non_existent_pool_id = 999;
+
+			assert_err!(
+				StakingRewards::set_pool_admin(
+					RuntimeOrigin::signed(admin),
+					non_existent_pool_id,
+					new_admin
+				),
+				Error::<MockRuntime>::NonExistentPool
+			);
+		});
+	}
+
+	#[test]
+	fn fails_for_non_admin() {
+		new_test_ext().execute_with(|| {
+			let new_admin = 2;
+			let non_admin = 3;
+			let pool_id = 0;
+			create_default_pool();
+
+			assert_err!(
+				StakingRewards::set_pool_admin(
+					RuntimeOrigin::signed(non_admin),
+					pool_id,
+					new_admin
+				),
+				BadOrigin
+			);
+		});
+	}
+}
+
+mod set_pool_expiry_block {
+	use super::*;
+
+	#[test]
+	fn success_permissioned_admin() {
+		new_test_ext().execute_with(|| {
+			let pool_id = 0;
+			let new_expiry_block = System::block_number() + DEFAULT_EXPIRE_AFTER + 1u64;
+			create_default_pool_permissioned_admin();
+
+			assert_ok!(StakingRewards::set_pool_expiry_block(
+				RuntimeOrigin::root(),
+				pool_id,
+				DispatchTime::At(new_expiry_block),
+			));
+
+			// Check state
+			assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().expiry_block, new_expiry_block);
+			assert_eq!(
+				*events().last().unwrap(),
+				Event::<MockRuntime>::PoolExpiryBlockModified { pool_id, new_expiry_block }
+			);
+		});
+	}
+
+	#[test]
+	fn success_signed_admin() {
+		new_test_ext().execute_with(|| {
+			let admin = 1;
+			let pool_id = 0;
+			let new_expiry_block = System::block_number() + DEFAULT_EXPIRE_AFTER + 1u64;
+			create_default_pool();
+
+			assert_ok!(StakingRewards::set_pool_expiry_block(
+				RuntimeOrigin::signed(admin),
+				pool_id,
+				DispatchTime::At(new_expiry_block)
+			));
+
+			// Check state
+			assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().expiry_block, new_expiry_block);
+			assert_eq!(
+				*events().last().unwrap(),
+				Event::<MockRuntime>::PoolExpiryBlockModified { pool_id, new_expiry_block }
+			);
+		});
+	}
+
+	#[test]
+	fn extends_reward_accumulation() {
+		new_test_ext().execute_with(|| {
+			let admin = 1;
+			let staker = 2;
+			let pool_id = 0;
+			let new_expiry_block = 300u64;
+			System::set_block_number(10);
+			create_default_pool();
+
+			// Regular reward accumulation
+			assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker), pool_id, 1000));
+			System::set_block_number(20);
+			assert_hypothetically_earned(
+				staker,
+				DEFAULT_REWARD_RATE_PER_BLOCK * 10,
+				pool_id,
+				NativeOrWithId::<u32>::Native,
+			);
+
+			// Expiry was block 210, so earned 200 at block 250
+			System::set_block_number(250);
+			assert_hypothetically_earned(
+				staker,
+				DEFAULT_REWARD_RATE_PER_BLOCK * 200,
+				pool_id,
+				NativeOrWithId::<u32>::Native,
+			);
+
+			// Extend expiry 50 more blocks
+			assert_ok!(StakingRewards::set_pool_expiry_block(
+				RuntimeOrigin::signed(admin),
+				pool_id,
+				DispatchTime::At(new_expiry_block)
+			));
+			System::set_block_number(350);
+
+			// Staker has been in pool with rewards active for 250 blocks total
+			assert_hypothetically_earned(
+				staker,
+				DEFAULT_REWARD_RATE_PER_BLOCK * 250,
+				pool_id,
+				NativeOrWithId::<u32>::Native,
+			);
+		});
+	}
+
+	#[test]
+	fn fails_to_cutback_expiration() {
+		new_test_ext().execute_with(|| {
+			let admin = 1;
+			let pool_id = 0;
+			create_default_pool();
+
+			assert_noop!(
+				StakingRewards::set_pool_expiry_block(
+					RuntimeOrigin::signed(admin),
+					pool_id,
+					DispatchTime::After(30)
+				),
+				Error::<MockRuntime>::ExpiryCut
+			);
+		});
+	}
+
+	#[test]
+	fn fails_for_non_existent_pool() {
+		new_test_ext().execute_with(|| {
+			let admin = 1;
+			let non_existent_pool_id = 999;
+			let new_expiry_block = 200u64;
+
+			assert_err!(
+				StakingRewards::set_pool_expiry_block(
+					RuntimeOrigin::signed(admin),
+					non_existent_pool_id,
+					DispatchTime::After(new_expiry_block)
+				),
+				Error::<MockRuntime>::NonExistentPool
+			);
+		});
+	}
+
+	#[test]
+	fn fails_for_non_admin() {
+		new_test_ext().execute_with(|| {
+			let non_admin = 2;
+			let pool_id = 0;
+			let new_expiry_block = 200u64;
+			create_default_pool();
+
+			assert_err!(
+				StakingRewards::set_pool_expiry_block(
+					RuntimeOrigin::signed(non_admin),
+					pool_id,
+					DispatchTime::After(new_expiry_block)
+				),
+				BadOrigin
+			);
+		});
+	}
+
+	#[test]
+	fn fails_for_expiry_block_in_the_past() {
+		new_test_ext().execute_with(|| {
+			let admin = 1;
+			let pool_id = 0;
+			create_default_pool();
+			System::set_block_number(50);
+			assert_err!(
+				StakingRewards::set_pool_expiry_block(
+					RuntimeOrigin::signed(admin),
+					pool_id,
+					DispatchTime::At(40u64)
+				),
+				Error::<MockRuntime>::ExpiryBlockMustBeInTheFuture
+			);
+		});
+	}
+}
+
+mod set_pool_reward_rate_per_block {
+	use super::*;
+
+	#[test]
+	fn success_signed_admin() {
+		new_test_ext().execute_with(|| {
+			let pool_id = 0;
+			let new_reward_rate = 200;
+			create_default_pool();
+
+			// Pool Admin can modify
+			assert_ok!(StakingRewards::set_pool_reward_rate_per_block(
+				RuntimeOrigin::signed(DEFAULT_ADMIN),
+				pool_id,
+				new_reward_rate
+			));
+
+			// Check state
+			assert_eq!(
+				Pools::<MockRuntime>::get(pool_id).unwrap().reward_rate_per_block,
+				new_reward_rate
+			);
+
+			// Check event
+			assert_eq!(
+				*events().last().unwrap(),
+				Event::<MockRuntime>::PoolRewardRateModified {
+					pool_id,
+					new_reward_rate_per_block: new_reward_rate
+				}
+			);
+		});
+	}
+
+	#[test]
+	fn success_permissioned_admin() {
+		new_test_ext().execute_with(|| {
+			let pool_id = 0;
+			let new_reward_rate = 200;
+			create_default_pool_permissioned_admin();
+
+			// Root can modify
+			assert_ok!(StakingRewards::set_pool_reward_rate_per_block(
+				RuntimeOrigin::root(),
+				pool_id,
+				new_reward_rate
+			));
+
+			// Check state
+			assert_eq!(
+				Pools::<MockRuntime>::get(pool_id).unwrap().reward_rate_per_block,
+				new_reward_rate
+			);
+
+			// Check event
+			assert_eq!(
+				*events().last().unwrap(),
+				Event::<MockRuntime>::PoolRewardRateModified {
+					pool_id,
+					new_reward_rate_per_block: new_reward_rate
+				}
+			);
+		});
+	}
+
+	#[test]
+	fn staker_rewards_are_affected_correctly() {
+		new_test_ext().execute_with(|| {
+			let admin = 1;
+			let staker = 2;
+			let pool_id = 0;
+			let new_reward_rate = 150;
+			create_default_pool();
+
+			// Stake some tokens, and accumulate 10 blocks of rewards at the default pool rate (100)
+			System::set_block_number(10);
+			assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker), pool_id, 1000));
+			System::set_block_number(20);
+
+			// Increase the reward rate
+			assert_ok!(StakingRewards::set_pool_reward_rate_per_block(
+				RuntimeOrigin::signed(admin),
+				pool_id,
+				new_reward_rate
+			));
+
+			// Accumulate 10 blocks of rewards at the new rate
+			System::set_block_number(30);
+
+			// Check that rewards are calculated correctly with the updated rate
+			assert_hypothetically_earned(
+				staker,
+				10 * 100 + 10 * new_reward_rate,
+				pool_id,
+				NativeOrWithId::<u32>::Native,
+			);
+		});
+	}
+
+	#[test]
+	fn fails_for_non_existent_pool() {
+		new_test_ext().execute_with(|| {
+			let admin = 1;
+			let non_existent_pool_id = 999;
+			let new_reward_rate = 200;
+
+			assert_err!(
+				StakingRewards::set_pool_reward_rate_per_block(
+					RuntimeOrigin::signed(admin),
+					non_existent_pool_id,
+					new_reward_rate
+				),
+				Error::<MockRuntime>::NonExistentPool
+			);
+		});
+	}
+
+	#[test]
+	fn fails_for_non_admin() {
+		new_test_ext().execute_with(|| {
+			let non_admin = 2;
+			let pool_id = 0;
+			let new_reward_rate = 200;
+			create_default_pool();
+
+			assert_err!(
+				StakingRewards::set_pool_reward_rate_per_block(
+					RuntimeOrigin::signed(non_admin),
+					pool_id,
+					new_reward_rate
+				),
+				BadOrigin
+			);
+		});
+	}
+
+	#[test]
+	fn fails_to_decrease() {
+		new_test_ext().execute_with(|| {
+			create_default_pool_permissioned_admin();
+
+			assert_noop!(
+				StakingRewards::set_pool_reward_rate_per_block(
+					RuntimeOrigin::root(),
+					0,
+					DEFAULT_REWARD_RATE_PER_BLOCK - 1
+				),
+				Error::<MockRuntime>::RewardRateCut
+			);
+		});
+	}
+}
+
+mod deposit_reward_tokens {
+	use super::*;
+
+	#[test]
+	fn success() {
+		new_test_ext().execute_with(|| {
+			let depositor = 1;
+			let pool_id = 0;
+			let amount = 1000;
+			let reward_asset_id = NativeOrWithId::<u32>::Native;
+			create_default_pool();
+			let pool_account_id = StakingRewards::pool_account_id(&pool_id);
+
+			let depositor_balance_before =
+				<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &depositor);
+			let pool_balance_before = <<MockRuntime as Config>::Assets>::balance(
+				reward_asset_id.clone(),
+				&pool_account_id,
+			);
+			assert_ok!(StakingRewards::deposit_reward_tokens(
+				RuntimeOrigin::signed(depositor),
+				pool_id,
+				amount
+			));
+			let depositor_balance_after =
+				<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &depositor);
+			let pool_balance_after =
+				<<MockRuntime as Config>::Assets>::balance(reward_asset_id, &pool_account_id);
+
+			assert_eq!(pool_balance_after - pool_balance_before, amount);
+			assert_eq!(depositor_balance_before - depositor_balance_after, amount);
+		});
+	}
+
+	#[test]
+	fn fails_for_non_existent_pool() {
+		new_test_ext().execute_with(|| {
+			assert_err!(
+				StakingRewards::deposit_reward_tokens(RuntimeOrigin::signed(1), 999, 100),
+				Error::<MockRuntime>::NonExistentPool
+			);
+		});
+	}
+
+	#[test]
+	fn fails_for_insufficient_balance() {
+		new_test_ext().execute_with(|| {
+			create_default_pool();
+			assert_err!(
+				StakingRewards::deposit_reward_tokens(RuntimeOrigin::signed(1), 0, 100_000_000),
+				ArithmeticError::Underflow
+			);
+		});
+	}
+}
+
+mod cleanup_pool {
+	use super::*;
+
+	#[test]
+	fn success() {
+		new_test_ext().execute_with(|| {
+			let pool_id = 0;
+			let admin = DEFAULT_ADMIN;
+			let admin_balance_before = <Balances as fungible::Inspect<u128>>::balance(&admin);
+
+			create_default_pool();
+			assert!(Pools::<MockRuntime>::get(pool_id).is_some());
+
+			assert_ok!(StakingRewards::cleanup_pool(RuntimeOrigin::signed(admin), pool_id));
+
+			assert_eq!(
+				<Balances as fungible::Inspect<u128>>::balance(&admin),
+				// `100_000` initial pool account balance from Genesis config
+				admin_balance_before + 100_000,
+			);
+			assert_eq!(Pools::<MockRuntime>::get(pool_id), None);
+			assert_eq!(PoolStakers::<MockRuntime>::iter_prefix_values(pool_id).count(), 0);
+			assert_eq!(PoolCost::<MockRuntime>::get(pool_id), None);
+		});
+	}
+
+	#[test]
+	fn success_only_when_pool_empty() {
+		new_test_ext().execute_with(|| {
+			let pool_id = 0;
+			let staker = 20;
+			let admin = DEFAULT_ADMIN;
+
+			create_default_pool();
+
+			// stake to prevent pool cleanup
+			assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker), pool_id, 100));
+
+			assert_noop!(
+				StakingRewards::cleanup_pool(RuntimeOrigin::signed(admin), pool_id),
+				Error::<MockRuntime>::NonEmptyPool
+			);
+
+			// unstake partially
+			assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(staker), pool_id, 50, None));
+
+			assert_noop!(
+				StakingRewards::cleanup_pool(RuntimeOrigin::signed(admin), pool_id),
+				Error::<MockRuntime>::NonEmptyPool
+			);
+
+			// unstake all
+			assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(staker), pool_id, 50, None));
+
+			assert_ok!(StakingRewards::cleanup_pool(RuntimeOrigin::signed(admin), pool_id),);
+
+			assert_eq!(Pools::<MockRuntime>::get(pool_id), None);
+			assert_eq!(PoolStakers::<MockRuntime>::iter_prefix_values(pool_id).count(), 0);
+			assert_eq!(PoolCost::<MockRuntime>::get(pool_id), None);
+		});
+	}
+
+	#[test]
+	fn fails_on_wrong_origin() {
+		new_test_ext().execute_with(|| {
+			let caller = 888;
+			let pool_id = 0;
+			create_default_pool();
+
+			assert_noop!(
+				StakingRewards::cleanup_pool(RuntimeOrigin::signed(caller), pool_id),
+				BadOrigin
+			);
+		});
+	}
+}
+
+/// This integration test
+/// 1. Considers 2 stakers each staking and unstaking at different intervals, asserts their
+///    claimable rewards are adjusted as expected, and that harvesting works.
+/// 2. Checks that rewards are correctly halted after the pool's expiry block, and resume when the
+///    pool is extended.
+/// 3. Checks that reward rates adjustment works correctly.
+///
+/// Note: There are occasionally off by 1 errors due to rounding. In practice this is
+/// insignificant.
+#[test]
+fn integration() {
+	new_test_ext().execute_with(|| {
+		let admin = 1;
+		let staker1 = 10u128;
+		let staker2 = 20;
+		let staked_asset_id = NativeOrWithId::<u32>::WithId(1);
+		let reward_asset_id = NativeOrWithId::<u32>::Native;
+		let reward_rate_per_block = 100;
+		let lifetime = 24u64.into();
+		System::set_block_number(1);
+		assert_ok!(StakingRewards::create_pool(
+			RuntimeOrigin::root(),
+			Box::new(staked_asset_id.clone()),
+			Box::new(reward_asset_id.clone()),
+			reward_rate_per_block,
+			DispatchTime::After(lifetime),
+			Some(admin)
+		));
+		let pool_id = 0;
+
+		// Block 7: Staker 1 stakes 100 tokens.
+		System::set_block_number(7);
+		assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker1), pool_id, 100));
+		// At this point
+		// - Staker 1 has earned 0 tokens.
+		// - Staker 1 is earning 100 tokens per block.
+
+		// Check that Staker 1 has earned 0 tokens.
+		assert_hypothetically_earned(staker1, 0, pool_id, reward_asset_id.clone());
+
+		// Block 9: Staker 2 stakes 100 tokens.
+		System::set_block_number(9);
+		assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker2), pool_id, 100));
+		// At this point
+		// - Staker 1 has earned 200 (100*2) tokens.
+		// - Staker 2 has earned 0 tokens.
+		// - Staker 1 is earning 50 tokens per block.
+		// - Staker 2 is earning 50 tokens per block.
+
+		// Check that Staker 1 has earned 200 tokens and Staker 2 has earned 0 tokens.
+		assert_hypothetically_earned(staker1, 200, pool_id, reward_asset_id.clone());
+		assert_hypothetically_earned(staker2, 0, pool_id, reward_asset_id.clone());
+
+		// Block 12: Staker 1 stakes an additional 100 tokens.
+		System::set_block_number(12);
+		assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker1), pool_id, 100));
+		// At this point
+		// - Staker 1 has earned 350 (200 + (50 * 3)) tokens.
+		// - Staker 2 has earned 150 (50 * 3) tokens.
+		// - Staker 1 is earning 66.66 tokens per block.
+		// - Staker 2 is earning 33.33 tokens per block.
+
+		// Check that Staker 1 has earned 350 tokens and Staker 2 has earned 150 tokens.
+		assert_hypothetically_earned(staker1, 350, pool_id, reward_asset_id.clone());
+		assert_hypothetically_earned(staker2, 150, pool_id, reward_asset_id.clone());
+
+		// Block 22: Staker 1 unstakes 100 tokens.
+		System::set_block_number(22);
+		assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(staker1), pool_id, 100, None));
+		// - Staker 1 has earned 1016 (350 + 66.66 * 10) tokens.
+		// - Staker 2 has earned 483 (150 + 33.33 * 10) tokens.
+		// - Staker 1 is earning 50 tokens per block.
+		// - Staker 2 is earning 50 tokens per block.
+		assert_hypothetically_earned(staker1, 1016, pool_id, reward_asset_id.clone());
+		assert_hypothetically_earned(staker2, 483, pool_id, reward_asset_id.clone());
+
+		// Block 23: Staker 1 unstakes 100 tokens.
+		System::set_block_number(23);
+		assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(staker1), pool_id, 100, None));
+		// - Staker 1 has earned 1065 (1015 + 50) tokens.
+		// - Staker 2 has earned 533 (483 + 50) tokens.
+		// - Staker 1 is earning 0 tokens per block.
+		// - Staker 2 is earning 100 tokens per block.
+		assert_hypothetically_earned(staker1, 1066, pool_id, reward_asset_id.clone());
+		assert_hypothetically_earned(staker2, 533, pool_id, reward_asset_id.clone());
+
+		// Block 50: Stakers should only have earned 2 blocks worth of tokens (expiry is 25).
+		System::set_block_number(50);
+		// - Staker 1 has earned 1065 tokens.
+		// - Staker 2 has earned 733 (533 + 2 * 100) tokens.
+		// - Staker 1 is earning 0 tokens per block.
+		// - Staker 2 is earning 0 tokens per block.
+		assert_hypothetically_earned(staker1, 1066, pool_id, reward_asset_id.clone());
+		assert_hypothetically_earned(staker2, 733, pool_id, reward_asset_id.clone());
+
+		// Block 51: Extend the pool expiry block to 60.
+		System::set_block_number(51);
+		// - Staker 1 is earning 0 tokens per block.
+		// - Staker 2 is earning 100 tokens per block.
+		assert_ok!(StakingRewards::set_pool_expiry_block(
+			RuntimeOrigin::signed(admin),
+			pool_id,
+			DispatchTime::At(60u64),
+		));
+		assert_hypothetically_earned(staker1, 1066, pool_id, reward_asset_id.clone());
+		assert_hypothetically_earned(staker2, 733, pool_id, reward_asset_id.clone());
+
+		// Block 53: Check rewards are resumed.
+		// - Staker 1 has earned 1065 tokens.
+		// - Staker 2 has earned 933 (733 + 2 * 100) tokens.
+		// - Staker 2 is earning 100 tokens per block.
+		System::set_block_number(53);
+		assert_hypothetically_earned(staker1, 1066, pool_id, reward_asset_id.clone());
+		assert_hypothetically_earned(staker2, 933, pool_id, reward_asset_id.clone());
+
+		// Block 55: Increase the block reward.
+		// - Staker 1 has earned 1065 tokens.
+		// - Staker 2 has earned 1133 (933 + 2 * 100) tokens.
+		// - Staker 2 is earning 50 tokens per block.
+		System::set_block_number(55);
+		assert_ok!(StakingRewards::set_pool_reward_rate_per_block(
+			RuntimeOrigin::signed(admin),
+			pool_id,
+			150
+		));
+		assert_hypothetically_earned(staker1, 1066, pool_id, reward_asset_id.clone());
+		assert_hypothetically_earned(staker2, 1133, pool_id, reward_asset_id.clone());
+
+		// Block 57: Staker2 harvests their rewards.
+		System::set_block_number(57);
+		// - Staker 2 has earned 1433 (1133 + 2 * 150) tokens.
+		assert_hypothetically_earned(staker2, 1433, pool_id, reward_asset_id.clone());
+		// Get the pre-harvest balance.
+		let balance_before: <MockRuntime as Config>::Balance =
+			<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker2);
+		assert_ok!(StakingRewards::harvest_rewards(RuntimeOrigin::signed(staker2), pool_id, None));
+		let balance_after =
+			<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker2);
+		assert_eq!(balance_after - balance_before, 1433u128);
+
+		// Block 60: Check rewards were adjusted correctly.
+		// - Staker 1 has earned 1065 tokens.
+		// - Staker 2 has earned 450 (3 * 150) tokens.
+		System::set_block_number(60);
+		assert_hypothetically_earned(staker1, 1066, pool_id, reward_asset_id.clone());
+		assert_hypothetically_earned(staker2, 450, pool_id, reward_asset_id.clone());
+
+		// Finally, check events.
+		assert_eq!(
+			events(),
+			[
+				Event::PoolCreated {
+					creator: PermissionedAccountId::get(),
+					pool_id,
+					staked_asset_id,
+					reward_asset_id,
+					reward_rate_per_block: 100,
+					expiry_block: 25,
+					admin,
+				},
+				Event::Staked { staker: staker1, pool_id, amount: 100 },
+				Event::Staked { staker: staker2, pool_id, amount: 100 },
+				Event::Staked { staker: staker1, pool_id, amount: 100 },
+				Event::Unstaked { caller: staker1, staker: staker1, pool_id, amount: 100 },
+				Event::Unstaked { caller: staker1, staker: staker1, pool_id, amount: 100 },
+				Event::PoolExpiryBlockModified { pool_id, new_expiry_block: 60 },
+				Event::PoolRewardRateModified { pool_id, new_reward_rate_per_block: 150 },
+				Event::RewardsHarvested { caller: staker2, staker: staker2, pool_id, amount: 1433 }
+			]
+		);
+	});
+}
diff --git a/substrate/frame/asset-rewards/src/weights.rs b/substrate/frame/asset-rewards/src/weights.rs
new file mode 100644
index 00000000000..c9e2d0fd251
--- /dev/null
+++ b/substrate/frame/asset-rewards/src/weights.rs
@@ -0,0 +1,368 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// 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.
+
+//! Autogenerated weights for `pallet_asset_rewards`
+//!
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0
+//! DATE: 2025-01-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! WORST CASE MAP SIZE: `1000000`
+//! HOSTNAME: `runner-ys-ssygq-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
+//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024`
+
+// Executed Command:
+// target/production/substrate-node
+// benchmark
+// pallet
+// --steps=50
+// --repeat=20
+// --extrinsic=*
+// --wasm-execution=compiled
+// --heap-pages=4096
+// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json
+// --pallet=pallet_asset_rewards
+// --chain=dev
+// --header=./substrate/HEADER-APACHE2
+// --output=./substrate/frame/asset-rewards/src/weights.rs
+// --template=./substrate/.maintain/frame-weight-template.hbs
+
+#![cfg_attr(rustfmt, rustfmt_skip)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+#![allow(missing_docs)]
+
+use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
+use core::marker::PhantomData;
+
+/// Weight functions needed for `pallet_asset_rewards`.
+pub trait WeightInfo {
+	fn create_pool() -> Weight;
+	fn stake() -> Weight;
+	fn unstake() -> Weight;
+	fn harvest_rewards() -> Weight;
+	fn set_pool_reward_rate_per_block() -> Weight;
+	fn set_pool_admin() -> Weight;
+	fn set_pool_expiry_block() -> Weight;
+	fn deposit_reward_tokens() -> Weight;
+	fn cleanup_pool() -> Weight;
+}
+
+/// Weights for `pallet_asset_rewards` using the Substrate node and recommended hardware.
+pub struct SubstrateWeight<T>(PhantomData<T>);
+impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
+	/// Storage: `Assets::Asset` (r:2 w:0)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::NextPoolId` (r:1 w:1)
+	/// Proof: `AssetRewards::NextPoolId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `Balances::Holds` (r:1 w:1)
+	/// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(373), added: 2848, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolCost` (r:0 w:1)
+	/// Proof: `AssetRewards::PoolCost` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::Pools` (r:0 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	fn create_pool() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `495`
+		//  Estimated: `6360`
+		// Minimum execution time: 62_655_000 picoseconds.
+		Weight::from_parts(63_723_000, 6360)
+			.saturating_add(T::DbWeight::get().reads(5_u64))
+			.saturating_add(T::DbWeight::get().writes(5_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
+	/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:1 w:0)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	fn stake() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `935`
+		//  Estimated: `3615`
+		// Minimum execution time: 54_463_000 picoseconds.
+		Weight::from_parts(55_974_000, 3615)
+			.saturating_add(T::DbWeight::get().reads(5_u64))
+			.saturating_add(T::DbWeight::get().writes(4_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
+	/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:1 w:0)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	fn unstake() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `935`
+		//  Estimated: `3615`
+		// Minimum execution time: 55_749_000 picoseconds.
+		Weight::from_parts(57_652_000, 3615)
+			.saturating_add(T::DbWeight::get().reads(5_u64))
+			.saturating_add(T::DbWeight::get().writes(4_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:0)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	fn harvest_rewards() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `1021`
+		//  Estimated: `6208`
+		// Minimum execution time: 69_372_000 picoseconds.
+		Weight::from_parts(70_278_000, 6208)
+			.saturating_add(T::DbWeight::get().reads(5_u64))
+			.saturating_add(T::DbWeight::get().writes(4_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	fn set_pool_reward_rate_per_block() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `347`
+		//  Estimated: `3615`
+		// Minimum execution time: 19_284_000 picoseconds.
+		Weight::from_parts(19_791_000, 3615)
+			.saturating_add(T::DbWeight::get().reads(1_u64))
+			.saturating_add(T::DbWeight::get().writes(1_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	fn set_pool_admin() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `347`
+		//  Estimated: `3615`
+		// Minimum execution time: 17_388_000 picoseconds.
+		Weight::from_parts(18_390_000, 3615)
+			.saturating_add(T::DbWeight::get().reads(1_u64))
+			.saturating_add(T::DbWeight::get().writes(1_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	fn set_pool_expiry_block() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `347`
+		//  Estimated: `3615`
+		// Minimum execution time: 19_780_000 picoseconds.
+		Weight::from_parts(20_676_000, 3615)
+			.saturating_add(T::DbWeight::get().reads(1_u64))
+			.saturating_add(T::DbWeight::get().writes(1_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:0)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	fn deposit_reward_tokens() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `840`
+		//  Estimated: `6208`
+		// Minimum execution time: 57_746_000 picoseconds.
+		Weight::from_parts(59_669_000, 6208)
+			.saturating_add(T::DbWeight::get().reads(5_u64))
+			.saturating_add(T::DbWeight::get().writes(4_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:0)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:2 w:2)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolCost` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolCost` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`)
+	/// Storage: `Balances::Holds` (r:1 w:1)
+	/// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(373), added: 2848, mode: `MaxEncodedLen`)
+	fn cleanup_pool() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `1236`
+		//  Estimated: `6208`
+		// Minimum execution time: 110_443_000 picoseconds.
+		Weight::from_parts(113_149_000, 6208)
+			.saturating_add(T::DbWeight::get().reads(9_u64))
+			.saturating_add(T::DbWeight::get().writes(8_u64))
+	}
+}
+
+// For backwards compatibility and tests.
+impl WeightInfo for () {
+	/// Storage: `Assets::Asset` (r:2 w:0)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::NextPoolId` (r:1 w:1)
+	/// Proof: `AssetRewards::NextPoolId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `Balances::Holds` (r:1 w:1)
+	/// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(373), added: 2848, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolCost` (r:0 w:1)
+	/// Proof: `AssetRewards::PoolCost` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::Pools` (r:0 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	fn create_pool() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `495`
+		//  Estimated: `6360`
+		// Minimum execution time: 62_655_000 picoseconds.
+		Weight::from_parts(63_723_000, 6360)
+			.saturating_add(RocksDbWeight::get().reads(5_u64))
+			.saturating_add(RocksDbWeight::get().writes(5_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
+	/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:1 w:0)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	fn stake() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `935`
+		//  Estimated: `3615`
+		// Minimum execution time: 54_463_000 picoseconds.
+		Weight::from_parts(55_974_000, 3615)
+			.saturating_add(RocksDbWeight::get().reads(5_u64))
+			.saturating_add(RocksDbWeight::get().writes(4_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
+	/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:1 w:0)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
+	/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	fn unstake() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `935`
+		//  Estimated: `3615`
+		// Minimum execution time: 55_749_000 picoseconds.
+		Weight::from_parts(57_652_000, 3615)
+			.saturating_add(RocksDbWeight::get().reads(5_u64))
+			.saturating_add(RocksDbWeight::get().writes(4_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:0)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	fn harvest_rewards() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `1021`
+		//  Estimated: `6208`
+		// Minimum execution time: 69_372_000 picoseconds.
+		Weight::from_parts(70_278_000, 6208)
+			.saturating_add(RocksDbWeight::get().reads(5_u64))
+			.saturating_add(RocksDbWeight::get().writes(4_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	fn set_pool_reward_rate_per_block() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `347`
+		//  Estimated: `3615`
+		// Minimum execution time: 19_284_000 picoseconds.
+		Weight::from_parts(19_791_000, 3615)
+			.saturating_add(RocksDbWeight::get().reads(1_u64))
+			.saturating_add(RocksDbWeight::get().writes(1_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	fn set_pool_admin() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `347`
+		//  Estimated: `3615`
+		// Minimum execution time: 17_388_000 picoseconds.
+		Weight::from_parts(18_390_000, 3615)
+			.saturating_add(RocksDbWeight::get().reads(1_u64))
+			.saturating_add(RocksDbWeight::get().writes(1_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	fn set_pool_expiry_block() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `347`
+		//  Estimated: `3615`
+		// Minimum execution time: 19_780_000 picoseconds.
+		Weight::from_parts(20_676_000, 3615)
+			.saturating_add(RocksDbWeight::get().reads(1_u64))
+			.saturating_add(RocksDbWeight::get().writes(1_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:0)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:1 w:1)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	fn deposit_reward_tokens() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `840`
+		//  Estimated: `6208`
+		// Minimum execution time: 57_746_000 picoseconds.
+		Weight::from_parts(59_669_000, 6208)
+			.saturating_add(RocksDbWeight::get().reads(5_u64))
+			.saturating_add(RocksDbWeight::get().writes(4_u64))
+	}
+	/// Storage: `AssetRewards::Pools` (r:1 w:1)
+	/// Proof: `AssetRewards::Pools` (`max_values`: None, `max_size`: Some(150), added: 2625, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolStakers` (r:1 w:0)
+	/// Proof: `AssetRewards::PoolStakers` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Asset` (r:1 w:1)
+	/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
+	/// Storage: `Assets::Account` (r:2 w:2)
+	/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
+	/// Storage: `System::Account` (r:2 w:2)
+	/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
+	/// Storage: `AssetRewards::PoolCost` (r:1 w:1)
+	/// Proof: `AssetRewards::PoolCost` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`)
+	/// Storage: `Balances::Holds` (r:1 w:1)
+	/// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(373), added: 2848, mode: `MaxEncodedLen`)
+	fn cleanup_pool() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `1236`
+		//  Estimated: `6208`
+		// Minimum execution time: 110_443_000 picoseconds.
+		Weight::from_parts(113_149_000, 6208)
+			.saturating_add(RocksDbWeight::get().reads(9_u64))
+			.saturating_add(RocksDbWeight::get().writes(8_u64))
+	}
+}
diff --git a/substrate/frame/support/src/traits.rs b/substrate/frame/support/src/traits.rs
index 728426cc84c..4a83c809a6a 100644
--- a/substrate/frame/support/src/traits.rs
+++ b/substrate/frame/support/src/traits.rs
@@ -96,8 +96,9 @@ mod storage;
 #[cfg(feature = "experimental")]
 pub use storage::MaybeConsideration;
 pub use storage::{
-	Consideration, Footprint, Incrementable, Instance, LinearStoragePrice, PartialStorageInfoTrait,
-	StorageInfo, StorageInfoTrait, StorageInstance, TrackedStorageKey, WhitelistedStorageKeys,
+	Consideration, ConstantStoragePrice, Footprint, Incrementable, Instance, LinearStoragePrice,
+	PartialStorageInfoTrait, StorageInfo, StorageInfoTrait, StorageInstance, TrackedStorageKey,
+	WhitelistedStorageKeys,
 };
 
 mod dispatch;
diff --git a/substrate/frame/support/src/traits/storage.rs b/substrate/frame/support/src/traits/storage.rs
index 2b8e4370738..676b73e03d3 100644
--- a/substrate/frame/support/src/traits/storage.rs
+++ b/substrate/frame/support/src/traits/storage.rs
@@ -200,6 +200,18 @@ where
 	}
 }
 
+/// Constant `Price` regardless of the given [`Footprint`].
+pub struct ConstantStoragePrice<Price, Balance>(PhantomData<(Price, Balance)>);
+impl<Price, Balance> Convert<Footprint, Balance> for ConstantStoragePrice<Price, Balance>
+where
+	Price: Get<Balance>,
+	Balance: From<u64> + sp_runtime::Saturating,
+{
+	fn convert(_: Footprint) -> Balance {
+		Price::get()
+	}
+}
+
 /// Some sort of cost taken from account temporarily in order to offset the cost to the chain of
 /// holding some data [`Footprint`] in state.
 ///
diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml
index 17a7c02e825..fc0b2d5a140 100644
--- a/umbrella/Cargo.toml
+++ b/umbrella/Cargo.toml
@@ -57,6 +57,7 @@ std = [
 	"pallet-asset-conversion-tx-payment?/std",
 	"pallet-asset-conversion?/std",
 	"pallet-asset-rate?/std",
+	"pallet-asset-rewards?/std",
 	"pallet-asset-tx-payment?/std",
 	"pallet-assets-freezer?/std",
 	"pallet-assets?/std",
@@ -256,6 +257,7 @@ runtime-benchmarks = [
 	"pallet-asset-conversion-tx-payment?/runtime-benchmarks",
 	"pallet-asset-conversion?/runtime-benchmarks",
 	"pallet-asset-rate?/runtime-benchmarks",
+	"pallet-asset-rewards?/runtime-benchmarks",
 	"pallet-asset-tx-payment?/runtime-benchmarks",
 	"pallet-assets-freezer?/runtime-benchmarks",
 	"pallet-assets?/runtime-benchmarks",
@@ -386,6 +388,7 @@ try-runtime = [
 	"pallet-asset-conversion-tx-payment?/try-runtime",
 	"pallet-asset-conversion?/try-runtime",
 	"pallet-asset-rate?/try-runtime",
+	"pallet-asset-rewards?/try-runtime",
 	"pallet-asset-tx-payment?/try-runtime",
 	"pallet-assets-freezer?/try-runtime",
 	"pallet-assets?/try-runtime",
@@ -543,7 +546,7 @@ with-tracing = [
 	"sp-tracing?/with-tracing",
 	"sp-tracing?/with-tracing",
 ]
-runtime-full = ["assets-common", "binary-merkle-tree", "bp-header-chain", "bp-messages", "bp-parachains", "bp-polkadot", "bp-polkadot-core", "bp-relayers", "bp-runtime", "bp-test-utils", "bp-xcm-bridge-hub", "bp-xcm-bridge-hub-router", "bridge-hub-common", "bridge-runtime-common", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", "cumulus-pallet-parachain-system-proc-macro", "cumulus-pallet-session-benchmarking", "cumulus-pallet-solo-to-para", "cumulus-pallet-weight-reclaim", "cumulus-pallet-xcm", "cumulus-pallet-xcmp-queue", "cumulus-ping", "cumulus-primitives-aura", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", "cumulus-primitives-proof-size-hostfunction", "cumulus-primitives-storage-weight-reclaim", "cumulus-primitives-timestamp", "cumulus-primitives-utility", "frame-benchmarking", "frame-benchmarking-pallet-pov", "frame-election-provider-solution-type", "frame-election-provider-support", "frame-executive", "frame-metadata-hash-extension", "frame-support", "frame-support-procedural", "frame-support-procedural-tools-derive", "frame-system", "frame-system-benchmarking", "frame-system-rpc-runtime-api", "frame-try-runtime", "pallet-alliance", "pallet-asset-conversion", "pallet-asset-conversion-ops", "pallet-asset-conversion-tx-payment", "pallet-asset-rate", "pallet-asset-tx-payment", "pallet-assets", "pallet-assets-freezer", "pallet-atomic-swap", "pallet-aura", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", "pallet-bags-list", "pallet-balances", "pallet-beefy", "pallet-beefy-mmr", "pallet-bounties", "pallet-bridge-grandpa", "pallet-bridge-messages", "pallet-bridge-parachains", "pallet-bridge-relayers", "pallet-broker", "pallet-child-bounties", "pallet-collator-selection", "pallet-collective", "pallet-collective-content", "pallet-contracts", "pallet-contracts-proc-macro", "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", "pallet-delegated-staking", "pallet-democracy", "pallet-dev-mode", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", "pallet-elections-phragmen", "pallet-fast-unstake", "pallet-glutton", "pallet-grandpa", "pallet-identity", "pallet-im-online", "pallet-indices", "pallet-insecure-randomness-collective-flip", "pallet-lottery", "pallet-membership", "pallet-message-queue", "pallet-migrations", "pallet-mixnet", "pallet-mmr", "pallet-multisig", "pallet-nft-fractionalization", "pallet-nfts", "pallet-nfts-runtime-api", "pallet-nis", "pallet-node-authorization", "pallet-nomination-pools", "pallet-nomination-pools-benchmarking", "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-offences-benchmarking", "pallet-paged-list", "pallet-parameters", "pallet-preimage", "pallet-proxy", "pallet-ranked-collective", "pallet-recovery", "pallet-referenda", "pallet-remark", "pallet-revive", "pallet-revive-proc-macro", "pallet-revive-uapi", "pallet-root-offences", "pallet-root-testing", "pallet-safe-mode", "pallet-salary", "pallet-scheduler", "pallet-scored-pool", "pallet-session", "pallet-session-benchmarking", "pallet-skip-feeless-payment", "pallet-society", "pallet-staking", "pallet-staking-reward-curve", "pallet-staking-reward-fn", "pallet-staking-runtime-api", "pallet-state-trie-migration", "pallet-statement", "pallet-sudo", "pallet-timestamp", "pallet-tips", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-transaction-storage", "pallet-treasury", "pallet-tx-pause", "pallet-uniques", "pallet-utility", "pallet-verify-signature", "pallet-vesting", "pallet-whitelist", "pallet-xcm", "pallet-xcm-benchmarks", "pallet-xcm-bridge-hub", "pallet-xcm-bridge-hub-router", "parachains-common", "polkadot-core-primitives", "polkadot-parachain-primitives", "polkadot-primitives", "polkadot-runtime-common", "polkadot-runtime-metrics", "polkadot-runtime-parachains", "polkadot-sdk-frame", "sc-chain-spec-derive", "sc-tracing-proc-macro", "slot-range-helper", "snowbridge-beacon-primitives", "snowbridge-core", "snowbridge-ethereum", "snowbridge-outbound-queue-merkle-tree", "snowbridge-outbound-queue-runtime-api", "snowbridge-pallet-ethereum-client", "snowbridge-pallet-ethereum-client-fixtures", "snowbridge-pallet-inbound-queue", "snowbridge-pallet-inbound-queue-fixtures", "snowbridge-pallet-outbound-queue", "snowbridge-pallet-system", "snowbridge-router-primitives", "snowbridge-runtime-common", "snowbridge-system-runtime-api", "sp-api", "sp-api-proc-macro", "sp-application-crypto", "sp-arithmetic", "sp-authority-discovery", "sp-block-builder", "sp-consensus-aura", "sp-consensus-babe", "sp-consensus-beefy", "sp-consensus-grandpa", "sp-consensus-pow", "sp-consensus-slots", "sp-core", "sp-crypto-ec-utils", "sp-crypto-hashing", "sp-crypto-hashing-proc-macro", "sp-debug-derive", "sp-externalities", "sp-genesis-builder", "sp-inherents", "sp-io", "sp-keyring", "sp-keystore", "sp-metadata-ir", "sp-mixnet", "sp-mmr-primitives", "sp-npos-elections", "sp-offchain", "sp-runtime", "sp-runtime-interface", "sp-runtime-interface-proc-macro", "sp-session", "sp-staking", "sp-state-machine", "sp-statement-store", "sp-std", "sp-storage", "sp-timestamp", "sp-tracing", "sp-transaction-pool", "sp-transaction-storage-proof", "sp-trie", "sp-version", "sp-version-proc-macro", "sp-wasm-interface", "sp-weights", "staging-parachain-info", "staging-xcm", "staging-xcm-builder", "staging-xcm-executor", "substrate-bip39", "testnet-parachains-constants", "tracing-gum-proc-macro", "xcm-procedural", "xcm-runtime-apis"]
+runtime-full = ["assets-common", "binary-merkle-tree", "bp-header-chain", "bp-messages", "bp-parachains", "bp-polkadot", "bp-polkadot-core", "bp-relayers", "bp-runtime", "bp-test-utils", "bp-xcm-bridge-hub", "bp-xcm-bridge-hub-router", "bridge-hub-common", "bridge-runtime-common", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", "cumulus-pallet-parachain-system-proc-macro", "cumulus-pallet-session-benchmarking", "cumulus-pallet-solo-to-para", "cumulus-pallet-weight-reclaim", "cumulus-pallet-xcm", "cumulus-pallet-xcmp-queue", "cumulus-ping", "cumulus-primitives-aura", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", "cumulus-primitives-proof-size-hostfunction", "cumulus-primitives-storage-weight-reclaim", "cumulus-primitives-timestamp", "cumulus-primitives-utility", "frame-benchmarking", "frame-benchmarking-pallet-pov", "frame-election-provider-solution-type", "frame-election-provider-support", "frame-executive", "frame-metadata-hash-extension", "frame-support", "frame-support-procedural", "frame-support-procedural-tools-derive", "frame-system", "frame-system-benchmarking", "frame-system-rpc-runtime-api", "frame-try-runtime", "pallet-alliance", "pallet-asset-conversion", "pallet-asset-conversion-ops", "pallet-asset-conversion-tx-payment", "pallet-asset-rate", "pallet-asset-rewards", "pallet-asset-tx-payment", "pallet-assets", "pallet-assets-freezer", "pallet-atomic-swap", "pallet-aura", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", "pallet-bags-list", "pallet-balances", "pallet-beefy", "pallet-beefy-mmr", "pallet-bounties", "pallet-bridge-grandpa", "pallet-bridge-messages", "pallet-bridge-parachains", "pallet-bridge-relayers", "pallet-broker", "pallet-child-bounties", "pallet-collator-selection", "pallet-collective", "pallet-collective-content", "pallet-contracts", "pallet-contracts-proc-macro", "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", "pallet-delegated-staking", "pallet-democracy", "pallet-dev-mode", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", "pallet-elections-phragmen", "pallet-fast-unstake", "pallet-glutton", "pallet-grandpa", "pallet-identity", "pallet-im-online", "pallet-indices", "pallet-insecure-randomness-collective-flip", "pallet-lottery", "pallet-membership", "pallet-message-queue", "pallet-migrations", "pallet-mixnet", "pallet-mmr", "pallet-multisig", "pallet-nft-fractionalization", "pallet-nfts", "pallet-nfts-runtime-api", "pallet-nis", "pallet-node-authorization", "pallet-nomination-pools", "pallet-nomination-pools-benchmarking", "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-offences-benchmarking", "pallet-paged-list", "pallet-parameters", "pallet-preimage", "pallet-proxy", "pallet-ranked-collective", "pallet-recovery", "pallet-referenda", "pallet-remark", "pallet-revive", "pallet-revive-proc-macro", "pallet-revive-uapi", "pallet-root-offences", "pallet-root-testing", "pallet-safe-mode", "pallet-salary", "pallet-scheduler", "pallet-scored-pool", "pallet-session", "pallet-session-benchmarking", "pallet-skip-feeless-payment", "pallet-society", "pallet-staking", "pallet-staking-reward-curve", "pallet-staking-reward-fn", "pallet-staking-runtime-api", "pallet-state-trie-migration", "pallet-statement", "pallet-sudo", "pallet-timestamp", "pallet-tips", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-transaction-storage", "pallet-treasury", "pallet-tx-pause", "pallet-uniques", "pallet-utility", "pallet-verify-signature", "pallet-vesting", "pallet-whitelist", "pallet-xcm", "pallet-xcm-benchmarks", "pallet-xcm-bridge-hub", "pallet-xcm-bridge-hub-router", "parachains-common", "polkadot-core-primitives", "polkadot-parachain-primitives", "polkadot-primitives", "polkadot-runtime-common", "polkadot-runtime-metrics", "polkadot-runtime-parachains", "polkadot-sdk-frame", "sc-chain-spec-derive", "sc-tracing-proc-macro", "slot-range-helper", "snowbridge-beacon-primitives", "snowbridge-core", "snowbridge-ethereum", "snowbridge-outbound-queue-merkle-tree", "snowbridge-outbound-queue-runtime-api", "snowbridge-pallet-ethereum-client", "snowbridge-pallet-ethereum-client-fixtures", "snowbridge-pallet-inbound-queue", "snowbridge-pallet-inbound-queue-fixtures", "snowbridge-pallet-outbound-queue", "snowbridge-pallet-system", "snowbridge-router-primitives", "snowbridge-runtime-common", "snowbridge-system-runtime-api", "sp-api", "sp-api-proc-macro", "sp-application-crypto", "sp-arithmetic", "sp-authority-discovery", "sp-block-builder", "sp-consensus-aura", "sp-consensus-babe", "sp-consensus-beefy", "sp-consensus-grandpa", "sp-consensus-pow", "sp-consensus-slots", "sp-core", "sp-crypto-ec-utils", "sp-crypto-hashing", "sp-crypto-hashing-proc-macro", "sp-debug-derive", "sp-externalities", "sp-genesis-builder", "sp-inherents", "sp-io", "sp-keyring", "sp-keystore", "sp-metadata-ir", "sp-mixnet", "sp-mmr-primitives", "sp-npos-elections", "sp-offchain", "sp-runtime", "sp-runtime-interface", "sp-runtime-interface-proc-macro", "sp-session", "sp-staking", "sp-state-machine", "sp-statement-store", "sp-std", "sp-storage", "sp-timestamp", "sp-tracing", "sp-transaction-pool", "sp-transaction-storage-proof", "sp-trie", "sp-version", "sp-version-proc-macro", "sp-wasm-interface", "sp-weights", "staging-parachain-info", "staging-xcm", "staging-xcm-builder", "staging-xcm-executor", "substrate-bip39", "testnet-parachains-constants", "tracing-gum-proc-macro", "xcm-procedural", "xcm-runtime-apis"]
 runtime = [
 	"frame-benchmarking",
 	"frame-benchmarking-pallet-pov",
@@ -870,6 +873,11 @@ default-features = false
 optional = true
 path = "../substrate/frame/asset-rate"
 
+[dependencies.pallet-asset-rewards]
+default-features = false
+optional = true
+path = "../substrate/frame/asset-rewards"
+
 [dependencies.pallet-asset-tx-payment]
 default-features = false
 optional = true
diff --git a/umbrella/src/lib.rs b/umbrella/src/lib.rs
index 3504f081f29..a132f16a2c3 100644
--- a/umbrella/src/lib.rs
+++ b/umbrella/src/lib.rs
@@ -312,6 +312,10 @@ pub use pallet_asset_conversion_tx_payment;
 #[cfg(feature = "pallet-asset-rate")]
 pub use pallet_asset_rate;
 
+/// FRAME asset rewards pallet.
+#[cfg(feature = "pallet-asset-rewards")]
+pub use pallet_asset_rewards;
+
 /// pallet to manage transaction payments in assets.
 #[cfg(feature = "pallet-asset-tx-payment")]
 pub use pallet_asset_tx_payment;
-- 
GitLab