feat(fees): FIX-6 Stage 2a — fee unificado (cierra V2-CA3)

Pone la fórmula OCAFB-5 en el fee-path VIVO: OriginFeeAdapter
(OnChargeTransaction custom) reemplaza al FungibleAdapter tier-blind.
El firmante paga su fracción según (HardwareTier × ChainAction ×
SubscriptionTier, tiers leídos ON-CHAIN de TierProfiles — jamás
caller-supplied), y el subsidy pool og/subsy (alimentado por la
emisión Stage 1) cubre la fracción subsidiada. Inmutable #16 en cada
transacción. OU=() = burn 100% gas fees (matriz CHN-2).

unified_fee (u128, outcome tipado FeeOutcome — sin el sentinel
u64::MAX in-band): user = fórmula vendor Mathi byte-fiel (test de
paridad grid contra ce4fafb); pool = net × subsidy_bp/1e4 sobre el
COSTO BASE (anti-regresivo, sub_mult ORTOGONAL: un Ultra NO drena el
pool más que un Free — el premium lo paga el usuario, no el commons).

Hallazgos de la verificación adversarial (workflow ultracode, 4
lentes) cerrados de raíz:
- HIGH: clasificador cubre eth_instantiate_with_code (call_index 10)
  — evadía el entitlement-gate DeployContract vía fallback Write.
- MEDIUM ledger envenenable: removido el dispatchable legacy
  compute_fee_for_action (tiers caller-supplied, escribía AccountFees
  sin cobrar). AccountFees ahora escrito SOLO por el fee-path vivo
  (note_settlement). Eventos que nunca se emitían eliminados; el
  preview read-only vive en query_fee / FeesApi.
- MEDIUM drenaje del pool por sub_mult: pool sobre net, no sobre el
  precio premium (Opción B anti-regresiva).
- Dev UX: GenesisConfig siembra perfiles iniciales; dev/local
  arrancan Ultra (los Slices 3.x de contracts no se bloquean por
  Free+DeployContract); mainnet vacío (Default conservador hasta
  attestation).

Decisión de diseño (autoridad Pilar 4): replay de attestation =
RECHAZO explícito, no dedup silencioso (notificado a Ergon para el
reference impl). Orden OCAFB-5 subsidy-antes-de-entitlement preservado
byte-fiel + notificado a Mathi para el ratify 2026-06-21.

set_tier_profile Root-only interim (sunset a attestation-driven vía
Mathi PerceptionAdapter + DID binding FIX-3/FIX-4). spec_version 1→2
(formato de extrinsic intacto, TRANSACTION_VERSION=1).

Tests: 49 crate fees (paridad grid + lobo blanco ortogonalidad +
refund + pool seco + tip passthrough + entitlement blocked) + 21
suites workspace verde. Toolchain cargo +1.88.

Nota WASM: el build local del runtime WASM falla en la
post-validación del substrate-wasm-builder con UnknownOpcode(252)
— límite del toolchain (Rust 1.88 emite opcodes 0xFC que el parser
del builder de stable2509 no deserializa). Reproducido idéntico en
el HEAD intacto (v0.0.57): es ambiental, no de este cambio. El node
real + WASM artifact siguen VPS-gated (toolchain alineado /
wasm32v1-none cuando el wasm-builder lo soporte).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
davidherranandClaude Fable 5 committed 2026-06-10 20:22:14 -05:00
1 parent e6a803b54f
commit 3364994cce
10 files changed
+1435 -417

No files matched your search

