From 1fdc47e8597e03f8d98e38bc54ac742e526a1e2b Mon Sep 17 00:00:00 2001 From: DavidHerran Date: Fri, 3 Jul 2026 11:58:55 -0500 Subject: [PATCH] =?UTF-8?q?feat(econom=C3=ADa):=20techo=20duro=201B=20+=20?= =?UTF-8?q?fees=20con=20dientes=20(David=20Level=20A=202026-07-03,=20RV=20?= =?UTF-8?q?6=E2=86=927)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dos decisiones económicas ratificadas por David, verificadas en fuente: 1. TECHO DURO 1B (Opción A) — la emisión TRANSFIERE del pool Ecosystem finito (og/ecosy, 400M pre-asignado en génesis), JAMÁS mint_into. La aserción de compilación de 1.000M deja de ser promesa de génesis y se vuelve ley que la producción de bloques obedece: el total issuance nunca crece, el pool se agota en ~7.6 años (10 OGC/bloque), después economía circular pura. Cuando el pool no cubre el reward completo, libera el remanente y emite EcosystemPoolDepleted una vez. Evento renombrado BlockRewardMinted → BlockRewardReleased (honestidad: es redistribución, no acuñación). - origin-chain-emission: Currency.transfer (Expendable) desde ecosystem_account; +EcosystemPalletId config, +DepletionAnnounced storage one-shot, +2 tests (hard_cap_pool_se_agota_y_para, depletion_event_se_anuncia_una_sola_vez). - runtime: EmissionEcosystemPalletId (og/ecosy) wired; génesis asigna el 40% a la cuenta PalletId determinista (sin llave humana), no al placeholder ECO!. 2. FEES CON DIENTES — WeightToFee/LengthToFee de IdentityFee (polvo ~10^-10 OGC, #16 = adorno) a ConstantMultiplier calibrado: un transfer simple ≈ 1 MILLI_OGC (0.001 OGC). Consecuencias: el descuento Pi5 (0.1×) = ahorro real (0.0001 OGC); la quema del 100% de gas es deflación medible desde el testnet; el spam cuesta OGC real. Factor 7e6 (weight) + 1e11 (length). Test de regresión weight_to_fee_transfer_tiene_dientes_no_polvo blinda contra revertir a polvo. Calibrado a pesos placeholder — re-tunear con benchmarks reales (leftover FIX-2). RUNTIME_VERSION 6→7. Workspace 275 tests verde, 0 fallos. TRANSACTION_VERSION intacto. Co-Authored-By: Claude Fable 5 --- crates/origin-chain-emission/src/lib.rs | 246 ++++++++++++++---- crates/origin-chain-runtime/src/configs.rs | 51 +++- .../src/genesis_config_presets.rs | 9 +- crates/origin-chain-runtime/src/lib.rs | 41 ++- 4 files changed, 282 insertions(+), 65 deletions(-) diff --git a/crates/origin-chain-emission/src/lib.rs b/crates/origin-chain-emission/src/lib.rs index 447158e..2064b65 100644 --- a/crates/origin-chain-emission/src/lib.rs +++ b/crates/origin-chain-emission/src/lib.rs @@ -1,16 +1,26 @@ //! # pallet-origin-emission — FIX-6 Stage 1: emisión que paga a los validadores //! //! Cierra el crítico V2-CA2 del audit: la cadena NO tenía emisión (Aura PoA -//! mintea 0), así que los validadores ganaban **0 OGC**. Ahora, en cada bloque, -//! se mintea un `BlockReward` y se reparte: +//! mintea 0), así que los validadores ganaban **0 OGC**. En cada bloque se +//! **libera** un `BlockReward` **desde el pool Ecosystem** (el 40% = 400M +//! pre-asignado en génesis) y se reparte: //! //! - **Autor** (el validador Aura que produjo el bloque) — el grueso. //! - **Treasury** (la cuenta-tesorería de governance, og/gvtry) — financia DAO. //! - **Subsidy pool** (proof-of-equity, og/subsy) — financia el subsidio de //! hardware anti-clasista (Inmutable #16). //! -//! La inflación resultante (~5% anual con el reward calibrado) materializa la -//! emisión del BM V2 (40% Ecosystem distribuido en 10-15 años). +//! ## TECHO DURO 1B (David ratificó 2026-07-03, Opción A) +//! +//! La emisión **TRANSFIERE del pool Ecosystem finito, NUNCA `mint_into`**. La +//! aserción de compilación fija la oferta en 1.000M OGC; mintear violaría esa +//! constitución matemática. Por eso las "recompensas" son en realidad una +//! **liberación programada y finita**: el pool de 400M se agota matemáticamente +//! en ~10-15 años (10 OGC/bloque × ~5.256M bloques/año ≈ 52.6M/año), y a partir +//! de ahí los validadores se sostienen puramente del reciclaje de la economía +//! circular (fees, marketplace). El total issuance JAMÁS crece — el BM V2 se +//! cumple a rajatabla. Cuando el pool no cubre el reward completo, se libera lo +//! que quede y se emite `EcosystemPoolDepleted` una vez. //! //! ## sub_mult ORTOGONAL (ratificado David 2026-06-10) //! @@ -44,7 +54,8 @@ extern crate alloc; #[frame_support::pallet] pub mod pallet { use frame_support::pallet_prelude::*; - use frame_support::traits::fungible::Mutate; + use frame_support::traits::fungible::{Inspect, Mutate}; + use frame_support::traits::tokens::{Fortitude, Preservation}; use frame_support::traits::FindAuthor; use frame_support::PalletId; use frame_system::pallet_prelude::*; @@ -60,17 +71,26 @@ pub mod pallet { type RuntimeEvent: From> + IsType<::RuntimeEvent>; - /// Currency que se mintea (la emisión aumenta el total issuance). + /// Currency de la que se LIBERA (transfer desde el pool Ecosystem). El + /// bound `Mutate` da `transfer`; el pallet JAMÁS llama `mint_into` — el + /// techo duro de 1B se preserva porque la emisión mueve OGC existentes, + /// no acuña nuevos (David 2026-07-03, Opción A). type Currency: Mutate; /// Cómo encontrar el autor del bloque. En el runtime se cablea con la /// FindAuthor de Aura (pallet_session::FindAccountFromAuthorIndex). type FindAuthor: FindAuthor; - /// Recompensa por bloque en base units OGC (calibra la inflación ~5%/año). + /// Recompensa por bloque en base units OGC (calibra el ritmo de + /// liberación; el pool finito lo agota en ~10-15 años). #[pallet::constant] type BlockReward: Get; + /// PalletId del pool Ecosystem (40% = 400M pre-asignado en génesis). La + /// fuente FINITA de la que se liberan las recompensas de bloque. + #[pallet::constant] + type EcosystemPalletId: Get; + /// Share del autor en basis points (10_000 = 100%). E.g. 7_000 = 70%. #[pallet::constant] type AuthorShareBps: Get; @@ -94,11 +114,21 @@ pub mod pallet { #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); - /// Total OGC emitido acumulado (tracking de inflación on-chain). + /// Total OGC liberado acumulado del pool Ecosystem (tracking on-chain del + /// avance hacia el agotamiento del techo duro; NO es inflación — es + /// redistribución de OGC pre-existentes). #[pallet::storage] #[pallet::getter(fn total_emitted)] pub type TotalEmitted = StorageValue<_, u128, ValueQuery>; + /// Flag one-shot: `true` una vez que el pool Ecosystem dejó de cubrir un + /// `BlockReward` completo (techo duro alcanzado). Evita re-emitir el evento + /// `EcosystemPoolDepleted` cada bloque. Default `false` (Rust bool default, + /// sin migración — como `EmissionPaused`). + #[pallet::storage] + #[pallet::getter(fn depletion_announced)] + pub type DepletionAnnounced = StorageValue<_, bool, ValueQuery>; + /// Kill-switch de governance (Root) para la emisión per-block. `false` = /// emisión ACTIVA (default — el Rust default de `bool` lo da gratis, sin /// `OnEmpty` ni migración). `true` = pausada. La activación de NPoS lo pone en @@ -113,18 +143,27 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// Block reward minteado y repartido autor/treasury/subsidy. - BlockRewardMinted { + /// Block reward LIBERADO del pool Ecosystem y repartido autor/treasury/ + /// subsidy (transfer, no mint — techo duro 1B intacto). + BlockRewardReleased { /// El validador Aura autor del bloque. author: T::AccountId, - /// Monto minteado al autor. + /// Monto liberado al autor. author_amount: u128, - /// Monto minteado al treasury. + /// Monto liberado al treasury. treasury_amount: u128, - /// Monto minteado al subsidy pool. + /// Monto liberado al subsidy pool. subsidy_amount: u128, }, - /// El kill-switch de emisión cambió (Root). `paused=true` detiene el mint. + /// El pool Ecosystem se agotó (techo duro alcanzado): dejó de cubrir un + /// `BlockReward` completo. Se emite UNA vez. A partir de aquí los + /// validadores se sostienen de la economía circular (fees/marketplace). + EcosystemPoolDepleted { + /// Total liberado del pool a lo largo de su vida (~400M). + total_released: u128, + }, + /// El kill-switch de emisión cambió (Root). `paused=true` detiene la + /// liberación por bloque. EmissionPauseChanged { /// Nuevo estado: `true` = emisión pausada, `false` = activa. paused: bool, @@ -145,7 +184,7 @@ pub mod pallet { /// mintea el BlockReward y lo reparte. Si no hay autor (e.g. bloque de /// inherentes sin pre-digest), no mintea. fn on_initialize(_n: BlockNumberFor) -> Weight { - // Kill-switch (governance): si la emisión está pausada, no mintea nada. + // Kill-switch (governance): si la emisión está pausada, no libera nada. // Lee 1 storage value; default `false` ⇒ activa (cero regresión). if EmissionPaused::::get() { return T::DbWeight::get().reads(1); @@ -153,7 +192,16 @@ pub mod pallet { let digest = >::digest(); let pre_runtime = digest.logs.iter().filter_map(|d| d.as_pre_runtime()); if let Some(author) = T::FindAuthor::find_author(pre_runtime) { - let reward = T::BlockReward::get(); + let pool = Self::ecosystem_account(); + // TECHO DURO 1B: liberamos del pool FINITO, jamás minteamos. + // Expendable = el pool puede vaciarse hasta 0 (es su destino en + // ~10-15 años). Liberamos hasta lo que quede disponible. + let available = T::Currency::reducible_balance( + &pool, + Preservation::Expendable, + Fortitude::Polite, + ); + let reward = T::BlockReward::get().min(available); if reward > 0 { let author_amount = reward.saturating_mul(T::AuthorShareBps::get() as u128) / 10_000; @@ -164,36 +212,58 @@ pub mod pallet { .saturating_sub(author_amount) .saturating_sub(treasury_amount); - // mint_into aumenta el total issuance (emisión real). Puede - // fallar (e.g. destino por debajo del existential deposit): - // TotalEmitted y el evento SOLO registran lo que de verdad - // se acuñó, o la contabilidad miente (audit ultra 2026-07-02). - let author_minted = - T::Currency::mint_into(&author, author_amount).unwrap_or_default(); - let treasury_minted = - T::Currency::mint_into(&Self::treasury_account(), treasury_amount) - .unwrap_or_default(); - let subsidy_minted = - T::Currency::mint_into(&Self::subsidy_account(), subsidy_amount) - .unwrap_or_default(); + // transfer NO cambia el total issuance (redistribuye OGC + // existentes del pool). Una fracción no entregable (e.g. + // destino bajo ED) se QUEDA en el pool — no se pierde ni se + // acuña. TotalEmitted y el evento registran SOLO lo movido. + let author_moved = T::Currency::transfer( + &pool, + &author, + author_amount, + Preservation::Expendable, + ) + .unwrap_or_default(); + let treasury_moved = T::Currency::transfer( + &pool, + &Self::treasury_account(), + treasury_amount, + Preservation::Expendable, + ) + .unwrap_or_default(); + let subsidy_moved = T::Currency::transfer( + &pool, + &Self::subsidy_account(), + subsidy_amount, + Preservation::Expendable, + ) + .unwrap_or_default(); - let total_minted = author_minted - .saturating_add(treasury_minted) - .saturating_add(subsidy_minted); - if total_minted > 0 { - TotalEmitted::::mutate(|t| *t = t.saturating_add(total_minted)); - Self::deposit_event(Event::BlockRewardMinted { + let released = author_moved + .saturating_add(treasury_moved) + .saturating_add(subsidy_moved); + if released > 0 { + TotalEmitted::::mutate(|t| *t = t.saturating_add(released)); + Self::deposit_event(Event::BlockRewardReleased { author, - author_amount: author_minted, - treasury_amount: treasury_minted, - subsidy_amount: subsidy_minted, + author_amount: author_moved, + treasury_amount: treasury_moved, + subsidy_amount: subsidy_moved, }); } } + // Anuncio one-shot: el pool ya no cubre un BlockReward completo + // (techo duro alcanzado). A partir de aquí, economía circular. + if available < T::BlockReward::get() && !DepletionAnnounced::::get() { + DepletionAnnounced::::put(true); + Self::deposit_event(Event::EcosystemPoolDepleted { + total_released: TotalEmitted::::get(), + }); + } } - // Placeholder de minteo (pendiente benchmarking real, leftover FIX-2) + - // la lectura de EmissionPaused que el kill-switch añadió a este path. - Weight::from_parts(30_000, 0).saturating_add(T::DbWeight::get().reads(1)) + // Placeholder de weight (pendiente benchmarking real, leftover FIX-2): + // reads = EmissionPaused + pool balance + DepletionAnnounced; los 3 + // transfers dominan (read+write ×2 cuentas c/u) + TotalEmitted. + T::DbWeight::get().reads_writes(3, 6) } } @@ -219,6 +289,12 @@ pub mod pallet { } impl Pallet { + /// Cuenta del pool Ecosystem (40% = 400M en génesis) — la fuente FINITA + /// de la que se liberan las recompensas de bloque (techo duro 1B). + pub fn ecosystem_account() -> T::AccountId { + T::EcosystemPalletId::get().into_account_truncating() + } + /// Cuenta-tesorería (vault) — la MISMA que governance (og/gvtry). pub fn treasury_account() -> T::AccountId { T::TreasuryPalletId::get().into_account_truncating() @@ -240,6 +316,7 @@ mod tests { use frame_support::{ assert_noop, assert_ok, construct_runtime, derive_impl, parameter_types, PalletId, }; + use sp_runtime::traits::AccountIdConversion; use sp_runtime::{AccountId32, BuildStorage, DispatchError}; type Block = frame_system::mocking::MockBlock; @@ -281,6 +358,7 @@ mod tests { pub const BlockRewardCfg: u128 = 1_000; pub const AuthorShareBpsCfg: u16 = 7_000; // 70% pub const TreasuryShareBpsCfg: u16 = 2_000; // 20% (subsidy = 10% residual) + pub const EcosystemPid: PalletId = PalletId(*b"og/ecosy"); pub const TreasuryPid: PalletId = PalletId(*b"og/gvtry"); pub const SubsidyPid: PalletId = PalletId(*b"og/subsy"); } @@ -292,53 +370,83 @@ mod tests { type BlockReward = BlockRewardCfg; type AuthorShareBps = AuthorShareBpsCfg; type TreasuryShareBps = TreasuryShareBpsCfg; + type EcosystemPalletId = EcosystemPid; type TreasuryPalletId = TreasuryPid; type SubsidyPalletId = SubsidyPid; } - fn new_test_ext() -> sp_io::TestExternalities { - let storage = frame_system::GenesisConfig::::default() + fn ecosystem_account() -> AccountId32 { + EcosystemPid::get().into_account_truncating() + } + + /// Fondea el pool Ecosystem con `pool_balance` en génesis (la fuente finita + /// de la que se libera; sin esto, transfer no mueve nada — techo duro). + fn new_test_ext_with_pool(pool_balance: u128) -> sp_io::TestExternalities { + let mut storage = frame_system::GenesisConfig::::default() .build_storage() .expect("genesis builds"); + pallet_balances::GenesisConfig:: { + balances: vec![(ecosystem_account(), pool_balance)], + ..Default::default() + } + .assimilate_storage(&mut storage) + .expect("balances genesis"); let mut ext: sp_io::TestExternalities = storage.into(); ext.execute_with(|| System::set_block_number(1)); ext } + /// Default: pool amplio (1M) — cubre miles de bloques sin agotarse. + fn new_test_ext() -> sp_io::TestExternalities { + new_test_ext_with_pool(1_000_000) + } + fn author() -> AccountId32 { AccountId32::new([1u8; 32]) } #[test] - fn block_reward_minted_and_split_correctly() { + fn block_reward_released_and_split_correctly() { new_test_ext().execute_with(|| { + let pool = Emission::ecosystem_account(); let treasury = Emission::treasury_account(); let subsidy = Emission::subsidy_account(); - // Pre: autor/treasury/subsidy en 0 (no genesis-funded). + // Pre: pool 1M, autor/treasury/subsidy en 0. + assert_eq!(>::balance(&pool), 1_000_000); assert_eq!(>::balance(&author()), 0); assert_eq!(Emission::total_emitted(), 0); + let issuance_before = >::total_issuance(); - // Un bloque: mintea 1000 → autor 700, treasury 200, subsidy 100. + // Un bloque: libera 1000 del pool → autor 700, treasury 200, subsidy 100. >::on_initialize(1); assert_eq!(>::balance(&author()), 700); assert_eq!(>::balance(&treasury), 200); assert_eq!(>::balance(&subsidy), 100); assert_eq!(Emission::total_emitted(), 1000); + // TECHO DURO: el pool bajó 1000; el total issuance NO cambió (transfer). + assert_eq!(>::balance(&pool), 999_000); + assert_eq!( + >::total_issuance(), + issuance_before, + "transfer no acuña: issuance constante" + ); }); } #[test] fn emission_accumulates_over_blocks() { new_test_ext().execute_with(|| { + let pool = Emission::ecosystem_account(); >::on_initialize(1); >::on_initialize(2); >::on_initialize(3); - // 3 bloques × 1000 = 3000 emitido; autor acumula 3 × 700 = 2100. + // 3 bloques × 1000 = 3000 liberado; autor acumula 3 × 700 = 2100. assert_eq!(Emission::total_emitted(), 3_000); assert_eq!(>::balance(&author()), 2_100); + assert_eq!(>::balance(&pool), 997_000); }); } @@ -356,6 +464,52 @@ mod tests { }); } + // ─── Techo duro 1B: el pool finito se agota y la emisión para ────────── + + #[test] + fn hard_cap_pool_se_agota_y_para() { + // Pool = 2500 = 2.5 rewards. Bloque 1: 1000 (pool 1500). Bloque 2: 1000 + // (pool 500). Bloque 3: solo 500 disponible → libera 500 (pool 0) + + // anuncia agotamiento. Bloque 4: 0 disponible → nada. + new_test_ext_with_pool(2_500).execute_with(|| { + let pool = Emission::ecosystem_account(); + let issuance = >::total_issuance(); + + >::on_initialize(1); + assert_eq!(>::balance(&pool), 1_500); + >::on_initialize(2); + assert_eq!(>::balance(&pool), 500); + assert!(!Emission::depletion_announced()); + + // Bloque 3: libera el remanente parcial (500) y anuncia agotamiento. + >::on_initialize(3); + assert_eq!(>::balance(&pool), 0); + assert!(Emission::depletion_announced()); + + // Bloque 4+: pool seco, cero liberación adicional. + >::on_initialize(4); + assert_eq!(>::balance(&pool), 0); + + // TECHO DURO: total liberado == pool inicial, NUNCA más; issuance intacto. + assert_eq!(Emission::total_emitted(), 2_500); + assert_eq!(>::total_issuance(), issuance); + }); + } + + #[test] + fn depletion_event_se_anuncia_una_sola_vez() { + new_test_ext_with_pool(500).execute_with(|| { + // Pool < 1 reward desde el inicio: bloque 1 libera 500 y anuncia. + >::on_initialize(1); + assert!(Emission::depletion_announced()); + let emitted = Emission::total_emitted(); + // Bloques siguientes: flag ya en true ⇒ no re-anuncia, no re-libera. + >::on_initialize(2); + >::on_initialize(3); + assert_eq!(Emission::total_emitted(), emitted, "pool seco: sin más liberación"); + }); + } + // ─── Kill-switch de governance (FIX-6 Stage 2b — anti-doble-emisión) ── #[test] diff --git a/crates/origin-chain-runtime/src/configs.rs b/crates/origin-chain-runtime/src/configs.rs index 47df042..e3d2cb3 100644 --- a/crates/origin-chain-runtime/src/configs.rs +++ b/crates/origin-chain-runtime/src/configs.rs @@ -167,6 +167,23 @@ parameter_types! { /// benchmarking real (leftover FIX-2). pub FeeMultiplierOne: pallet_transaction_payment::Multiplier = pallet_transaction_payment::Multiplier::from_u32(1); + + /// Factor WeightToFee (base units OGC por unidad de ref_time). Calibrado + /// (David 2026-07-03) para que OCAFB-5 tenga DIENTES: con IdentityFee el fee + /// base de un transfer era ~2×10^-10 OGC (polvo cósmico) y el #16 anti- + /// clasista era un adorno. Con este factor, un transfer simple (ExtrinsicBase + /// 108.157M + transfer_keep_alive ~39M ≈ 1.47×10^8 ref_time) cuesta: + /// 1.47×10^8 × 7×10^6 ≈ 1.03×10^15 base units ≈ **1 MILLI_OGC (0.001 OGC)**. + /// Consecuencias: el descuento Pi5 (0.1×) = ahorro real (0.0001 OGC); la + /// quema del 100% de gas es deflación medible desde el testnet; el spam + /// cuesta OGC real. Calibrado a pesos placeholder — re-tunear con benchmarks + /// reales (leftover FIX-2). `ConstantMultiplier` usa solo ref_time. + pub const WeightFeeFactor: Balance = 7_000_000; + + /// Factor LengthToFee: 10^11 base units por byte ⇒ 1 KiB ≈ 0.0001 OGC. Da + /// costo real (anti-bloat) a extrinsics grandes (código de contrato, remarks) + /// sin dominar el fee de un tx normal (150 bytes ≈ 0.000015 OGC). + pub const LengthFeeFactor: Balance = 100_000_000_000; } /// FIX-6 Stage 2a: clasificador call→ChainAction del fee unificado (taxonomía @@ -223,8 +240,11 @@ impl pallet_transaction_payment::Config for Runtime { type OnChargeTransaction = origin_chain_fees::OriginFeeAdapter; type OperationalFeeMultiplier = OperationalFeeMultiplier; - type WeightToFee = frame_support::weights::IdentityFee; - type LengthToFee = frame_support::weights::IdentityFee; + /// Calibrado (David 2026-07-03): un transfer simple ≈ 1 MILLI_OGC. NUNCA + /// `IdentityFee` — con 18 decimales el fee base era polvo (~10^-10 OGC) y + /// anulaba el #16. Ver `WeightFeeFactor`. + type WeightToFee = frame_support::weights::ConstantMultiplier; + type LengthToFee = frame_support::weights::ConstantMultiplier; /// NUNCA `()` aquí: colapsa `NextFeeMultiplier` a 0 (ver `FeeMultiplierOne`). type FeeMultiplierUpdate = pallet_transaction_payment::ConstFeeMultiplier; type WeightInfo = pallet_transaction_payment::weights::SubstrateWeight; @@ -481,22 +501,31 @@ impl origin_chain_governance::Config for Runtime { // origin_chain_emission::Config — FIX-6 Stage 1 (audit V2-CA2) // ============================================================ // -// En cada bloque mintea EmissionBlockReward y lo reparte: 70% al autor (el -// validador Aura), 20% al treasury (og/gvtry, la MISMA cuenta de governance), -// 10% al subsidy pool (og/subsy, proof-of-equity que financia el subsidio de -// hardware #16 — ortogonal a la suscripción, David 2026-06-10). Cierra "los -// validadores ganan 0 OGC". FindAuthor = el mapeo Aura→validador de session. +// En cada bloque LIBERA EmissionBlockReward del pool Ecosystem (og/ecosy, 400M) +// y lo reparte: 70% al autor (el validador Aura), 20% al treasury (og/gvtry, la +// MISMA cuenta de governance), 10% al subsidy pool (og/subsy, proof-of-equity +// que financia el subsidio de hardware #16 — ortogonal a la suscripción, David +// 2026-06-10). Cierra "los validadores ganan 0 OGC". FindAuthor = el mapeo +// Aura→validador de session. // -// BlockReward 10 OGC × ~5.256M bloques/año ≈ 52.6M OGC/año ≈ 5.26% inflación -// sobre el genesis de 1B (materializa la emisión del 40% Ecosystem BM V2). +// TECHO DURO 1B (David 2026-07-03, Opción A): transfer del pool finito, NUNCA +// mint. BlockReward 10 OGC × ~5.256M bloques/año ≈ 52.6M OGC/año ⇒ el pool de +// 400M se agota en ~7.6 años; después, economía circular. El total issuance +// JAMÁS crece — la aserción de compilación de 1B se cumple a rajatabla. // Stage 2 introduce pallet_staking NPoS sobre este substrate. parameter_types! { - /// Recompensa por bloque: 10 OGC (~5% inflación anual). + /// Recompensa por bloque: 10 OGC. NO es inflación — es el ritmo de + /// LIBERACIÓN del pool Ecosystem finito (400M): 10 × ~5.256M bloques/año ≈ + /// 52.6M/año ⇒ el pool se agota en ~7.6 años (techo duro 1B, David 2026-07-03). pub const EmissionBlockReward: Balance = 10 * OGC; /// 70% al autor (validador Aura). pub const EmissionAuthorShareBps: u16 = 7_000; /// 20% al treasury (el residual 10% va al subsidy pool). pub const EmissionTreasuryShareBps: u16 = 2_000; + /// PalletId del pool Ecosystem (40% = 400M en génesis) — la FUENTE finita de + /// las recompensas. La emisión transfiere de aquí, jamás mintea (techo duro). + pub const EmissionEcosystemPalletId: frame_support::PalletId = + frame_support::PalletId(*b"og/ecosy"); /// PalletId del subsidy pool (proof-of-equity, #16 anti-clasista). pub const EmissionSubsidyPalletId: frame_support::PalletId = frame_support::PalletId(*b"og/subsy"); @@ -510,6 +539,8 @@ impl origin_chain_emission::Config for Runtime { type BlockReward = EmissionBlockReward; type AuthorShareBps = EmissionAuthorShareBps; type TreasuryShareBps = EmissionTreasuryShareBps; + // La FUENTE finita: el pool Ecosystem (og/ecosy) = 400M pre-asignado en génesis. + type EcosystemPalletId = EmissionEcosystemPalletId; // Reusa el MISMO vault de governance (og/gvtry) para el treasury. type TreasuryPalletId = GovernanceTreasuryPalletId; type SubsidyPalletId = EmissionSubsidyPalletId; diff --git a/crates/origin-chain-runtime/src/genesis_config_presets.rs b/crates/origin-chain-runtime/src/genesis_config_presets.rs index deea688..0a39da0 100644 --- a/crates/origin-chain-runtime/src/genesis_config_presets.rs +++ b/crates/origin-chain-runtime/src/genesis_config_presets.rs @@ -39,7 +39,7 @@ #![cfg(feature = "std")] use crate::{ - configs::{AuraId, GovernanceTreasuryPalletId, GrandpaId}, + configs::{AuraId, EmissionEcosystemPalletId, GovernanceTreasuryPalletId, GrandpaId}, opaque::SessionKeys, Balance, ChainAccountId, Ogc, RuntimeGenesisConfig, }; @@ -360,7 +360,12 @@ pub fn mainnet_genesis_config_genesis() -> RuntimeGenesisConfig { account_from_sr25519(bytes) }; - let ecosystem_pool = make_placeholder(b"ECO!"); // Ecosystem/Rewards Pool canonical + // El 40% Ecosystem (400M) se asigna a la cuenta PalletId `og/ecosy` — la + // MISMA de la que `pallet-origin-emission` LIBERA las recompensas de bloque + // (transfer, techo duro 1B; David 2026-07-03). No es un placeholder key- + // controlado: es la cuenta determinista del pool, sin llave humana. + let ecosystem_pool: ChainAccountId = + EmissionEcosystemPalletId::get().into_account_truncating(); let foundation_treasury = make_placeholder(b"FNDT"); // Foundation multisig = sudo_key interim let public_sale = make_placeholder(b"SALE"); // Public Sale canonical let founding_team = make_placeholder(b"TEAM"); // Founding Team canonical (4yr vesting) diff --git a/crates/origin-chain-runtime/src/lib.rs b/crates/origin-chain-runtime/src/lib.rs index 7f4ba16..e4ca5d2 100644 --- a/crates/origin-chain-runtime/src/lib.rs +++ b/crates/origin-chain-runtime/src/lib.rs @@ -243,7 +243,15 @@ pub const IMPL_NAME: &str = "origin-chain-charon"; /// `TotalEmitted` + evento registran SOLO lo realmente acuñado (resultados de /// `mint_into` ya no se descartan). Cambia comportamiento ⇒ spec bump; /// TRANSACTION_VERSION intacto. -pub const RUNTIME_VERSION: u32 = 6; +/// v7: economía Level A (David 2026-07-03, 2 decisiones) — (a) **TECHO DURO 1B**: +/// la emisión TRANSFIERE del pool Ecosystem finito (og/ecosy, 400M) en vez de +/// `mint_into`; el total issuance jamás crece, el pool se agota en ~7.6 años, +/// después economía circular (evento `BlockRewardReleased` + `EcosystemPoolDepleted` +/// one-shot). (b) **fees con dientes**: `WeightToFee`/`LengthToFee` de `IdentityFee` +/// (polvo ~10^-10 OGC) a `ConstantMultiplier` calibrado — un transfer ≈ 1 MILLI_OGC, +/// el #16 Pi5 0.1× = ahorro real, quema deflacionaria medible. Comportamiento ⇒ +/// spec bump; TRANSACTION_VERSION intacto. +pub const RUNTIME_VERSION: u32 = 7; /// Canonical authoring version per Substrate semver canonical. pub const AUTHORING_VERSION: u32 = 1; @@ -835,12 +843,12 @@ mod tests { use super::*; #[test] - fn runtime_version_v6_ultra_audit_fixes() { - // v6 = 3 fixes del audit ultra 2026-07-02: ConstFeeMultiplier (el `()` - // colapsaba NextFeeMultiplier a 0), NativeToEthRatio=1 (OGC 18 dec), - // y emisión con contabilidad honesta de mint_into. Cambios de - // comportamiento, no de formato de extrinsic ⇒ TRANSACTION_VERSION intacto. - assert_eq!(RUNTIME_VERSION, 6); + fn runtime_version_v7_hard_cap_and_fee_teeth() { + // v7 = economía Level A (David 2026-07-03): techo duro 1B (emisión = + // transfer del pool Ecosystem, no mint) + fees con dientes (ConstantMultiplier + // calibrado, transfer ≈ 1 MILLI_OGC). Cambios de comportamiento, no de + // formato de extrinsic ⇒ TRANSACTION_VERSION intacto. + assert_eq!(RUNTIME_VERSION, 7); assert_eq!(TRANSACTION_VERSION, 1); } @@ -952,6 +960,25 @@ mod tests { assert!(!next.is_zero()); } + #[test] + fn weight_to_fee_transfer_tiene_dientes_no_polvo() { + // Calibración David 2026-07-03 (v7): un transfer simple debe costar ~1 + // MILLI_OGC, NO el polvo ~10^-10 OGC de IdentityFee (que anulaba el #16). + // Guard de regresión: si alguien revierte a IdentityFee, el fee cae a + // ~1.5×10^8 base units (muy por debajo de MILLI_OGC/2) y esto rompe. + use frame_support::weights::{Weight, WeightToFee as _}; + // transfer ≈ ExtrinsicBaseWeight (108.157M) + transfer_keep_alive (~39M). + let transfer_weight = Weight::from_parts(108_157_000 + 39_051_000, 0); + let fee = <::WeightToFee>::weight_to_fee( + &transfer_weight, + ); + assert!( + fee >= MILLI_OGC / 2 && fee <= MILLI_OGC * 3, + "fee de transfer = {fee} base units; esperado ~1 MILLI_OGC ({MILLI_OGC}), \ + ni polvo ni exorbitante" + ); + } + #[test] fn revive_deposit_per_byte_canonical_milli_ogc() { // Asset Hub pattern adapted OGC tokenomics — 1 MILLI_OGC per byte canonical.