feat: Implement inflation mechanism for validator rewards (#304)

## 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 <noreply@anthropic.com>
This commit is contained in:
Steve Degosserie 2025-11-22 11:00:50 +01:00 committed by GitHub
parent 2cc1a4d3f0
commit 7f09949e64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1453 additions and 28 deletions

View file

@ -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<u64> 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"));
}
<Test as pallet_external_validators_rewards::Config>::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() {
<Test as pallet_external_validators_rewards::Config>::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();
<Test as pallet_external_validators_rewards::Config>::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::<Test> { balances }
.assimilate_storage(&mut t)
.unwrap();

View file

@ -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 =
<Test as pallet_external_validators_rewards::Config>::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"
);
})
}

View file

@ -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 {

View file

@ -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 <http://www.gnu.org/licenses/>.
//! 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<Perbill> providing the target annual inflation rate
/// * `SessionsPerEra` - Get<u32> providing the number of sessions per era
/// * `BlocksPerSession` - Get<u32> providing the number of blocks per session
/// * `MillisecsPerBlock` - Get<u64> 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<Balances, AnnualRate, SessionsPerEra, BlocksPerSession, MillisecsPerBlock> Get<u128>
for ExternalRewardsEraInflationProvider<
Balances,
AnnualRate,
SessionsPerEra,
BlocksPerSession,
MillisecsPerBlock,
>
where
Balances: Inspect<crate::AccountId, Balance = u128>,
AnnualRate: Get<Perbill>,
SessionsPerEra: Get<u32>,
BlocksPerSession: Get<u32>,
MillisecsPerBlock: Get<u64>,
{
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<Perbill> providing the treasury allocation percentage
/// * `TreasuryAccount` - Get<AccountId> 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<Balances, TreasuryProportion, TreasuryAccount>(
sp_std::marker::PhantomData<(Balances, TreasuryProportion, TreasuryAccount)>,
);
impl<Balances, TreasuryProportion, TreasuryAccount>
ExternalRewardsInflationHandler<Balances, TreasuryProportion, TreasuryAccount>
where
Balances: frame_support::traits::fungible::Mutate<crate::AccountId, Balance = u128>,
TreasuryProportion: Get<Perbill>,
TreasuryAccount: Get<crate::AccountId>,
{
/// 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<u128> = const { RefCell::new(0) };
static BALANCES: RefCell<std::collections::HashMap<crate::AccountId, u128>> = RefCell::new(std::collections::HashMap::new());
}
struct MockBalances;
impl Inspect<crate::AccountId> 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<Self::Balance> {
frame_support::traits::tokens::WithdrawConsequence::Success
}
}
impl Unbalanced<crate::AccountId> for MockBalances {
fn write_balance(
_who: &crate::AccountId,
_amount: Self::Balance,
) -> Result<Option<Self::Balance>, 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<crate::AccountId, Self>) {
// No-op for tests
}
}
impl Mutate<crate::AccountId> for MockBalances {
fn mint_into(
who: &crate::AccountId,
amount: Self::Balance,
) -> Result<Self::Balance, sp_runtime::DispatchError> {
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<Self::Balance, sp_runtime::DispatchError> {
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);
}
}
}

View file

@ -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;

View file

@ -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<Vec<AccountId>> 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<MILLISECS_PER_BLOCK>,
>;
/// 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<AccountId>
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<Runtime>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();

View file

@ -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 ═══════════════════════╝
}
}

View file

@ -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<Vec<AccountId>> 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<MILLISECS_PER_BLOCK>,
>;
/// 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<AccountId>
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<Runtime>;
type SendMessage = RewardsSendAdapter;
type HandleInflation = ();
type HandleInflation = ExternalRewardsInflationHandler;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}

View file

@ -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 ═══════════════════════╝
}
}

View file

@ -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<Vec<AccountId>> 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<MILLISECS_PER_BLOCK>,
>;
/// 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<AccountId>
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<Runtime>;
type SendMessage = RewardsSendAdapter;
type HandleInflation = ();
type HandleInflation = ExternalRewardsInflationHandler;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}

View file

@ -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 ═══════════════════════╝
}
}

View file

@ -1,5 +1,5 @@
{
"version": "0.1.0-autogenerated.13402936638329471124",
"version": "0.1.0-autogenerated.427841660215020592",
"name": "@polkadot-api/descriptors",
"files": [
"dist"

Binary file not shown.