+54 -4
View File
@@ -159,13 +159,59 @@ parameter_types! {
pub const OperationalFeeMultiplier: u8 = 5;
}
/// FIX-6 Stage 2a: clasificador call→ChainAction del fee unificado (taxonomía
/// OCAFB-2). Vive aquí porque el runtime es el único sitio donde `RuntimeCall`
/// es concreto. Fallback conservador: `Write` (fee estándar — un wildcard
/// jamás regala subsidio; mismo principio que la alerta TLB de Ergon).
pub struct OriginCallClassifier;
impl origin_chain_fees::ClassifyChainAction<crate::RuntimeCall> for OriginCallClassifier {
fn classify(call: &crate::RuntimeCall) -> origin_chain_fees::ChainAction {
use origin_chain_fees::ChainAction;
match call {
crate::RuntimeCall::Balances(
pallet_balances::Call::transfer_allow_death { .. }
| pallet_balances::Call::transfer_keep_alive { .. }
| pallet_balances::Call::transfer_all { .. },
) => ChainAction::Transfer,
crate::RuntimeCall::Revive(
pallet_revive::Call::instantiate { .. }
| pallet_revive::Call::instantiate_with_code { .. }
// call_index 10: el deploy vía la capa EVM (Code::Upload). Sin
// este arm caería en el fallback Write y evadiría el
// entitlement-gate DeployContract (HIGH del audit Stage 2a).
| pallet_revive::Call::eth_instantiate_with_code { .. },
) => ChainAction::DeployContract,
crate::RuntimeCall::Revive(
pallet_revive::Call::call { .. }
| pallet_revive::Call::eth_call { .. }
// eth_transact: envoltorio EVM genérico (puede ser call/deploy/
// transfer). Lo tratamos como ejecución de contrato — su
// entitlement (Free=false) y subsidio coinciden con CallContract;
// refinar cuando la capa EVM se cablee plenamente.
| pallet_revive::Call::eth_transact { .. },
) => ChainAction::CallContract,
crate::RuntimeCall::Revive(pallet_revive::Call::upload_code { .. }) => {
ChainAction::UploadContractCode
}
_ => ChainAction::Write,
}
}
}
impl pallet_transaction_payment::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
/// On-charge transaction canonical: usa `pallet_transaction_payment` default
/// charge mechanism. Phase 2.4 lo refactor canonical para integrar
/// `OriginFees` trait (CHN-8 multi-tier fee matrix).
/// FIX-6 Stage 2a (cierra V2-CA3): `OriginFeeAdapter` reemplaza al
/// `FungibleAdapter` default — la fórmula OCAFB-5 entra al fee-path VIVO:
/// el firmante paga su fracción según (HardwareTier × ChainAction ×
/// SubscriptionTier, tiers leídos ON-CHAIN de `TierProfiles`, jamás
/// caller-supplied), y el subsidy pool `og/subsy` (alimentado por la
/// emisión Index 13) cubre la fracción subsidiada — Inmutable #16 en cada
/// transacción. `OU = ()` deliberado: burn matrix CHN-2 ratificada = gas
/// fees 100% burn (el burn total = fee constitucional completo, deflación
/// que contrapesa la emisión Stage 1).
type OnChargeTransaction =
pallet_transaction_payment::FungibleAdapter<crate::Balances, ()>;
origin_chain_fees::OriginFeeAdapter<crate::Balances, (), OriginCallClassifier>;
type OperationalFeeMultiplier = OperationalFeeMultiplier;
type WeightToFee = frame_support::weights::IdentityFee<Balance>;
type LengthToFee = frame_support::weights::IdentityFee<Balance>;
@@ -368,6 +414,10 @@ impl origin_chain_provenance::Config for Runtime {
impl origin_chain_fees::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
/// FIX-6 Stage 2a: la MISMA constante que alimenta la emisión (Index 13)
/// — pool-alimentación y pool-consumo apuntan a la misma cuenta og/subsy
/// (jamás re-declarar el literal: un typo divergiría las cuentas).
type SubsidyPalletId = EmissionSubsidyPalletId;
}
// ============================================================
@@ -45,6 +45,7 @@ use crate::{
};
use alloc::vec;
use alloc::vec::Vec;
use origin_chain_types::{HardwareTier, SubscriptionTier};
use sp_genesis_builder::PresetId;
// ============================================================
@@ -200,10 +201,14 @@ fn build_genesis(
endowed_accounts: Vec<(ChainAccountId, Balance)>,
validators: Vec<(ChainAccountId, AuraId, GrandpaId)>,
sudo_key: ChainAccountId,
// FIX-6 Stage 2a: perfiles de fee a sembrar en block 0. En dev/local las
// cuentas de prueba arrancan en un tier que puede desplegar contratos (Free
// está gated de DeployContract por Inmutable #37); en mainnet va vacío.
initial_fee_profiles: Vec<(ChainAccountId, HardwareTier, SubscriptionTier)>,
) -> RuntimeGenesisConfig {
use crate::{
AuraConfig, BalancesConfig, GrandpaConfig, ReviveConfig, SessionConfig, SudoConfig,
SystemConfig, TransactionPaymentConfig,
AuraConfig, BalancesConfig, FeesConfig, GrandpaConfig, ReviveConfig, SessionConfig,
SudoConfig, SystemConfig, TransactionPaymentConfig,
};
RuntimeGenesisConfig {
system: SystemConfig::default(),
@@ -212,6 +217,10 @@ fn build_genesis(
dev_accounts: None,
},
transaction_payment: TransactionPaymentConfig::default(),
// FIX-6 Stage 2a: perfiles de tier sembrados (el fee-path vivo los lee).
fees: FeesConfig {
initial_profiles: initial_fee_profiles,
},
aura: AuraConfig {
authorities: validators.iter().map(|(_, aura, _)| aura.clone()).collect(),
},
@@ -257,7 +266,11 @@ pub fn development_config_genesis() -> RuntimeGenesisConfig {
build_genesis(
vec![(alice_account.clone(), 100_000_000_u128 * Ogc::ONE)],
vec![alice_authority],
alice_account, // FIX-5: Alice = sudo key (Root) en dev
alice_account.clone(), // FIX-5: Alice = sudo key (Root) en dev
// FIX-6 Stage 2a: Alice arranca Ultra para poder desplegar contratos
// out-of-the-box en --dev (los Slices 3.x de contract testing no se
// bloquean por el entitlement-gate Free+DeployContract).
vec![(alice_account, HardwareTier::Ultra, SubscriptionTier::Ultra)],
)
}
@@ -279,13 +292,22 @@ pub fn local_testnet_config_genesis() -> RuntimeGenesisConfig {
build_genesis(
vec![
(alice.clone(), endowment),
(bob, endowment),
(charlie, endowment),
(dave, endowment),
(eve, endowment),
(bob.clone(), endowment),
(charlie.clone(), endowment),
(dave.clone(), endowment),
(eve.clone(), endowment),
],
vec![alice_authority, bob_authority],
alice, // FIX-5: Alice = sudo key (Root) en local_testnet
alice.clone(), // FIX-5: Alice = sudo key (Root) en local_testnet
// FIX-6 Stage 2a: las 5 cuentas dev arrancan Ultra para testing
// completo (contratos + compute) sin upgrade manual.
vec![
(alice, HardwareTier::Ultra, SubscriptionTier::Ultra),
(bob, HardwareTier::Ultra, SubscriptionTier::Ultra),
(charlie, HardwareTier::Ultra, SubscriptionTier::Ultra),
(dave, HardwareTier::Ultra, SubscriptionTier::Ultra),
(eve, HardwareTier::Ultra, SubscriptionTier::Ultra),
],
)
}
@@ -335,6 +357,10 @@ pub fn mainnet_genesis_config_genesis() -> RuntimeGenesisConfig {
// (multisig humano). PRE-LAUNCH Q1 2027: David ratifica la key real +
// sunset a governance (pallet-collective/referenda).
foundation_treasury,
// FIX-6 Stage 2a: mainnet arranca SIN perfiles sembrados — todos en el
// Default conservador (Desktop+Free) hasta que la attestation real
// (Mathi PerceptionAdapter + DID binding) asigne tiers verificados.
vec![],
)
}
+9 -3
View File
@@ -222,7 +222,10 @@ pub const IMPL_NAME: &str = "origin-chain-charon";
/// Canonical runtime version canonical. Bumped cada forkless upgrade via
/// `set_code` extrinsic governance canonical.
pub const RUNTIME_VERSION: u32 = 1;
/// v2: FIX-6 Stage 2a — fee unificado (OriginFeeAdapter reemplaza al
/// FungibleAdapter: cambia el COMPORTAMIENTO del cobro de fees, no el formato
/// de extrinsic — TRANSACTION_VERSION intacto).
pub const RUNTIME_VERSION: u32 = 2;
/// Canonical authoring version per Substrate semver canonical.
pub const AUTHORING_VERSION: u32 = 1;
@@ -794,8 +797,11 @@ mod tests {
use super::*;
#[test]
fn runtime_version_starts_at_one_canonical() {
assert_eq!(RUNTIME_VERSION, 1);
fn runtime_version_v2_fee_unificado() {
// v2 = FIX-6 Stage 2a (fee unificado). El formato de extrinsic NO
// cambió: solo el associated type OnChargeTransaction.
assert_eq!(RUNTIME_VERSION, 2);
assert_eq!(TRANSACTION_VERSION, 1);
}
#[test]