🛡
CULTIVA IA · Seguridad Blockchain
Auditoría — ChainMint Protocol
Informe de Escaneo
Substrate / FRAME
Análisis de vulnerabilidades en pallets de Polkadot parachain · 7 patrones críticos evaluados
REF: CULTIVA-SEC-2026-0047
16 Junio 2026 · v1.0 Final
⚠ Confidencial
4
Críticas
2
Altas
1
Medias
2
Pallets Escaneados
7
Patrones Evaluados
1
Resumen de Riesgo Global
9.2

Puntuación de Riesgo

⚠ NO LISTO PARA MAINNET

Se detectaron 4 vulnerabilidades críticas que pueden resultar en pérdida de fondos, crash del nodo o acceso no autorizado al tesoro. El lanzamiento debe postponerse hasta corrección completa.

Alcance del Escaneo

pallets/staking_rewards/src/lib.rs
Pallet de Staking — 847 líneas · FRAME v2
pallets/treasury_governance/src/lib.rs
Pallet de Gobernanza — 612 líneas · FRAME v2
Substrate v0.9.20
⚠ Pre-transaccional — sin #[transactional] automático
ID Vulnerabilidad Pallet Patrón Severidad Estado
CMT-001 Overflow aritmético en distribución de rewards staking_rewards Arithmetic Overflow Crítica Pendiente
CMT-002 Panic en retirada con unwrap() sin guard staking_rewards Don't Panic · DoS Crítica Pendiente
CMT-003 Escritura en storage antes de validación de origen treasury_governance Verify First, Write Last Crítica Pendiente
CMT-004 ensure_signed en función de ejecución del tesoro treasury_governance Bad Origin Crítica Pendiente
CMT-005 Pesos fijos para operación O(n) de distribución staking_rewards Weights and Fees Alta Pendiente
CMT-006 Validación de transacción unsigned insuficiente treasury_governance Unsigned Validation Alta Pendiente
CMT-007 RandomnessCollectiveFlip para sorteo de validadores staking_rewards Bad Randomness Media Pendiente
2
Hallazgos Detallados
CMT-001
Crítica
Desbordamiento Aritmético en Cálculo de Rewards
staking_rewards/lib.rs:L214

La función distribute_rewards() utiliza operadores aritméticos nativos de Rust (+, *) que en modo release hacen wrapping, no panican. Con suficientes stakers y una época de alta recompensa, el acumulador puede desbordarse y distribuir cantidades incorrectas, acuñando tokens de la nada.

Impacto
Inflación arbitraria
Explotabilidad
Media (requiere coordinación)
Afecta
Balance de todos los stakers
⚠ Código vulnerable (staking_rewards/lib.rs:L214-L218)
// ❌ VULNERABLE: operadores nativos hacen wrapping en release mode let total_reward = staker_count * reward_per_block; // puede overflow let accumulated = total_reward + previous_epoch_rewards; // puede overflow let per_staker = accumulated / staker_count; T::Currency::deposit_creating(&staker, per_staker.into());
✓ Corrección recomendada
// ✅ SEGURO: usar checked_* y retornar error si overflow let total_reward = staker_count .checked_mul(reward_per_block) .ok_or(Error::<T>::ArithmeticOverflow)?; let accumulated = total_reward .checked_add(previous_epoch_rewards) .ok_or(Error::<T>::ArithmeticOverflow)?; let per_staker = accumulated.checked_div(staker_count).ok_or(Error::<T>::DivisionByZero)?;
CMT-002
Crítica
Panic DoS — unwrap() sin guard en retirada de stake
staking_rewards/lib.rs:L341

La función withdraw_stake() llama a .unwrap() sobre el lookup en storage. Si un atacante elimina la entrada del staker entre bloques (condición de carrera en edge case de migración), el nodo hace panic y deja de procesar bloques — DoS completo.

