La seguridad de los programas de Solana es seguridad de cuentas. Los llamadores eligen qué cuentas y programas entran en una instrucción; Sealevel aplica bloqueos, reglas de escritura de propiedad y presupuestos de cómputo, no el saldo de tu bóveda, la lista de administradores o la vinculación de la menta.
Esta página es el mapa conceptual para Seguridad y Auditoría de Programas: superficie de ataque de Sealevel, validación de cuentas, comprobaciones de firmante y propietario, ataques de PDA y semillas, reinicialización, aritmética, riesgo de CPI y reentrada, y un flujo de trabajo de auditoría práctico en Agave 4.1.1.
Cada instrucción es una interfaz adversarial: demuestra quién firmó, quién posee cada cuenta, qué PDA y tipo tocas, luego muta el estado con matemáticas comprobadas y orden de CPI seguro.
Insight: Los exploits rara vez necesitan una ruptura criptográfica novedosa. Sustituyen una bóveda incorrecta, omiten un firmante, reinicializan un administrador, envuelven un saldo o hacen una CPI a un programa malicioso que devuelve "éxito".
Conceptos Clave:validación de cuentas, comprobaciones de firmante/propietario, semillas y bump de PDA, guardas de reinicialización, aritmética comprobada, listas blancas de CPI, interacciones de efectos de comprobaciones (CEI), diseño de fallo cerrado, flujo de trabajo de auditoría.
Cuándo Usar Este Modelo: Diseñar nuevas instrucciones, revisar #[derive(Accounts)], prepararse para auditoría externa, triaje de hallazgos o enseñar el modelo de amenazas de Solana.
Limitaciones / Compensaciones: Las restricciones de Anchor capturan muchas clases pero no las reglas de negocio; UncheckedAccount y remaining_accounts reintroducen riesgo; las revisiones de seguridad no reemplazan el fuzzing o la revisión de diseño económico.
Sealevel ejecuta programas como ejecutables sin estado. El estado duradero vive en cuentas que la transacción lista por adelantado. Los clientes (o atacantes) suministran esas cuentas, indicadores de firmante y indicadores de escritura. El runtime verifica que los firmantes declarados realmente firmaron y que solo el programa propietario puede escribir datos de cuenta. No sabe que "esta TokenAccount debe coincidir con vault.mint" o "solo la autoridad puede retirar".
Esa brecha es la superficie de ataque:
El cliente / atacante suministra: program_id | datos de instrucción | AccountMeta[] (claves, is_signer, is_writable)El runtime garantiza: firmas coinciden con is_signer | bloqueos de is_writable | reglas de escritura del propietario | presupuesto de CUTu programa debe garantizar: identidad correcta firmada | propietarios y tipos correctos | PDAs correctos | montos correctos | CPIs seguras
Fallar cerrado es la postura predeterminada: si la validación está incompleta, rechaza con un error tipificado. Nunca asumas "clientes honestos" o "nuestro frontend solo crea buenas transacciones". Los frontends y los indexadores no forman parte del límite de confianza; las comprobaciones en cadena sí lo son.
Anchor 0.32.1 codifica comprobaciones comunes (Signer, Account<T>, semillas, has_one, Program<T>). Eso no es una revisión completa. Aparecen lagunas con AccountInfo, UncheckedAccount, remaining_accounts, init_if_needed sin protección y las restricciones de reglas de negocio no se pueden expresar.
Modelo de amenazas para un programa típico de bóveda o mercado:
Los atacantes pasan cuentas que parecen utilizables: mismo tamaño, propiedad de tu programa o deserializables en el tipo incorrecto. La validación significa más que "se deserializa".
Lista de verificación por rol de cuenta:
Propietario - programa esperado (Account<T> para tus tipos; require_keys_eq! para SPL Token, Sysvar, etc.).
Tipo / discriminador - disco de cuenta de Anchor o constante nativa para que Vault nunca sea Usuario.
Restricciones relacionales - la menta coincide con la bóveda, el propietario de la cuenta de token es el PDA de la bóveda, la configuración coincide con el mercado.
Mutabilidad - solo las cuentas que pretendes cambiar son mut.
PDA Canónico - seeds + bump almacenado para que la dirección se derive, no se elija libremente.
#[derive(Accounts)]pub struct Deposit<'info> { // `has_one = mint` requeriría un campo de cuenta `mint` en esta estructura; la // restricción explícita de menta a continuación cubre el enlace sin uno. #[account(mut, has_one = authority)] pub vault: Account<'info, Vault>, #[account( mut, constraint = user_ata.mint == vault.mint @ ErrorCode::MintMismatch, constraint = user_ata.owner == user.key() @ ErrorCode::WrongAtaOwner, )] pub user_ata: Account<'info, TokenAccount>, pub authority: Signer<'info>, pub user: Signer<'info>, pub token_program: Program<'info, Token>,}
UncheckedAccount y AccountInfo en bruto omiten las comprobaciones de propietario y disco hasta que las agregues. Trata cada campo de este tipo como un hallazgo hasta que se demuestre que está validado.
Firmante: cualquier ruta que mueva fondos, cambie la autoridad, cierre cuentas o actualice la configuración sensible necesita una firma criptográfica de la clave correcta (o un PDA firmado a través de invoke_signed con semillas controladas por el programa).
Propietario: solo el programa propietario puede reescribir datos. Leer una cuenta que tu programa no posee sin verificar el propietario es cómo entran la confusión de tipos y las mentas falsas. Para las cuentas de tokens, el propietario debe ser el programa Token o Token-2022 que pretendes; para tu estado, el propietario debe ser tu program_id.
Patrón crítico común: la bóveda almacena authority: Pubkey pero la instrucción nunca requiere authority como Signer o nunca la vincula con has_one.
Los PDAs son identidad de autoridad y dirección. Las semillas débiles o ambiguas permiten a un atacante presentar un PDA diferente que tu programa aún "acepta".
Reglas generales:
Incluye todas las dimensiones de identidad en las semillas (usuario, menta, ID de mercado, prefijo de rol).
Almacena el bump canónico al inicializar; reutiliza bump = account.bump más tarde (sin búsqueda ambigua de bump en rutas activas).
Firma las CPI solo con las mismas semillas utilizadas para crear el PDA.
No permitas que los usuarios suministren bytes de semilla arbitrarios sin separación de dominio (b"vault", etiquetas de versión).
La inicialización escribe discriminadores, autoridades y saldos. Volver a ejecutar la inicialización (o init_if_needed sin un indicador) puede restablecer el administrador al atacante o revivir una forma de cuenta cerrada.
Prefiere init de un solo uso para estado permanente. Si necesitas init_if_needed, requiere !is_initialized (o equivalente) antes de escribir campos de autoridad, y nunca reabras cuentas cerradas sin una ruta deliberada y auditada. Al cerrar, pon a cero los datos / disco y transfiere los lamports para que la cuenta no pueda tratarse como estado activo.
Las compilaciones de lanzamiento de BPF se envuelven en +, -, * simples. Las matemáticas de bóveda, la acuñación de acciones, la acumulación de tarifas y las fórmulas de AMM necesitan checked_* (u operaciones de saturación donde sea intencional) y a menudo intermedios u128 antes de volver a convertir a u64.
El redondeo debe favorecer al protocolo en las mentas y redenciones dirigidas al usuario para que el polvo no pueda ser explotado para obtener valor gratuito. La división por cero y los casos extremos de piscina vacía son errores de seguridad, no meras experiencias de usuario.
La invocación entre programas extiende la confianza a otro programa. Riesgos:
ID de programa incorrecto - "programa de token" impostor que no hace nada o roba.
Cuentas restantes no validadas - cuentas de devolución de llamada controladas por el atacante.
Estado después de CPI - el llamado no puede volver a entrar en tu programa (el runtime lo rechaza con ReentrancyNotAllowed), pero puede mutar cuentas que ya deserializaste, por lo que tu copia en memoria se vuelve obsoleta. Una instrucción de nivel superior posterior en la misma transacción también puede leer saldos que aún parecen pre-actualización.
Mitigaciones: lista blanca de llamados con Program<'info, T> o require_keys_eq!; actualiza el estado del programa antes de la CPI externa (CEI); evita las devoluciones de llamada no confiables a menos que diseñes comprobaciones explícitas de deuda al estilo de préstamos rápidos; pasa solo las cuentas que el llamado necesita.
vault.balance = vault.balance.checked_sub(amount).ok_or(ErrorCode::Overflow)?;// efectos primero, luego interaccióntoken::transfer(cpi_ctx, amount)?;
Solana no es idéntica a la reentrada de EVM, pero un orden de llamada incorrecto y objetivos de CPI no confiables aún pierden fondos.
El trabajo de seguridad es tanto un proceso como código:
Congela una etiqueta RC, bloquea dependencias (Anchor 0.32.1, pines de Agave/CLI), documenta el modelo de amenazas.
Mapea internamente cada instrucción a las clases del Catálogo de Ataques de Sealevel (firmante, propietario, sustitución, PDA, reinicio, matemáticas, CPI, cierre, oráculo).
Pruebas negativas en LiteSVM / pruebas de Anchor: firmante incorrecto, menta incorrecta, programa incorrecto, doble inicialización, cantidades de desbordamiento.
Fuzzing de análisis y rutas con muchas matemáticas cuando la complejidad lo justifique.
Compilación verificable para que los auditores coincidan el código fuente con el hash en cadena.
Auditoría externa + ciclo de corrección; diff de nuevo después de cada cambio en Accounts o aritmética.
Recompensa por errores y plan de actualización/reversión para riesgo residual.
Propietario + disco para Account<T>, muchas restricciones de PDA
Reglas de negocio, rutas no comprobadas
Predeterminado para todas las cuentas de estado
require_* manual
Restricciones relacionales y económicas
Fácil de omitir en nuevas ixs
Cada revisión de nuevo manejador
CEI + lista blanca de programas
Robos clásicos de CPI / programas impostores
Errores de diseño económico
Cualquier movimiento de token o devolución de llamada
Matemáticas comprobadas + pruebas de propiedades
Desbordamientos y casos extremos de matemáticas de acciones
Juegos de oráculos y MEV
Bóvedas, AMMs, préstamos
Auditoría externa + recompensa
Ojos frescos, clases de catálogo conocidas
Ataques de presupuesto infinito, futuras actualizaciones
Pre-lanzamiento y actualizaciones importantes
Oráculos son entradas no confiables: verifica el propietario de la fuente, la obsolescencia y la confianza antes de usar precios. Falla cerrado con datos incorrectos.
Extensiones de Token-2022 (hooks, delegados permanentes) cambian quién puede mover tokens. Fija el ID del programa de token; no asumas semántica clásica de SPL Token.
Cierre y alquiler: requiere el firmante y destino correctos; pon a cero los datos para que las cuentas no puedan revivir bajo comprobaciones débiles. Prefiere el close = de Anchor con restricciones.
Autoridad de actualización es la raíz. Multisig o gobernanza, compilaciones verificables y opcionalmente inmutabilidad después de la madurez son seguridad operativa.
Clientes (@solana/kit 7.0.0, billeteras) ayudan solo a usuarios honestos. Los programas defienden contra todos los demás.
En Agave 4.1.1 (CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1), ejercita conjuntos de cuentas adversarias en LiteSVM o validadores locales. Trata cada nueva instrucción como una nueva superficie de ataque.
"El runtime valida las reglas de mi bóveda." Valida firmas, bloqueos, propietarios para escrituras y CU. Las reglas del protocolo son solo tuyas.
"Anchor significa que somos seguros." Anchor codifica comprobaciones comunes; las cuentas no comprobadas, las restricciones incorrectas y los errores de matemáticas aún se envían.
"Si se deserializa, la cuenta está bien." La deserialización no es vinculación de menta, autoridad o identidad de PDA.
"Los PDAs no se pueden falsificar." Semillas incorrectas o incompletas producen un PDA válido diferente bajo tu programa. Las semillas definen la política de seguridad.
"init_if_needed siempre es seguro." Sin un indicador de inicialización (o equivalente), reinicializar restablece campos críticos.
"Rust previene el desbordamiento en cadena." El envoltorio de compilación de lanzamiento es real; usa matemáticas comprobadas para rutas de valor.
"La CPI a Token siempre es segura." Solo si el ID del programa es el programa Token/Token-2022 real y las cuentas coinciden con las restricciones de menta y propietario.
"Arreglaremos la seguridad en la auditoría." Las auditorías son muestreo. Diseña primero el fallo cerrado; las auditorías detectan errores residuales.
"Las cuentas de solo lectura no pueden hacernos daño." Las configuraciones falsas, los oráculos y las mentas a menudo son de solo lectura pero controlan completamente los resultados económicos.
"Cerrar una cuenta termina todo el riesgo." El cierre incompleto o el destino incorrecto crean problemas de robo de alquiler y reactivación.
¿Cuál es la superficie de ataque de Sealevel para un programa?
Todo lo que el llamador puede elegir: qué cuentas y programas aparecen, quién firma, los datos de la instrucción y los objetivos de CPI. El runtime no aplica tus invariantes de menta, autoridad o saldo. Ver Catálogo de Ataques de Sealevel.
¿Por qué las comprobaciones de firmante faltantes son tan comunes y graves?
Sin un Signer requerido (o ruta invoke_signed de PDA), cualquier clave de cuenta puede ser nombrada como "autoridad" mientras que solo el pagador de la tarifa firma realmente. Eso permite retiros no autorizados y toma de control de administrador. Ver Comprobaciones de Firmante y Propietario.
¿Qué es la sustitución de cuentas?
Pasar una bóveda, cuenta de token, menta o configuración diferente a la que pretende el protocolo, a menudo todavía propiedad de un programa plausible. Solución con restricciones relacionales (has_one, igualdad de menta, semillas). Ver Ataques de Validación de Cuentas.
¿Cómo funcionan los ataques de semillas de PDA?
Si las semillas omiten campos de identidad o los bumps no son canónicos, los atacantes derivan PDAs alternativos que tu instrucción aún acepta, suplantando bóvedas o escrows. Almacena bumps y fija listas completas de semillas. Ver Ataques de PDA y Semillas.
¿Cuándo es posible la reinicialización?
Cuando la inicialización puede ejecutarse nuevamente en estado activo: init_if_needed sin guardas, discriminadores faltantes o cuentas cerradas que se recrean bajo comprobaciones débiles. Ver Ataques de Reinicialización.
¿Solana necesita aritmética comprobada?
Sí, para rutas de valor. La aritmética simple puede envolverse en cadena; las fórmulas de acciones y swaps deben usar checked_* y un redondeo cuidadoso. Ver Aritmética y Desbordamiento.
¿Qué es CEI en un contexto de CPI de Solana?
Comprobaciones-efectos-interacciones: valida, actualiza el estado de tu programa, luego llama externamente. Limita las lecturas al estilo de reentrada de saldos obsoletos durante CPIs no confiables o recursivas. Ver Riesgos de CPI y Reentrada.
¿Cómo valido un programa de destino de CPI?
Usa Program<'info, Token> (o tu tipo conocido) o require_keys_eq! contra un ID constante. Nunca hagas CPI a una clave de programa proporcionada por el usuario sin una lista blanca explícita.
¿Son Token y Token-2022 intercambiables en términos de seguridad?
No. Diferentes IDs de programa y comportamientos de extensión (hooks, tarifas de transferencia, delegados) cambian quién puede mover fondos. Fija el programa y prueba las mentas habilitadas para extensiones.
¿Qué debe cubrir una pre-auditoría interna?
Cada instrucción mapeada a clases de catálogo, pruebas unitarias negativas, pines de dependencias, notas de compilación verificables y riesgos residuales conocidos. Ver Flujo de Trabajo de Auditoría.
¿Significa "fallar cerrado" rechazar todas las cuentas desconocidas?
Significa rechazar cuando falta o es inconsistente la prueba requerida: firmante incorrecto, propietario, menta, PDA, oráculo obsoleto o programa no permitido en lista blanca. Las cuentas opcionales aún necesitan reglas explícitas si están presentes.
¿Cómo interactúa la seguridad con el paralelismo de Sealevel?
La seguridad es corrección por instrucción; el paralelismo trata sobre conjuntos de bloqueos. No debilites la validación solo para pasar menos cuentas.
¿Debería la autoridad de actualización ser una única clave activa?
Evita para valor de producción en riesgo. Prefiere multisig o gobernanza, monitorea las actualizaciones y considera congelar la autoridad después de la madurez.
¿Dónde encajan los oráculos en la seguridad del programa?
Como entradas no confiables. Verifica el propietario de la fuente, la obsolescencia y la confianza antes de usar precios. Falla cerrado con datos faltantes u obsoletos.
¿Cuál es la forma más rápida de revisar una nueva instrucción?
Lee Accounts (firmantes, propietarios, semillas, relaciones, init vs mut, IDs de programa), luego el orden del manejador (matemáticas, escrituras, CPI), luego pruebas negativas para cada modo de fallo.