feat(economía): techo duro 1B + fees con dientes (David Level A 2026-07-03, RV 6→7)
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
7b8863fd47
commit
1fdc47e859
4 files changed
+282
-65
No files matched your search
@@ -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<Event<Self>>
|
||||
+ IsType<<Self as frame_system::Config>::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<Self::AccountId, Balance = u128>;
|
||||
|
||||
/// Cómo encontrar el autor del bloque. En el runtime se cablea con la
|
||||
/// FindAuthor de Aura (pallet_session::FindAccountFromAuthorIndex).
|
||||
type FindAuthor: FindAuthor<Self::AccountId>;
|
||||
|
||||
/// 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<u128>;
|
||||
|
||||
/// 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<PalletId>;
|
||||
|
||||
/// Share del autor en basis points (10_000 = 100%). E.g. 7_000 = 70%.
|
||||
#[pallet::constant]
|
||||
type AuthorShareBps: Get<u16>;
|
||||
@@ -94,11 +114,21 @@ pub mod pallet {
|
||||
#[pallet::storage_version(STORAGE_VERSION)]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
/// 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<T> = 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<T> = 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<T: Config> {
|
||||
/// 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<T>) -> 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::<T>::get() {
|
||||
return T::DbWeight::get().reads(1);
|
||||
@@ -153,7 +192,16 @@ pub mod pallet {
|
||||
let digest = <frame_system::Pallet<T>>::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::<T>::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::<T>::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::<T>::get() {
|
||||
DepletionAnnounced::<T>::put(true);
|
||||
Self::deposit_event(Event::EcosystemPoolDepleted {
|
||||
total_released: TotalEmitted::<T>::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<T: Config> Pallet<T> {
|
||||
/// 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<Test>;
|
||||
@@ -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::<Test>::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::<Test>::default()
|
||||
.build_storage()
|
||||
.expect("genesis builds");
|
||||
pallet_balances::GenesisConfig::<Test> {
|
||||
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!(<Balances as Inspect<_>>::balance(&pool), 1_000_000);
|
||||
assert_eq!(<Balances as Inspect<_>>::balance(&author()), 0);
|
||||
assert_eq!(Emission::total_emitted(), 0);
|
||||
let issuance_before = <Balances as Inspect<_>>::total_issuance();
|
||||
|
||||
// Un bloque: mintea 1000 → autor 700, treasury 200, subsidy 100.
|
||||
// Un bloque: libera 1000 del pool → autor 700, treasury 200, subsidy 100.
|
||||
<Emission as Hooks<u64>>::on_initialize(1);
|
||||
|
||||
assert_eq!(<Balances as Inspect<_>>::balance(&author()), 700);
|
||||
assert_eq!(<Balances as Inspect<_>>::balance(&treasury), 200);
|
||||
assert_eq!(<Balances as Inspect<_>>::balance(&subsidy), 100);
|
||||
assert_eq!(Emission::total_emitted(), 1000);
|
||||
// TECHO DURO: el pool bajó 1000; el total issuance NO cambió (transfer).
|
||||
assert_eq!(<Balances as Inspect<_>>::balance(&pool), 999_000);
|
||||
assert_eq!(
|
||||
<Balances as Inspect<_>>::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();
|
||||
<Emission as Hooks<u64>>::on_initialize(1);
|
||||
<Emission as Hooks<u64>>::on_initialize(2);
|
||||
<Emission as Hooks<u64>>::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!(<Balances as Inspect<_>>::balance(&author()), 2_100);
|
||||
assert_eq!(<Balances as Inspect<_>>::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 = <Balances as Inspect<_>>::total_issuance();
|
||||
|
||||
<Emission as Hooks<u64>>::on_initialize(1);
|
||||
assert_eq!(<Balances as Inspect<_>>::balance(&pool), 1_500);
|
||||
<Emission as Hooks<u64>>::on_initialize(2);
|
||||
assert_eq!(<Balances as Inspect<_>>::balance(&pool), 500);
|
||||
assert!(!Emission::depletion_announced());
|
||||
|
||||
// Bloque 3: libera el remanente parcial (500) y anuncia agotamiento.
|
||||
<Emission as Hooks<u64>>::on_initialize(3);
|
||||
assert_eq!(<Balances as Inspect<_>>::balance(&pool), 0);
|
||||
assert!(Emission::depletion_announced());
|
||||
|
||||
// Bloque 4+: pool seco, cero liberación adicional.
|
||||
<Emission as Hooks<u64>>::on_initialize(4);
|
||||
assert_eq!(<Balances as Inspect<_>>::balance(&pool), 0);
|
||||
|
||||
// TECHO DURO: total liberado == pool inicial, NUNCA más; issuance intacto.
|
||||
assert_eq!(Emission::total_emitted(), 2_500);
|
||||
assert_eq!(<Balances as Inspect<_>>::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.
|
||||
<Emission as Hooks<u64>>::on_initialize(1);
|
||||
assert!(Emission::depletion_announced());
|
||||
let emitted = Emission::total_emitted();
|
||||
// Bloques siguientes: flag ya en true ⇒ no re-anuncia, no re-libera.
|
||||
<Emission as Hooks<u64>>::on_initialize(2);
|
||||
<Emission as Hooks<u64>>::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]
|
||||
|
||||
Reference in new issue
Block a user