⚠ Código vulnerable (staking_rewards/lib.rs:L341)
pub fn withdraw_stake(origin: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult { let who = ensure_signed(origin)?; let stake_info = StakerInfo::<T>::get(&who).unwrap(); // ❌ PANIC si None ensure!(stake_info.amount >= amount, Error::<T>::InsufficientStake); // ... }
✓ Corrección recomendada
let stake_info = StakerInfo::<T>::get(&who) .ok_or(Error::<T>::StakerNotFound)?; // ✅ Error controlado
CMT-003
Crítica
Write-Before-Verify en Propuesta de Tesoro
treasury_governance/lib.rs:L189

En Substrate v0.9.20 (pre-transaccional), las escrituras a storage persisten aunque la transacción falle después. La función submit_proposal() escribe el estado antes de validar el depósito del proponente, dejando propuestas "fantasma" que pueden ser ejecutadas sin fondos reales.

⚠ Flujo vulnerable
// ❌ VULNERABLE en Substrate v0.9.20: storage escrito antes de validar Proposals::<T>::insert(proposal_id, &proposal); // WRITE ProposalCount::<T>::mutate(|n| *n += 1); // WRITE T::Currency::reserve(&who, deposit)?; // VALIDATE (late!) // Si reserve falla, las escrituras YA persisten
✓ Corrección recomendada
// ✅ SEGURO: Validar TODO antes de escribir T::Currency::reserve(&who, deposit)?; // VALIDATE first ensure!(amount > T::MinProposalAmount::get(), Error::<T>::BelowMinimum); Proposals::<T>::insert(proposal_id, &proposal); // WRITE (safe) ProposalCount::<T>::mutate(|n| *n += 1); Self::deposit_event(Event::ProposalSubmitted { id: proposal_id }); // EVENT last
CMT-004
Crítica
Origen Incorrecto — cualquier usuario puede ejecutar el tesoro
treasury_governance/lib.rs:L298

La función execute_spending() que transfiere fondos del tesoro solo verifica ensure_signed — cualquier cuenta puede llamarla con cualquier propuesta aprobada. No existe verificación de quórum de votos ni origen privilegiado.

⚠ Código vulnerable
pub fn execute_spending(origin: OriginFor<T>, proposal_id: ProposalIndex) -> DispatchResult { let _who = ensure_signed(origin)?; // ❌ cualquier usuario puede ejecutar! let proposal = Proposals::<T>::get(proposal_id).ok_or(Error::<T>::InvalidProposal)?; T::Currency::transfer(&TreasuryAccount::get(), &proposal.beneficiary, proposal.value, ...)?; }
✓ Corrección recomendada
// ✅ SEGURO: usar origen privilegiado o verificar quórum pub fn execute_spending(origin: OriginFor<T>, proposal_id: ProposalIndex) -> DispatchResult { T::SpendOrigin::ensure_origin(origin)?; // ✅ ForceOrigin configurado en runtime let proposal = Proposals::<T>::get(proposal_id).ok_or(Error::<T>::InvalidProposal)?; ensure!(proposal.approved_votes >= T::QuorumThreshold::get(), Error::<T>::QuorumNotMet); T::Currency::transfer(&TreasuryAccount::get(), &proposal.beneficiary, proposal.value, ...)?; }
CMT-005
Alta
Pesos Fijos para Distribución O(n) de Rewards
staking_rewards/lib.rs:L201

La función distribute_rewards() itera sobre todos los stakers (O(n)) pero declara un peso fijo de 10_000. Con miles de stakers, el bloque puede superar su tiempo límite y el peso no cubre el coste real, permitiendo spam gratuito de distribuciones.

⚠ Código vulnerable
#[pallet::weight(10_000)] // ❌ peso fijo independiente de n stakers pub fn distribute_rewards(origin: OriginFor<T>) -> DispatchResult { for staker in Stakers::<T>::iter() { // O(n) — n puede ser muy grande // ... } }
✓ Corrección recomendada — acotar el input
// ✅ Procesar en lotes con límite y peso proporcional #[pallet::weight(T::WeightInfo::distribute_rewards(T::MaxStakersPerBatch::get()))] pub fn distribute_rewards_batch(origin: OriginFor<T>, max_count: u32) -> DispatchResultWithPostInfo { ensure!(max_count <= T::MaxStakersPerBatch::get(), Error::<T>::BatchTooLarge); // procesar solo max_count stakers por llamada }
3
Checklist de Auditoría — Estado Actual

🔴 Aritmética (CRÍTICA)

Sin + − × ÷ nativos en dispatchables
Aritmética usa checked_* o saturating_*
Conversiones usan try_into() con manejo de error

🔴 Prevención de Panics (CRÍTICA)

Sin unwrap() ni expect() en dispatchables
Sin indexación de arrays/slices sin bounds check
Inputs de usuario validados con ensure!

🟠 Pesos y DoS (ALTA)

Pesos proporcionales al coste computacional
Parámetros de input tienen límites máximos
Framework de benchmarking configurado

🔴 Control de Acceso (CRÍTICA)

Operaciones privilegiadas usan ensure_root o custom origins
ensure_signed solo para operaciones de usuario
!
Pallet sudo eliminado antes de producción

🟠 Seguridad de Storage (ALTA)

Usando Substrate v0.9.25+ o #[transactional] manual
Validación antes de escrituras en storage
Eventos emitidos tras operaciones exitosas

🟡 Otros Patrones (MEDIA)

BABE randomness (no RandomnessCollectiveFlip)
Unsigned transactions con replay protection
Randomness usa random(subject) no random_seed()
4
Plan de Remediación — 3 Semanas Pre-Launch
Semana 1
Fixes Críticos
Corregir CMT-001 (overflow), CMT-002 (panic), CMT-004 (bad origin). Migrar a checked_* y ensure_root.
CMT-001, 002, 004
Semana 1–2
Storage Safety
Corregir CMT-003. Reordenar validate→write→event. Actualizar a Substrate v0.9.25 o añadir #[transactional].
CMT-003
Semana 2
Pesos y Unsigned
Corregir CMT-005 (pesos variables, procesado en lotes). CMT-006: añadir replay protection en validate_unsigned.
CMT-005, 006
Semana 3
Randomness + Re-auditoría
Migrar a BABE randomness (CMT-007). Re-test completo con fuzz + benchmarks. Verificación final CULTIVA IA.
CMT-007 + Go/No-Go