#![cfg_attr(not(feature = "std"), no_std)] //! # Identity & KYC Pezpallet - TRUSTLESS MODEL //! //! A privacy-preserving, decentralized citizenship verification system. //! //! ## Overview //! //! This pezpallet implements a **TRUSTLESS** citizenship verification where: //! - NO personal data is stored on-chain (only hash) //! - NO central authority/bot approves applications //! - Existing citizens vouch for new applicants (referral-based) //! - Direct responsibility: Referrers are accountable for their referrals //! //! ## Security Design (Kurdish People Safety) //! //! This system is designed to protect vulnerable populations (like Kurdish people) //! from hostile regimes that might try to identify applicants: //! - Only H256 hash of identity stored on-chain //! - Actual documents stored off-chain (IPFS/encrypted) //! - No admin can see or leak personal data //! - Referral chain creates accountability without central authority //! //! ## Citizenship Workflow //! //! ### 1. Application Phase //! - User creates identity hash off-chain: `H256(name + email + documents)` //! - User calls `apply_for_citizenship(identity_hash, referrer_account)` //! - Referrer MUST be an existing citizen (KycLevel::Approved) //! - Status changes to `PendingReferral` //! //! ### 2. Referrer Approval Phase //! - Referrer reviews applicant (off-chain verification) //! - Referrer calls `approve_referral(applicant)` to vouch for them //! - Status changes to `ReferrerApproved` //! - Referrer takes personal responsibility for this referral //! //! ### 3. Self-Confirmation Phase (Welati NFT Only) //! - Applicant calls `confirm_citizenship()` to complete the process //! - Status changes to `Approved` //! - Citizen NFT (Welati) is minted via self-confirmation //! - Referral hooks are triggered //! //! ## KYC Levels //! //! - **NotStarted** - No application submitted //! - **PendingReferral** - Waiting for referrer approval //! - **ReferrerApproved** - Referrer approved, ready for self-confirmation //! - **Approved** - Full citizen with all rights //! - **Revoked** - Citizenship revoked (governance decision) //! //! ## Privacy Features //! //! - **Hash-only storage**: No personal data on-chain //! - **Off-chain documents**: IPFS or encrypted storage //! - **No admin access**: Decentralized verification //! - **Referral accountability**: Social trust, not central authority //! //! ## Direct Responsibility Model //! //! When a citizen is found to be malicious: //! - ONLY their direct referrer is penalized //! - Penalty: Trust score reduction + potential citizenship review //! - Chain reactions are limited to direct relationships //! - Good referrals from bad actors are NOT penalized //! //! ## Interface //! //! ### User Extrinsics //! //! - `apply_for_citizenship(identity_hash, referrer)` - Submit citizenship application //! - `confirm_citizenship()` - Self-confirm after referrer approval (Welati only) //! - `renounce_citizenship()` - Voluntarily give up citizenship //! //! ### Referrer Extrinsics //! //! - `approve_referral(applicant)` - Vouch for an applicant //! //! ### Governance Extrinsics (Root only) //! //! - `revoke_citizenship(who)` - Revoke citizenship (governance decision) //! //! ## Runtime Integration Example //! //! ```ignore //! impl pezpallet_identity_kyc::Config for Runtime { //! type RuntimeEvent = RuntimeEvent; //! type Currency = Balances; //! type WeightInfo = pezpallet_identity_kyc::weights::BizinikiwiWeight; //! type OnKycApproved = Referral; //! type CitizenNftProvider = Tiki; //! type KycApplicationDeposit = ConstU128<1_000_000_000_000>; // Spam prevention //! type MaxStringLength = ConstU32<128>; //! type MaxCidLength = ConstU32<64>; //! } //! ``` pub use pezpallet::*; pub mod types; use types::*; pub mod weights; pub use weights::WeightInfo; #[cfg(test)] mod mock; #[cfg(test)] mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; extern crate alloc; use pezframe_support::{pezpallet_prelude::*, traits::ReservableCurrency}; use pezframe_system::pezpallet_prelude::*; use pezsp_core::H256; #[pezframe_support::pezpallet] pub mod pezpallet { use super::*; #[pezpallet::pezpallet] pub struct Pezpallet(_); #[pezpallet::config] pub trait Config: pezframe_system::Config>> { type Currency: ReservableCurrency; /// Origin that can revoke citizenship (governance/root) type GovernanceOrigin: EnsureOrigin; type WeightInfo: WeightInfo; /// Default referrer account (founder) - used when no valid referrer is provided type DefaultReferrer: Get; /// Hook called when citizenship is approved - used by referral pezpallet type OnKycApproved: crate::types::OnKycApproved; /// Hook called when citizenship is revoked - used by referral pezpallet for penalty type OnCitizenshipRevoked: crate::types::OnCitizenshipRevoked; /// Provider for minting citizen NFTs - used by tiki pezpallet type CitizenNftProvider: crate::types::CitizenNftProvider; /// Deposit required to apply (spam prevention, returned on approval) #[pezpallet::constant] type KycApplicationDeposit: Get>; /// Max string length for legacy storage #[pezpallet::constant] type MaxStringLength: Get; /// Max CID length for legacy storage #[pezpallet::constant] type MaxCidLength: Get; } pub type BalanceOf = <::Currency as pezframe_support::traits::Currency< ::AccountId, >>::Balance; // ============= STORAGE ============= /// Citizenship applications (applicant -> application) /// PRIVACY: Only hash stored, no personal data #[pezpallet::storage] #[pezpallet::getter(fn applications)] pub type Applications = StorageMap<_, Blake2_128Concat, T::AccountId, CitizenshipApplication>; /// Current citizenship status per account #[pezpallet::storage] #[pezpallet::getter(fn kyc_status_of)] pub type KycStatuses = StorageMap<_, Blake2_128Concat, T::AccountId, KycLevel, ValueQuery>; /// Identity hashes of approved citizens (for verification) /// Can be used to prove citizenship without revealing identity #[pezpallet::storage] #[pezpallet::getter(fn identity_hash_of)] pub type IdentityHashes = StorageMap<_, Blake2_128Concat, T::AccountId, H256>; /// Referrer of approved citizens (for direct responsibility tracking) /// Kept permanently for penalty system even after application is removed #[pezpallet::storage] #[pezpallet::getter(fn citizen_referrer)] pub type CitizenReferrers = StorageMap<_, Blake2_128Concat, T::AccountId, T::AccountId>; // ============= LEGACY STORAGE (for migration) ============= /// Legacy: Identity info storage (deprecated, kept for migration) #[pezpallet::storage] pub type Identities = StorageMap<_, Blake2_128Concat, T::AccountId, IdentityInfo>; /// Legacy: Pending KYC applications (deprecated, kept for migration) #[pezpallet::storage] pub type PendingKycApplications = StorageMap< _, Blake2_128Concat, T::AccountId, KycApplication, >; // ============= GENESIS CONFIG ============= /// Genesis configuration for bootstrapping initial citizens /// BOOTSTRAP: Solves chicken-egg problem - first citizens need to exist for others to join #[pezpallet::genesis_config] #[derive(pezframe_support::DefaultNoBound)] pub struct GenesisConfig { /// List of founding citizens (AccountId, IdentityHash) /// These accounts start with Approved status and can accept referrals immediately pub founding_citizens: alloc::vec::Vec<(T::AccountId, H256)>, #[serde(skip)] pub _phantom: core::marker::PhantomData, } #[pezpallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { // Initialize founding citizens with Approved status for (account, identity_hash) in &self.founding_citizens { // Set status to Approved (citizen) KycStatuses::::insert(account, KycLevel::Approved); // Store identity hash IdentityHashes::::insert(account, *identity_hash); } } } // ============= EVENTS ============= #[pezpallet::event] #[pezpallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// New citizenship application submitted CitizenshipApplied { applicant: T::AccountId, referrer: T::AccountId, identity_hash: H256 }, /// Referrer approved the application ReferralApproved { referrer: T::AccountId, applicant: T::AccountId }, /// Applicant self-confirmed their citizenship (Welati NFT minted) CitizenshipConfirmed { who: T::AccountId }, /// Citizenship was revoked (by governance) CitizenshipRevoked { who: T::AccountId }, /// User renounced their citizenship CitizenshipRenounced { who: T::AccountId }, /// Application was cancelled by the applicant ApplicationCancelled { who: T::AccountId }, } // ============= ERRORS ============= #[pezpallet::error] pub enum Error { /// Application already exists for this account ApplicationAlreadyExists, /// No application found for this account ApplicationNotFound, /// Referrer is not a citizen (must have Approved status) ReferrerNotCitizen, /// Cannot refer yourself SelfReferral, /// Cannot approve referral in current state (must be PendingReferral) CannotApproveInCurrentState, /// Cannot confirm in current state (must be ReferrerApproved) CannotConfirmInCurrentState, /// Cannot revoke in current state (must be Approved) CannotRevokeInCurrentState, /// User is not a citizen (cannot renounce) NotACitizen, /// Only the referrer can approve this application NotTheReferrer, /// Cannot cancel application in current state (must be PendingReferral) CannotCancelInCurrentState, } // ============= EXTRINSICS ============= #[pezpallet::call] impl Pezpallet { /// Apply for citizenship with identity hash and referrer /// /// TRUSTLESS: No admin involved, referrer vouches for applicant /// PRIVACY: Only hash stored, actual identity is off-chain /// /// # Arguments /// - `identity_hash`: H256 hash of identity documents (calculated off-chain) /// - `referrer`: Optional account of existing citizen who will vouch for you. /// If None or invalid, DefaultReferrer (founder) is used. /// /// # Workflow /// 1. Applicant submits hash + optional referrer /// 2. If referrer is None/invalid, DefaultReferrer is used /// 3. Deposit is reserved (spam prevention) /// 4. Status becomes PendingReferral /// 5. Referrer must call approve_referral #[pezpallet::call_index(0)] #[pezpallet::weight(T::WeightInfo::apply_for_citizenship())] pub fn apply_for_citizenship( origin: OriginFor, identity_hash: H256, referrer: Option, ) -> DispatchResult { let applicant = ensure_signed(origin)?; // Must not have existing application ensure!( KycStatuses::::get(&applicant) == KycLevel::NotStarted, Error::::ApplicationAlreadyExists ); // Determine the actual referrer: // 1. Use provided referrer if valid (approved citizen and not self) // 2. Fall back to DefaultReferrer otherwise let actual_referrer = referrer .filter(|r| *r != applicant) // Not self-referral .filter(|r| KycStatuses::::get(r) == KycLevel::Approved) // Must be citizen .unwrap_or_else(T::DefaultReferrer::get); // Verify the actual referrer is valid (including DefaultReferrer) ensure!( KycStatuses::::get(&actual_referrer) == KycLevel::Approved, Error::::ReferrerNotCitizen ); // Cannot refer yourself (even with DefaultReferrer) ensure!(applicant != actual_referrer, Error::::SelfReferral); // Reserve deposit (spam prevention, returned on approval) let deposit = T::KycApplicationDeposit::get(); T::Currency::reserve(&applicant, deposit)?; // Store application (only hash, no personal data) let application = CitizenshipApplication { identity_hash, referrer: actual_referrer.clone() }; Applications::::insert(&applicant, application); // Update status KycStatuses::::insert(&applicant, KycLevel::PendingReferral); Self::deposit_event(Event::CitizenshipApplied { applicant, referrer: actual_referrer, identity_hash, }); Ok(()) } /// Referrer approves an applicant's citizenship application /// /// TRUSTLESS: Referrer takes personal responsibility for this referral /// ACCOUNTABILITY: If applicant turns out malicious, referrer is penalized /// /// # Arguments /// - `applicant`: Account of the person you're vouching for /// /// # Requirements /// - Caller must be the referrer specified in the application /// - Application must be in PendingReferral state #[pezpallet::call_index(1)] #[pezpallet::weight(T::WeightInfo::approve_referral())] pub fn approve_referral(origin: OriginFor, applicant: T::AccountId) -> DispatchResult { let caller = ensure_signed(origin)?; // Must be in PendingReferral state ensure!( KycStatuses::::get(&applicant) == KycLevel::PendingReferral, Error::::CannotApproveInCurrentState ); // Get application let application = Applications::::get(&applicant).ok_or(Error::::ApplicationNotFound)?; // Only the referrer can approve ensure!(application.referrer == caller, Error::::NotTheReferrer); // Update status to ReferrerApproved KycStatuses::::insert(&applicant, KycLevel::ReferrerApproved); Self::deposit_event(Event::ReferralApproved { referrer: caller, applicant }); Ok(()) } /// Self-confirm citizenship after referrer approval /// /// TRUSTLESS: Applicant confirms themselves, no admin needed /// WELATI ONLY: This mints the Citizen NFT via self-confirmation /// /// # Workflow /// 1. Deposit is returned /// 2. Identity hash is stored permanently /// 3. Status becomes Approved /// 4. Citizen NFT (Welati) is minted /// 5. Referral hooks are triggered #[pezpallet::call_index(2)] #[pezpallet::weight(T::WeightInfo::confirm_citizenship())] pub fn confirm_citizenship(origin: OriginFor) -> DispatchResult { let applicant = ensure_signed(origin)?; // Must be in ReferrerApproved state ensure!( KycStatuses::::get(&applicant) == KycLevel::ReferrerApproved, Error::::CannotConfirmInCurrentState ); // Get application let application = Applications::::take(&applicant).ok_or(Error::::ApplicationNotFound)?; // Return deposit let deposit = T::KycApplicationDeposit::get(); T::Currency::unreserve(&applicant, deposit); // Store identity hash permanently (for proof of citizenship) IdentityHashes::::insert(&applicant, application.identity_hash); // Store referrer permanently (for direct responsibility tracking) // This is needed even after Applications is removed for penalty system CitizenReferrers::::insert(&applicant, application.referrer.clone()); // Update status to Approved KycStatuses::::insert(&applicant, KycLevel::Approved); // Mint citizen NFT with self-confirmation (Welati tiki) if let Err(e) = T::CitizenNftProvider::mint_citizen_nft_confirmed(&applicant) { log::warn!("Failed to mint citizen NFT for {applicant:?}: {e:?}"); // Don't fail - user is still a citizen } // Trigger referral hooks (for referral pezpallet) // Pass referrer parameter to avoid data loss between pallets T::OnKycApproved::on_kyc_approved(&applicant, &application.referrer); Self::deposit_event(Event::CitizenshipConfirmed { who: applicant }); Ok(()) } /// Revoke citizenship (governance only) /// /// Used for malicious actors identified by governance /// DIRECT RESPONSIBILITY: Triggers penalty for the referrer via referral pezpallet #[pezpallet::call_index(3)] #[pezpallet::weight(T::WeightInfo::revoke_citizenship())] pub fn revoke_citizenship(origin: OriginFor, who: T::AccountId) -> DispatchResult { T::GovernanceOrigin::ensure_origin(origin)?; ensure!( KycStatuses::::get(&who) == KycLevel::Approved, Error::::CannotRevokeInCurrentState ); // Update status KycStatuses::::insert(&who, KycLevel::Revoked); // Burn citizen NFT if let Err(e) = T::CitizenNftProvider::burn_citizen_nft(&who) { log::warn!("Failed to burn citizen NFT for {who:?}: {e:?}"); } // Trigger direct responsibility penalty for the referrer // This hook notifies the referral pezpallet to penalize the referrer T::OnCitizenshipRevoked::on_citizenship_revoked(&who); Self::deposit_event(Event::CitizenshipRevoked { who }); Ok(()) } /// Renounce citizenship (voluntary exit) /// /// Users can freely leave the system #[pezpallet::call_index(4)] #[pezpallet::weight(T::WeightInfo::renounce_citizenship())] pub fn renounce_citizenship(origin: OriginFor) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(KycStatuses::::get(&who) == KycLevel::Approved, Error::::NotACitizen); // Burn citizen NFT T::CitizenNftProvider::burn_citizen_nft(&who)?; // Reset status KycStatuses::::insert(&who, KycLevel::NotStarted); // Remove identity hash IdentityHashes::::remove(&who); Self::deposit_event(Event::CitizenshipRenounced { who }); Ok(()) } /// Cancel pending application and retrieve deposit /// /// Useful if referrer is unresponsive or user made a mistake. /// SAFETY: Only works in PendingReferral state (not yet approved) #[pezpallet::call_index(5)] #[pezpallet::weight(T::WeightInfo::cancel_application())] pub fn cancel_application(origin: OriginFor) -> DispatchResult { let applicant = ensure_signed(origin)?; // Must be in PendingReferral state (not yet approved by referrer) ensure!( KycStatuses::::get(&applicant) == KycLevel::PendingReferral, Error::::CannotCancelInCurrentState ); // Remove application Applications::::remove(&applicant); // Reset status KycStatuses::::insert(&applicant, KycLevel::NotStarted); // Unreserve deposit let deposit = T::KycApplicationDeposit::get(); T::Currency::unreserve(&applicant, deposit); Self::deposit_event(Event::ApplicationCancelled { who: applicant }); Ok(()) } } } // ============= TRAIT IMPLEMENTATIONS ============= pub use types::KycStatus; impl types::KycStatus for Pezpallet { fn get_kyc_status(who: &T::AccountId) -> KycLevel { KycStatuses::::get(who) } } impl IdentityInfoProvider for Pezpallet { fn get_identity_info(who: &T::AccountId) -> Option> { // Legacy: Return from old storage if exists Identities::::get(who) } } /// Helper methods for checking citizenship impl Pezpallet { /// Check if account is a citizen pub fn is_citizen(who: &T::AccountId) -> bool { KycStatuses::::get(who) == KycLevel::Approved } /// Count total number of citizens pub fn citizen_count() -> u32 { KycStatuses::::iter() .filter(|(_, status)| *status == KycLevel::Approved) .count() as u32 } /// Get the referrer of a citizen or applicant /// Checks both pending applications and approved citizen records pub fn get_referrer(who: &T::AccountId) -> Option { // First check permanent storage (for approved citizens) CitizenReferrers::::get(who) // Then check pending applications .or_else(|| Applications::::get(who).map(|app| app.referrer)) } /// Get identity hash of a citizen pub fn get_identity_hash(who: &T::AccountId) -> Option { IdentityHashes::::get(who) } } /// Trait for trust pezpallet integration pub trait CitizenshipStatusProvider { fn is_citizen(who: &AccountId) -> bool; } impl CitizenshipStatusProvider for Pezpallet { fn is_citizen(who: &T::AccountId) -> bool { KycStatuses::::get(who) == KycLevel::Approved } }