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;
}
// ============================================================