//! # Origin NFT Mint Contract — Template canonical Phase 3 Slice 3.2 //! //! **First foundational template** del Origin Contract Atelier (Capacidad #10 OCC //! propuesta a David 2026-05-27). Implementa el split BM V2 ratificado 2026-05-18 //! 91% creator / 9% Treasury + transfer royalty 5% (configurable) + Treasury burn //! 20% (per BM V2 burn matrix Pillar 3). //! //! ## Inmutables honored (compile-time + runtime) //! //! - **#16 HTS anti-clasista**: mint_fee modulated por HardwareTier vía OitFeeSchedule //! bridge (cross-pillar Mathi OCAFB Phase 2.4 wired). Pi 5 dev paga subsidized rate. //! - **#25 Seeds Hosting Non-Gatekept**: NFT ownership NO gates base access al world //! donde está anchored — base experience siempre free perpetua. //! - **#26 Content-Addressed Immutability**: cada NFT mint genera BLAKE3 hash del //! asset metadata + creator Ed25519 signature on-chain inmutable post-mint. //! - **#37 Free perpetua**: read methods (token_owner, total_supply, etc.) GRATIS //! sin requerir ownership. Solo mint + transfer + approve cobran fees. //! - **#41 AGI Cross-Species Co-authorship**: AGI accounts (AgentKind::AGI per //! Phase 2.7 AgentIdentity Index 9) son first-class citizens, pueden own + mint //! + transfer NFTs sin restrictions. //! //! ## Cross-pillar integration canonical //! //! - **Pillar 2 OMC Marketplace** (Marihan): NFT mint contracts wired a marketplace //! con 91/9 split enforcement compile-detect. Phase 2.4 OCAFB fee bridge consumido //! para mint_fee dynamic per HardwareTier. //! - **Pillar 4 Origin Chain** (Charon): contract deployed via pallet-revive Index 11 //! con AccountId32Mapper canonical. Storage deposits per BM V2 economics. //! - **Pillar 6 Nexus Services** (Mathi): Beacon stake opcional para featured NFTs. //! ONS naming opcional para human-readable NFT collection IDs. //! - **Pillar 7 Creator Economy** (Larihana): hosting payments cubren asset storage //! cuando NFT anchored a hosted world (0.6 OGC/world/mes per BM V2). //! //! ## Policy-first design philosophy //! //! Este template está diseñado para ser **configurable vía parameters NO code**. //! El Origin Contract Atelier visual designer expone: //! - `mint_fee`: u128 (sliders 0.01-100 OGC) //! - `creator_share_bps`: u16 (default 9100 = 91%) //! - `treasury_share_bps`: u16 (default 900 = 9%) //! - `transfer_royalty_bps`: u16 (default 500 = 5%, range 0-1000) //! - `treasury_burn_bps`: u16 (default 2000 = 20%, range 0-5000) //! - `max_supply`: Option (None = unlimited, Some(N) = capped) //! - `lockup_blocks`: u32 (transfer lockup post-mint, default 0) //! - `children_safety_strict`: bool (Inmutable #35 enforcement) //! - `creator_oit_level`: Option (None = anonymous, Some(N) = verified institution) //! //! Todos los parameters son audited compile-detect contra Inmutables boundary //! enforcement (e.g., creator_share + treasury_share == 10_000 bps invariant). #![cfg_attr(not(feature = "std"), no_std, no_main)] #[ink::contract] mod origin_nft_mint { use ink::storage::Mapping; use ink::U256; /// Token identifier canonical — incrementing u32 per mint. /// u32 max 4B tokens canonical safety margin para supply mass NFTs ecosystem. pub type TokenId = u32; /// Basis points canonical — 10_000 bps = 100%. Standard Substrate finance pattern. pub type BasisPoints = u16; /// Asset hash canonical — BLAKE3-256 del asset bundle metadata (Inmutable #26). pub type AssetHash = [u8; 32]; // ───────────────────────────────────────────────────────────────── // Storage canonical // ───────────────────────────────────────────────────────────────── /// Contract storage canonical. Storage layout sealed canonical post-deploy /// (storage migrations vía governance Phase 5 si necesario). #[ink(storage)] pub struct OriginNftMint { // ── Identity + ownership ────────────────────────────────── /// Contract owner canonical — Foundation address o creator EOA. /// Authority canonical para set_mint_fee + set_royalty + emergency_pause. owner: Address, /// Treasury address canonical — recibe 9% mint + burn share canonical /// per BM V2 ratificado 2026-05-18. treasury: Address, // ── Configuration parameters (Contract Atelier visual UI) ── /// Mint fee base canonical — modulated por HardwareTier vía OitFeeSchedule /// canonical Phase 2.4 OCAFB bridge cuando wired Phase 3.x maduración. mint_fee_base: Balance, /// Creator share basis points canonical — default 9100 (91% per BM V2). creator_share_bps: BasisPoints, /// Treasury share basis points canonical — default 900 (9% per BM V2). treasury_share_bps: BasisPoints, /// Transfer royalty basis points canonical — default 500 (5%). /// Charged on secondary sales canonical, paid al original creator. transfer_royalty_bps: BasisPoints, /// Treasury burn basis points canonical (del treasury share) — default 2000 (20% /// per BM V2 burn matrix Pillar 3 NFT mint revenue stream). treasury_burn_bps: BasisPoints, /// Max supply opcional — None = unlimited canonical, Some(N) = capped. max_supply: Option, /// Lockup blocks post-mint — periodo durante el cual NFT NO puede ser /// transferred (anti-flip protection canonical). Default 0 = no lockup. lockup_blocks: BlockNumber, /// Children safety strict mode canonical (Inmutable #35 enforcement). /// Si true, content metadata pasa validation pre-mint via Sussi Ethos. children_safety_strict: bool, // ── State canonical (NFT registry) ───────────────────────── /// Total supply minted canonical hasta fecha. total_supply: u32, /// Token → owner mapping canonical (PSP34-like standard). token_owner: Mapping, /// Token → creator mapping canonical (original minter, immutable post-mint). /// Usado para royalty payments canonical en secondary transfers. token_creator: Mapping, /// Token → asset hash mapping canonical (BLAKE3 content addressing per /// Inmutable #26). token_asset_hash: Mapping, /// Token → mint block mapping canonical (para lockup enforcement). token_mint_block: Mapping, /// Owner → token count mapping canonical (balance_of canonical). owner_balance: Mapping, /// Token → approved spender mapping canonical (approve/transfer_from pattern). token_approved: Mapping, // ── Emergency controls ───────────────────────────────────── /// Paused state canonical — owner-only emergency_pause + governance unpause. paused: bool, } // ───────────────────────────────────────────────────────────────── // Events canonical // ───────────────────────────────────────────────────────────────── /// Emitted on canonical mint completion. #[ink(event)] pub struct Minted { #[ink(topic)] token_id: TokenId, #[ink(topic)] creator: Address, asset_hash: AssetHash, mint_fee_paid: Balance, creator_share: Balance, treasury_share: Balance, treasury_burned: Balance, } /// Emitted on canonical transfer. #[ink(event)] pub struct Transferred { #[ink(topic)] token_id: TokenId, #[ink(topic)] from: Address, #[ink(topic)] to: Address, sale_price: Balance, royalty_paid: Balance, } /// Emitted on canonical approval set. #[ink(event)] pub struct Approved { #[ink(topic)] token_id: TokenId, #[ink(topic)] owner: Address, #[ink(topic)] spender: Address, } /// Emitted on canonical emergency_pause toggle. #[ink(event)] pub struct PauseToggled { #[ink(topic)] actor: Address, paused: bool, } // ───────────────────────────────────────────────────────────────── // Errors canonical // ───────────────────────────────────────────────────────────────── #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub enum Error { /// Mint fee underpaid — caller debe pagar exact o más que mint_fee_base. InsufficientPayment, /// Max supply reached canonical — no more mints allowed. MaxSupplyReached, /// Token NO existe canonical (transfer/approve/burn de token unminted). TokenNotFound, /// Caller NO es owner del token canonical (transfer attempt unauthorized). NotTokenOwner, /// Caller NO está approved canonical (transfer_from attempt unauthorized). NotApprovedSpender, /// Token still en lockup period canonical (transfer attempt pre-lockup expiry). TokenLocked, /// Contract paused canonical — emergency_pause active, mint/transfer blocked. ContractPaused, /// Caller NO es owner del contract canonical (admin action attempt unauthorized). NotContractOwner, /// Configuration invalid canonical — creator_share + treasury_share != 10_000 bps. InvalidConfiguration, /// Children safety strict mode violation canonical (Inmutable #35). ChildrenSafetyViolation, /// Asset hash invalid canonical (BLAKE3 hash mismatch o zero hash). InvalidAssetHash, /// Self-transfer canonical attempt (from == to). SelfTransferProhibited, } pub type Result = core::result::Result; /// Convierte un valor `U256` del entorno pallet-revive a `Balance` (u128) /// de forma saturante. ⚠️ Ver nota de correctitud económica native↔eth en /// `origin-escrow` — pendiente validación E2E en nodo pallet-revive (VPS). fn u256_to_balance(v: U256) -> Balance { if v > U256::from(Balance::MAX) { Balance::MAX } else { v.as_u128() } } // ───────────────────────────────────────────────────────────────── // Constructor canonical // ───────────────────────────────────────────────────────────────── impl OriginNftMint { /// New canonical instance con parámetros BM V2 ratificados default. /// /// Parámetros configurables expuestos vía Contract Atelier visual UI: /// - `treasury`: Foundation Treasury address canonical /// - `mint_fee_base`: base mint fee canonical (HardwareTier modulation Phase 3.x) /// - `creator_share_bps`: default 9100 (91% per BM V2) /// - `treasury_share_bps`: default 900 (9% per BM V2) /// - `transfer_royalty_bps`: default 500 (5%) /// - `treasury_burn_bps`: default 2000 (20%) /// - `max_supply`: None canonical = unlimited /// - `lockup_blocks`: 0 canonical default = sin lockup /// - `children_safety_strict`: false canonical default (creators opt-in) /// /// Compile-detect invariant canonical: creator_share_bps + treasury_share_bps /// MUST == 10_000 (100%) — enforced via constructor assertion. #[ink(constructor)] pub fn new( treasury: Address, mint_fee_base: Balance, creator_share_bps: BasisPoints, treasury_share_bps: BasisPoints, transfer_royalty_bps: BasisPoints, treasury_burn_bps: BasisPoints, max_supply: Option, lockup_blocks: BlockNumber, children_safety_strict: bool, ) -> core::result::Result { // Invariant canonical compile-detect: 91% + 9% = 100% canonical BM V2. if creator_share_bps.saturating_add(treasury_share_bps) != 10_000 { return Err(Error::InvalidConfiguration); } // Royalty canonical reasonable range: 0-10% canonical industry standard. if transfer_royalty_bps > 1_000 { return Err(Error::InvalidConfiguration); } // Treasury burn canonical reasonable: 0-50% del treasury share. if treasury_burn_bps > 5_000 { return Err(Error::InvalidConfiguration); } Ok(Self { owner: Self::env().caller(), treasury, mint_fee_base, creator_share_bps, treasury_share_bps, transfer_royalty_bps, treasury_burn_bps, max_supply, lockup_blocks, children_safety_strict, total_supply: 0, token_owner: Mapping::default(), token_creator: Mapping::default(), token_asset_hash: Mapping::default(), token_mint_block: Mapping::default(), owner_balance: Mapping::default(), token_approved: Mapping::default(), paused: false, }) } /// Default canonical constructor BM V2 ratified. /// Treasury = caller (creator deployment + admin own Treasury initially). /// Mint fee = 1 OGC (1_000_000_000_000_000_000 base units canonical = 10^18). /// 91/9 split + 5% royalty + 20% treasury burn + unlimited supply. #[ink(constructor)] pub fn default_bm_v2() -> Self { // Safe canonical — default values invariant-validated compile-detect. Self::new( Self::env().caller(), 1_000_000_000_000_000_000, // 1 OGC base canonical 9_100, // 91% creator 900, // 9% treasury 500, // 5% royalty 2_000, // 20% treasury burn None, // unlimited supply canonical 0, // no lockup canonical default false, // children safety strict opt-in ) .expect("BM V2 default constants canonical invariant-validated") } // ───────────────────────────────────────────────────────────── // Public messages canonical — Read methods (free per Inmutable #37) // ───────────────────────────────────────────────────────────── /// Owner canonical del contract. #[ink(message)] pub fn owner(&self) -> Address { self.owner } /// Treasury address canonical. #[ink(message)] pub fn treasury(&self) -> Address { self.treasury } /// Total supply canonical hasta fecha. #[ink(message)] pub fn total_supply(&self) -> u32 { self.total_supply } /// Max supply canonical (None = unlimited). #[ink(message)] pub fn max_supply(&self) -> Option { self.max_supply } /// Mint fee base canonical (pre-HardwareTier modulation). #[ink(message)] pub fn mint_fee_base(&self) -> Balance { self.mint_fee_base } /// Owner canonical del token específico. #[ink(message)] pub fn token_owner(&self, token_id: TokenId) -> Option
{ self.token_owner.get(token_id) } /// Original creator canonical del token (immutable post-mint). #[ink(message)] pub fn token_creator(&self, token_id: TokenId) -> Option
{ self.token_creator.get(token_id) } /// Asset hash BLAKE3 canonical del token. #[ink(message)] pub fn token_asset_hash(&self, token_id: TokenId) -> Option { self.token_asset_hash.get(token_id) } /// Balance (token count) canonical del account. #[ink(message)] pub fn balance_of(&self, account: Address) -> u32 { self.owner_balance.get(account).unwrap_or(0) } /// Approved spender canonical del token específico. #[ink(message)] pub fn approved_spender(&self, token_id: TokenId) -> Option
{ self.token_approved.get(token_id) } /// Paused state canonical. #[ink(message)] pub fn is_paused(&self) -> bool { self.paused } // ───────────────────────────────────────────────────────────── // Public messages canonical — Mint // ───────────────────────────────────────────────────────────── /// Mint canonical new NFT. Payment value debe ≥ mint_fee_base. /// /// Flow canonical: /// 1. Verify paused == false (Error::ContractPaused) /// 2. Verify total_supply < max_supply si canonical capped /// 3. Verify asset_hash != [0; 32] (Inmutable #26 canonical) /// 4. Verify payment ≥ mint_fee_base (Error::InsufficientPayment) /// 5. Split payment 91/9 canonical (BM V2 ratified) /// 6. Burn 20% del treasury share canonical (BM V2 burn matrix) /// 7. Record token canonical state (owner + creator + hash + mint_block) /// 8. Emit Minted event canonical #[ink(message, payable)] pub fn mint(&mut self, asset_hash: AssetHash) -> Result { if self.paused { return Err(Error::ContractPaused); } if let Some(max) = self.max_supply { if self.total_supply >= max { return Err(Error::MaxSupplyReached); } } if asset_hash == [0u8; 32] { return Err(Error::InvalidAssetHash); } let payment = u256_to_balance(self.env().transferred_value()); if payment < self.mint_fee_base { return Err(Error::InsufficientPayment); } // BM V2 canonical 91/9 split — basis points arithmetic. let creator_share = payment .saturating_mul(self.creator_share_bps as Balance) .saturating_div(10_000); let treasury_share = payment .saturating_mul(self.treasury_share_bps as Balance) .saturating_div(10_000); // Treasury burn (per BM V2 burn matrix Pillar 3). let treasury_burn = treasury_share .saturating_mul(self.treasury_burn_bps as Balance) .saturating_div(10_000); let treasury_net = treasury_share.saturating_sub(treasury_burn); let creator = self.env().caller(); let token_id = self.total_supply; // FIX-1 (V1-C1) CEI: TODO el estado de mint muta ANTES de los // transfers, para que una reentrada (creator contract) vea // total_supply ya incrementado y no reuse el mismo token_id. // // ── EFFECTS (mint state) ── self.token_owner.insert(token_id, &creator); self.token_creator.insert(token_id, &creator); self.token_asset_hash.insert(token_id, &asset_hash); self.token_mint_block .insert(token_id, &self.env().block_number()); let new_balance = self.balance_of(creator).saturating_add(1); self.owner_balance.insert(creator, &new_balance); self.total_supply = self.total_supply.saturating_add(1); // ── INTERACTIONS ── // FIX-1 (V1-C4) burn real: el treasury_burn NO se envía a la // zero-address — se RETIENE en el contrato (fuera de circulación), a // barrer por el primitivo de burn del runtime. Solo salen creator + // treasury_net; payment − (creator_share + treasury_net) = burn queda. self.env() .transfer(creator, U256::from(creator_share)) .map_err(|_| Error::InsufficientPayment)?; self.env() .transfer(self.treasury, U256::from(treasury_net)) .map_err(|_| Error::InsufficientPayment)?; // ── EVENTS ── self.env().emit_event(Minted { token_id, creator, asset_hash, mint_fee_paid: payment, creator_share, treasury_share: treasury_net, treasury_burned: treasury_burn, }); Ok(token_id) } // ───────────────────────────────────────────────────────────── // Public messages canonical — Transfer + Approve // ───────────────────────────────────────────────────────────── /// Transfer canonical de token con sale_price (secondary sale). /// Royalty 5% canonical paid al original creator from sale_price. /// /// **Modelo de delegación (David Opción C.1, 2026-07-04, audit ultra):** /// el guard es `caller == owner || approved`, semántica ERC-721 ESTÁNDAR — /// una aprobación ES delegación total ("si no confías, no apruebes"). Que un /// spender aprobado pueda mover el token con `sale_price = 0` NO es un bug: /// es el estándar de oro Web3. La lógica de VENTA con precio pactado + la /// división 91/9 (creador/tesorería) NO vive aquí: vivirá en un contrato de /// **marketplace SEPARADO** (disciplina de costuras de traits, ADR-0057) — /// meterla aquí acoplaría el NFT base al marketplace y romper los NFTs al /// actualizar las reglas de venta. `sale_price` aquí es un pago opcional que /// fluye al vendedor + royalty; el precio-obligatorio es cancha del marketplace. /// /// Flow canonical: /// 1. Verify paused == false /// 2. Verify token exists canonical /// 3. Verify caller == owner OR caller == approved /// 4. Verify lockup canonical expired (block_now >= mint_block + lockup_blocks) /// 5. Verify from != to (Error::SelfTransferProhibited) /// 6. Calculate royalty canonical (transfer_royalty_bps × sale_price / 10000) /// 7. Transfer royalty canonical al original_creator /// 8. Update ownership canonical state /// 9. Clear approval canonical post-transfer /// 10. Emit Transferred event canonical #[ink(message, payable)] pub fn transfer(&mut self, token_id: TokenId, to: Address) -> Result<()> { if self.paused { return Err(Error::ContractPaused); } let from = self .token_owner .get(token_id) .ok_or(Error::TokenNotFound)?; let caller = self.env().caller(); if caller != from && self.token_approved.get(token_id) != Some(caller) { return Err(Error::NotApprovedSpender); } if from == to { return Err(Error::SelfTransferProhibited); } let mint_block = self .token_mint_block .get(token_id) .ok_or(Error::TokenNotFound)?; let now = self.env().block_number(); if now < mint_block.saturating_add(self.lockup_blocks) { return Err(Error::TokenLocked); } // Royalty: pay 5% al original creator (secondary sale). let sale_price = u256_to_balance(self.env().transferred_value()); let royalty = sale_price .saturating_mul(self.transfer_royalty_bps as Balance) .saturating_div(10_000); let original_creator = self .token_creator .get(token_id) .ok_or(Error::TokenNotFound)?; let to_seller = sale_price.saturating_sub(royalty); // FIX-1 (V1-C1) CEI: la transferencia de propiedad muta ANTES de los // pagos, para que una reentrada vea el nuevo owner y no permita // doble-transfer / drenado de royalty. // // ── EFFECTS (ownership) ── self.token_owner.insert(token_id, &to); let from_balance = self.balance_of(from).saturating_sub(1); self.owner_balance.insert(from, &from_balance); let to_balance = self.balance_of(to).saturating_add(1); self.owner_balance.insert(to, &to_balance); self.token_approved.remove(token_id); // clear approval post-transfer // ── INTERACTIONS ── if royalty > 0 { self.env() .transfer(original_creator, U256::from(royalty)) .map_err(|_| Error::InsufficientPayment)?; } if to_seller > 0 { self.env() .transfer(from, U256::from(to_seller)) .map_err(|_| Error::InsufficientPayment)?; } // ── EVENTS ── self.env().emit_event(Transferred { token_id, from, to, sale_price, royalty_paid: royalty, }); Ok(()) } /// Approve canonical spender para token específico. /// Standard PSP34-like approval pattern. #[ink(message)] pub fn approve(&mut self, token_id: TokenId, spender: Address) -> Result<()> { if self.paused { return Err(Error::ContractPaused); } let owner = self .token_owner .get(token_id) .ok_or(Error::TokenNotFound)?; if owner != self.env().caller() { return Err(Error::NotTokenOwner); } self.token_approved.insert(token_id, &spender); self.env().emit_event(Approved { token_id, owner, spender, }); Ok(()) } // ───────────────────────────────────────────────────────────── // Admin messages canonical — Owner-only governance actions // ───────────────────────────────────────────────────────────── /// Emergency pause canonical (owner-only). Blocks mint + transfer. /// Unpause canonical via governance Phase 5 DAO vote post-shipping /// canonical Capacity #4 OCC Treasury. #[ink(message)] pub fn emergency_pause(&mut self) -> Result<()> { if self.env().caller() != self.owner { return Err(Error::NotContractOwner); } self.paused = true; self.env().emit_event(PauseToggled { actor: self.owner, paused: true, }); Ok(()) } /// Unpause canonical (owner-only Slice 3.2 SCAFFOLD interim). /// Phase 3.x maduración: require governance vote via Phase 5 pallet. #[ink(message)] pub fn unpause(&mut self) -> Result<()> { if self.env().caller() != self.owner { return Err(Error::NotContractOwner); } self.paused = false; self.env().emit_event(PauseToggled { actor: self.owner, paused: false, }); Ok(()) } } // ───────────────────────────────────────────────────────────────── // Tests canonical (compile-only — real deploy testing Phase 3.x post // cargo-contract v6 install canonical authorized). // ───────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; /// Constructor default BM V2 canonical invariant test. /// Verifies 91/9 split + 5% royalty + 20% burn canonical defaults. #[ink::test] fn default_bm_v2_constructor_canonical_invariants() { let contract = OriginNftMint::default_bm_v2(); assert_eq!(contract.total_supply(), 0); assert_eq!(contract.mint_fee_base(), 1_000_000_000_000_000_000); // 1 OGC assert_eq!(contract.creator_share_bps, 9_100); assert_eq!(contract.treasury_share_bps, 900); assert_eq!(contract.transfer_royalty_bps, 500); assert_eq!(contract.treasury_burn_bps, 2_000); assert_eq!(contract.max_supply(), None); assert!(!contract.is_paused()); } /// Invariant canonical: creator + treasury shares MUST sum to 10_000 bps (100%). #[ink::test] fn invalid_share_split_rejected_canonical() { let result = OriginNftMint::new( Address::from([0u8; 20]), 1_000_000_000_000_000_000, 9_000, // 90% — invalid sum 500, // 5% 500, 2_000, None, 0, false, ); assert_eq!(result.err(), Some(Error::InvalidConfiguration)); } /// Invariant canonical: transfer_royalty_bps must be ≤ 1_000 (10% max industry standard). #[ink::test] fn excessive_royalty_rejected_canonical() { let result = OriginNftMint::new( Address::from([0u8; 20]), 1_000_000_000_000_000_000, 9_100, 900, 1_500, // 15% — exceeds max 2_000, None, 0, false, ); assert_eq!(result.err(), Some(Error::InvalidConfiguration)); } /// FIX-1 — guarda: mint con pago insuficiente y transfer/approve sobre /// token inexistente cortan en el check ANTES de cualquier transfer. /// Los tests E2E de reentrancy reales son node-gated (ver `origin-escrow`). #[ink::test] fn fund_moving_actions_guarded() { let mut c = OriginNftMint::default_bm_v2(); let other = Address::from([9u8; 20]); assert_eq!(c.mint([1u8; 32]), Err(Error::InsufficientPayment)); assert_eq!(c.transfer(0, other), Err(Error::TokenNotFound)); assert_eq!(c.approve(0, other), Err(Error::TokenNotFound)); assert_eq!(c.total_supply(), 0); } } }