From 7f09949e64ceb24d2aa9400173c38e92556485fd Mon Sep 17 00:00:00 2001 From: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Date: Sat, 22 Nov 2025 11:00:50 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20Implement=20inflation=20mec?= =?UTF-8?q?hanism=20for=20validator=20rewards=20(#304)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR introduces a configurable inflation system for validator rewards with an annual target rate and optional treasury allocation. ## Changes ### Inflation Mechanism - **Annual inflation rate runtime parameter**: Set to 5% default - **EraInflationProvider**: Calculates per-era inflation based on total issuance and annual rate - Formula: `per_era_inflation = (total_issuance × annual_rate) / eras_per_year` ### Treasury Allocation - **InflationTreasuryProportion parameter**: Set to 20% default - **ExternalRewardsInflationHandler**: Mints inflation and distributes between: - 80% to rewards account (for validator rewards) - 20% to treasury account - Treasury receives allocation via `mul_floor()`, with remainder going to rewards to ensure no tokens lost to rounding ### Runtime Integration - Configured across all three runtimes: mainnet, testnet, and stagenet - Consistent parameters across all environments ### Testing - Updated all tests to account for 80/20 split between rewards and treasury - Added precision tolerance (±1 unit) for Perbill rounding edge cases --------- Co-authored-by: Claude --- .../external-validators-rewards/src/mock.rs | 57 +- .../external-validators-rewards/src/tests.rs | 548 +++++++++++++++ operator/runtime/common/src/constants.rs | 4 + operator/runtime/common/src/inflation.rs | 635 ++++++++++++++++++ operator/runtime/common/src/lib.rs | 1 + operator/runtime/mainnet/src/configs/mod.rs | 60 +- .../mainnet/src/configs/runtime_params.rs | 18 + operator/runtime/stagenet/src/configs/mod.rs | 60 +- .../stagenet/src/configs/runtime_params.rs | 18 + operator/runtime/testnet/src/configs/mod.rs | 60 +- .../testnet/src/configs/runtime_params.rs | 18 + test/.papi/descriptors/package.json | 2 +- test/.papi/metadata/datahaven.scale | Bin 622752 -> 623368 bytes 13 files changed, 1453 insertions(+), 28 deletions(-) create mode 100644 operator/runtime/common/src/inflation.rs diff --git a/operator/pallets/external-validators-rewards/src/mock.rs b/operator/pallets/external-validators-rewards/src/mock.rs index e723f649..f1c1a5e0 100644 --- a/operator/pallets/external-validators-rewards/src/mock.rs +++ b/operator/pallets/external-validators-rewards/src/mock.rs @@ -148,6 +148,8 @@ impl ExternalIndexProvider for TimestampProvider { parameter_types! { pub const RewardsEthereumSovereignAccount: u64 = 0xffffffffffffffff; + pub const TreasuryAccount: u64 = 999; + pub const InflationTreasuryProportion: sp_runtime::Perbill = sp_runtime::Perbill::from_percent(20); pub EraInflationProvider: u128 = Mock::mock().era_inflation.unwrap_or(42); } @@ -172,17 +174,43 @@ impl pallet_external_validators_rewards::Config for Test { pub struct InflationMinter; impl HandleInflation for InflationMinter { - fn mint_inflation(account: &u64, amount: u128) -> sp_runtime::DispatchResult { - if amount == 0 { + fn mint_inflation(rewards_account: &u64, total_amount: u128) -> sp_runtime::DispatchResult { + use sp_runtime::traits::Zero; + + if total_amount.is_zero() { log::error!(target: "ext_validators_rewards", "No rewards to distribute"); return Err(DispatchError::Other("No rewards to distribute")); } - ::Currency::mint_into( - account, - amount.into(), - ) - .map(|_| ()) - .map_err(|_| DispatchError::Other("Failed to mint inflation")) + + // Get treasury allocation proportion + let treasury_proportion = InflationTreasuryProportion::get(); + + // Calculate amounts + let treasury_amount = treasury_proportion.mul_floor(total_amount); + let rewards_amount = total_amount.saturating_sub(treasury_amount); + + // Mint rewards to the rewards account + if !rewards_amount.is_zero() { + ::Currency::mint_into( + rewards_account, + rewards_amount, + ) + .map(|_| ()) + .map_err(|_| DispatchError::Other("Failed to mint rewards inflation"))?; + } + + // Mint treasury portion if non-zero + if !treasury_amount.is_zero() { + let treasury_account = TreasuryAccount::get(); + ::Currency::mint_into( + &treasury_account, + treasury_amount, + ) + .map(|_| ()) + .map_err(|_| DispatchError::Other("Failed to mint treasury inflation"))?; + } + + Ok(()) } } @@ -245,7 +273,18 @@ pub fn new_test_ext() -> sp_io::TestExternalities { .build_storage() .unwrap(); - let balances = vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)]; + let balances = vec![ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (5, 100), + (TreasuryAccount::get(), ExistentialDeposit::get().into()), // Treasury needs existential deposit + ( + RewardsEthereumSovereignAccount::get(), + ExistentialDeposit::get().into(), + ), // Rewards account needs existential deposit + ]; pallet_balances::GenesisConfig:: { balances } .assimilate_storage(&mut t) .unwrap(); diff --git a/operator/pallets/external-validators-rewards/src/tests.rs b/operator/pallets/external-validators-rewards/src/tests.rs index 92e2aefc..1091674a 100644 --- a/operator/pallets/external-validators-rewards/src/tests.rs +++ b/operator/pallets/external-validators-rewards/src/tests.rs @@ -16,6 +16,7 @@ use { crate::{self as pallet_external_validators_rewards, mock::*}, + frame_support::traits::fungible::Mutate, pallet_external_validators::traits::{ActiveEraInfo, OnEraEnd, OnEraStart}, sp_std::collections::btree_map::BTreeMap, }; @@ -211,3 +212,550 @@ fn test_on_era_end_with_zero_points() { ); }) } +#[test] +fn test_inflation_minting() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }); + // Set inflation amount directly for this test + mock.era_inflation = Some(10_000_000); // 10 million tokens per era + }); + + let rewards_account = RewardsEthereumSovereignAccount::get(); + let initial_rewards_balance = Balances::free_balance(&rewards_account); + + // Reward some validators to create reward points + let points = vec![10u32, 30u32, 50u32]; + let accounts = vec![1u64, 3u64, 5u64]; + let accounts_points: Vec<(u64, crate::RewardPoints)> = accounts + .iter() + .cloned() + .zip(points.iter().cloned()) + .collect(); + ExternalValidatorsRewards::reward_by_ids(accounts_points); + + // Trigger era end which should mint inflation + ExternalValidatorsRewards::on_era_end(1); + + // Verify inflation was minted (80% to rewards, 20% to treasury) + let final_rewards_balance = Balances::free_balance(&rewards_account); + let inflation_amount = + ::EraInflationProvider::get(); + let rewards_amount = inflation_amount * 80 / 100; // 80% goes to rewards + + assert_eq!( + final_rewards_balance, + initial_rewards_balance + rewards_amount, + "Inflation should have been minted to rewards account" + ); + }) +} + +#[test] +fn test_inflation_calculation_with_different_rates() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Test with different inflation amounts + for inflation_amount in [1_000_000u128, 5_000_000u128, 10_000_000u128] { + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }); + mock.era_inflation = Some(inflation_amount); + }); + + let rewards_account = RewardsEthereumSovereignAccount::get(); + let initial_balance = Balances::free_balance(&rewards_account); + + // Add some reward points + ExternalValidatorsRewards::reward_by_ids([(1, 100)]); + + // Trigger era end + ExternalValidatorsRewards::on_era_end(1); + + // Verify correct amount was minted (80% to rewards, 20% to treasury) + let final_balance = Balances::free_balance(&rewards_account); + let rewards_amount = inflation_amount * 80 / 100; + assert_eq!( + final_balance - initial_balance, + rewards_amount, + "Incorrect inflation amount minted for rate {}", + inflation_amount + ); + + // Clean up for next iteration + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 2, + start: None, + }); + }); + } + }) +} + +#[test] +fn test_no_inflation_with_zero_points() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }); + mock.era_inflation = Some(10_000_000); + }); + + let rewards_account = RewardsEthereumSovereignAccount::get(); + let initial_balance = Balances::free_balance(&rewards_account); + + // Don't add any reward points (or add zero points) + // This should prevent inflation from being minted + + ExternalValidatorsRewards::on_era_end(1); + + // Verify no inflation was minted because there were no reward points + let final_balance = Balances::free_balance(&rewards_account); + assert_eq!( + final_balance, initial_balance, + "No inflation should be minted when there are no reward points" + ); + }) +} + +#[test] +fn test_inflation_calculation_accuracy() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Test that the inflation calculation doesn't lose precision + let expected_inflation = 12_345_678_901_234u128; // Large number with precision + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }); + mock.era_inflation = Some(expected_inflation); + }); + + let rewards_account = RewardsEthereumSovereignAccount::get(); + let initial_balance = Balances::free_balance(&rewards_account); + + // Add reward points + ExternalValidatorsRewards::reward_by_ids([(1, 100), (2, 200)]); + + // Trigger era end + ExternalValidatorsRewards::on_era_end(1); + + // Verify amount was minted (80% to rewards, minor rounding acceptable) + let final_balance = Balances::free_balance(&rewards_account); + let rewards_amount = expected_inflation * 80 / 100; + let actual_minted = final_balance - initial_balance; + // Allow 1 unit difference due to Perbill rounding in treasury calculation + assert!( + actual_minted >= rewards_amount.saturating_sub(1) && + actual_minted <= rewards_amount + 1, + "Inflation calculation should maintain precision (within 1 unit). Expected: {}, Got: {}", + rewards_amount, + actual_minted + ); + }) +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Treasury Allocation Tests +// ═══════════════════════════════════════════════════════════════════════════ + +#[test] +fn test_treasury_receives_20_percent_of_inflation() { + new_test_ext().execute_with(|| { + run_to_block(1); + + let base_inflation = 1_000_000u128; + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }); + mock.era_inflation = Some(base_inflation); + }); + + let rewards_account = RewardsEthereumSovereignAccount::get(); + let treasury_account = TreasuryAccount::get(); + + let initial_rewards = Balances::free_balance(&rewards_account); + let initial_treasury = Balances::free_balance(&treasury_account); + + // Add validators to trigger inflation + ExternalValidatorsRewards::reward_by_ids([ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (5, 100), + ]); + + ExternalValidatorsRewards::on_era_end(1); + + let final_rewards = Balances::free_balance(&rewards_account); + let final_treasury = Balances::free_balance(&treasury_account); + + let rewards_received = final_rewards - initial_rewards; + let treasury_received = final_treasury - initial_treasury; + + // Treasury should receive 20% of total inflation + let expected_treasury = base_inflation * 20 / 100; + let expected_rewards = base_inflation * 80 / 100; + + assert_eq!( + treasury_received, expected_treasury, + "Treasury should receive exactly 20% of inflation" + ); + assert_eq!( + rewards_received, expected_rewards, + "Rewards account should receive exactly 80% of inflation" + ); + assert_eq!( + treasury_received + rewards_received, + base_inflation, + "Total minted should equal base inflation" + ); + }) +} + +#[test] +fn test_treasury_allocation_with_different_amounts() { + new_test_ext().execute_with(|| { + run_to_block(1); + + let treasury_account = TreasuryAccount::get(); + let rewards_account = RewardsEthereumSovereignAccount::get(); + + for (era, inflation) in [(1, 100_000u128), (2, 5_000_000u128), (3, 999_999_999u128)] { + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: era, + start: None, + }); + mock.era_inflation = Some(inflation); + }); + + let treasury_before = Balances::free_balance(&treasury_account); + let rewards_before = Balances::free_balance(&rewards_account); + + ExternalValidatorsRewards::reward_by_ids([(1, 100), (2, 100)]); + ExternalValidatorsRewards::on_era_end(era); + + let treasury_after = Balances::free_balance(&treasury_account); + let rewards_after = Balances::free_balance(&rewards_account); + + let treasury_increase = treasury_after - treasury_before; + let rewards_increase = rewards_after - rewards_before; + + // Treasury gets mul_floor of 20%, rewards gets the remainder + // So treasury + rewards should equal total inflation + assert_eq!( + treasury_increase + rewards_increase, + inflation, + "Era {}: Treasury + Rewards should equal total inflation", + era + ); + + // Treasury should be approximately 20% (within 1 unit due to rounding) + let expected_treasury = inflation * 20 / 100; + assert!( + treasury_increase >= expected_treasury.saturating_sub(1) + && treasury_increase <= expected_treasury + 1, + "Era {}: Treasury should get approximately 20%", + era + ); + } + }) +} + +#[test] +fn test_treasury_allocation_maintains_precision() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Use prime number that doesn't divide evenly by 5 (20%) + let inflation = 1_234_567u128; + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }); + mock.era_inflation = Some(inflation); + }); + + let treasury_account = TreasuryAccount::get(); + let rewards_account = RewardsEthereumSovereignAccount::get(); + + let treasury_before = Balances::free_balance(&treasury_account); + let rewards_before = Balances::free_balance(&rewards_account); + + ExternalValidatorsRewards::reward_by_ids([(1, 100)]); + ExternalValidatorsRewards::on_era_end(1); + + let treasury_after = Balances::free_balance(&treasury_account); + let rewards_after = Balances::free_balance(&rewards_account); + + let treasury_increase = treasury_after - treasury_before; + let rewards_increase = rewards_after - rewards_before; + let total_minted = treasury_increase + rewards_increase; + + // Total minted should equal total inflation (no rounding loss to exceed inflation) + assert!( + total_minted <= inflation, + "Total minted should not exceed inflation due to rounding" + ); + + // But should be very close (within 1 token for rounding) + assert!( + inflation - total_minted < 100, + "Rounding loss should be minimal" + ); + }) +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Edge Case Tests +// ═══════════════════════════════════════════════════════════════════════════ + +#[test] +fn test_single_validator_network() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }); + mock.era_inflation = Some(1_000_000); + }); + + let rewards_account = RewardsEthereumSovereignAccount::get(); + let initial_balance = Balances::free_balance(&rewards_account); + + // Only one validator participates + ExternalValidatorsRewards::reward_by_ids([(1, 100)]); + + ExternalValidatorsRewards::on_era_end(1); + + let final_balance = Balances::free_balance(&rewards_account); + let inflation_received = final_balance - initial_balance; + + // Single validator should still trigger full inflation (for rewards portion) + assert!( + inflation_received > 0, + "Single validator should receive rewards" + ); + }) +} + +#[test] +fn test_very_large_inflation_no_overflow() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Use close to u128::MAX to test overflow protection + let large_inflation = u128::MAX / 2; + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }); + mock.era_inflation = Some(large_inflation); + }); + + let rewards_account = RewardsEthereumSovereignAccount::get(); + let treasury_account = TreasuryAccount::get(); + + let rewards_before = Balances::free_balance(&rewards_account); + let treasury_before = Balances::free_balance(&treasury_account); + + ExternalValidatorsRewards::reward_by_ids([(1, 100)]); + ExternalValidatorsRewards::on_era_end(1); + + let rewards_after = Balances::free_balance(&rewards_account); + let treasury_after = Balances::free_balance(&treasury_account); + + // Should not panic or overflow + assert!(rewards_after >= rewards_before, "Rewards should increase"); + assert!( + treasury_after >= treasury_before, + "Treasury should increase" + ); + + // Total should not exceed input + let total_increase = (rewards_after - rewards_before) + (treasury_after - treasury_before); + assert!( + total_increase <= large_inflation, + "Total minted should not exceed inflation amount" + ); + }) +} + +#[test] +fn test_very_small_inflation_amounts() { + new_test_ext().execute_with(|| { + run_to_block(1); + + // Test with very small amounts + for tiny_amount in [1u128, 2u128, 5u128, 10u128] { + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: tiny_amount as u32, + start: None, + }); + mock.era_inflation = Some(tiny_amount); + }); + + let rewards_account = RewardsEthereumSovereignAccount::get(); + let treasury_account = TreasuryAccount::get(); + + let rewards_before = Balances::free_balance(&rewards_account); + let treasury_before = Balances::free_balance(&treasury_account); + + ExternalValidatorsRewards::reward_by_ids([(1, 100)]); + ExternalValidatorsRewards::on_era_end(tiny_amount as u32); + + let rewards_after = Balances::free_balance(&rewards_account); + let treasury_after = Balances::free_balance(&treasury_account); + + let total_minted = + (rewards_after - rewards_before) + (treasury_after - treasury_before); + + // Should handle small amounts gracefully (may round to 0 for treasury) + assert!( + total_minted <= tiny_amount, + "Amount {} should not exceed inflation", + tiny_amount + ); + } + }) +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Integration and Regression Tests +// ═══════════════════════════════════════════════════════════════════════════ + +#[test] +fn test_consistent_inflation_across_eras() { + new_test_ext().execute_with(|| { + run_to_block(1); + + let base_inflation = 5_000_000u128; + let rewards_account = RewardsEthereumSovereignAccount::get(); + + // Run multiple eras with identical conditions + for era in 1..=5 { + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: era, + start: None, + }); + mock.era_inflation = Some(base_inflation); + }); + + let balance_before = Balances::free_balance(&rewards_account); + + // Same participation every era + ExternalValidatorsRewards::reward_by_ids([(1, 100), (2, 100), (3, 100)]); + + ExternalValidatorsRewards::on_era_end(era); + + let balance_after = Balances::free_balance(&rewards_account); + let inflation = balance_after - balance_before; + + // Each era should mint the same amount given identical conditions + let expected = base_inflation * 80 / 100; // 80% to rewards account + assert_eq!( + inflation, expected, + "Era {}: Inflation should be consistent across eras", + era + ); + } + }) +} + +#[test] +fn test_no_unexpected_balance_changes() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }); + mock.era_inflation = Some(1_000_000); + }); + + // Check balances of non-participating accounts don't change + let observer_account = 99u64; + let _ = Balances::mint_into(&observer_account, 1000); // Give it some balance + + let observer_balance_before = Balances::free_balance(&observer_account); + + ExternalValidatorsRewards::reward_by_ids([(1, 100), (2, 100)]); + ExternalValidatorsRewards::on_era_end(1); + + let observer_balance_after = Balances::free_balance(&observer_account); + + assert_eq!( + observer_balance_before, observer_balance_after, + "Non-participating accounts should not be affected" + ); + }) +} + +#[test] +fn test_total_issuance_increases_correctly() { + new_test_ext().execute_with(|| { + run_to_block(1); + + let inflation = 10_000_000u128; + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }); + mock.era_inflation = Some(inflation); + }); + + let total_issuance_before = Balances::total_issuance(); + + ExternalValidatorsRewards::reward_by_ids([ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (5, 100), + ]); + + ExternalValidatorsRewards::on_era_end(1); + + let total_issuance_after = Balances::total_issuance(); + + // Total issuance should increase by exactly the inflation amount + assert_eq!( + total_issuance_after - total_issuance_before, + inflation, + "Total issuance should increase by inflation amount" + ); + }) +} diff --git a/operator/runtime/common/src/constants.rs b/operator/runtime/common/src/constants.rs index 380566dd..4ebb1979 100644 --- a/operator/runtime/common/src/constants.rs +++ b/operator/runtime/common/src/constants.rs @@ -35,6 +35,10 @@ pub mod time { pub const HOURS: BlockNumber = MINUTES * 60; pub const DAYS: BlockNumber = HOURS * 24; pub const WEEKS: BlockNumber = DAYS * 7; + + /// Milliseconds per year (365.25 days to account for leap years) + /// Used for inflation calculations + pub const MILLISECONDS_PER_YEAR: u128 = 31_557_600_000; } pub mod gas { diff --git a/operator/runtime/common/src/inflation.rs b/operator/runtime/common/src/inflation.rs new file mode 100644 index 00000000..f4c3093a --- /dev/null +++ b/operator/runtime/common/src/inflation.rs @@ -0,0 +1,635 @@ +// Copyright 2025 DataHaven +// This file is part of DataHaven. +// +// DataHaven 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. +// +// DataHaven 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 DataHaven. If not, see . + +//! Common inflation handling utilities for validator rewards. +//! +//! This module provides reusable implementations for calculating and minting +//! inflation-based rewards that can be shared across different runtime configurations. + +use crate::constants::time::MILLISECONDS_PER_YEAR; +use frame_support::traits::{fungible::Inspect, Get}; +use sp_runtime::Perbill; + +/// Generic era inflation provider that calculates per-era inflation based on annual inflation rate. +/// +/// # Type Parameters +/// * `R` - Runtime type that provides access to necessary pallets and configuration +/// * `Balances` - Pallet implementing fungible::Inspect for total issuance +/// * `AnnualRate` - Get providing the target annual inflation rate +/// * `SessionsPerEra` - Get providing the number of sessions per era +/// * `BlocksPerSession` - Get providing the number of blocks per session +/// * `MillisecsPerBlock` - Get providing milliseconds per block +/// +/// # Calculation +/// 1. Gets total token issuance from Balances pallet +/// 2. Retrieves annual inflation rate from runtime parameters +/// 3. Calculates eras per year based on era duration +/// 4. Divides annual inflation by eras per year to get per-era amount +pub struct ExternalRewardsEraInflationProvider< + Balances, + AnnualRate, + SessionsPerEra, + BlocksPerSession, + MillisecsPerBlock, +>( + sp_std::marker::PhantomData<( + Balances, + AnnualRate, + SessionsPerEra, + BlocksPerSession, + MillisecsPerBlock, + )>, +); + +impl Get + for ExternalRewardsEraInflationProvider< + Balances, + AnnualRate, + SessionsPerEra, + BlocksPerSession, + MillisecsPerBlock, + > +where + Balances: Inspect, + AnnualRate: Get, + SessionsPerEra: Get, + BlocksPerSession: Get, + MillisecsPerBlock: Get, +{ + fn get() -> u128 { + use sp_runtime::traits::Zero; + + let total_issuance = Balances::total_issuance(); + if total_issuance.is_zero() { + return 0; + } + + let annual_inflation_rate = AnnualRate::get(); + + // Calculate eras per year + // - SessionsPerEra: number of sessions in an era + // - BlocksPerSession: number of blocks in an epoch (session) + // - MillisecsPerBlock: milliseconds per block (6000ms = 6s) + // - Year in milliseconds: 365.25 * 24 * 60 * 60 * 1000 + + let sessions_per_era = SessionsPerEra::get() as u128; + let blocks_per_session = BlocksPerSession::get() as u128; + let millisecs_per_block = MillisecsPerBlock::get() as u128; + + let millisecs_per_era = sessions_per_era + .saturating_mul(blocks_per_session) + .saturating_mul(millisecs_per_block); + + if millisecs_per_era.is_zero() { + log::error!( + target: "ext_validators_rewards", + "Invalid era duration configuration" + ); + return 0; + } + + let eras_per_year = MILLISECONDS_PER_YEAR.saturating_div(millisecs_per_era); + if eras_per_year.is_zero() { + log::error!( + target: "ext_validators_rewards", + "Eras per year is zero, check configuration" + ); + return 0; + } + + // Calculate per-era inflation + let annual_inflation = annual_inflation_rate.mul_floor(total_issuance); + let per_era_inflation = annual_inflation.saturating_div(eras_per_year); + + log::info!( + target: "ext_validators_rewards", + "Per-era inflation: {}", + per_era_inflation + ); + + per_era_inflation + } +} + +/// Generic implementation of inflation handler that mints tokens and splits between rewards and treasury. +/// +/// # Type Parameters +/// * `Balances` - Currency pallet implementing fungible::Mutate for minting +/// * `TreasuryProportion` - Get providing the treasury allocation percentage +/// * `TreasuryAccount` - Get providing the treasury account +/// +/// # Functionality +/// 1. Validates the total amount is non-zero +/// 2. Calculates treasury allocation based on configured proportion +/// 3. Mints rewards portion to the rewards account +/// 4. Mints treasury portion to the treasury account +/// +/// This struct provides a mint_inflation method that can be called from wrapper implementations +/// in your runtime to avoid circular dependencies. +pub struct ExternalRewardsInflationHandler( + sp_std::marker::PhantomData<(Balances, TreasuryProportion, TreasuryAccount)>, +); + +impl + ExternalRewardsInflationHandler +where + Balances: frame_support::traits::fungible::Mutate, + TreasuryProportion: Get, + TreasuryAccount: Get, +{ + /// Mints inflation tokens and splits them between rewards and treasury accounts + pub fn mint_inflation( + rewards_account: &crate::AccountId, + total_amount: u128, + ) -> sp_runtime::DispatchResult { + use sp_runtime::traits::Zero; + + if total_amount.is_zero() { + log::error!( + target: "ext_validators_rewards", + "Attempted to mint zero inflation" + ); + return Err(sp_runtime::DispatchError::Other( + "Cannot mint zero inflation", + )); + } + + // Get treasury allocation proportion + let treasury_proportion = TreasuryProportion::get(); + + // Calculate amounts + let treasury_amount = treasury_proportion.mul_floor(total_amount); + let rewards_amount = total_amount.saturating_sub(treasury_amount); + + log::debug!( + target: "ext_validators_rewards", + "Minting inflation: total={}, treasury={}, rewards={}", + total_amount, + treasury_amount, + rewards_amount + ); + + // Mint rewards to the rewards account + if !rewards_amount.is_zero() { + Balances::mint_into(rewards_account, rewards_amount).map_err(|e| { + log::error!( + target: "ext_validators_rewards", + "Failed to mint rewards inflation: {:?}", + e + ); + sp_runtime::DispatchError::Other("Failed to mint rewards inflation") + })?; + } + + // Mint treasury portion if non-zero + if !treasury_amount.is_zero() { + let treasury_account = TreasuryAccount::get(); + Balances::mint_into(&treasury_account, treasury_amount).map_err(|e| { + log::error!( + target: "ext_validators_rewards", + "Failed to mint treasury inflation: {:?}", + e + ); + sp_runtime::DispatchError::Other("Failed to mint treasury inflation") + })?; + + log::info!( + target: "ext_validators_rewards", + "Successfully minted {} to treasury from inflation", + treasury_amount + ); + } + + log::info!( + target: "ext_validators_rewards", + "Successfully minted {} total inflation ({} to rewards, {} to treasury)", + total_amount, + rewards_amount, + treasury_amount + ); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::{ + parameter_types, + traits::fungible::{Inspect, Mutate, Unbalanced}, + }; + use sp_runtime::Perbill; + use std::cell::RefCell; + + // Mock balances storage + thread_local! { + static TOTAL_ISSUANCE: RefCell = const { RefCell::new(0) }; + static BALANCES: RefCell> = RefCell::new(std::collections::HashMap::new()); + } + + struct MockBalances; + + impl Inspect for MockBalances { + type Balance = u128; + + fn total_issuance() -> Self::Balance { + TOTAL_ISSUANCE.with(|v| *v.borrow()) + } + + fn minimum_balance() -> Self::Balance { + 0 + } + + fn balance(_who: &crate::AccountId) -> Self::Balance { + 0 + } + + fn total_balance(_who: &crate::AccountId) -> Self::Balance { + 0 + } + + fn reducible_balance( + _who: &crate::AccountId, + _preservation: frame_support::traits::tokens::Preservation, + _force: frame_support::traits::tokens::Fortitude, + ) -> Self::Balance { + 0 + } + + fn can_deposit( + _who: &crate::AccountId, + _amount: Self::Balance, + _provenance: frame_support::traits::tokens::Provenance, + ) -> frame_support::traits::tokens::DepositConsequence { + frame_support::traits::tokens::DepositConsequence::Success + } + + fn can_withdraw( + _who: &crate::AccountId, + _amount: Self::Balance, + ) -> frame_support::traits::tokens::WithdrawConsequence { + frame_support::traits::tokens::WithdrawConsequence::Success + } + } + + impl Unbalanced for MockBalances { + fn write_balance( + _who: &crate::AccountId, + _amount: Self::Balance, + ) -> Result, sp_runtime::DispatchError> { + Ok(None) + } + + fn set_total_issuance(amount: Self::Balance) -> () { + TOTAL_ISSUANCE.with(|v| *v.borrow_mut() = amount); + } + + fn handle_dust(_dust: frame_support::traits::fungible::Dust) { + // No-op for tests + } + } + + impl Mutate for MockBalances { + fn mint_into( + who: &crate::AccountId, + amount: Self::Balance, + ) -> Result { + BALANCES.with(|b| { + let mut balances = b.borrow_mut(); + let balance = balances.entry(*who).or_insert(0); + *balance = balance.saturating_add(amount); + }); + TOTAL_ISSUANCE.with(|v| { + let mut issuance = v.borrow_mut(); + *issuance = issuance.saturating_add(amount); + }); + Ok(amount) + } + + fn burn_from( + _who: &crate::AccountId, + _amount: Self::Balance, + _preservation: frame_support::traits::tokens::Preservation, + _precision: frame_support::traits::tokens::Precision, + _force: frame_support::traits::tokens::Fortitude, + ) -> Result { + Ok(0) + } + } + + fn treasury_account_id() -> crate::AccountId { + crate::AccountId::from([1u8; 20]) + } + + parameter_types! { + pub TreasuryAccountId: crate::AccountId = treasury_account_id(); + } + + fn reset_balances() { + TOTAL_ISSUANCE.with(|v| *v.borrow_mut() = 0); + BALANCES.with(|b| b.borrow_mut().clear()); + } + + fn get_balance(who: &crate::AccountId) -> u128 { + BALANCES.with(|b| *b.borrow().get(who).unwrap_or(&0)) + } + + fn set_total_issuance(amount: u128) { + TOTAL_ISSUANCE.with(|v| *v.borrow_mut() = amount); + } + + mod era_inflation_provider { + use super::*; + + parameter_types! { + pub const AnnualRate5Percent: Perbill = Perbill::from_percent(5); + pub const AnnualRate10Percent: Perbill = Perbill::from_percent(10); + pub const SessionsPerEra: u32 = 6; + pub const BlocksPerSession: u32 = 600; + pub const MillisecsPerBlock: u64 = 6000; + } + + type TestInflationProvider = ExternalRewardsEraInflationProvider< + MockBalances, + AnnualRate5Percent, + SessionsPerEra, + BlocksPerSession, + MillisecsPerBlock, + >; + + type TestInflationProvider10Pct = ExternalRewardsEraInflationProvider< + MockBalances, + AnnualRate10Percent, + SessionsPerEra, + BlocksPerSession, + MillisecsPerBlock, + >; + + #[test] + fn returns_zero_when_total_issuance_is_zero() { + reset_balances(); + set_total_issuance(0); + assert_eq!(TestInflationProvider::get(), 0); + } + + #[test] + fn calculates_correct_per_era_inflation_5_percent() { + reset_balances(); + // 1 billion tokens total issuance + let total_issuance = 1_000_000_000_000_000_000u128; + set_total_issuance(total_issuance); + + // With 6 sessions per era, 600 blocks per session, 6000ms per block: + // millisecs_per_era = 6 * 600 * 6000 = 21,600,000ms = 6 hours + // eras_per_year = 31,557,600,000 / 21,600,000 = 1461 eras + // annual_inflation = 5% of 1B = 50M + // per_era_inflation = 50M / 1461 ≈ 34,223,312 + + let per_era_inflation = TestInflationProvider::get(); + + // Expected: 5% of total_issuance / number of eras per year + // With the calculation: total_issuance * rate * millisecs_per_era / MILLISECONDS_PER_YEAR + let expected = 34_223_134_839_151u128; + assert_eq!(per_era_inflation, expected); + } + + #[test] + fn calculates_correct_per_era_inflation_10_percent() { + reset_balances(); + let total_issuance = 1_000_000_000_000_000_000u128; + set_total_issuance(total_issuance); + + let per_era_inflation = TestInflationProvider10Pct::get(); + + // Expected: 10% of total_issuance / number of eras per year + let expected = 68_446_269_678_302u128; + assert_eq!(per_era_inflation, expected); + } + + #[test] + fn scales_with_total_issuance() { + reset_balances(); + + // Test with 100M tokens + set_total_issuance(100_000_000_000_000_000u128); + let inflation_100m = TestInflationProvider::get(); + + // Test with 1B tokens (10x more) + set_total_issuance(1_000_000_000_000_000_000u128); + let inflation_1b = TestInflationProvider::get(); + + // Inflation should scale proportionally (allow 1 unit tolerance for rounding) + let expected = inflation_100m * 10; + assert!( + inflation_1b >= expected.saturating_sub(1) && inflation_1b <= expected + 1, + "Expected inflation to be ~{}, got {}", + expected, + inflation_1b + ); + } + + #[test] + fn handles_different_era_durations() { + reset_balances(); + set_total_issuance(1_000_000_000_000_000_000u128); + + parameter_types! { + pub const LongEraSessionsPerEra: u32 = 12; // Double the sessions + } + + type LongEraProvider = ExternalRewardsEraInflationProvider< + MockBalances, + AnnualRate5Percent, + LongEraSessionsPerEra, + BlocksPerSession, + MillisecsPerBlock, + >; + + let standard_era = TestInflationProvider::get(); + let long_era = LongEraProvider::get(); + + // Longer eras should have roughly 2x the inflation per era + // (since there are half as many eras per year) + assert!(long_era > standard_era * 19 / 10); // Allow some rounding tolerance + assert!(long_era < standard_era * 21 / 10); + } + } + + mod inflation_handler { + use super::*; + + parameter_types! { + pub const TreasuryProportion20Pct: Perbill = Perbill::from_percent(20); + pub const TreasuryProportion50Pct: Perbill = Perbill::from_percent(50); + pub const TreasuryProportion0Pct: Perbill = Perbill::from_percent(0); + pub const TreasuryProportion100Pct: Perbill = Perbill::from_percent(100); + } + + type TestHandler = ExternalRewardsInflationHandler< + MockBalances, + TreasuryProportion20Pct, + TreasuryAccountId, + >; + + type TestHandler50Pct = ExternalRewardsInflationHandler< + MockBalances, + TreasuryProportion50Pct, + TreasuryAccountId, + >; + + type TestHandler0Pct = ExternalRewardsInflationHandler< + MockBalances, + TreasuryProportion0Pct, + TreasuryAccountId, + >; + + type TestHandler100Pct = ExternalRewardsInflationHandler< + MockBalances, + TreasuryProportion100Pct, + TreasuryAccountId, + >; + + #[test] + fn rejects_zero_amount() { + reset_balances(); + let rewards_account = crate::AccountId::from([2u8; 20]); + + let result = TestHandler::mint_inflation(&rewards_account, 0); + assert!(result.is_err()); + } + + #[test] + fn splits_inflation_correctly_20_percent_treasury() { + reset_balances(); + let rewards_account = crate::AccountId::from([2u8; 20]); + let total_inflation = 1_000_000u128; + + let result = TestHandler::mint_inflation(&rewards_account, total_inflation); + assert!(result.is_ok()); + + let rewards_balance = get_balance(&rewards_account); + let treasury_balance = get_balance(&TreasuryAccountId::get()); + + // 20% to treasury, 80% to rewards + assert_eq!(treasury_balance, 200_000); + assert_eq!(rewards_balance, 800_000); + assert_eq!(rewards_balance + treasury_balance, total_inflation); + } + + #[test] + fn splits_inflation_correctly_50_percent_treasury() { + reset_balances(); + let rewards_account = crate::AccountId::from([2u8; 20]); + let total_inflation = 1_000_000u128; + + let result = TestHandler50Pct::mint_inflation(&rewards_account, total_inflation); + assert!(result.is_ok()); + + let rewards_balance = get_balance(&rewards_account); + let treasury_balance = get_balance(&TreasuryAccountId::get()); + + // 50% to treasury, 50% to rewards + assert_eq!(treasury_balance, 500_000); + assert_eq!(rewards_balance, 500_000); + } + + #[test] + fn handles_zero_percent_treasury() { + reset_balances(); + let rewards_account = crate::AccountId::from([2u8; 20]); + let total_inflation = 1_000_000u128; + + let result = TestHandler0Pct::mint_inflation(&rewards_account, total_inflation); + assert!(result.is_ok()); + + let rewards_balance = get_balance(&rewards_account); + let treasury_balance = get_balance(&TreasuryAccountId::get()); + + // 0% to treasury, 100% to rewards + assert_eq!(treasury_balance, 0); + assert_eq!(rewards_balance, total_inflation); + } + + #[test] + fn handles_hundred_percent_treasury() { + reset_balances(); + let rewards_account = crate::AccountId::from([2u8; 20]); + let total_inflation = 1_000_000u128; + + let result = TestHandler100Pct::mint_inflation(&rewards_account, total_inflation); + assert!(result.is_ok()); + + let rewards_balance = get_balance(&rewards_account); + let treasury_balance = get_balance(&TreasuryAccountId::get()); + + // 100% to treasury, 0% to rewards + assert_eq!(treasury_balance, total_inflation); + assert_eq!(rewards_balance, 0); + } + + #[test] + fn updates_total_issuance() { + reset_balances(); + let rewards_account = crate::AccountId::from([2u8; 20]); + let total_inflation = 1_000_000u128; + + let issuance_before = MockBalances::total_issuance(); + + let result = TestHandler::mint_inflation(&rewards_account, total_inflation); + assert!(result.is_ok()); + + let issuance_after = MockBalances::total_issuance(); + + assert_eq!(issuance_after, issuance_before + total_inflation); + } + + #[test] + fn handles_large_amounts() { + reset_balances(); + let rewards_account = crate::AccountId::from([2u8; 20]); + let total_inflation = u128::MAX / 2; // Very large amount + + let result = TestHandler::mint_inflation(&rewards_account, total_inflation); + assert!(result.is_ok()); + + let rewards_balance = get_balance(&rewards_account); + let treasury_balance = get_balance(&TreasuryAccountId::get()); + + // Verify proportions are maintained even with large numbers + assert_eq!(rewards_balance + treasury_balance, total_inflation); + } + + #[test] + fn handles_rounding_correctly() { + reset_balances(); + let rewards_account = crate::AccountId::from([2u8; 20]); + // Amount that will cause rounding: 100 with 20% treasury + let total_inflation = 100u128; + + let result = TestHandler::mint_inflation(&rewards_account, total_inflation); + assert!(result.is_ok()); + + let rewards_balance = get_balance(&rewards_account); + let treasury_balance = get_balance(&TreasuryAccountId::get()); + + // 20% of 100 = 20, rewards = 80 + assert_eq!(treasury_balance, 20); + assert_eq!(rewards_balance, 80); + assert_eq!(rewards_balance + treasury_balance, total_inflation); + } + } +} diff --git a/operator/runtime/common/src/lib.rs b/operator/runtime/common/src/lib.rs index 92c20e98..fc614aa7 100644 --- a/operator/runtime/common/src/lib.rs +++ b/operator/runtime/common/src/lib.rs @@ -22,6 +22,7 @@ pub use constants::*; pub mod benchmarking; pub mod deal_with_fees; pub mod impl_on_charge_evm_transaction; +pub mod inflation; pub mod migrations; pub use migrations::*; pub mod safe_mode; diff --git a/operator/runtime/mainnet/src/configs/mod.rs b/operator/runtime/mainnet/src/configs/mod.rs index dc16b9ca..13970526 100644 --- a/operator/runtime/mainnet/src/configs/mod.rs +++ b/operator/runtime/mainnet/src/configs/mod.rs @@ -32,7 +32,7 @@ use super::{ }; use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; -use sp_runtime::RuntimeDebug; +use sp_runtime::{traits::AccountIdConversion, RuntimeDebug}; /// A description of our proxy types. /// Proxy types are used to restrict the calls that can be made by a proxy account. @@ -897,6 +897,14 @@ parameter_types! { pub const TreasuryId: PalletId = PalletId(*b"pc/trsry"); pub TreasuryAccount: AccountId = Treasury::account_id(); pub const MaxSpendBalance: crate::Balance = crate::Balance::max_value(); + + /// PalletId for the External Validator Rewards account. + /// This account receives minted inflation tokens before they are bridged to Ethereum + /// for distribution to validators via EigenLayer. + /// + /// Governance/Sudo can transfer funds using: pallet_balances::force_transfer + pub const ExternalValidatorRewardsId: PalletId = PalletId(*b"dh/evrew"); + pub ExternalValidatorRewardsAccount: AccountId = ExternalValidatorRewardsId::get().into_account_truncating(); } type RootOrTreasuryCouncilOrigin = EitherOfDiverse< @@ -1416,6 +1424,41 @@ impl Get> for GetWhitelistedValidators { } } +/// Type alias for the era inflation provider using common runtime implementation. +/// +/// Calculates per-era inflation based on: +/// - Total token issuance (from Balances pallet) +/// - Annual inflation rate (from InflationTargetedAnnualRate dynamic parameter) +/// - Era duration calculated from SessionsPerEra, EpochDurationInBlocks, and MILLISECS_PER_BLOCK +pub type ExternalRewardsEraInflationProvider = + datahaven_runtime_common::inflation::ExternalRewardsEraInflationProvider< + Balances, + runtime_params::dynamic_params::runtime_config::InflationTargetedAnnualRate, + SessionsPerEra, + EpochDurationInBlocks, + ConstU64, + >; + +/// Wrapper struct for the inflation handler using common runtime implementation. +/// +/// Handles minting of inflation tokens by: +/// 1. Splitting total inflation between rewards and treasury based on InflationTreasuryProportion +/// 2. Minting rewards portion to the rewards account +/// 3. Minting treasury portion to the treasury account +pub struct ExternalRewardsInflationHandler; + +impl pallet_external_validators_rewards::types::HandleInflation + for ExternalRewardsInflationHandler +{ + fn mint_inflation(who: &AccountId, amount: u128) -> sp_runtime::DispatchResult { + datahaven_runtime_common::inflation::ExternalRewardsInflationHandler::< + Balances, + runtime_params::dynamic_params::runtime_config::InflationTreasuryProportion, + TreasuryAccount, + >::mint_inflation(who, amount) + } +} + // Stub SendMessage implementation for rewards pallet pub struct RewardsSendAdapter; impl pallet_external_validators_rewards::types::SendMessage for RewardsSendAdapter { @@ -1479,16 +1522,21 @@ impl pallet_external_validators_rewards::Config for Runtime { type RuntimeEvent = RuntimeEvent; type EraIndexProvider = ExternalValidators; type HistoryDepth = ConstU32<64>; - type BackingPoints = ConstU32<20>; - type DisputeStatementPoints = ConstU32<20>; - type EraInflationProvider = ConstU128<0>; + + // NOT USED: DataHaven is a solochain with BABE+GRANDPA consensus, not a parachain. + // Backing and dispute points are only relevant for parachain validation. + // These are set to 0 to make it explicit they're unused. + type BackingPoints = ConstU32<0>; + type DisputeStatementPoints = ConstU32<0>; + + type EraInflationProvider = ExternalRewardsEraInflationProvider; type ExternalIndexProvider = ExternalValidators; type GetWhitelistedValidators = GetWhitelistedValidators; type Hashing = Keccak256; type Currency = Balances; - type RewardsEthereumSovereignAccount = TreasuryAccount; + type RewardsEthereumSovereignAccount = ExternalValidatorRewardsAccount; type SendMessage = RewardsSendAdapter; - type HandleInflation = (); + type HandleInflation = ExternalRewardsInflationHandler; type WeightInfo = mainnet_weights::pallet_external_validators_rewards::WeightInfo; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); diff --git a/operator/runtime/mainnet/src/configs/runtime_params.rs b/operator/runtime/mainnet/src/configs/runtime_params.rs index e039bb1a..4d1527e3 100644 --- a/operator/runtime/mainnet/src/configs/runtime_params.rs +++ b/operator/runtime/mainnet/src/configs/runtime_params.rs @@ -339,6 +339,24 @@ pub mod dynamic_params { #[allow(non_upper_case_globals)] /// The AVS ethereum address for Datahaven. Via this address we relay slashing requests or other requests. pub static DatahavenAVSAddress: H160 = H160::repeat_byte(0x0); + + // ╔══════════════════════ Validator Rewards Inflation ═══════════════════════╗ + + #[codec(index = 37)] + #[allow(non_upper_case_globals)] + /// Targeted annual inflation rate. + /// Default: 5% per annum + /// This rate is divided across all eras in a year to calculate per-era inflation. + pub static InflationTargetedAnnualRate: Perbill = Perbill::from_percent(5); + + #[codec(index = 38)] + #[allow(non_upper_case_globals)] + /// Proportion of inflation rewards allocated to the treasury. + /// Default: 20% of minted rewards go to treasury, 80% to validator rewards + /// The treasury portion is minted separately and sent to the treasury account. + pub static InflationTreasuryProportion: Perbill = Perbill::from_percent(20); + + // ╚══════════════════════ Validator Rewards Inflation ═══════════════════════╝ } } diff --git a/operator/runtime/stagenet/src/configs/mod.rs b/operator/runtime/stagenet/src/configs/mod.rs index bd569552..9591759a 100644 --- a/operator/runtime/stagenet/src/configs/mod.rs +++ b/operator/runtime/stagenet/src/configs/mod.rs @@ -32,7 +32,7 @@ use super::{ }; use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; -use sp_runtime::RuntimeDebug; +use sp_runtime::{traits::AccountIdConversion, RuntimeDebug}; /// A description of our proxy types. /// Proxy types are used to restrict the calls that can be made by a proxy account. @@ -900,6 +900,14 @@ parameter_types! { pub const TreasuryId: PalletId = PalletId(*b"pc/trsry"); pub TreasuryAccount: AccountId = Treasury::account_id(); pub const MaxSpendBalance: crate::Balance = crate::Balance::max_value(); + + /// PalletId for the External Validator Rewards account. + /// This account receives minted inflation tokens before they are bridged to Ethereum + /// for distribution to validators via EigenLayer. + /// + /// Governance/Sudo can transfer funds using: pallet_balances::force_transfer + pub const ExternalValidatorRewardsId: PalletId = PalletId(*b"dh/evrew"); + pub ExternalValidatorRewardsAccount: AccountId = ExternalValidatorRewardsId::get().into_account_truncating(); } type RootOrTreasuryCouncilOrigin = EitherOfDiverse< @@ -1421,6 +1429,41 @@ impl Get> for GetWhitelistedValidators { } } +/// Type alias for the era inflation provider using common runtime implementation. +/// +/// Calculates per-era inflation based on: +/// - Total token issuance (from Balances pallet) +/// - Annual inflation rate (from InflationTargetedAnnualRate dynamic parameter) +/// - Era duration calculated from SessionsPerEra, EpochDurationInBlocks, and MILLISECS_PER_BLOCK +pub type ExternalRewardsEraInflationProvider = + datahaven_runtime_common::inflation::ExternalRewardsEraInflationProvider< + Balances, + runtime_params::dynamic_params::runtime_config::InflationTargetedAnnualRate, + SessionsPerEra, + EpochDurationInBlocks, + ConstU64, + >; + +/// Wrapper struct for the inflation handler using common runtime implementation. +/// +/// Handles minting of inflation tokens by: +/// 1. Splitting total inflation between rewards and treasury based on InflationTreasuryProportion +/// 2. Minting rewards portion to the rewards account +/// 3. Minting treasury portion to the treasury account +pub struct ExternalRewardsInflationHandler; + +impl pallet_external_validators_rewards::types::HandleInflation + for ExternalRewardsInflationHandler +{ + fn mint_inflation(who: &AccountId, amount: u128) -> sp_runtime::DispatchResult { + datahaven_runtime_common::inflation::ExternalRewardsInflationHandler::< + Balances, + runtime_params::dynamic_params::runtime_config::InflationTreasuryProportion, + TreasuryAccount, + >::mint_inflation(who, amount) + } +} + // Stub SendMessage implementation for rewards pallet pub struct RewardsSendAdapter; impl pallet_external_validators_rewards::types::SendMessage for RewardsSendAdapter { @@ -1484,17 +1527,22 @@ impl pallet_external_validators_rewards::Config for Runtime { type RuntimeEvent = RuntimeEvent; type EraIndexProvider = ExternalValidators; type HistoryDepth = ConstU32<64>; - type BackingPoints = ConstU32<20>; - type DisputeStatementPoints = ConstU32<20>; - type EraInflationProvider = ConstU128<0>; + + // NOT USED: DataHaven is a solochain with BABE+GRANDPA consensus, not a parachain. + // Backing and dispute points are only relevant for parachain validation. + // These are set to 0 to make it explicit they're unused. + type BackingPoints = ConstU32<0>; + type DisputeStatementPoints = ConstU32<0>; + + type EraInflationProvider = ExternalRewardsEraInflationProvider; type ExternalIndexProvider = ExternalValidators; type GetWhitelistedValidators = GetWhitelistedValidators; type Hashing = Keccak256; type Currency = Balances; - type RewardsEthereumSovereignAccount = TreasuryAccount; + type RewardsEthereumSovereignAccount = ExternalValidatorRewardsAccount; type WeightInfo = stagenet_weights::pallet_external_validators_rewards::WeightInfo; type SendMessage = RewardsSendAdapter; - type HandleInflation = (); + type HandleInflation = ExternalRewardsInflationHandler; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/operator/runtime/stagenet/src/configs/runtime_params.rs b/operator/runtime/stagenet/src/configs/runtime_params.rs index 1280d2ba..333b3dfb 100644 --- a/operator/runtime/stagenet/src/configs/runtime_params.rs +++ b/operator/runtime/stagenet/src/configs/runtime_params.rs @@ -344,6 +344,24 @@ pub mod dynamic_params { #[allow(non_upper_case_globals)] /// The AVS ethereum address for Datahaven. Via this address we relay slashing requests or other requests. pub static DatahavenAVSAddress: H160 = H160::repeat_byte(0x0); + + // ╔══════════════════════ Validator Rewards Inflation ═══════════════════════╗ + + #[codec(index = 37)] + #[allow(non_upper_case_globals)] + /// Targeted annual inflation rate. + /// Default: 5% per annum + /// This rate is divided across all eras in a year to calculate per-era inflation. + pub static InflationTargetedAnnualRate: Perbill = Perbill::from_percent(5); + + #[codec(index = 38)] + #[allow(non_upper_case_globals)] + /// Proportion of inflation rewards allocated to the treasury. + /// Default: 20% of minted rewards go to treasury, 80% to validator rewards + /// The treasury portion is minted separately and sent to the treasury account. + pub static InflationTreasuryProportion: Perbill = Perbill::from_percent(20); + + // ╚══════════════════════ Validator Rewards Inflation ═══════════════════════╝ } } diff --git a/operator/runtime/testnet/src/configs/mod.rs b/operator/runtime/testnet/src/configs/mod.rs index 64a74bdd..faa43057 100644 --- a/operator/runtime/testnet/src/configs/mod.rs +++ b/operator/runtime/testnet/src/configs/mod.rs @@ -32,7 +32,7 @@ use super::{ }; use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; -use sp_runtime::RuntimeDebug; +use sp_runtime::{traits::AccountIdConversion, RuntimeDebug}; /// A description of our proxy types. /// Proxy types are used to restrict the calls that can be made by a proxy account. @@ -903,6 +903,14 @@ parameter_types! { pub const TreasuryId: PalletId = PalletId(*b"pc/trsry"); pub TreasuryAccount: AccountId = Treasury::account_id(); pub const MaxSpendBalance: crate::Balance = crate::Balance::max_value(); + + /// PalletId for the External Validator Rewards account. + /// This account receives minted inflation tokens before they are bridged to Ethereum + /// for distribution to validators via EigenLayer. + /// + /// Governance/Sudo can transfer funds using: pallet_balances::force_transfer + pub const ExternalValidatorRewardsId: PalletId = PalletId(*b"dh/evrew"); + pub ExternalValidatorRewardsAccount: AccountId = ExternalValidatorRewardsId::get().into_account_truncating(); } type RootOrTreasuryCouncilOrigin = EitherOfDiverse< @@ -1422,6 +1430,41 @@ impl Get> for GetWhitelistedValidators { } } +/// Type alias for the era inflation provider using common runtime implementation. +/// +/// Calculates per-era inflation based on: +/// - Total token issuance (from Balances pallet) +/// - Annual inflation rate (from InflationTargetedAnnualRate dynamic parameter) +/// - Era duration calculated from SessionsPerEra, EpochDurationInBlocks, and MILLISECS_PER_BLOCK +pub type ExternalRewardsEraInflationProvider = + datahaven_runtime_common::inflation::ExternalRewardsEraInflationProvider< + Balances, + runtime_params::dynamic_params::runtime_config::InflationTargetedAnnualRate, + SessionsPerEra, + EpochDurationInBlocks, + ConstU64, + >; + +/// Wrapper struct for the inflation handler using common runtime implementation. +/// +/// Handles minting of inflation tokens by: +/// 1. Splitting total inflation between rewards and treasury based on InflationTreasuryProportion +/// 2. Minting rewards portion to the rewards account +/// 3. Minting treasury portion to the treasury account +pub struct ExternalRewardsInflationHandler; + +impl pallet_external_validators_rewards::types::HandleInflation + for ExternalRewardsInflationHandler +{ + fn mint_inflation(who: &AccountId, amount: u128) -> sp_runtime::DispatchResult { + datahaven_runtime_common::inflation::ExternalRewardsInflationHandler::< + Balances, + runtime_params::dynamic_params::runtime_config::InflationTreasuryProportion, + TreasuryAccount, + >::mint_inflation(who, amount) + } +} + // Stub SendMessage implementation for rewards pallet pub struct RewardsSendAdapter; impl pallet_external_validators_rewards::types::SendMessage for RewardsSendAdapter { @@ -1485,17 +1528,22 @@ impl pallet_external_validators_rewards::Config for Runtime { type RuntimeEvent = RuntimeEvent; type EraIndexProvider = ExternalValidators; type HistoryDepth = ConstU32<64>; - type BackingPoints = ConstU32<20>; - type DisputeStatementPoints = ConstU32<20>; - type EraInflationProvider = ConstU128<0>; + + // NOT USED: DataHaven is a solochain with BABE+GRANDPA consensus, not a parachain. + // Backing and dispute points are only relevant for parachain validation. + // These are set to 0 to make it explicit they're unused. + type BackingPoints = ConstU32<0>; + type DisputeStatementPoints = ConstU32<0>; + + type EraInflationProvider = ExternalRewardsEraInflationProvider; type ExternalIndexProvider = ExternalValidators; type GetWhitelistedValidators = GetWhitelistedValidators; type Hashing = Keccak256; type Currency = Balances; - type RewardsEthereumSovereignAccount = TreasuryAccount; + type RewardsEthereumSovereignAccount = ExternalValidatorRewardsAccount; type WeightInfo = testnet_weights::pallet_external_validators_rewards::WeightInfo; type SendMessage = RewardsSendAdapter; - type HandleInflation = (); + type HandleInflation = ExternalRewardsInflationHandler; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/operator/runtime/testnet/src/configs/runtime_params.rs b/operator/runtime/testnet/src/configs/runtime_params.rs index 0bc9ac09..51a189a8 100644 --- a/operator/runtime/testnet/src/configs/runtime_params.rs +++ b/operator/runtime/testnet/src/configs/runtime_params.rs @@ -340,6 +340,24 @@ pub mod dynamic_params { #[allow(non_upper_case_globals)] /// The AVS ethereum address for Datahaven. Via this address we relay slashing requests or other requests. pub static DatahavenAVSAddress: H160 = H160::repeat_byte(0x0); + + // ╔══════════════════════ Validator Rewards Inflation ═══════════════════════╗ + + #[codec(index = 37)] + #[allow(non_upper_case_globals)] + /// Targeted annual inflation rate. + /// Default: 5% per annum + /// This rate is divided across all eras in a year to calculate per-era inflation. + pub static InflationTargetedAnnualRate: Perbill = Perbill::from_percent(5); + + #[codec(index = 38)] + #[allow(non_upper_case_globals)] + /// Proportion of inflation rewards allocated to the treasury. + /// Default: 20% of minted rewards go to treasury, 80% to validator rewards + /// The treasury portion is minted separately and sent to the treasury account. + pub static InflationTreasuryProportion: Perbill = Perbill::from_percent(20); + + // ╚══════════════════════ Validator Rewards Inflation ═══════════════════════╝ } } diff --git a/test/.papi/descriptors/package.json b/test/.papi/descriptors/package.json index 4b8553bd..3a795e80 100644 --- a/test/.papi/descriptors/package.json +++ b/test/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.13402936638329471124", + "version": "0.1.0-autogenerated.427841660215020592", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/test/.papi/metadata/datahaven.scale b/test/.papi/metadata/datahaven.scale index b58d3a099012a790f0a5dbda8cf3ee23fb84259a..e28b0323c895c3f769f79ae023dcefe1f6ba3381 100644 GIT binary patch delta 28272 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 r+6bYZOSfz^CPd6}boYwx}G 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 Ck$lSl