Files
davidherranandClaude Fable 5 0d56e0bcbf feat(ink): escrow auto-release (C.2) + NFT modelo delegación documentado (C.1)
Decisiones C de David (2026-07-04):

ESCROW — Opción 2 (auto-release por tiempo, el tiempo como juez determinista):
El audit encontró [HIGH] que si alguien depositaba pero nunca marcaba un
milestone, los fondos quedaban ATRAPADOS (solo salían por disputa+árbitro), y los
campos auto_release_blocks/last_release_block eran código MUERTO. Fix: nueva
`claim_auto_release` (permisionless) que, tras `auto_release_blocks` sin actividad
ni disputa, libera el remanente al beneficiario (menos fee) y cierra el escrow.
`deposit()` arranca el reloj. CRÍTICO para la economía M2M: una AGI que contrata a
otra no queda en limbo porque alguien olvidó un hito. El depositor disconforme
levanta disputa ANTES del vencimiento. +test E2E (ink v6). Campos ya no muertos.

NFT — Opción 1 (delegación ERC-721 estándar, modularidad ADR-0057): el audit
marcó que un spender aprobado puede mover el token con pago cero. NO es bug — es
el estándar Web3 ("si no confías, no apruebes"). La venta con precio pactado +
split 91/9 vive en un contrato de MARKETPLACE SEPARADO (acoplar rompería los NFTs
al actualizar reglas de venta). Documentado el modelo en `transfer`. Sin cambio de
código. Contratos: escrow 6 tests, nft 4 tests verde.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:30:13 -05:00

720 lines
32 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # 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<u32> (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<u8> (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<u32>,
/// 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<TokenId, Address>,
/// Token → creator mapping canonical (original minter, immutable post-mint).
/// Usado para royalty payments canonical en secondary transfers.
token_creator: Mapping<TokenId, Address>,
/// Token → asset hash mapping canonical (BLAKE3 content addressing per
/// Inmutable #26).
token_asset_hash: Mapping<TokenId, AssetHash>,
/// Token → mint block mapping canonical (para lockup enforcement).
token_mint_block: Mapping<TokenId, BlockNumber>,
/// Owner → token count mapping canonical (balance_of canonical).
owner_balance: Mapping<Address, u32>,
/// Token → approved spender mapping canonical (approve/transfer_from pattern).
token_approved: Mapping<TokenId, Address>,
// ── 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<T> = core::result::Result<T, Error>;
/// 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<u32>,
lockup_blocks: BlockNumber,
children_safety_strict: bool,
) -> core::result::Result<Self, Error> {
// 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<u32> {
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<Address> {
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<Address> {
self.token_creator.get(token_id)
}
/// Asset hash BLAKE3 canonical del token.
#[ink(message)]
pub fn token_asset_hash(&self, token_id: TokenId) -> Option<AssetHash> {
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<Address> {
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<TokenId> {
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);
}
}
}