diff --git a/contracts/origin-escrow/lib.rs b/contracts/origin-escrow/lib.rs index 94faa76..4925a4d 100644 --- a/contracts/origin-escrow/lib.rs +++ b/contracts/origin-escrow/lib.rs @@ -51,7 +51,10 @@ //! - `milestones`: Vec canonical (amount + due_block + description_hash) //! - `escrow_fee_bps`: u16 (default 150 = 1.5%, BM V2 baseline) //! - `dispute_window_blocks`: u32 (window canonical para raise dispute post-release) -//! - `auto_release_blocks`: u32 (auto-release if no dispute canonical post-completion) +//! - `auto_release_blocks`: u32 (VIVO desde 2026-07-04, David Opción C.2): el +//! tiempo como juez determinista — `claim_auto_release` (permisionless) libera el +//! remanente al beneficiario si transcurre esta ventana sin actividad ni disputa. +//! Cierra el fund-lock del caso sin-milestones para la economía M2M. `0` = off. //! - `early_release_allowed`: bool (depositor pueda release early canonical) #![cfg_attr(not(feature = "std"), no_std, no_main)] @@ -195,6 +198,12 @@ mod origin_escrow { InvalidConfiguration, TransferFailed, EarlyReleaseProhibited, + /// Audit 2026-07-04 (C, David Opción 2): el auto-release está deshabilitado + /// para este escrow (`auto_release_blocks == 0`). + AutoReleaseDisabled, + /// Audit 2026-07-04: aún no transcurrió `auto_release_blocks` desde la + /// última actividad (depósito o release) — el tiempo-juez no ha vencido. + AutoReleaseNotReady, } pub type Result = core::result::Result; @@ -317,6 +326,12 @@ mod origin_escrow { } self.deposited_amount = value; self.state = EscrowState::Active; + // Audit 2026-07-04 (C, David Opción 2): arranca el reloj del auto- + // release. `last_release_block` es el reloj de "última actividad" + // (depósito o release de milestone); si no hay actividad ni disputa + // por `auto_release_blocks`, cualquiera puede gatillar `claim_auto_release`. + // Cierra el fund-lock del caso sin-milestones para la economía M2M. + self.last_release_block = Some(self.env().block_number()); self.env().emit_event(Deposited { depositor: self.depositor, amount: value, @@ -324,6 +339,65 @@ mod origin_escrow { Ok(()) } + /// Audit 2026-07-04 (C, David Opción 2): AUTO-RELEASE por tiempo — el + /// tiempo es el JUEZ determinista. **Permisionless**: cualquiera lo gatilla + /// cuando el escrow lleva `Active` (sin disputa) y transcurrió + /// `auto_release_blocks` desde la última actividad (depósito o release). + /// Libera el REMANENTE (deposited − released) al beneficiario (menos el fee + /// de tesorería) y cierra el escrow. Cierra el fund-lock del caso "se + /// depositó pero nadie marcó un milestone" — CRÍTICO para la economía M2M: + /// una AGI que contrata a otra no puede quedar en limbo infinito porque + /// alguien olvidó marcar un hito. El depositor que NO esté conforme debe + /// levantar disputa ANTES del vencimiento (la disputa mueve el estado a + /// Disputed y congela este path). + #[ink(message)] + pub fn claim_auto_release(&mut self) -> Result<()> { + // Solo Active — una disputa habría movido el estado a Disputed. + if self.state != EscrowState::Active { + return Err(Error::InvalidState); + } + // auto_release_blocks == 0 ⇒ feature deshabilitado para este escrow. + if self.auto_release_blocks == 0 { + return Err(Error::AutoReleaseDisabled); + } + let reference = self.last_release_block.unwrap_or(0); + let now = self.env().block_number(); + if now < reference.saturating_add(self.auto_release_blocks) { + return Err(Error::AutoReleaseNotReady); + } + + let remaining = self.deposited_amount.saturating_sub(self.released_amount); + let fee = remaining + .saturating_mul(self.escrow_fee_bps as Balance) + .saturating_div(10_000); + let net = remaining.saturating_sub(fee); + + // FIX-1 CEI: TODO el estado muta ANTES de los transfers. + self.released_amount = self.deposited_amount; + self.state = EscrowState::Completed; + self.last_release_block = Some(now); + + // ── INTERACTIONS ── + if net > 0 { + self.env() + .transfer(self.beneficiary, U256::from(net)) + .map_err(|_| Error::TransferFailed)?; + } + if fee > 0 { + self.env() + .transfer(self.treasury, U256::from(fee)) + .map_err(|_| Error::TransferFailed)?; + } + + // ── EVENTS ── + self.env().emit_event(EscrowCompleted { + total_released: self.released_amount, + total_refunded: 0, + treasury_fee: fee, + }); + Ok(()) + } + /// Add milestone canonical (depositor-only, pre-active state). /// Sum of milestone amounts MUST eventually equal total_amount. #[ink(message)] @@ -647,5 +721,34 @@ mod origin_escrow { // +0.4 OGC: Σ = 1.0 OGC == total → ok (boundary) assert!(e.add_milestone(400_000_000_000_000_000, 100, [1u8; 32]).is_ok()); } + + /// Audit 2026-07-04 (C, David Opción 2): AUTO-RELEASE por tiempo cierra el + /// fund-lock del caso sin-milestones (economía M2M). Deposita, avanza el + /// tiempo más allá de `auto_release_blocks` sin disputa, y el remanente + /// se libera al beneficiario. Antes del vencimiento → AutoReleaseNotReady. + #[ink::test] + fn auto_release_libera_al_beneficiario_por_tiempo() { + let total = 1_000_000_000_000_000_000u128; // 1 OGC (= total_amount de mk) + let depositor = Address::from([1u8; 20]); + let mut e = mk(); // auto_release_blocks = 1000, fee 1.5% + let contract = ink::env::test::callee(); + ink::env::test::set_contract_balance(depositor, U256::from(total * 3)); + ink::env::test::set_contract_balance(contract, U256::from(total * 3)); + + // Depósito (depositor) → Active, arranca el reloj en el bloque actual (0). + ink::env::test::set_caller(depositor); + ink::env::test::set_value_transferred(U256::from(total)); + assert!(e.deposit().is_ok()); + assert_eq!(e.state(), EscrowState::Active); + + // Sin milestones ni disputa. Antes del vencimiento → not ready. + assert_eq!(e.claim_auto_release(), Err(Error::AutoReleaseNotReady)); + + // El tiempo (juez determinista) vence: avanza > auto_release_blocks. + ink::env::test::set_block_number::(1001); + assert!(e.claim_auto_release().is_ok(), "auto-release tras el vencimiento"); + assert_eq!(e.state(), EscrowState::Completed); + assert_eq!(e.released_amount(), total, "remanente liberado"); + } } } diff --git a/contracts/origin-nft-mint/lib.rs b/contracts/origin-nft-mint/lib.rs index b8500d1..8a6dac2 100644 --- a/contracts/origin-nft-mint/lib.rs +++ b/contracts/origin-nft-mint/lib.rs @@ -492,6 +492,17 @@ mod origin_nft_mint { /// 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