diff --git a/Cargo.lock b/Cargo.lock index e2cf44a10a..206e451ab5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3514,7 +3514,7 @@ dependencies = [ [[package]] name = "fc-consensus" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "async-trait", "fp-consensus", @@ -3530,7 +3530,7 @@ dependencies = [ [[package]] name = "fc-db" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "async-trait", "fp-storage", @@ -3550,7 +3550,7 @@ dependencies = [ [[package]] name = "fc-mapping-sync" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fc-db", "fc-storage", @@ -3571,7 +3571,7 @@ dependencies = [ [[package]] name = "fc-rpc" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3621,7 +3621,7 @@ dependencies = [ [[package]] name = "fc-rpc-core" version = "1.1.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3634,7 +3634,7 @@ dependencies = [ [[package]] name = "fc-storage" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3786,7 +3786,7 @@ dependencies = [ [[package]] name = "fp-account" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "hex", "impl-serde", @@ -3805,7 +3805,7 @@ dependencies = [ [[package]] name = "fp-consensus" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "parity-scale-codec", @@ -3817,7 +3817,7 @@ dependencies = [ [[package]] name = "fp-ethereum" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3831,7 +3831,7 @@ dependencies = [ [[package]] name = "fp-evm" version = "3.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "evm", "frame-support", @@ -3846,7 +3846,7 @@ dependencies = [ [[package]] name = "fp-rpc" version = "3.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3863,7 +3863,7 @@ dependencies = [ [[package]] name = "fp-self-contained" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "frame-support", "parity-scale-codec", @@ -3875,7 +3875,7 @@ dependencies = [ [[package]] name = "fp-storage" version = "2.0.0" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "parity-scale-codec", "serde", @@ -6005,7 +6005,7 @@ checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" [[package]] name = "local-runtime" -version = "5.23.0" +version = "5.24.0" dependencies = [ "array-bytes 6.1.0", "astar-primitives", @@ -6025,7 +6025,7 @@ dependencies = [ "pallet-assets", "pallet-aura", "pallet-balances", - "pallet-block-reward", + "pallet-block-rewards-hybrid", "pallet-chain-extension-assets", "pallet-chain-extension-dapps-staking", "pallet-chain-extension-unified-accounts", @@ -7381,7 +7381,7 @@ dependencies = [ [[package]] name = "pallet-base-fee" version = "1.0.0" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "frame-support", @@ -7454,6 +7454,25 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-block-rewards-hybrid" +version = "0.1.0" +dependencies = [ + "astar-primitives", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "serde", + "sp-arithmetic", + "sp-core", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-bounties" version = "4.0.0-dev" @@ -7813,7 +7832,7 @@ dependencies = [ [[package]] name = "pallet-ethereum" version = "4.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -7860,7 +7879,7 @@ dependencies = [ [[package]] name = "pallet-evm" version = "6.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "environmental", "evm", @@ -7885,7 +7904,7 @@ dependencies = [ [[package]] name = "pallet-evm-chain-id" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "frame-support", "frame-system", @@ -7922,7 +7941,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-blake2" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", ] @@ -7930,7 +7949,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-bn128" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "sp-core", @@ -7965,7 +7984,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-dispatch" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "frame-support", @@ -7975,7 +7994,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-ed25519" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ed25519-dalek", "fp-evm", @@ -7984,7 +8003,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-modexp" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "num", @@ -7993,7 +8012,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-sha3fips" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "tiny-keccak", @@ -8002,7 +8021,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-simple" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "ripemd", @@ -13131,7 +13150,7 @@ dependencies = [ "pallet-aura", "pallet-authorship", "pallet-balances", - "pallet-block-reward", + "pallet-block-rewards-hybrid", "pallet-chain-extension-assets", "pallet-chain-extension-dapps-staking", "pallet-chain-extension-unified-accounts", diff --git a/Cargo.toml b/Cargo.toml index 70c72ef58f..e451b03095 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -273,6 +273,7 @@ orml-xcm-support = { git = "https://github.com/open-web3-stack/open-runtime-modu # Astar pallets & modules # (wasm) pallet-block-reward = { path = "./pallets/block-reward", default-features = false } +pallet-block-rewards-hybrid = { path = "./pallets/block-rewards-hybrid", default-features = false } pallet-collator-selection = { path = "./pallets/collator-selection", default-features = false } pallet-dapps-staking = { path = "./pallets/dapps-staking", default-features = false } pallet-xc-asset-config = { path = "./pallets/xc-asset-config", default-features = false } diff --git a/bin/collator/src/local/chain_spec.rs b/bin/collator/src/local/chain_spec.rs index 798370c075..d18ee5c097 100644 --- a/bin/collator/src/local/chain_spec.rs +++ b/bin/collator/src/local/chain_spec.rs @@ -21,8 +21,8 @@ use local_runtime::{ wasm_binary_unwrap, AccountId, AuraConfig, AuraId, BalancesConfig, BlockRewardConfig, CouncilConfig, DemocracyConfig, EVMConfig, GenesisConfig, GrandpaConfig, GrandpaId, - Precompiles, Signature, SudoConfig, SystemConfig, TechnicalCommitteeConfig, TreasuryConfig, - VestingConfig, + Precompiles, RewardDistributionConfig, Signature, SudoConfig, SystemConfig, + TechnicalCommitteeConfig, TreasuryConfig, VestingConfig, }; use sc_service::ChainType; use sp_core::{crypto::Ss58Codec, sr25519, Pair, Public}; @@ -117,8 +117,8 @@ fn testnet_genesis( }, block_reward: BlockRewardConfig { // Make sure sum is 100 - reward_config: pallet_block_reward::RewardDistributionConfig { - base_treasury_percent: Perbill::from_percent(25), + reward_config: RewardDistributionConfig { + treasury_percent: Perbill::from_percent(25), base_staker_percent: Perbill::from_percent(30), dapps_percent: Perbill::from_percent(20), collators_percent: Perbill::zero(), diff --git a/bin/collator/src/parachain/chain_spec/shibuya.rs b/bin/collator/src/parachain/chain_spec/shibuya.rs index bdd00127c0..ac4478afe1 100644 --- a/bin/collator/src/parachain/chain_spec/shibuya.rs +++ b/bin/collator/src/parachain/chain_spec/shibuya.rs @@ -23,8 +23,9 @@ use sc_service::ChainType; use shibuya_runtime::{ wasm_binary_unwrap, AccountId, AuraConfig, AuraId, Balance, BalancesConfig, BlockRewardConfig, CollatorSelectionConfig, CouncilConfig, DemocracyConfig, EVMChainIdConfig, EVMConfig, - GenesisConfig, ParachainInfoConfig, Precompiles, SessionConfig, SessionKeys, Signature, - SudoConfig, SystemConfig, TechnicalCommitteeConfig, TreasuryConfig, VestingConfig, SBY, + GenesisConfig, ParachainInfoConfig, Precompiles, RewardDistributionConfig, SessionConfig, + SessionKeys, Signature, SudoConfig, SystemConfig, TechnicalCommitteeConfig, TreasuryConfig, + VestingConfig, SBY, }; use sp_core::{sr25519, Pair, Public}; @@ -115,8 +116,8 @@ fn make_genesis( balances: BalancesConfig { balances }, block_reward: BlockRewardConfig { // Make sure sum is 100 - reward_config: pallet_block_reward::RewardDistributionConfig { - base_treasury_percent: Perbill::from_percent(10), + reward_config: RewardDistributionConfig { + treasury_percent: Perbill::from_percent(10), base_staker_percent: Perbill::from_percent(20), dapps_percent: Perbill::from_percent(20), collators_percent: Perbill::from_percent(5), diff --git a/pallets/block-rewards-hybrid/Cargo.toml b/pallets/block-rewards-hybrid/Cargo.toml new file mode 100644 index 0000000000..b5b6486d18 --- /dev/null +++ b/pallets/block-rewards-hybrid/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "pallet-block-rewards-hybrid" +version = "0.1.0" +license = "Apache-2.0" +description = "FRAME pallet for managing block reward issuance & distribution" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +parity-scale-codec = { workspace = true } +serde = { workspace = true } + +astar-primitives = { workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +scale-info = { workspace = true } +sp-arithmetic = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +frame-benchmarking = { workspace = true, optional = true } + +[dev-dependencies] +pallet-balances = { workspace = true } +pallet-timestamp = { workspace = true } +sp-core = { workspace = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "sp-core/std", + "scale-info/std", + "sp-std/std", + "serde/std", + "frame-support/std", + "frame-system/std", + "pallet-timestamp/std", + "pallet-balances/std", + "astar-primitives/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "astar-primitives/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/block-rewards-hybrid/src/benchmarking.rs b/pallets/block-rewards-hybrid/src/benchmarking.rs new file mode 100644 index 0000000000..d24f80f049 --- /dev/null +++ b/pallets/block-rewards-hybrid/src/benchmarking.rs @@ -0,0 +1,61 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use frame_benchmarking::v2::*; +use frame_system::{Pallet as System, RawOrigin}; + +/// Assert that the last event equals the provided one. +fn assert_last_event(generic_event: ::RuntimeEvent) { + System::::assert_last_event(generic_event.into()); +} + +#[benchmarks(where T: Config)] +mod benchmarks { + use super::*; + + #[benchmark] + fn set_configuration() { + let reward_config = RewardDistributionConfig::default(); + assert!(reward_config.is_consistent()); + + #[extrinsic_call] + _(RawOrigin::Root, reward_config.clone()); + + assert_last_event::(Event::::DistributionConfigurationChanged(reward_config).into()); + } + + impl_benchmark_test_suite!( + Pallet, + crate::benchmarking::tests::new_test_ext(), + crate::mock::TestRuntime, + ); +} + +#[cfg(test)] +mod tests { + use crate::mock; + use frame_support::sp_io::TestExternalities; + + pub fn new_test_ext() -> TestExternalities { + mock::ExternalityBuilder::build() + } +} diff --git a/pallets/block-rewards-hybrid/src/lib.rs b/pallets/block-rewards-hybrid/src/lib.rs new file mode 100644 index 0000000000..7b6d3c7c81 --- /dev/null +++ b/pallets/block-rewards-hybrid/src/lib.rs @@ -0,0 +1,385 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! # Block Reward Distribution Pallet +//! +//! - [`Config`] +//! +//! ## Overview +//! +//! Pallet that implements block reward issuance and distribution mechanics. +//! +//! After issuing a block reward, pallet will calculate how to distribute the reward +//! based on configurable parameters and chain state. +//! +//! Major on-chain factors which can influence reward distribution are total issuance and total value locked by dapps staking. +//! +//! ## Interface +//! +//! ### Dispatchable Function +//! +//! - `set_configuration` - used to change reward distribution configuration parameters +//! +//! ### Other +//! +//! - `on_timestamp_set` - This pallet implements the `OnTimestampSet` trait to handle block production. +//! Note: We assume that it's impossible to set timestamp two times in a block. +//! +//! ## Usage +//! +//! 1. Pallet should be set as a handler of `OnTimestampSet`. +//! 2. `DappsStakingTvlProvider` handler should be defined as an impl of `TvlProvider` trait. For example: +//! ```nocompile +//! pub struct TvlProvider(); +//! impl Get for TvlProvider { +//! fn tvl() -> Balance { +//! DappsStaking::total_locked_value() +//! } +//! } +//! ``` +//! 3. `BeneficiaryPayout` handler should be defined as an impl of `BeneficiaryPayout` trait. For example: +//! ```nocompile +//! pub struct BeneficiaryPayout(); +//! impl BeneficiaryPayout> for BeneficiaryPayout { +//! +//! fn treasury(reward: NegativeImbalanceOf) { +//! Balances::resolve_creating(&TREASURY_POT.into_account(), reward); +//! } +//! +//! fn collators(reward: NegativeImbalanceOf) { +//! Balances::resolve_creating(&COLLATOR_POT.into_account(), reward); +//! } +//! +//! fn dapps_staking(stakers: NegativeImbalanceOf, dapps: NegativeImbalanceOf) { +//! DappsStaking::rewards(stakers, dapps); +//! } +//! } +//! ``` +//! 4. Set `MaxBlockRewardAmount` to the max reward amount distributed per block. +//! Max amount will be reached if `ideal_dapps_staking_tvl` is reached. +//! + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +use astar_primitives::Balance; +use frame_support::pallet_prelude::*; +use frame_support::{ + log, + traits::{Currency, Get, Imbalance, OnTimestampSet}, +}; +use frame_system::{ensure_root, pallet_prelude::*}; +use sp_runtime::{ + traits::{CheckedAdd, Zero}, + Perbill, +}; +use sp_std::vec; + +#[cfg(any(feature = "runtime-benchmarks"))] +pub mod benchmarking; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub mod weights; +pub use weights::WeightInfo; + +#[frame_support::pallet] +pub mod pallet { + + use super::*; + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + // Negative imbalance type of this pallet. + pub(crate) type NegativeImbalanceOf = <::Currency as Currency< + ::AccountId, + >>::NegativeImbalance; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The currency trait. + type Currency: Currency; + + /// Provides information about how much value is locked by dapps staking + type DappsStakingTvlProvider: Get; + + /// Used to payout rewards + type BeneficiaryPayout: BeneficiaryPayout>; + + /// The amount of issuance for each block. + #[pallet::constant] + type MaxBlockRewardAmount: Get; + + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::storage] + #[pallet::getter(fn reward_config)] + pub type RewardDistributionConfigStorage = + StorageValue<_, RewardDistributionConfig, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// Distribution configuration has been updated. + DistributionConfigurationChanged(RewardDistributionConfig), + } + + #[pallet::error] + pub enum Error { + /// Sum of all rations must be one whole (100%) + InvalidDistributionConfiguration, + } + + #[pallet::genesis_config] + #[cfg_attr(feature = "std", derive(Default))] + pub struct GenesisConfig { + pub reward_config: RewardDistributionConfig, + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + assert!(self.reward_config.is_consistent()); + RewardDistributionConfigStorage::::put(self.reward_config.clone()) + } + } + + #[pallet::call] + impl Pallet { + /// Sets the reward distribution configuration parameters which will be used from next block reward distribution. + /// + /// It is mandatory that all components of configuration sum up to one whole (**100%**), + /// otherwise an error `InvalidDistributionConfiguration` will be raised. + /// + /// - `reward_distro_params` - reward distribution params + /// + /// Emits `DistributionConfigurationChanged` with config embeded into event itself. + /// + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::set_configuration())] + pub fn set_configuration( + origin: OriginFor, + reward_distro_params: RewardDistributionConfig, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + + ensure!( + reward_distro_params.is_consistent(), + Error::::InvalidDistributionConfiguration + ); + RewardDistributionConfigStorage::::put(reward_distro_params.clone()); + + Self::deposit_event(Event::::DistributionConfigurationChanged( + reward_distro_params, + )); + + Ok(().into()) + } + } + + impl OnTimestampSet for Pallet { + fn on_timestamp_set(_moment: Moment) { + let rewards = Self::calculate_rewards(T::MaxBlockRewardAmount::get()); + let inflation = T::Currency::issue(rewards.sum()); + Self::distribute_rewards(inflation, rewards); + } + } + + impl Pallet { + /// Calculates the amount of rewards for each beneficiary + /// + /// # Arguments + /// * `block_reward` - the block reward amount + /// + fn calculate_rewards(block_reward: Balance) -> Rewards { + let distro_params = Self::reward_config(); + + // Pre-calculate balance which will be deposited for each beneficiary + let base_staker_balance = distro_params.base_staker_percent * block_reward; + let dapps_reward = distro_params.dapps_percent * block_reward; + let collators_reward = distro_params.collators_percent * block_reward; + let treasury_reward = distro_params.treasury_percent * block_reward; + + // This is part is the TVL dependant staker reward + let adjustable_balance = distro_params.adjustable_percent * block_reward; + + // Calculate total staker reward + let adjustable_staker_part = if distro_params.ideal_dapps_staking_tvl.is_zero() { + adjustable_balance + } else { + Self::tvl_percentage() / distro_params.ideal_dapps_staking_tvl * adjustable_balance + }; + + let staker_reward = base_staker_balance.saturating_add(adjustable_staker_part); + + Rewards { + treasury_reward, + staker_reward, + dapps_reward, + collators_reward, + } + } + + /// Distribute reward between beneficiaries. + /// + /// # Arguments + /// * `inflation` - inflation issued for this block + /// * `rewards` - rewards that will be split and distributed + /// + fn distribute_rewards(inflation: NegativeImbalanceOf, rewards: Rewards) { + // Prepare imbalances + let (dapps_imbalance, remainder) = inflation.split(rewards.dapps_reward); + let (stakers_imbalance, remainder) = remainder.split(rewards.staker_reward); + let (collator_imbalance, remainder) = remainder.split(rewards.collators_reward); + let (treasury_imbalance, _) = remainder.split(rewards.treasury_reward); + + // Payout beneficiaries + T::BeneficiaryPayout::treasury(treasury_imbalance); + T::BeneficiaryPayout::collators(collator_imbalance); + T::BeneficiaryPayout::dapps_staking(stakers_imbalance, dapps_imbalance); + } + + /// Provides TVL as percentage of total issuance + fn tvl_percentage() -> Perbill { + let total_issuance = T::Currency::total_issuance(); + if total_issuance.is_zero() { + log::warn!("Total issuance is zero - this should be impossible."); + Zero::zero() + } else { + Perbill::from_rational(T::DappsStakingTvlProvider::get(), total_issuance) + } + } + } +} + +/// List of configuration parameters used to calculate reward distribution portions for all the beneficiaries. +/// +/// Note that if `ideal_dapps_staking_tvl` is set to `Zero`, entire `adjustable_percent` goes to the stakers. +/// +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +pub struct RewardDistributionConfig { + /// Base percentage of reward that goes to treasury + #[codec(compact)] + pub treasury_percent: Perbill, + /// Base percentage of reward that goes to stakers + #[codec(compact)] + pub base_staker_percent: Perbill, + /// Percentage of rewards that goes to dApps + #[codec(compact)] + pub dapps_percent: Perbill, + /// Percentage of reward that goes to collators + #[codec(compact)] + pub collators_percent: Perbill, + /// Adjustable reward percentage that either goes to treasury or to stakers + #[codec(compact)] + pub adjustable_percent: Perbill, + /// Target dapps-staking TVL percentage at which adjustable inflation towards stakers becomes saturated + #[codec(compact)] + pub ideal_dapps_staking_tvl: Perbill, +} + +impl Default for RewardDistributionConfig { + /// `default` values based on configuration at the time of writing this code. + /// Should be overriden by desired params. + fn default() -> Self { + RewardDistributionConfig { + treasury_percent: Perbill::from_percent(40), + base_staker_percent: Perbill::from_percent(25), + dapps_percent: Perbill::from_percent(25), + collators_percent: Perbill::from_percent(10), + adjustable_percent: Zero::zero(), + ideal_dapps_staking_tvl: Zero::zero(), + } + } +} + +impl RewardDistributionConfig { + /// `true` if sum of all percentages is `one whole`, `false` otherwise. + pub fn is_consistent(&self) -> bool { + // TODO: perhaps this can be writen in a more cleaner way? + // experimental-only `try_reduce` could be used but it's not available + // https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.try_reduce + + let variables = vec![ + &self.treasury_percent, + &self.base_staker_percent, + &self.dapps_percent, + &self.collators_percent, + &self.adjustable_percent, + ]; + + let mut accumulator = Perbill::zero(); + for config_param in variables { + let result = accumulator.checked_add(config_param); + if let Some(mid_result) = result { + accumulator = mid_result; + } else { + return false; + } + } + + Perbill::one() == accumulator + } +} + +/// Represents rewards distribution balances for each beneficiary +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +pub struct Rewards { + treasury_reward: Balance, + staker_reward: Balance, + dapps_reward: Balance, + collators_reward: Balance, +} + +impl Rewards { + fn sum(&self) -> Balance { + self.treasury_reward + .saturating_add(self.staker_reward) + .saturating_add(self.dapps_reward) + .saturating_add(self.collators_reward) + } +} + +/// Defines functions used to payout the beneficiaries of block rewards +pub trait BeneficiaryPayout { + /// Payout reward to the treasury + fn treasury(reward: Imbalance); + + /// Payout reward to the collators + fn collators(reward: Imbalance); + + /// Payout reward to dapps staking + /// + /// # Arguments + /// + /// * `stakers` - reward that goes towards staker reward pot + /// * `dapps` - reward that goes towards dapps reward pot + /// + fn dapps_staking(stakers: Imbalance, dapps: Imbalance); +} diff --git a/pallets/block-rewards-hybrid/src/mock.rs b/pallets/block-rewards-hybrid/src/mock.rs new file mode 100644 index 0000000000..80e10e6eeb --- /dev/null +++ b/pallets/block-rewards-hybrid/src/mock.rs @@ -0,0 +1,206 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::{self as pallet_block_reward, NegativeImbalanceOf}; + +use frame_support::{ + construct_runtime, parameter_types, + sp_io::TestExternalities, + traits::Currency, + traits::{ConstU32, Get}, + weights::Weight, + PalletId, +}; + +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{AccountIdConversion, BlakeTwo256, IdentityLookup}, +}; +use sp_std::cell::RefCell; + +pub(crate) type AccountId = u64; +pub(crate) type BlockNumber = u64; +pub(crate) type Balance = u128; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +/// Value shouldn't be less than 2 for testing purposes, otherwise we cannot test certain corner cases. +pub(crate) const EXISTENTIAL_DEPOSIT: Balance = 2; + +construct_runtime!( + pub struct TestRuntime + where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + Timestamp: pallet_timestamp, + BlockReward: pallet_block_reward, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); +} + +impl frame_system::Config for TestRuntime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type RuntimeCall = RuntimeCall; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub const MaxLocks: u32 = 4; + pub const ExistentialDeposit: Balance = EXISTENTIAL_DEPOSIT; +} + +impl pallet_balances::Config for TestRuntime { + type MaxLocks = MaxLocks; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 3; +} + +impl pallet_timestamp::Config for TestRuntime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +// A fairly high block reward so we can detect slight changes in reward distribution +// due to TVL changes. +pub(crate) const BLOCK_REWARD: Balance = 1_000_000; + +// Fake accounts used to simulate reward beneficiaries balances +pub(crate) const TREASURY_POT: PalletId = PalletId(*b"moktrsry"); +pub(crate) const COLLATOR_POT: PalletId = PalletId(*b"mokcolat"); +pub(crate) const STAKERS_POT: PalletId = PalletId(*b"mokstakr"); +pub(crate) const DAPPS_POT: PalletId = PalletId(*b"mokdapps"); + +thread_local! { + static TVL: RefCell = RefCell::new(1_000_000_000); +} + +// Type used as TVL provider +pub struct TvlProvider(); +impl Get for TvlProvider { + fn get() -> Balance { + TVL.with(|t| t.borrow().clone()) + } +} + +pub(crate) fn set_tvl(v: Balance) { + TVL.with(|t| *t.borrow_mut() = v) +} + +// Type used as beneficiary payout handle +pub struct BeneficiaryPayout(); +impl pallet_block_reward::BeneficiaryPayout> + for BeneficiaryPayout +{ + fn treasury(reward: NegativeImbalanceOf) { + Balances::resolve_creating(&TREASURY_POT.into_account_truncating(), reward); + } + + fn collators(reward: NegativeImbalanceOf) { + Balances::resolve_creating(&COLLATOR_POT.into_account_truncating(), reward); + } + + fn dapps_staking( + stakers: NegativeImbalanceOf, + dapps: NegativeImbalanceOf, + ) { + Balances::resolve_creating(&STAKERS_POT.into_account_truncating(), stakers); + Balances::resolve_creating(&DAPPS_POT.into_account_truncating(), dapps); + } +} + +parameter_types! { + pub const RewardAmount: Balance = BLOCK_REWARD; +} + +impl pallet_block_reward::Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type MaxBlockRewardAmount = RewardAmount; + type DappsStakingTvlProvider = TvlProvider; + type BeneficiaryPayout = BeneficiaryPayout; + type WeightInfo = (); +} + +pub struct ExternalityBuilder; + +impl ExternalityBuilder { + pub fn build() -> TestExternalities { + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + // This will cause some initial issuance + pallet_balances::GenesisConfig:: { + balances: vec![(1, 9000), (2, 800), (3, 10000)], + } + .assimilate_storage(&mut storage) + .ok(); + + let mut ext = TestExternalities::from(storage); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} diff --git a/pallets/block-rewards-hybrid/src/tests.rs b/pallets/block-rewards-hybrid/src/tests.rs new file mode 100644 index 0000000000..0ba92d500a --- /dev/null +++ b/pallets/block-rewards-hybrid/src/tests.rs @@ -0,0 +1,445 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::{pallet::Error, Event, *}; +use frame_support::{assert_noop, assert_ok, traits::OnTimestampSet}; +use mock::{Balance, *}; +use sp_runtime::{ + traits::{AccountIdConversion, BadOrigin, Zero}, + Perbill, +}; + +#[test] +fn default_reward_distribution_config_is_consitent() { + let reward_config = RewardDistributionConfig::default(); + assert!(reward_config.is_consistent()); +} + +#[test] +fn reward_distribution_config_is_consistent() { + // 1 + let reward_config = RewardDistributionConfig { + treasury_percent: Perbill::from_percent(100), + base_staker_percent: Zero::zero(), + dapps_percent: Zero::zero(), + collators_percent: Zero::zero(), + adjustable_percent: Zero::zero(), + ideal_dapps_staking_tvl: Zero::zero(), + }; + assert!(reward_config.is_consistent()); + + // 2 + let reward_config = RewardDistributionConfig { + treasury_percent: Zero::zero(), + base_staker_percent: Perbill::from_percent(100), + dapps_percent: Zero::zero(), + collators_percent: Zero::zero(), + adjustable_percent: Zero::zero(), + ideal_dapps_staking_tvl: Zero::zero(), + }; + assert!(reward_config.is_consistent()); + + // 3 + let reward_config = RewardDistributionConfig { + treasury_percent: Zero::zero(), + base_staker_percent: Zero::zero(), + dapps_percent: Zero::zero(), + collators_percent: Zero::zero(), + adjustable_percent: Perbill::from_percent(100), + ideal_dapps_staking_tvl: Perbill::from_percent(13), + }; + assert!(reward_config.is_consistent()); + + // 4 + // 100% + let reward_config = RewardDistributionConfig { + treasury_percent: Perbill::from_rational(4663701u32, 100000000u32), + base_staker_percent: Perbill::from_rational(2309024u32, 10000000u32), + dapps_percent: Perbill::from_rational(173094531u32, 1000000000u32), + collators_percent: Perbill::from_rational(29863296u32, 1000000000u32), + adjustable_percent: Perbill::from_rational(519502763u32, 1000000000u32), + ideal_dapps_staking_tvl: Perbill::from_percent(60), + }; + assert!(reward_config.is_consistent()); +} + +#[test] +fn reward_distribution_config_not_consistent() { + // 1 + let reward_config = RewardDistributionConfig { + treasury_percent: Perbill::from_percent(100), + ..Default::default() + }; + assert!(!reward_config.is_consistent()); + + // 2 + let reward_config = RewardDistributionConfig { + adjustable_percent: Perbill::from_percent(100), + ..Default::default() + }; + assert!(!reward_config.is_consistent()); + + // 3 + // 99% + let reward_config = RewardDistributionConfig { + treasury_percent: Perbill::from_percent(10), + base_staker_percent: Perbill::from_percent(20), + dapps_percent: Perbill::from_percent(20), + collators_percent: Perbill::from_percent(30), + adjustable_percent: Perbill::from_percent(19), + ideal_dapps_staking_tvl: Zero::zero(), + }; + assert!(!reward_config.is_consistent()); + + // 4 + // 101% + let reward_config = RewardDistributionConfig { + treasury_percent: Perbill::from_percent(10), + base_staker_percent: Perbill::from_percent(20), + dapps_percent: Perbill::from_percent(20), + collators_percent: Perbill::from_percent(31), + adjustable_percent: Perbill::from_percent(20), + ideal_dapps_staking_tvl: Zero::zero(), + }; + assert!(!reward_config.is_consistent()); +} + +#[test] +fn set_configuration_fails() { + ExternalityBuilder::build().execute_with(|| { + // 1 + assert_noop!( + BlockReward::set_configuration(RuntimeOrigin::signed(1), Default::default()), + BadOrigin + ); + + // 2 + let reward_config = RewardDistributionConfig { + treasury_percent: Perbill::from_percent(100), + ..Default::default() + }; + assert!(!reward_config.is_consistent()); + assert_noop!( + BlockReward::set_configuration(RuntimeOrigin::root(), reward_config), + Error::::InvalidDistributionConfiguration, + ); + }) +} + +#[test] +fn set_configuration_is_ok() { + ExternalityBuilder::build().execute_with(|| { + // custom config so it differs from the default one + let reward_config = RewardDistributionConfig { + treasury_percent: Perbill::from_percent(3), + base_staker_percent: Perbill::from_percent(14), + dapps_percent: Perbill::from_percent(18), + collators_percent: Perbill::from_percent(31), + adjustable_percent: Perbill::from_percent(34), + ideal_dapps_staking_tvl: Perbill::from_percent(87), + }; + assert!(reward_config.is_consistent()); + + assert_ok!(BlockReward::set_configuration( + RuntimeOrigin::root(), + reward_config.clone() + )); + System::assert_last_event(mock::RuntimeEvent::BlockReward( + Event::DistributionConfigurationChanged(reward_config.clone()), + )); + + assert_eq!( + RewardDistributionConfigStorage::::get(), + reward_config + ); + }) +} + +#[test] +fn inflation_and_total_issuance_as_expected() { + ExternalityBuilder::build().execute_with(|| { + let init_issuance = ::Currency::total_issuance(); + + for block in 0..10 { + assert_eq!( + ::Currency::total_issuance(), + block * BLOCK_REWARD + init_issuance + ); + BlockReward::on_timestamp_set(0); + assert_eq!( + ::Currency::total_issuance(), + (block + 1) * BLOCK_REWARD + init_issuance + ); + } + }) +} + +#[test] +fn reward_distribution_as_expected() { + ExternalityBuilder::build().execute_with(|| { + // Ensure that initially, all beneficiaries have no free balance + let init_balance_snapshot = FreeBalanceSnapshot::new(); + assert!(init_balance_snapshot.is_zero()); + + // Prepare a custom config (easily discernable percentages for visual verification) + let reward_config = RewardDistributionConfig { + treasury_percent: Perbill::from_percent(10), + base_staker_percent: Perbill::from_percent(20), + dapps_percent: Perbill::from_percent(25), + collators_percent: Perbill::from_percent(5), + adjustable_percent: Perbill::from_percent(40), + ideal_dapps_staking_tvl: Perbill::from_percent(50), + }; + assert!(reward_config.is_consistent()); + assert_ok!(BlockReward::set_configuration( + RuntimeOrigin::root(), + reward_config.clone() + )); + + // Issue rewards a couple of times and verify distribution is as expected + // also ensure that the non distributed reward amount is burn + // (that the total issuance is only increased by the amount that has been rewarded) + for _block in 1..=100 { + // TVL amount is updated every block + // to ensure TVL ratio as expected + adjust_tvl(30); + let init_balance_state = FreeBalanceSnapshot::new(); + let total_issuance_before = ::Currency::total_issuance(); + let distributed_rewards = Rewards::calculate(&reward_config); + + BlockReward::on_timestamp_set(0); + + let final_balance_state = FreeBalanceSnapshot::new(); + init_balance_state.assert_distribution(&final_balance_state, &distributed_rewards); + + assert_eq!( + ::Currency::total_issuance(), + total_issuance_before + distributed_rewards.sum() + ); + } + }) +} + +#[test] +fn non_distributed_reward_amount_is_burned() { + ExternalityBuilder::build().execute_with(|| { + // Ensure that initially, all beneficiaries have no free balance + let init_balance_snapshot = FreeBalanceSnapshot::new(); + assert!(init_balance_snapshot.is_zero()); + + // Prepare a custom config (easily discernible percentages for visual verification) + let reward_config = RewardDistributionConfig { + treasury_percent: Perbill::from_percent(10), + base_staker_percent: Perbill::from_percent(20), + dapps_percent: Perbill::from_percent(25), + collators_percent: Perbill::from_percent(5), + adjustable_percent: Perbill::from_percent(40), + ideal_dapps_staking_tvl: Perbill::from_percent(50), + }; + assert!(reward_config.is_consistent()); + assert_ok!(BlockReward::set_configuration( + RuntimeOrigin::root(), + reward_config.clone() + )); + + for tvl in [30, 50, 70, 100] { + for _block in 1..=100 { + // TVL amount is updated every block + // to ensure TVL ratio as expected + adjust_tvl(tvl); + let total_issuance_before = ::Currency::total_issuance(); + let distributed_rewards = Rewards::calculate(&reward_config); + let burned_amount = BLOCK_REWARD - distributed_rewards.sum(); + + BlockReward::on_timestamp_set(0); + + assert_eq!( + ::Currency::total_issuance(), + total_issuance_before + BLOCK_REWARD - burned_amount + ); + } + } + }) +} + +#[test] +fn reward_distribution_no_adjustable_part() { + ExternalityBuilder::build().execute_with(|| { + let reward_config = RewardDistributionConfig { + treasury_percent: Perbill::from_percent(10), + base_staker_percent: Perbill::from_percent(45), + dapps_percent: Perbill::from_percent(40), + collators_percent: Perbill::from_percent(5), + adjustable_percent: Perbill::zero(), + ideal_dapps_staking_tvl: Perbill::from_percent(50), // this is irrelevant + }; + assert!(reward_config.is_consistent()); + assert_ok!(BlockReward::set_configuration( + RuntimeOrigin::root(), + reward_config.clone() + )); + + // no adjustable part so we don't expect rewards to change with TVL percentage + let const_rewards = Rewards::calculate(&reward_config); + + for _block in 1..=100 { + let init_balance_state = FreeBalanceSnapshot::new(); + let rewards = Rewards::calculate(&reward_config); + + assert_eq!(rewards, const_rewards); + + BlockReward::on_timestamp_set(0); + + let final_balance_state = FreeBalanceSnapshot::new(); + init_balance_state.assert_distribution(&final_balance_state, &rewards); + } + }) +} + +#[test] +fn reward_distribution_all_zero_except_one() { + ExternalityBuilder::build().execute_with(|| { + let reward_config = RewardDistributionConfig { + treasury_percent: Perbill::zero(), + base_staker_percent: Perbill::zero(), + dapps_percent: Perbill::zero(), + collators_percent: Perbill::zero(), + adjustable_percent: Perbill::one(), + ideal_dapps_staking_tvl: Perbill::from_percent(50), // this is irrelevant + }; + assert!(reward_config.is_consistent()); + assert_ok!(BlockReward::set_configuration( + RuntimeOrigin::root(), + reward_config.clone() + )); + + for _block in 1..=10 { + let init_balance_state = FreeBalanceSnapshot::new(); + let rewards = Rewards::calculate(&reward_config); + + BlockReward::on_timestamp_set(0); + + let final_balance_state = FreeBalanceSnapshot::new(); + init_balance_state.assert_distribution(&final_balance_state, &rewards); + } + }) +} + +/// Represents free balance snapshot at a specific point in time +#[derive(PartialEq, Eq, Clone, RuntimeDebug)] +struct FreeBalanceSnapshot { + treasury: Balance, + collators: Balance, + stakers: Balance, + dapps: Balance, +} + +impl FreeBalanceSnapshot { + /// Creates a new free balance snapshot using current balance state. + /// + /// Future balance changes won't be reflected in this instance. + fn new() -> Self { + Self { + treasury: ::Currency::free_balance( + &TREASURY_POT.into_account_truncating(), + ), + collators: ::Currency::free_balance( + &COLLATOR_POT.into_account_truncating(), + ), + stakers: ::Currency::free_balance( + &STAKERS_POT.into_account_truncating(), + ), + dapps: ::Currency::free_balance( + &DAPPS_POT.into_account_truncating(), + ), + } + } + + /// `true` if all free balances equal `Zero`, `false` otherwise + fn is_zero(&self) -> bool { + self.treasury.is_zero() + && self.collators.is_zero() + && self.stakers.is_zero() + && self.dapps.is_zero() + } + + /// Asserts that `post_reward_state` is as expected. + /// + /// Increase in balances, based on `rewards` values, is verified. + /// + fn assert_distribution(&self, post_reward_state: &Self, rewards: &Rewards) { + assert_eq!( + self.treasury + rewards.treasury_reward, + post_reward_state.treasury + ); + assert_eq!( + self.stakers + rewards.staker_reward, + post_reward_state.stakers + ); + assert_eq!( + self.collators + rewards.collators_reward, + post_reward_state.collators + ); + assert_eq!(self.dapps + rewards.dapps_reward, post_reward_state.dapps); + } +} + +impl Rewards { + /// Pre-calculates the reward distribution, using the provided `RewardDistributionConfig`. + /// Method assumes that total issuance will be increased by `BLOCK_REWARD`. + /// + /// Both current `total_issuance` and `TVL` are used. If these are changed after calling this function, + /// they won't be reflected in the struct. + /// + fn calculate(reward_config: &RewardDistributionConfig) -> Self { + // Calculate `tvl-independent` portions + let treasury_reward = reward_config.treasury_percent * BLOCK_REWARD; + let base_staker_reward = reward_config.base_staker_percent * BLOCK_REWARD; + let dapps_reward = reward_config.dapps_percent * BLOCK_REWARD; + let collators_reward = reward_config.collators_percent * BLOCK_REWARD; + let adjustable_reward = reward_config.adjustable_percent * BLOCK_REWARD; + + // Calculate `tvl-dependent` portions + let total_issuance = ::Currency::total_issuance(); + let tvl = ::DappsStakingTvlProvider::get(); + let tvl_percentage = Perbill::from_rational(tvl, total_issuance); + + // Calculate factor for adjusting staker reward portion + let factor = if reward_config.ideal_dapps_staking_tvl.is_zero() { + Perbill::one() + } else { + tvl_percentage / reward_config.ideal_dapps_staking_tvl + }; + + // Adjustable reward portions + let adjustable_staker_reward = factor * adjustable_reward; + + let staker_reward = base_staker_reward + adjustable_staker_reward; + + Self { + treasury_reward, + staker_reward, + dapps_reward, + collators_reward, + } + } +} + +fn adjust_tvl(desired_percent: u128) { + set_tvl(::Currency::total_issuance() / 100 * desired_percent); +} diff --git a/pallets/block-rewards-hybrid/src/weights.rs b/pallets/block-rewards-hybrid/src/weights.rs new file mode 100644 index 0000000000..12c0be364f --- /dev/null +++ b/pallets/block-rewards-hybrid/src/weights.rs @@ -0,0 +1,82 @@ + +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Autogenerated weights for block_rewards_hybrid +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-11-16, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `devserver-01`, CPU: `Intel(R) Xeon(R) E-2236 CPU @ 3.40GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("shibuya-dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/astar-collator +// benchmark +// pallet +// --chain=shibuya-dev +// --steps=50 +// --repeat=20 +// --pallet=block_rewards_hybrid +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./benchmark-results/shibuya-dev/rewards_hybrid_weights.rs +// --template=./scripts/templates/weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for block_rewards_hybrid. +pub trait WeightInfo { + fn set_configuration() -> Weight; +} + +/// Weights for block_rewards_hybrid using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: BlockReward RewardDistributionConfigStorage (r:0 w:1) + /// Proof: BlockReward RewardDistributionConfigStorage (max_values: Some(1), max_size: Some(24), added: 519, mode: MaxEncodedLen) + fn set_configuration() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_645_000 picoseconds. + Weight::from_parts(8_848_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: BlockReward RewardDistributionConfigStorage (r:0 w:1) + /// Proof: BlockReward RewardDistributionConfigStorage (max_values: Some(1), max_size: Some(24), added: 519, mode: MaxEncodedLen) + fn set_configuration() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_645_000 picoseconds. + Weight::from_parts(8_848_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} \ No newline at end of file diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index 799532ed12..dfb5ab155a 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "local-runtime" -version = "5.23.0" +version = "5.24.0" build = "build.rs" authors.workspace = true edition.workspace = true @@ -65,7 +65,7 @@ pallet-transaction-payment-rpc-runtime-api = { workspace = true } # Astar pallets astar-primitives = { workspace = true } -pallet-block-reward = { workspace = true } +pallet-block-rewards-hybrid = { workspace = true } pallet-chain-extension-dapps-staking = { workspace = true } pallet-chain-extension-unified-accounts = { workspace = true } pallet-chain-extension-xvm = { workspace = true } @@ -111,7 +111,7 @@ std = [ "pallet-assets/std", "pallet-aura/std", "pallet-balances/std", - "pallet-block-reward/std", + "pallet-block-rewards-hybrid/std", "pallet-contracts/std", "pallet-contracts-primitives/std", "pallet-chain-extension-dapps-staking/std", @@ -177,7 +177,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "pallet-dapps-staking/runtime-benchmarks", - "pallet-block-reward/runtime-benchmarks", + "pallet-block-rewards-hybrid/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-ethereum/runtime-benchmarks", @@ -198,7 +198,7 @@ try-runtime = [ "frame-system/try-runtime", "pallet-aura/try-runtime", "pallet-balances/try-runtime", - "pallet-block-reward/try-runtime", + "pallet-block-rewards-hybrid/try-runtime", "pallet-contracts/try-runtime", "pallet-dapps-staking/try-runtime", "pallet-grandpa/try-runtime", diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index 5b910c3ce8..f267be3e8f 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -65,6 +65,7 @@ pub use astar_primitives::{ evm::EvmRevertCodeHandler, AccountId, Address, AssetId, Balance, BlockNumber, Hash, Header, Index, Signature, }; +pub use pallet_block_rewards_hybrid::RewardDistributionConfig; pub use crate::precompiles::WhitelistedCalls; #[cfg(feature = "std")] @@ -423,7 +424,7 @@ impl Get for DappsStakingTvlProvider { } pub struct BeneficiaryPayout(); -impl pallet_block_reward::BeneficiaryPayout for BeneficiaryPayout { +impl pallet_block_rewards_hybrid::BeneficiaryPayout for BeneficiaryPayout { fn treasury(reward: NegativeImbalance) { Balances::resolve_creating(&TreasuryPalletId::get().into_account_truncating(), reward); } @@ -438,16 +439,16 @@ impl pallet_block_reward::BeneficiaryPayout for BeneficiaryPa } parameter_types! { - pub const RewardAmount: Balance = 2_664 * MILLIAST; + pub const MaxBlockRewardAmount: Balance = 230_718 * MILLIAST; } -impl pallet_block_reward::Config for Runtime { +impl pallet_block_rewards_hybrid::Config for Runtime { type Currency = Balances; type DappsStakingTvlProvider = DappsStakingTvlProvider; type BeneficiaryPayout = BeneficiaryPayout; - type RewardAmount = RewardAmount; + type MaxBlockRewardAmount = MaxBlockRewardAmount; type RuntimeEvent = RuntimeEvent; - type WeightInfo = pallet_block_reward::weights::SubstrateWeight; + type WeightInfo = pallet_block_rewards_hybrid::weights::SubstrateWeight; } parameter_types! { @@ -1027,7 +1028,7 @@ construct_runtime!( Balances: pallet_balances, Vesting: pallet_vesting, DappsStaking: pallet_dapps_staking, - BlockReward: pallet_block_reward, + BlockReward: pallet_block_rewards_hybrid, TransactionPayment: pallet_transaction_payment, EVM: pallet_evm, Ethereum: pallet_ethereum, @@ -1157,7 +1158,7 @@ mod benches { [pallet_balances, Balances] [pallet_timestamp, Timestamp] [pallet_dapps_staking, DappsStaking] - [pallet_block_reward, BlockReward] + [pallet_block_rewards_hybrid, BlockReward] [pallet_ethereum_checked, EthereumChecked] [pallet_dynamic_evm_base_fee, DynamicEvmBaseFee] ); diff --git a/runtime/shibuya/Cargo.toml b/runtime/shibuya/Cargo.toml index 6267f2260c..2912090981 100644 --- a/runtime/shibuya/Cargo.toml +++ b/runtime/shibuya/Cargo.toml @@ -95,7 +95,7 @@ orml-xtokens = { workspace = true } # Astar pallets astar-primitives = { workspace = true } -pallet-block-reward = { workspace = true } +pallet-block-rewards-hybrid = { workspace = true } pallet-chain-extension-dapps-staking = { workspace = true } pallet-chain-extension-unified-accounts = { workspace = true } pallet-chain-extension-xvm = { workspace = true } @@ -158,7 +158,7 @@ std = [ "pallet-aura/std", "pallet-assets/std", "pallet-balances/std", - "pallet-block-reward/std", + "pallet-block-rewards-hybrid/std", "pallet-contracts/std", "pallet-contracts-primitives/std", "pallet-chain-extension-dapps-staking/std", @@ -239,7 +239,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "pallet-dapps-staking/runtime-benchmarks", - "pallet-block-reward/runtime-benchmarks", + "pallet-block-rewards-hybrid/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-collective/runtime-benchmarks", @@ -266,7 +266,7 @@ try-runtime = [ "frame-system/try-runtime", "pallet-aura/try-runtime", "pallet-balances/try-runtime", - "pallet-block-reward/try-runtime", + "pallet-block-rewards-hybrid/try-runtime", "pallet-dapps-staking/try-runtime", "pallet-sudo/try-runtime", "pallet-timestamp/try-runtime", diff --git a/runtime/shibuya/src/lib.rs b/runtime/shibuya/src/lib.rs index fd7addf29d..3b9725bd23 100644 --- a/runtime/shibuya/src/lib.rs +++ b/runtime/shibuya/src/lib.rs @@ -72,6 +72,7 @@ pub use astar_primitives::{ xcm::AssetLocationIdConverter, AccountId, Address, AssetId, Balance, BlockNumber, Hash, Header, Index, Signature, }; +pub use pallet_block_rewards_hybrid::RewardDistributionConfig; pub use crate::precompiles::WhitelistedCalls; @@ -537,7 +538,7 @@ impl Get for DappsStakingTvlProvider { } pub struct BeneficiaryPayout(); -impl pallet_block_reward::BeneficiaryPayout for BeneficiaryPayout { +impl pallet_block_rewards_hybrid::BeneficiaryPayout for BeneficiaryPayout { fn treasury(reward: NegativeImbalance) { Balances::resolve_creating(&TreasuryPalletId::get().into_account_truncating(), reward); } @@ -552,16 +553,16 @@ impl pallet_block_reward::BeneficiaryPayout for BeneficiaryPa } parameter_types! { - pub const RewardAmount: Balance = 2_530 * MILLISBY; + pub const MaxBlockRewardAmount: Balance = 230_718 * MILLISBY; } -impl pallet_block_reward::Config for Runtime { +impl pallet_block_rewards_hybrid::Config for Runtime { type Currency = Balances; type DappsStakingTvlProvider = DappsStakingTvlProvider; type BeneficiaryPayout = BeneficiaryPayout; - type RewardAmount = RewardAmount; + type MaxBlockRewardAmount = MaxBlockRewardAmount; type RuntimeEvent = RuntimeEvent; - type WeightInfo = pallet_block_reward::weights::SubstrateWeight; + type WeightInfo = pallet_block_rewards_hybrid::weights::SubstrateWeight; } parameter_types! { @@ -1240,7 +1241,7 @@ construct_runtime!( Balances: pallet_balances = 31, Vesting: pallet_vesting = 32, DappsStaking: pallet_dapps_staking = 34, - BlockReward: pallet_block_reward = 35, + BlockReward: pallet_block_rewards_hybrid = 35, Assets: pallet_assets = 36, Authorship: pallet_authorship = 40, @@ -1311,10 +1312,44 @@ pub type Executive = frame_executive::Executive< Migrations, >; +pub use frame_support::traits::{OnRuntimeUpgrade, StorageVersion}; +pub struct HybridInflationModelMigration; +impl OnRuntimeUpgrade for HybridInflationModelMigration { + fn on_runtime_upgrade() -> Weight { + let mut reward_config = pallet_block_rewards_hybrid::RewardDistributionConfig { + // 4.66% + treasury_percent: Perbill::from_rational(4_663_701u32, 100_000_000u32), + // 23.09% + base_staker_percent: Perbill::from_rational(2_309_024u32, 10_000_000u32), + // 17.31% + dapps_percent: Perbill::from_rational(173_094_531u32, 1_000_000_000u32), + // 2.99% + collators_percent: Perbill::from_rational(29_863_296u32, 1_000_000_000u32), + // 51.95% + adjustable_percent: Perbill::from_rational(519_502_763u32, 1_000_000_000u32), + // 60.00% + ideal_dapps_staking_tvl: Perbill::from_percent(60), + }; + + // This HAS to be tested prior to update - we need to ensure that config is consistent + #[cfg(feature = "try-runtime")] + assert!(reward_config.is_consistent()); + + // This should never execute but we need to have code in place that ensures config is consistent + if !reward_config.is_consistent() { + reward_config = Default::default(); + } + + pallet_block_rewards_hybrid::RewardDistributionConfigStorage::::put(reward_config); + + ::DbWeight::get().writes(1) + } +} + /// All migrations that will run on the next runtime upgrade. /// /// Once done, migrations should be removed from the tuple. -pub type Migrations = (); +pub type Migrations = HybridInflationModelMigration; type EventRecord = frame_system::EventRecord< ::RuntimeEvent, @@ -1392,7 +1427,7 @@ mod benches { [pallet_balances, Balances] [pallet_timestamp, Timestamp] [pallet_dapps_staking, DappsStaking] - [pallet_block_reward, BlockReward] + [block_rewards_hybrid, BlockReward] [pallet_xc_asset_config, XcAssetConfig] [pallet_collator_selection, CollatorSelection] [pallet_xcm, PolkadotXcm]