diff --git a/pezcumulus/teyrchains/pezpallets/messaging/src/lib.rs b/pezcumulus/teyrchains/pezpallets/messaging/src/lib.rs index bdaa1802..a98818c8 100644 --- a/pezcumulus/teyrchains/pezpallets/messaging/src/lib.rs +++ b/pezcumulus/teyrchains/pezpallets/messaging/src/lib.rs @@ -194,6 +194,8 @@ pub mod pezpallet { EraPurged { era: u32 }, /// Era rotated EraRotated { old_era: u32, new_era: u32 }, + /// Oldest message was evicted from a full inbox (FIFO) + InboxOverflow { recipient: T::AccountId, era: u32 }, } // ============= ERRORS ============= @@ -435,6 +437,10 @@ pub mod pezpallet { if inbox.len() >= T::MaxInboxSize::get() as usize { // FIFO: remove oldest message to make room inbox.remove(0); + Self::deposit_event(Event::InboxOverflow { + recipient: to.clone(), + era: current_era, + }); } inbox.try_push(message).map_err(|_| Error::::InboxFull)?; Ok(()) diff --git a/pezcumulus/teyrchains/pezpallets/pez-rewards/src/lib.rs b/pezcumulus/teyrchains/pezpallets/pez-rewards/src/lib.rs index fecdf9f1..5f74bc52 100644 --- a/pezcumulus/teyrchains/pezpallets/pez-rewards/src/lib.rs +++ b/pezcumulus/teyrchains/pezpallets/pez-rewards/src/lib.rs @@ -480,7 +480,10 @@ pub mod pezpallet { Self::distribute_parliamentary_rewards(current_epoch, total_reward_pool)?; // Remaining 90% for trust score rewards - let trust_score_pool = total_reward_pool * 90u32.into() / 100u32.into(); + let trust_score_pool = total_reward_pool + .checked_mul(&90u32.into()) + .and_then(|v| v.checked_div(&100u32.into())) + .unwrap_or_else(Zero::zero); // Calculate total trust score of all users in this epoch let mut total_trust_score = 0u128; @@ -670,9 +673,18 @@ pub mod pezpallet { epoch: u32, total_incentive_pool: BalanceOf, ) -> DispatchResult { - let parliamentary_allocation = - total_incentive_pool * PARLIAMENTARY_REWARD_PERCENT.into() / 100u32.into(); - let per_nft_reward = parliamentary_allocation / PARLIAMENTARY_NFT_COUNT.into(); + let parliamentary_allocation = total_incentive_pool + .checked_mul(&PARLIAMENTARY_REWARD_PERCENT.into()) + .and_then(|v| v.checked_div(&100u32.into())) + .unwrap_or_else(Zero::zero); + let per_nft_reward = parliamentary_allocation + .checked_div(&PARLIAMENTARY_NFT_COUNT.into()) + .unwrap_or_else(Zero::zero); + + // Skip the loop entirely if per_nft_reward rounds to zero + if per_nft_reward.is_zero() { + return Ok(()); + } let incentive_pot = Self::incentive_pot_account_id(); diff --git a/pezcumulus/teyrchains/pezpallets/pez-treasury/src/lib.rs b/pezcumulus/teyrchains/pezpallets/pez-treasury/src/lib.rs index 56d429fa..217c45ed 100644 --- a/pezcumulus/teyrchains/pezpallets/pez-treasury/src/lib.rs +++ b/pezcumulus/teyrchains/pezpallets/pez-treasury/src/lib.rs @@ -26,7 +26,7 @@ //! //! - **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 +//! - **Distribution**: 75% to Incentive Pot, 25% to Government Pot //! - **Automatic Halving**: Monthly release amount halves at the start of each new period //! //! ## Security Features @@ -360,10 +360,10 @@ pub mod pezpallet { } pub fn do_monthly_release() -> DispatchResult { - ensure!(TreasuryStartBlock::::get().is_some(), Error::::TreasuryNotInitialized); + let start_block = TreasuryStartBlock::::get() + .ok_or(Error::::TreasuryNotInitialized)?; let current_block = pezframe_system::Pezpallet::::block_number(); - let start_block = TreasuryStartBlock::::get().unwrap(); let next_month = NextReleaseMonth::::get(); ensure!( @@ -441,7 +441,7 @@ pub mod pezpallet { }; MonthlyReleases::::insert(next_month, release_info); - NextReleaseMonth::::put(next_month + 1); + NextReleaseMonth::::put(next_month.saturating_add(1)); Self::deposit_event(Event::MonthlyFundsReleased { month_index: next_month, diff --git a/pezcumulus/teyrchains/pezpallets/ping/src/lib.rs b/pezcumulus/teyrchains/pezpallets/ping/src/lib.rs index 8960e379..91f01ede 100644 --- a/pezcumulus/teyrchains/pezpallets/ping/src/lib.rs +++ b/pezcumulus/teyrchains/pezpallets/ping/src/lib.rs @@ -116,7 +116,7 @@ pub mod pezpallet { fn on_finalize(n: BlockNumberFor) { for (para, payload) in Targets::::get().into_iter() { let seq = PingCount::::mutate(|seq| { - *seq += 1; + *seq = seq.saturating_add(1); *seq }); match send_xcm::( diff --git a/pezcumulus/teyrchains/pezpallets/presale/src/lib.rs b/pezcumulus/teyrchains/pezpallets/presale/src/lib.rs index 1671aa1d..1e873ab4 100644 --- a/pezcumulus/teyrchains/pezpallets/presale/src/lib.rs +++ b/pezcumulus/teyrchains/pezpallets/presale/src/lib.rs @@ -979,14 +979,16 @@ pub mod pezpallet { if let Some(contribution_info) = Contributions::::get(presale_id, contributor) { if !contribution_info.refunded && contribution_info.amount > 0 { - let platform_fee = contribution_info + // Calculate net amount in treasury (original - platform fee already deducted at contribution) + let platform_fee_at_contribution = contribution_info .amount .saturating_mul(T::PlatformFeePercent::get() as u128) / 100; - let non_refundable = platform_fee.saturating_mul(50) / 100; + let net_in_treasury = + contribution_info.amount.saturating_sub(platform_fee_at_contribution); - let refund_amount: T::Balance = - contribution_info.amount.saturating_sub(non_refundable).into(); + // Refund the full net amount (cancelled presale = no additional fee) + let refund_amount: T::Balance = net_in_treasury.into(); T::Assets::transfer( presale.payment_asset, @@ -1000,7 +1002,7 @@ pub mod pezpallet { if let Some(info) = maybe_info { info.refunded = true; info.refunded_at = Some(current_block); - info.refund_fee_paid = 0; + info.refund_fee_paid = platform_fee_at_contribution; } Ok::<_, Error>(()) })?; @@ -1063,16 +1065,16 @@ pub mod pezpallet { if let Some(contribution_info) = Contributions::::get(presale_id, contributor) { // Skip if already refunded or zero amount if !contribution_info.refunded && contribution_info.amount > 0 { - // Calculate non-refundable portion (burn + stakers = 50% of platform fee) - let platform_fee = contribution_info + // Calculate net amount in treasury (original - platform fee already deducted at contribution) + let platform_fee_at_contribution = contribution_info .amount .saturating_mul(T::PlatformFeePercent::get() as u128) / 100; - let non_refundable = platform_fee.saturating_mul(50) / 100; // 1% (burn 25% + stakers 25%) + let net_in_treasury = + contribution_info.amount.saturating_sub(platform_fee_at_contribution); - // Refund = 99% (contribution - non_refundable portion) - let refund_amount: T::Balance = - contribution_info.amount.saturating_sub(non_refundable).into(); + // Refund the full net amount (failed presale = no additional fee) + let refund_amount: T::Balance = net_in_treasury.into(); T::Assets::transfer( presale.payment_asset, diff --git a/pezcumulus/teyrchains/pezpallets/presale/src/tests.rs b/pezcumulus/teyrchains/pezpallets/presale/src/tests.rs index 5b2892ff..05e997f0 100644 --- a/pezcumulus/teyrchains/pezpallets/presale/src/tests.rs +++ b/pezcumulus/teyrchains/pezpallets/presale/src/tests.rs @@ -1253,11 +1253,12 @@ fn batch_refund_failed_presale_works() { 10, // batch_size (refund up to 10 contributors) )); - // Check contributors got refunds minus non-refundable platform fee portion - // Platform fee = 500M * 2% = 10M, non-refundable = 10M * 50% = 5M - // Refund = 500M - 5M = 495M - assert_eq!(Assets::balance(2, 2), bob_initial + 495_000_000); - assert_eq!(Assets::balance(2, 3), charlie_initial + 495_000_000); + // Check contributors got refunds of net amount in treasury + // Platform fee = 500M * 2% = 10M (already distributed at contribution time) + // Treasury received net_amount = 500M - 10M = 490M per contributor + // Refund = 490M (full net amount, no additional fee for failed presale) + assert_eq!(Assets::balance(2, 2), bob_initial + 490_000_000); + assert_eq!(Assets::balance(2, 3), charlie_initial + 490_000_000); // Check contributions marked as refunded let bob_contribution = Presale::contributions(0, 2).unwrap(); diff --git a/pezcumulus/teyrchains/pezpallets/staking-score/src/lib.rs b/pezcumulus/teyrchains/pezpallets/staking-score/src/lib.rs index 41e18944..e618b0bb 100644 --- a/pezcumulus/teyrchains/pezpallets/staking-score/src/lib.rs +++ b/pezcumulus/teyrchains/pezpallets/staking-score/src/lib.rs @@ -327,13 +327,13 @@ pub mod pezpallet { let duration_in_blocks = current_block.saturating_sub(start_block); let score = if duration_in_blocks >= (12 * MONTH_IN_BLOCKS).into() { - amount_score * 2 // x2.0 (12+ months) + amount_score.saturating_mul(2) // x2.0 (12+ months) } else if duration_in_blocks >= (6 * MONTH_IN_BLOCKS).into() { - amount_score * 17 / 10 // x1.7 (6-11 months) + amount_score.saturating_mul(17) / 10 // x1.7 (6-11 months) } else if duration_in_blocks >= (3 * MONTH_IN_BLOCKS).into() { - amount_score * 14 / 10 // x1.4 (3-5 months) + amount_score.saturating_mul(14) / 10 // x1.4 (3-5 months) } else if duration_in_blocks >= MONTH_IN_BLOCKS.into() { - amount_score * 12 / 10 // x1.2 (1-2 months) + amount_score.saturating_mul(12) / 10 // x1.2 (1-2 months) } else { amount_score // x1.0 (< 1 month) }; diff --git a/pezcumulus/teyrchains/pezpallets/tiki/src/benchmarking.rs b/pezcumulus/teyrchains/pezpallets/tiki/src/benchmarking.rs index cce631cc..fd0cf6cb 100644 --- a/pezcumulus/teyrchains/pezpallets/tiki/src/benchmarking.rs +++ b/pezcumulus/teyrchains/pezpallets/tiki/src/benchmarking.rs @@ -161,13 +161,47 @@ mod benchmarks { Ok(()) } - // Temporarily skip this benchmark due to KYC complexity in benchmark environment - // #[benchmark] - // fn apply_for_citizenship() -> Result<(), BenchmarkError> { - // // KYC setup is complex in benchmark environment - // // This functionality is covered by force_mint_citizen_nft benchmark - // Ok(()) - // } + #[benchmark] + fn apply_for_citizenship() -> Result<(), BenchmarkError> { + let caller: T::AccountId = whitelisted_caller(); + + // Fund the caller + let funding = Balances::::minimum_balance() * 1_000_000_000u32.into(); + Balances::::make_free_balance_be(&caller, funding); + + // Ensure collection exists + ensure_collection_exists::(); + + // Set KYC status to Approved directly in storage + pezpallet_identity_kyc::KycStatuses::::insert( + &caller, + pezpallet_identity_kyc::types::KycLevel::Approved, + ); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone())); + + // Verify citizenship was granted + assert!(Tiki::::is_citizen(&caller)); + Ok(()) + } + + #[benchmark] + fn check_transfer_permission() -> Result<(), BenchmarkError> { + let caller: T::AccountId = whitelisted_caller(); + let dest: T::AccountId = account("dest", 0, 0); + + // Ensure collections exist past tiki collection so we have a valid non-tiki ID + ensure_collection_exists::(); + + // NextCollectionId is past tiki, so it's a non-tiki collection (call succeeds) + let non_tiki_id = pezpallet_nfts::NextCollectionId::::get().unwrap_or_default(); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), non_tiki_id, 0u32, caller.clone(), dest); + + Ok(()) + } impl_benchmark_test_suite!(Tiki, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/pezcumulus/teyrchains/pezpallets/tiki/src/lib.rs b/pezcumulus/teyrchains/pezpallets/tiki/src/lib.rs index 0d0ea2df..80df3e3a 100644 --- a/pezcumulus/teyrchains/pezpallets/tiki/src/lib.rs +++ b/pezcumulus/teyrchains/pezpallets/tiki/src/lib.rs @@ -455,7 +455,7 @@ pub mod pezpallet { /// Manually mint citizenship NFT (for testing/emergency) #[pezpallet::call_index(2)] - #[pezpallet::weight(::WeightInfo::grant_tiki())] + #[pezpallet::weight(::WeightInfo::force_mint_citizen_nft())] pub fn force_mint_citizen_nft( origin: OriginFor, dest: ::Source, @@ -469,7 +469,7 @@ pub mod pezpallet { /// Grant role through election system (called from pezpallet-voting) #[pezpallet::call_index(3)] - #[pezpallet::weight(::WeightInfo::grant_tiki())] + #[pezpallet::weight(::WeightInfo::grant_elected_role())] pub fn grant_elected_role( origin: OriginFor, dest: ::Source, @@ -490,7 +490,7 @@ pub mod pezpallet { /// Grant role through exam/test system #[pezpallet::call_index(4)] - #[pezpallet::weight(::WeightInfo::grant_tiki())] + #[pezpallet::weight(::WeightInfo::grant_earned_role())] pub fn grant_earned_role( origin: OriginFor, dest: ::Source, @@ -511,7 +511,7 @@ pub mod pezpallet { /// Apply for citizenship after KYC completion #[pezpallet::call_index(5)] - #[pezpallet::weight(::WeightInfo::grant_tiki())] + #[pezpallet::weight(::WeightInfo::apply_for_citizenship())] pub fn apply_for_citizenship(origin: OriginFor) -> DispatchResult { let who = ensure_signed(origin)?; @@ -530,7 +530,7 @@ pub mod pezpallet { /// Check NFT transfer for transfer blocking system #[pezpallet::call_index(6)] - #[pezpallet::weight(::WeightInfo::grant_tiki())] + #[pezpallet::weight(::WeightInfo::check_transfer_permission())] pub fn check_transfer_permission( _origin: OriginFor, collection_id: T::CollectionId, @@ -780,7 +780,7 @@ pub trait TikiProvider { impl TikiScoreProvider for Pezpallet { fn get_tiki_score(who: &T::AccountId) -> u32 { let tikis = Self::user_tikis(who); - tikis.iter().map(Self::get_bonus_for_tiki).sum() + tikis.iter().map(Self::get_bonus_for_tiki).fold(0u32, |acc, x| acc.saturating_add(x)) } } diff --git a/pezcumulus/teyrchains/pezpallets/tiki/src/weights.rs b/pezcumulus/teyrchains/pezpallets/tiki/src/weights.rs index c6d99976..57eefdf5 100644 --- a/pezcumulus/teyrchains/pezpallets/tiki/src/weights.rs +++ b/pezcumulus/teyrchains/pezpallets/tiki/src/weights.rs @@ -60,6 +60,8 @@ pub trait WeightInfo { fn force_mint_citizen_nft() -> Weight; fn grant_earned_role() -> Weight; fn grant_elected_role() -> Weight; + fn apply_for_citizenship() -> Weight; + fn check_transfer_permission() -> Weight; } /// Weights for `pezpallet_tiki` using the Bizinikiwi node and recommended hardware. @@ -178,6 +180,24 @@ impl WeightInfo for BizinikiwiWeight { .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } + /// Storage: `IdentityKyc::Verifications` (r:1 w:0) + /// Proof: `IdentityKyc::Verifications` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Plus all storage from `force_mint_citizen_nft` (9r + 9w) + fn apply_for_citizenship() -> Weight { + // Conservative estimate: force_mint_citizen_nft + 1 KYC verification read + // Minimum execution time: 120_000_000 picoseconds. + Weight::from_parts(125_000_000, 4326) + .saturating_add(T::DbWeight::get().reads(10_u64)) + .saturating_add(T::DbWeight::get().writes(9_u64)) + } + /// Storage: `Tiki::CitizenNft` (r:1 w:0) + /// Proof: `Tiki::CitizenNft` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + fn check_transfer_permission() -> Weight { + // Lightweight read-only check + // Minimum execution time: 12_000_000 picoseconds. + Weight::from_parts(13_000_000, 2527) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } } // For backwards compatibility and tests. @@ -295,4 +315,15 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } + fn apply_for_citizenship() -> Weight { + // Conservative estimate: force_mint_citizen_nft + 1 KYC verification read + Weight::from_parts(125_000_000, 4326) + .saturating_add(RocksDbWeight::get().reads(10_u64)) + .saturating_add(RocksDbWeight::get().writes(9_u64)) + } + fn check_transfer_permission() -> Weight { + // Lightweight read-only check + Weight::from_parts(13_000_000, 2527) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } } diff --git a/pezcumulus/teyrchains/pezpallets/token-wrapper/src/lib.rs b/pezcumulus/teyrchains/pezpallets/token-wrapper/src/lib.rs index 67b0ffb6..9712bf84 100644 --- a/pezcumulus/teyrchains/pezpallets/token-wrapper/src/lib.rs +++ b/pezcumulus/teyrchains/pezpallets/token-wrapper/src/lib.rs @@ -151,15 +151,16 @@ pub mod pezpallet { ) .map_err(|_| Error::::TransferFailed)?; - // Update total locked + // Mint wrapped tokens to user BEFORE updating TotalLocked + // If mint fails, the extrinsic reverts (including the transfer above) + T::Assets::mint_into(T::WrapperAssetId::get(), &who, amount) + .map_err(|_| Error::::MintFailed)?; + + // Update total locked only after both transfer and mint succeeded TotalLocked::::mutate(|total| { *total = total.saturating_add(amount); }); - // Mint wrapped tokens to user - T::Assets::mint_into(T::WrapperAssetId::get(), &who, amount) - .map_err(|_| Error::::MintFailed)?; - Self::deposit_event(Event::Wrapped { who, amount }); Ok(()) } @@ -188,6 +189,10 @@ pub mod pezpallet { let wrapped_balance = T::Assets::balance(T::WrapperAssetId::get(), &who); ensure!(wrapped_balance >= amount, Error::::InsufficientWrappedBalance); + // Verify pallet has sufficient backing before any state changes + let pallet_balance = T::Currency::free_balance(&Self::account_id()); + ensure!(pallet_balance >= amount, Error::::TransferFailed); + // Burn wrapped tokens from user T::Assets::burn_from( T::WrapperAssetId::get(), @@ -199,12 +204,8 @@ pub mod pezpallet { ) .map_err(|_| Error::::BurnFailed)?; - // Update total locked - TotalLocked::::mutate(|total| { - *total = total.saturating_sub(amount); - }); - // Transfer native tokens back to user (unlock) + // If this fails, the extrinsic reverts (including the burn above) T::Currency::transfer( &Self::account_id(), &who, @@ -213,6 +214,11 @@ pub mod pezpallet { ) .map_err(|_| Error::::TransferFailed)?; + // Update total locked only after both burn and transfer succeeded + TotalLocked::::mutate(|total| { + *total = total.saturating_sub(amount); + }); + Self::deposit_event(Event::Unwrapped { who, amount }); Ok(()) } diff --git a/pezcumulus/teyrchains/pezpallets/trust/src/lib.rs b/pezcumulus/teyrchains/pezpallets/trust/src/lib.rs index e6ef2f4c..684d00f5 100644 --- a/pezcumulus/teyrchains/pezpallets/trust/src/lib.rs +++ b/pezcumulus/teyrchains/pezpallets/trust/src/lib.rs @@ -400,7 +400,8 @@ pub mod pezpallet { .saturating_add(perwerde_u128.saturating_mul(300)) .saturating_add(tiki_u128.saturating_mul(300)); - let final_score_u128 = staking_u128 + // Safe: both operands are derived from u32 scores, product fits in u128 + let final_score_u128 = staking_u128 .saturating_mul(weighted_sum) .checked_div(base) .ok_or(Error::::CalculationOverflow)?; diff --git a/pezcumulus/teyrchains/pezpallets/welati/src/lib.rs b/pezcumulus/teyrchains/pezpallets/welati/src/lib.rs index e986924e..973c70a1 100644 --- a/pezcumulus/teyrchains/pezpallets/welati/src/lib.rs +++ b/pezcumulus/teyrchains/pezpallets/welati/src/lib.rs @@ -860,7 +860,8 @@ pub mod pezpallet { let total_citizen_count = Self::get_total_citizen_count(); let turnout_percentage = if total_citizen_count > 0 { - ((election.total_votes * 100) / total_citizen_count) as u8 + ((election.total_votes as u64).saturating_mul(100) / total_citizen_count as u64) + as u8 } else { 0 };