#![cfg_attr(not(feature = "std"), no_std)] //! # PEZ Treasury Pezpallet //! //! A pezpallet for managing the PEZ token distribution and treasury with automated halving mechanics. //! //! ## Overview //! //! This pezpallet manages the complete lifecycle of PEZ token distribution including: //! //! - **Genesis Distribution**: One-time initial distribution to treasury, presale, and founder //! accounts //! - **Halving Mechanism**: Automatic reduction of monthly releases every 48 months (4 years) //! - **Monthly Releases**: Scheduled distribution to incentive and government pots //! - **Multi-Pot System**: Separate accounts for treasury, incentive rewards, and governance //! //! ## Token Economics //! //! - **Total Supply**: 5,000,000,000 PEZ (5 billion tokens) //! - **Treasury Allocation**: 96.25% (4,812,500,000 PEZ) //! - **Presale Allocation**: 1.875% (93,750,000 PEZ) //! - **Founder Allocation**: 1.875% (93,750,000 PEZ) //! //! ## Halving Schedule //! //! - **Halving Period**: Every 48 months (4 years) //! - **Period Duration**: 20,736,000 blocks (~4 years at 10 blocks/minute) //! - **Distribution**: 70% to Incentive Pot, 30% to Government Pot //! - **Automatic Halving**: Monthly release amount halves at the start of each new period //! //! ## Security Features //! //! - **One-Time Genesis**: Genesis distribution can only occur once (protected by storage flag) //! - **Privileged Operations**: All extrinsics require privileged origin (root or governance) //! - **Block-Based Scheduling**: Monthly releases based on block numbers for determinism //! //! ## Interface //! //! ### Extrinsics //! //! - `force_genesis_distribution()` - Perform initial token distribution (one-time only, //! privileged) //! - `initialize_treasury()` - Initialize the halving mechanism and start monthly releases //! (privileged) //! - `release_monthly_funds()` - Release monthly funds to incentive and government pots //! (privileged) //! //! ### Storage //! //! - `HalvingInfo` - Current halving period data and monthly release amount //! - `MonthlyReleases` - Historical record of all monthly distributions //! - `GenesisDistributionDone` - Flag to prevent duplicate genesis distribution //! //! ### Runtime Integration Example //! //! ```ignore //! impl pezpallet_pez_treasury::Config for Runtime { //! type RuntimeEvent = RuntimeEvent; //! type Assets = Assets; //! type WeightInfo = pezpallet_pez_treasury::weights::BizinikiwiWeight; //! type PezAssetId = ConstU32<1>; // PEZ asset ID //! type TreasuryPalletId = TreasuryPalletId; //! type IncentivePotId = IncentivePotId; //! type GovernmentPotId = GovernmentPotId; //! type PresaleAccount = PresaleAccount; //! type FounderAccount = FounderAccount; //! type ForceOrigin = EnsureRoot; //! } //! ``` pub use pezpallet::*; pub use weights::WeightInfo; pub mod migrations; pub mod weights; #[cfg(test)] mod mock; #[cfg(test)] mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; use pezframe_support::{ traits::{ fungibles::{Inspect, Mutate}, tokens::Preservation, Get, }, PalletId, }; use pezframe_system::pezpallet_prelude::BlockNumberFor; use scale_info::TypeInfo; use pezsp_runtime::traits::{AccountIdConversion, Saturating, Zero}; #[pezframe_support::pezpallet] pub mod pezpallet { use super::*; use pezframe_support::pezpallet_prelude::*; use pezframe_system::pezpallet_prelude::*; // use pezsp_runtime::traits::CheckedDiv; pub const HALVING_PERIOD_MONTHS: u32 = 48; // 4 years = 48 months pub const BLOCKS_PER_MONTH: u32 = 432_000; // ~30 days * 24 hours * 60 minutes * 10 blocks/minute pub const HALVING_PERIOD_BLOCKS: u32 = HALVING_PERIOD_MONTHS * BLOCKS_PER_MONTH; pub const TOTAL_SUPPLY: u128 = 5_000_000_000 * 1_000_000_000_000; // 5 billion PEZ (12 decimal) pub const TREASURY_ALLOCATION: u128 = 4_812_500_000 * 1_000_000_000_000; // %96.25 pub const PRESALE_ALLOCATION: u128 = 93_750_000 * 1_000_000_000_000; // %1.875 pub const FOUNDER_ALLOCATION: u128 = 93_750_000 * 1_000_000_000_000; // %1.875 #[pezpallet::pezpallet] #[pezpallet::storage_version(migrations::STORAGE_VERSION)] pub struct Pezpallet(_); #[pezpallet::config] pub trait Config: pezframe_system::Config + TypeInfo { type Assets: Mutate; type WeightInfo: weights::WeightInfo; #[pezpallet::constant] type PezAssetId: Get<>::AssetId>; #[pezpallet::constant] type TreasuryPalletId: Get; #[pezpallet::constant] type IncentivePotId: Get; #[pezpallet::constant] type GovernmentPotId: Get; #[pezpallet::constant] type PresaleAccount: Get; #[pezpallet::constant] type FounderAccount: Get; type ForceOrigin: EnsureOrigin; } pub type BalanceOf = <::Assets as Inspect<::AccountId>>::Balance; #[pezpallet::storage] #[pezpallet::getter(fn halving_info)] pub type HalvingInfo = StorageValue<_, HalvingData, ValueQuery>; #[pezpallet::storage] #[pezpallet::getter(fn monthly_releases)] pub type MonthlyReleases = StorageMap<_, Blake2_128Concat, u32, MonthlyRelease, OptionQuery>; #[pezpallet::storage] #[pezpallet::getter(fn next_release_month)] pub type NextReleaseMonth = StorageValue<_, u32, ValueQuery>; #[pezpallet::storage] #[pezpallet::getter(fn treasury_start_block)] pub type TreasuryStartBlock = StorageValue<_, BlockNumberFor, OptionQuery>; #[pezpallet::storage] #[pezpallet::getter(fn genesis_distribution_done)] pub type GenesisDistributionDone = StorageValue<_, bool, ValueQuery>; #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct HalvingData { pub current_period: u32, pub period_start_block: BlockNumberFor, pub monthly_amount: BalanceOf, pub total_released: BalanceOf, } #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct MonthlyRelease { pub month_index: u32, pub release_block: BlockNumberFor, pub amount_released: BalanceOf, pub incentive_amount: BalanceOf, pub government_amount: BalanceOf, } impl Default for HalvingData { fn default() -> Self { Self { current_period: 0, period_start_block: Zero::zero(), monthly_amount: Zero::zero(), total_released: Zero::zero(), } } } #[pezpallet::event] #[pezpallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { TreasuryInitialized { start_block: BlockNumberFor, initial_monthly_amount: BalanceOf, }, MonthlyFundsReleased { month_index: u32, total_amount: BalanceOf, incentive_amount: BalanceOf, government_amount: BalanceOf, }, NewHalvingPeriod { period: u32, new_monthly_amount: BalanceOf, }, GenesisDistributionCompleted { treasury_amount: BalanceOf, presale_amount: BalanceOf, founder_amount: BalanceOf, }, } #[pezpallet::error] pub enum Error { TreasuryAlreadyInitialized, TreasuryNotInitialized, MonthlyReleaseAlreadyDone, InsufficientTreasuryBalance, InvalidHalvingPeriod, ReleaseTooEarly, GenesisDistributionAlreadyDone, } #[pezpallet::genesis_config] #[derive(pezframe_support::DefaultNoBound)] pub struct GenesisConfig { pub initialize_treasury: bool, #[serde(skip)] pub _phantom: core::marker::PhantomData, } #[pezpallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { if self.initialize_treasury { let _ = Pezpallet::::do_initialize_treasury(); } } } #[pezpallet::call] impl Pezpallet { #[pezpallet::call_index(0)] #[pezpallet::weight(T::WeightInfo::initialize_treasury())] pub fn initialize_treasury(origin: OriginFor) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; Self::do_initialize_treasury() } #[pezpallet::call_index(1)] #[pezpallet::weight(T::WeightInfo::release_monthly_funds())] pub fn release_monthly_funds(origin: OriginFor) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; Self::do_monthly_release() } #[pezpallet::call_index(2)] #[pezpallet::weight(T::WeightInfo::force_genesis_distribution())] pub fn force_genesis_distribution(origin: OriginFor) -> DispatchResult { T::ForceOrigin::ensure_origin(origin)?; Self::do_genesis_distribution() } } impl Pezpallet { pub fn treasury_account_id() -> T::AccountId { T::TreasuryPalletId::get().into_account_truncating() } pub fn incentive_pot_account_id() -> T::AccountId { T::IncentivePotId::get().into_account_truncating() } pub fn government_pot_account_id() -> T::AccountId { T::GovernmentPotId::get().into_account_truncating() } pub fn do_genesis_distribution() -> DispatchResult { // SECURITY: Ensure genesis distribution can only happen once ensure!( !GenesisDistributionDone::::get(), Error::::GenesisDistributionAlreadyDone ); let treasury_account = Self::treasury_account_id(); let presale_account = T::PresaleAccount::get(); let founder_account = T::FounderAccount::get(); let treasury_amount: BalanceOf = TREASURY_ALLOCATION .try_into() .map_err(|_| Error::::InsufficientTreasuryBalance)?; let presale_amount: BalanceOf = PRESALE_ALLOCATION .try_into() .map_err(|_| Error::::InsufficientTreasuryBalance)?; let founder_amount: BalanceOf = FOUNDER_ALLOCATION .try_into() .map_err(|_| Error::::InsufficientTreasuryBalance)?; T::Assets::mint_into(T::PezAssetId::get(), &treasury_account, treasury_amount)?; T::Assets::mint_into(T::PezAssetId::get(), &presale_account, presale_amount)?; T::Assets::mint_into(T::PezAssetId::get(), &founder_account, founder_amount)?; // Mark genesis distribution as completed GenesisDistributionDone::::put(true); Self::deposit_event(Event::GenesisDistributionCompleted { treasury_amount, presale_amount, founder_amount, }); Ok(()) } pub fn do_initialize_treasury() -> DispatchResult { ensure!( TreasuryStartBlock::::get().is_none(), Error::::TreasuryAlreadyInitialized ); let current_block = pezframe_system::Pezpallet::::block_number(); let treasury_balance = TREASURY_ALLOCATION; let first_period_total = treasury_balance.checked_div(2).ok_or(Error::::InvalidHalvingPeriod)?; let monthly_amount = first_period_total .checked_div(HALVING_PERIOD_MONTHS.into()) .ok_or(Error::::InvalidHalvingPeriod)?; let monthly_amount_balance: BalanceOf = monthly_amount.try_into().map_err(|_| Error::::InsufficientTreasuryBalance)?; let halving_data = HalvingData { current_period: 0, period_start_block: current_block, monthly_amount: monthly_amount_balance, total_released: Zero::zero(), }; TreasuryStartBlock::::put(current_block); HalvingInfo::::put(halving_data); NextReleaseMonth::::put(0); Self::deposit_event(Event::TreasuryInitialized { start_block: current_block, initial_monthly_amount: monthly_amount_balance, }); Ok(()) } pub fn do_monthly_release() -> DispatchResult { ensure!(TreasuryStartBlock::::get().is_some(), Error::::TreasuryNotInitialized); let current_block = pezframe_system::Pezpallet::::block_number(); let start_block = TreasuryStartBlock::::get().unwrap(); let next_month = NextReleaseMonth::::get(); ensure!( !MonthlyReleases::::contains_key(next_month), Error::::MonthlyReleaseAlreadyDone ); let blocks_passed = current_block.saturating_sub(start_block); let months_passed: u32 = (blocks_passed / BLOCKS_PER_MONTH.into()) .try_into() .map_err(|_| Error::::InvalidHalvingPeriod)?; // To release month 0, months_passed must be >= 1 (next_month + 1) // To release month 1, months_passed must be >= 2 ensure!(months_passed > next_month, Error::::ReleaseTooEarly); let mut halving_data = HalvingInfo::::get(); let current_period_passed_months = months_passed.saturating_sub(halving_data.current_period * HALVING_PERIOD_MONTHS); if current_period_passed_months >= HALVING_PERIOD_MONTHS { halving_data.current_period = halving_data.current_period.saturating_add(1); halving_data.monthly_amount = halving_data .monthly_amount .checked_div(&2u32.into()) .ok_or(Error::::InvalidHalvingPeriod)?; halving_data.period_start_block = current_block; Self::deposit_event(Event::NewHalvingPeriod { period: halving_data.current_period, new_monthly_amount: halving_data.monthly_amount, }); } let monthly_amount = halving_data.monthly_amount; let incentive_amount = monthly_amount .checked_mul(&75u32.into()) .and_then(|v| v.checked_div(&100u32.into())) .ok_or(Error::::InvalidHalvingPeriod)?; let government_amount = monthly_amount.saturating_sub(incentive_amount); let treasury_account = Self::treasury_account_id(); let incentive_pot = Self::incentive_pot_account_id(); let government_pot = Self::government_pot_account_id(); T::Assets::transfer( T::PezAssetId::get(), &treasury_account, &incentive_pot, incentive_amount, Preservation::Preserve, ) .map_err(|_| Error::::InsufficientTreasuryBalance)?; T::Assets::transfer( T::PezAssetId::get(), &treasury_account, &government_pot, government_amount, Preservation::Preserve, ) .map_err(|_| Error::::InsufficientTreasuryBalance)?; halving_data.total_released = halving_data.total_released.saturating_add(monthly_amount); HalvingInfo::::put(halving_data); let release_info = MonthlyRelease { month_index: next_month, release_block: current_block, amount_released: monthly_amount, incentive_amount, government_amount, }; MonthlyReleases::::insert(next_month, release_info); NextReleaseMonth::::put(next_month + 1); Self::deposit_event(Event::MonthlyFundsReleased { month_index: next_month, total_amount: monthly_amount, incentive_amount, government_amount, }); Ok(()) } pub fn get_current_halving_info() -> HalvingData { HalvingInfo::::get() } pub fn get_incentive_pot_balance() -> BalanceOf { let pot_account = Self::incentive_pot_account_id(); T::Assets::balance(T::PezAssetId::get(), &pot_account) } pub fn get_government_pot_balance() -> BalanceOf { let pot_account = Self::government_pot_account_id(); T::Assets::balance(T::PezAssetId::get(), &pot_account) } } }