fix(emission): integrity_test guard — shares autor+treasury ≤ 100% (audit ultra)

Hallazgo MEDIUM del audit ultra: si un cambio de config hiciera AuthorShareBps +
TreasuryShareBps > 10_000 bps, el `subsidy = reward.saturating_sub(author).
saturating_sub(treasury)` daría subsidy = 0 SILENCIOSAMENTE (el pool #16 se
quedaría sin fondos sin ruido). El hook `integrity_test()` lo convierte en un
fallo ruidoso del integrity-check del runtime. +test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
davidherranandClaude Fable 5 committed 2026-07-03 12:04:03 -05:00
1 parent 1fdc47e859
commit d6dd08da66
1 file changed
+24 -2
+24 -2
View File
@@ -180,9 +180,23 @@ pub mod pallet {
#[pallet::hooks] #[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
/// Guard de integridad (audit ultra 2026-07-02): las shares autor +
/// treasury JAMÁS pueden exceder 100% — el residual es el subsidy pool
/// (`subsidy = reward − author − treasury`). Si un futuro cambio de
/// config las hiciera sumar > 10_000 bps, el `saturating_sub` daría
/// subsidy = 0 SILENCIOSAMENTE. Esto lo convierte en un fallo ruidoso
/// en el build/integrity-check del runtime.
fn integrity_test() {
assert!(
T::AuthorShareBps::get().saturating_add(T::TreasuryShareBps::get()) <= 10_000,
"emisión: AuthorShareBps + TreasuryShareBps no pueden exceder 100% \
(el residual es el subsidy pool #16)"
);
}
/// Cada bloque: identifica al autor Aura (desde los pre-runtime digests), /// 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 /// libera el BlockReward del pool Ecosystem y lo reparte. Si no hay autor
/// inherentes sin pre-digest), no mintea. /// (e.g. bloque de inherentes sin pre-digest), no libera.
fn on_initialize(_n: BlockNumberFor<T>) -> Weight { fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
// Kill-switch (governance): si la emisión está pausada, no libera nada. // Kill-switch (governance): si la emisión está pausada, no libera nada.
// Lee 1 storage value; default `false` ⇒ activa (cero regresión). // Lee 1 storage value; default `false` ⇒ activa (cero regresión).
@@ -496,6 +510,14 @@ mod tests {
}); });
} }
#[test]
fn integrity_test_shares_no_exceden_100pct() {
new_test_ext().execute_with(|| {
// Mock: 7000 + 2000 = 9000 ≤ 10000 ⇒ pasa (el residual 1000 = subsidy).
<Emission as Hooks<u64>>::integrity_test();
});
}
#[test] #[test]
fn depletion_event_se_anuncia_una_sola_vez() { fn depletion_event_se_anuncia_una_sola_vez() {
new_test_ext_with_pool(500).execute_with(|| { new_test_ext_with_pool(500).execute_with(|| {