diff --git a/Cargo.lock b/Cargo.lock index aba15ac..72d56de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6379,6 +6379,20 @@ dependencies = [ "origin-chain-types", ] +[[package]] +name = "origin-chain-emission" +version = "0.0.1" +dependencies = [ + "frame-support", + "frame-system", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io 43.0.0", + "sp-runtime 44.0.0", +] + [[package]] name = "origin-chain-fees" version = "0.0.1" @@ -6477,6 +6491,7 @@ dependencies = [ "getrandom 0.3.4", "log", "origin-chain-agent-identity", + "origin-chain-emission", "origin-chain-fees", "origin-chain-governance", "origin-chain-provenance", diff --git a/Cargo.toml b/Cargo.toml index 6f00927..10444c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "crates/origin-chain-agent-identity", "crates/origin-chain-bridges", "crates/origin-chain-governance", + "crates/origin-chain-emission", "crates/origin-chain-runtime", "crates/origin-chain-node", ] @@ -201,6 +202,7 @@ origin-chain-fees = { path = "crates/origin-chain-fees", version = "0.0.1", defa origin-chain-agent-identity = { path = "crates/origin-chain-agent-identity", version = "0.0.1", default-features = false } origin-chain-bridges = { path = "crates/origin-chain-bridges", version = "0.0.1", default-features = false } origin-chain-governance = { path = "crates/origin-chain-governance", version = "0.0.1", default-features = false } +origin-chain-emission = { path = "crates/origin-chain-emission", version = "0.0.1", default-features = false } origin-chain-runtime = { path = "crates/origin-chain-runtime", version = "0.0.1", default-features = false } origin-chain-node = { path = "crates/origin-chain-node", version = "0.0.1" } diff --git a/contracts/origin-staking/lib.rs b/contracts/origin-staking/lib.rs index 3e49edf..d1a4365 100644 --- a/contracts/origin-staking/lib.rs +++ b/contracts/origin-staking/lib.rs @@ -221,9 +221,14 @@ mod origin_staking { 403_200, // 28 days lockup validator 100_800, // 7 days lockup beacon 201_600, // 14 days lockup reputation - 800, // 8% APR validator - 500, // 5% APR beacon - 300, // 3% APR reputation + // FIX-6 (V1/V2 fake APR): reward rates en 0 — NO hay rewards + // falsos en este contrato. Los validadores cobran vía el + // pallet-origin-emission (block rewards REALES en el runtime); las + // tasas reales de staking las define Stage 2 (NPoS por stake). + // Mostrar 8/5/3% sin un motor que los pagara era engañoso. + 0, // validator reward rate (era 8% APR ficticio) + 0, // beacon reward rate (era 5% APR ficticio) + 0, // reputation reward rate (era 3% APR ficticio) ) } diff --git a/crates/origin-chain-emission/Cargo.toml b/crates/origin-chain-emission/Cargo.toml new file mode 100644 index 0000000..3a30de7 --- /dev/null +++ b/crates/origin-chain-emission/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "origin-chain-emission" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +description = "Origin Chain emission pallet — FIX-6 Stage 1 (audit V2-CA2). Mintea un block reward por bloque y lo reparte autor (validador Aura) + treasury + subsidy pool (proof-of-equity, Inmutable #16 anti-clasista). Cierra 'los validadores ganan 0 OGC'." + +[dependencies] +# FRAME canonical +frame-support = { workspace = true } +frame-system = { workspace = true } + +# Substrate primitives canonical (AccountIdConversion para PalletId) +sp-runtime = { workspace = true } + +# SCALE codec canonical +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } + +[dev-dependencies] +# Mock runtime canonical — pallet_balances para testear el minteo real. +pallet-balances = { workspace = true, default-features = true } +sp-core = { workspace = true, default-features = true } +sp-io = { workspace = true, default-features = true } +sp-runtime = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = [ + "frame-support/std", + "frame-system/std", + "sp-runtime/std", + "parity-scale-codec/std", + "scale-info/std", +] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", +] diff --git a/crates/origin-chain-emission/src/lib.rs b/crates/origin-chain-emission/src/lib.rs new file mode 100644 index 0000000..403d712 --- /dev/null +++ b/crates/origin-chain-emission/src/lib.rs @@ -0,0 +1,282 @@ +//! # 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: +//! +//! - **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). +//! +//! ## sub_mult ORTOGONAL (ratificado David 2026-06-10) +//! +//! El subsidy pool financia el subsidio de hardware **sin importar la +//! suscripción premium** (Pro/Ultra) — son dimensiones ortogonales (V2-CA3). Esta +//! emisión alimenta el pool; el OnChargeTransaction de Stage 2 lo consumirá. +//! +//! ## NPoS diferido a Stage 2 +//! +//! Esto es la emisión simple Phase 1 PoA (paga al autor Aura directo). Stage 2 +//! introduce pallet_staking + EraPayout (nominators + slashing + selección por +//! stake). El reward por bloque aquí es el substrate sobre el que Stage 2 hereda. + +#![cfg_attr(not(feature = "std"), no_std)] +#![warn(missing_docs)] + +pub use pallet::*; + +extern crate alloc; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_support::traits::fungible::Mutate; + use frame_support::traits::FindAuthor; + use frame_support::PalletId; + use frame_system::pallet_prelude::*; + use sp_runtime::traits::AccountIdConversion; + + /// StorageVersion en código (FIX-2 lifecycle): arranca en 0 (genesis). + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + /// Configuración del pallet de emisión. + #[pallet::config] + pub trait Config: frame_system::Config { + /// Runtime event type. + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + + /// Currency que se mintea (la emisión aumenta el total issuance). + 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). + #[pallet::constant] + type BlockReward: Get; + + /// Share del autor en basis points (10_000 = 100%). E.g. 7_000 = 70%. + #[pallet::constant] + type AuthorShareBps: Get; + + /// Share del treasury en basis points. E.g. 2_000 = 20%. El residual va al + /// subsidy pool. + #[pallet::constant] + type TreasuryShareBps: Get; + + /// PalletId de la cuenta-tesorería (la MISMA de governance, og/gvtry). + #[pallet::constant] + type TreasuryPalletId: Get; + + /// PalletId del subsidy pool (proof-of-equity, Inmutable #16 anti-clasista). + #[pallet::constant] + type SubsidyPalletId: Get; + } + + /// Pallet definition. + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + /// Total OGC emitido acumulado (tracking de inflación on-chain). + #[pallet::storage] + #[pallet::getter(fn total_emitted)] + pub type TotalEmitted = StorageValue<_, u128, ValueQuery>; + + /// Eventos del pallet. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Block reward minteado y repartido autor/treasury/subsidy. + BlockRewardMinted { + /// El validador Aura autor del bloque. + author: T::AccountId, + /// Monto minteado al autor. + author_amount: u128, + /// Monto minteado al treasury. + treasury_amount: u128, + /// Monto minteado al subsidy pool. + subsidy_amount: u128, + }, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Cada bloque: identifica al autor Aura (desde los pre-runtime digests), + /// 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 { + 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(); + if reward > 0 { + let author_amount = + reward.saturating_mul(T::AuthorShareBps::get() as u128) / 10_000; + let treasury_amount = + reward.saturating_mul(T::TreasuryShareBps::get() as u128) / 10_000; + // El residual evita pérdida por redondeo de bps. + let subsidy_amount = reward + .saturating_sub(author_amount) + .saturating_sub(treasury_amount); + + // mint_into aumenta el total issuance (emisión real). + let _ = T::Currency::mint_into(&author, author_amount); + let _ = T::Currency::mint_into(&Self::treasury_account(), treasury_amount); + let _ = T::Currency::mint_into(&Self::subsidy_account(), subsidy_amount); + + TotalEmitted::::mutate(|t| *t = t.saturating_add(reward)); + Self::deposit_event(Event::BlockRewardMinted { + author, + author_amount, + treasury_amount, + subsidy_amount, + }); + } + } + Weight::from_parts(30_000, 0) + } + } + + impl Pallet { + /// Cuenta-tesorería (vault) — la MISMA que governance (og/gvtry). + pub fn treasury_account() -> T::AccountId { + T::TreasuryPalletId::get().into_account_truncating() + } + + /// Cuenta del subsidy pool (proof-of-equity, financia el subsidio #16). + pub fn subsidy_account() -> T::AccountId { + T::SubsidyPalletId::get().into_account_truncating() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate as origin_chain_emission; + use frame_support::traits::fungible::Inspect; + use frame_support::traits::{FindAuthor, Hooks}; + use frame_support::{construct_runtime, derive_impl, parameter_types, PalletId}; + use sp_runtime::{AccountId32, BuildStorage}; + + type Block = frame_system::mocking::MockBlock; + + construct_runtime!( + pub enum Test { + System: frame_system, + Balances: pallet_balances, + Emission: origin_chain_emission, + } + ); + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] + impl frame_system::Config for Test { + type Block = Block; + type AccountId = AccountId32; + type Lookup = sp_runtime::traits::IdentityLookup; + type AccountData = pallet_balances::AccountData; + } + + #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] + impl pallet_balances::Config for Test { + type AccountStore = System; + type Balance = u128; + } + + /// FindAuthor de prueba: devuelve siempre ALICE como autor del bloque. + pub struct MockFindAuthor; + impl FindAuthor for MockFindAuthor { + fn find_author<'a, I>(_digests: I) -> Option + where + I: 'a + IntoIterator, + { + Some(AccountId32::new([1u8; 32])) + } + } + + parameter_types! { + 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 TreasuryPid: PalletId = PalletId(*b"og/gvtry"); + pub const SubsidyPid: PalletId = PalletId(*b"og/subsy"); + } + + impl crate::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type FindAuthor = MockFindAuthor; + type BlockReward = BlockRewardCfg; + type AuthorShareBps = AuthorShareBpsCfg; + type TreasuryShareBps = TreasuryShareBpsCfg; + type TreasuryPalletId = TreasuryPid; + type SubsidyPalletId = SubsidyPid; + } + + fn new_test_ext() -> sp_io::TestExternalities { + let storage = frame_system::GenesisConfig::::default() + .build_storage() + .expect("genesis builds"); + let mut ext: sp_io::TestExternalities = storage.into(); + ext.execute_with(|| System::set_block_number(1)); + ext + } + + fn author() -> AccountId32 { + AccountId32::new([1u8; 32]) + } + + #[test] + fn block_reward_minted_and_split_correctly() { + new_test_ext().execute_with(|| { + let treasury = Emission::treasury_account(); + let subsidy = Emission::subsidy_account(); + + // Pre: autor/treasury/subsidy en 0 (no genesis-funded). + assert_eq!(>::balance(&author()), 0); + assert_eq!(Emission::total_emitted(), 0); + + // Un bloque: mintea 1000 → 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); + }); + } + + #[test] + fn emission_accumulates_over_blocks() { + new_test_ext().execute_with(|| { + >::on_initialize(1); + >::on_initialize(2); + >::on_initialize(3); + + // 3 bloques × 1000 = 3000 emitido; autor acumula 3 × 700 = 2100. + assert_eq!(Emission::total_emitted(), 3_000); + assert_eq!(>::balance(&author()), 2_100); + }); + } + + #[test] + fn split_no_pierde_por_redondeo() { + new_test_ext().execute_with(|| { + // author + treasury + subsidy == reward exacto (el residual al subsidy). + >::on_initialize(1); + let treasury = Emission::treasury_account(); + let subsidy = Emission::subsidy_account(); + let total = >::balance(&author()) + + >::balance(&treasury) + + >::balance(&subsidy); + assert_eq!(total, 1_000, "el reparto no pierde unidades"); + }); + } +} diff --git a/crates/origin-chain-runtime/Cargo.toml b/crates/origin-chain-runtime/Cargo.toml index 619457b..248fead 100644 --- a/crates/origin-chain-runtime/Cargo.toml +++ b/crates/origin-chain-runtime/Cargo.toml @@ -66,6 +66,10 @@ getrandom_03 = { workspace = true } # (CHN-4 Foundation Treasury Governance, AI-Driven Human-Guarded canonical) origin-chain-governance.workspace = true +# FIX-6 Stage 1: origin-chain-emission wired Index 13 — paga a los validadores +# Aura (mintea block reward + split treasury/subsidy). Cierra V2-CA2. +origin-chain-emission.workspace = true + # Phase 3 Slice 3.1 v0.0.39: pallet-revive wired runtime composition Index 11 # (SBS-5 ratificado supersedes CHN-6 v1.0 → PolkaVM RISC-V + ink! v6 + Solidity). # Smart contracts deployable canonical post Slice 3.1 SCAFFOLD COMPLETE (v0.0.41). @@ -131,6 +135,7 @@ std = [ "origin-chain-fees/std", "origin-chain-agent-identity/std", "origin-chain-governance/std", + "origin-chain-emission/std", "pallet-revive/std", "sp-offchain/std", "sp-genesis-builder/std", diff --git a/crates/origin-chain-runtime/src/configs.rs b/crates/origin-chain-runtime/src/configs.rs index 0012a8d..fb18120 100644 --- a/crates/origin-chain-runtime/src/configs.rs +++ b/crates/origin-chain-runtime/src/configs.rs @@ -416,6 +416,44 @@ impl origin_chain_governance::Config for Runtime { type TreasuryPalletId = GovernanceTreasuryPalletId; } +// ============================================================ +// 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. +// +// 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). +// Stage 2 introduce pallet_staking NPoS sobre este substrate. +parameter_types! { + /// Recompensa por bloque: 10 OGC (~5% inflación anual). + 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 subsidy pool (proof-of-equity, #16 anti-clasista). + pub const EmissionSubsidyPalletId: frame_support::PalletId = + frame_support::PalletId(*b"og/subsy"); +} + +impl origin_chain_emission::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = crate::Balances; + /// Aura → validador account vía pallet_session (FindAccountFromAuthorIndex). + type FindAuthor = pallet_session::FindAccountFromAuthorIndex; + type BlockReward = EmissionBlockReward; + type AuthorShareBps = EmissionAuthorShareBps; + type TreasuryShareBps = EmissionTreasuryShareBps; + // Reusa el MISMO vault de governance (og/gvtry) para el treasury. + type TreasuryPalletId = GovernanceTreasuryPalletId; + type SubsidyPalletId = EmissionSubsidyPalletId; +} + // ============================================================ // pallet_revive::Config — Phase 3 Slice 3.1 v0.0.39 wired // ============================================================ diff --git a/crates/origin-chain-runtime/src/lib.rs b/crates/origin-chain-runtime/src/lib.rs index 3307084..62ee42a 100644 --- a/crates/origin-chain-runtime/src/lib.rs +++ b/crates/origin-chain-runtime/src/lib.rs @@ -376,6 +376,14 @@ mod runtime { #[runtime::pallet_index(12)] pub type Sudo = pallet_sudo::Pallet; + /// Emission pallet (FIX-6 Stage 1 per audit V2-CA2). En cada bloque mintea un + /// block reward y lo reparte: autor Aura (validador) + treasury (og/gvtry) + + /// subsidy pool (og/subsy, proof-of-equity #16). Cierra "los validadores + /// ganan 0 OGC". sub_mult ortogonal (David 2026-06-10): el subsidy financia + /// el subsidio de hardware sin importar la suscripción. + #[runtime::pallet_index(13)] + pub type Emission = origin_chain_emission::Pallet; + // Substrate pallet-identity DEFERRED canonical (decision empírica // 2026-05-21 documentada en configs.rs). pallet-identity 41.0.0 ReservableCurrency // compat work canonical hereda al custom `pallet_agent_identity` Phase 2.7 @@ -951,6 +959,14 @@ mod tests { assert_eq!(::name(), "Sudo"); } + #[test] + fn emission_pallet_index_13_canonical() { + // FIX-6 Stage 1: Emission=13 (post Sudo=12). Paga validadores Aura (V2-CA2). + use frame_support::traits::PalletInfoAccess; + assert_eq!(::index(), 13); + assert_eq!(::name(), "Emission"); + } + #[test] fn revive_address_mapper_account32_returns_h160_canonical() { // AccountId32Mapper canonical: AccountId32 → H160 mapping deterministic.