PDA y ataques de semillas
Las Direcciones Derivadas del Programa (PDA) anclan el estado del protocolo a semillas deterministas. Los errores en las semillas y los bumps permiten a los atacantes suplantar bóvedas, eludir las comprobaciones de autoridad o colisionar con las PDA de otros programas.
Receta
Tarjeta de referencia rápida - lista para copiar y pegar.
#[account(
init,
payer = payer,
space = 8 + State::INIT_SPACE,
seeds = [b"state", user.key().as_ref()],
bump,
)]
pub state: Account<'info, State>,
// Almacenar el bump en la cuenta para invocaciones posteriores
state.bump = ctx.bumps.state;Cuándo usar esto:
- Creación de PDA de bóveda o escrow.
- Firma de CPI con
invoke_signed. - Auditoría de restricciones de
seedsen#[account]. - Migración de esquemas de semillas sin dejar fondos huérfanos.
Ejemplo de trabajo
use anchor_lang::prelude::*;
#[account]
pub struct Escrow {
pub maker: Pubkey,
pub bump: u8,
}
#[derive(Accounts)]
pub struct InitEscrow<'info> {
#[account(mut)]
pub maker: Signer<'info>,
#[account(
init,
payer = maker,
space = 8 + Escrow::INIT_SPACE,
seeds = [b"escrow", maker.key().as_ref()],
bump,
)]
pub escrow: Account<'info, Escrow>,
pub system_program: Program<'info, System>,
}
pub fn init_escrow(ctx: Context<InitEscrow>) -> Result<()> {
ctx.accounts.escrow.maker = ctx.accounts.maker.key();
ctx.accounts.escrow.bump = ctx.bumps.escrow;
Ok(())
}Lo que esto demuestra:
- Las semillas incluyen la pubkey del usuario: cada usuario obtiene una PDA de escrow única.
- El
bumpen la inicialización almacena el bump canónico parainvoke_signedposterior. seeds+bumpen instrucciones subsiguientes evitan la sustitución de PDA.
Análisis en profundidad
Cómo funciona
- Las PDA son pubkeys fuera de la curva derivadas de
(seeds, program_id). - El tiempo de ejecución encuentra el bump canónico (de 255 a 0) donde la dirección está fuera de la curva.
- Solo el programa propietario puede firmar para una PDA a través de
invoke_signedcon las semillas exactas. - Conjuntos de semillas incorrectos derivan direcciones diferentes: el atacante suministra cuentas de apariencia similar.
Patrones de ataque
| Patrón | Riesgo | Mitigación |
|---|---|---|
| Bump faltante en las semillas | Se acepta una PDA incorrecta | bump o bump = state.bump |
| Semillas controladas por el usuario únicamente | Colisiones predecibles | Incluir bytes de prefijo elegidos por el programa |
| Canonización de semillas | Múltiples bumps | Usar siempre el bump canónico de find_program_address |
| PDA entre programas | Mismas semillas, programa diferente | Nunca reutilizar cadenas de semillas entre programas |
Notas de Rust
let seeds = &[b"escrow", maker.as_ref(), &[escrow.bump]];
let signer_seeds = &[&seeds[..]];
// Pasar signer_seeds a CpiContext::new_with_signerTrampas
- Bump no almacenado - Volver a buscar el bump en cada transacción desperdicia CU y corre el riesgo de desajustes. Solución: Persistir el bump en la inicialización.
- Semillas opcionales del usuario - El atacante elige semillas que se mapean a su cuenta. Solución: Fijar los bytes de prefijo del protocolo; limitar la entrada del usuario a pubkeys.
init_if_neededen PDA - La reinicialización restablece el estado. Solución: Usarinituna vez + indicadoris_initialized.- Confusión de UTF-8 en semillas -
"escrow"vsb"escrow". Solución: Siempre literales de bytesb"...". - Longitud máxima de semillas - Límite total de semillas > 32 bytes por componente. Solución: Hashear entradas largas con
hashven 32 bytes. - PDA como autoridad de token - Debe firmar CPI de token con las semillas correctas. Solución: Hacer coincidir la cuenta de token propietaria con la PDA.
Alternativas
| Alternativa | Usar cuándo | No usar cuándo |
|---|---|---|
| PDA global única | Configuración singleton | Estado por usuario |
| Cuenta propiedad de Keypair | Se necesita firmante externo | La custodia debe permanecer en el programa |
| Registro de semillas con nombre | Muchos tipos de PDA | Escrow simple de un solo uso |
Preguntas frecuentes
¿Pueden los usuarios derivar mi PDA desde el cliente?
Sí, las PDA son públicas. La seguridad reside en la validación de semillas y las reglas de firma, no en el secreto.
¿Qué es la canonización de semillas de bump?
Solana elige el bump válido más alto (generalmente 255); usa siempre ese bump en invoke_signed.
¿Pueden dos programas compartir una dirección PDA?
No, program_id es parte de la derivación; mismas semillas + programa diferente = dirección diferente.
¿Cómo migro las semillas de PDA?
Doble escritura, migración de fondos, desaprobación de la ruta de semillas antigua; nunca cambies las semillas con fondos activos sin un plan.
¿Qué es un ataque de colisión de semillas?
El atacante pasa una cuenta que no es PDA y que coincide con la clave esperada sin tus semillas; prevenido por la restricción seeds.
¿Cuántas semillas puedo usar?
Hasta 16 componentes de semillas, máximo 32 bytes cada uno; consulta los límites del SDK de Solana.
¿Anchor encuentra el bump automáticamente?
bump sin valor encuentra y almacena en ctx.bumps durante la inicialización; usa bump = account.bump después.
¿Cómo pruebo un bump incorrecto?
LiteSVM prueba con un bump incorrecto en invoke_signed; espera un fallo en la verificación de la firma.
¿Pueden las PDA poseer cuentas de token?
Sí, el propietario de la cuenta de token es la PDA; el programa firma las transferencias con invoke_signed.
¿Son seguras las semillas de dirección del programa?
Incluir crate::ID en las semillas evita la suplantación entre despliegues cuando el ID del programa cambia por clúster.
Relacionado
- Comprobaciones de Signer y Owner - Firma de PDA
- Ataques de Reinicialización - Reinicialización de PDA
- Pruebas de CPI y PDA - Pruebas de PDA
- Direcciones Derivadas del Programa - Fundamentos de PDA
Versiones de Stack: Esta página fue escrita para Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, y LiteSVM 0.6.x.