Resumen ejecutivo
Se han detectado 5 vulnerabilidades en el programa Anchor de SolYield Finance, 3 de severidad critica que podrian permitir el robo total de fondos del protocolo antes de su lanzamiento en mainnet. Se requiere correccion de todos los hallazgos criticos y altos antes de cualquier despliegue en mainnet.
Resumen de riesgo
3
CRITICO
· Arbitrary CPI
· Missing Signer Check
· Improper PDA Validation
· Missing Signer Check
· Improper PDA Validation
1
ALTO
· Missing Ownership Check
1
MEDIO
· Sysvar Account Spoofing
0
INFO / OK
System Program OK
Solana 1.17 (sysvar OK)
Solana 1.17 (sysvar OK)
Hallazgos detallados
1
Codigo vulnerable
// BUG: token_program.key() nunca se valida contra spl_token::ID invoke( &spl_token::instruction::transfer( token_program.key, // UNVALIDATED — puede ser programa malicioso ctx.accounts.vault.key, ctx.accounts.destination.key, ctx.accounts.authority.key, &[], amount, )?, &[..., token_program.to_account_info()], )?;
⚠ Escenario de ataque
1
El atacante despliega un programa malicioso que ignora la logica de transferencia.2
Llama a withdraw() pasando su programa como token_program.3
El vault firma la CPI hacia el programa malicioso.4
El programa malicioso usa la firma para drenar todos los tokens del vault.Correccion recomendada
// CORRECTO: Usar Program<'info, Token> — valida el program ID automaticamente pub struct Withdraw<'info> { #[account(mut)] pub vault: Account<'info, TokenAccount>, #[account(mut)] pub destination: Account<'info, TokenAccount>, pub authority: Signer<'info>, pub token_program: Program<'info, Token>, // ✓ valida spl_token::ID }
building-secure-contracts/solana/arbitrary_cpi
trailofbits/solana-lints: unchecked-cpi-program-id
2
Codigo vulnerable
// BUG: admin es AccountInfo, no se verifica is_signer pub struct SetAdmin<'info> { #[account(mut)] pub state: Account<'info, ProtocolState>, pub admin: AccountInfo<'info>, // CUALQUIERA puede llamar esto } // Cualquier cuenta puede invocar set_admin y robar el admin del protocolo pub fn set_admin(ctx: Context<SetAdmin>, new_admin: Pubkey) -> Result<()> { ctx.accounts.state.admin = new_admin; // sin restriccion Ok(()) }
⚠ Escenario de ataque
1
El atacante llama a set_admin() con su propio Pubkey como new_admin.2
El estado del protocolo actualiza admin a la clave del atacante sin ninguna verificacion.3
El atacante ahora controla el protocolo y puede vaciar el vault o modificar parametros.Correccion recomendada
pub struct SetAdmin<'info> { #[account(mut, has_one = admin)] // ✓ verifica que admin == state.admin pub state: Account<'info, ProtocolState>, pub admin: Signer<'info>, // ✓ requiere firma del admin actual }
building-secure-contracts/solana/missing_signer_check
Anchor: Signer<'info>
3
Codigo vulnerable
pub fn deposit(ctx: Context<Deposit>, amount: u64, user_bump: u8) -> Result<()> { // BUG: bump controlado por el usuario — puede calcular PDA no canonico let vault_pda = Pubkey::create_program_address( &[b"vault", ctx.accounts.user.key.as_ref(), &[user_bump]], ctx.program_id, )?; // Un atacante puede proveer un bump que deriva una cuenta que controla require!(vault_pda == *ctx.accounts.vault.key, ErrorCode::InvalidVault); }
Correccion recomendada
// CORRECTO: Usar constraint seeds en Anchor — calcula bump canonico automaticamente #[derive(Accounts)] pub struct Deposit<'info> { #[account( seeds = [b"vault", user.key().as_ref()], bump, // ✓ Anchor calcula y verifica el bump canonico mut )] pub vault: Account<'info, VaultAccount>, pub user: Signer<'info>, pub system_program: Program<'info, System>, }
building-secure-contracts/solana/improper_pda_validation
Anchor: seeds + bump constraint
4
Codigo vulnerable
// BUG: AccountInfo sin verificacion de owner pub yield_config: AccountInfo<'info>, // cualquier cuenta puede pasar // Se deserializa sin comprobar que .owner == solyield::ID let yield_config: YieldConfig = YieldConfig::try_deserialize(/* sin owner check */)?;
Correccion recomendada
// CORRECTO: Account<T> verifica owner automaticamente pub yield_config: Account<'info, YieldConfig>, // ✓ Anchor garantiza que yield_config.owner == program_id
building-secure-contracts/solana/missing_ownership_check
5
Codigo vulnerable
// BUG: clock sin validacion de direccion de sysvar pub clock: AccountInfo<'info>, // podria ser cualquier cuenta
ⓘ Nota de mitigacion
Solana 1.8.1+ bloquea el acceso a sysvars no validos. Con Solana 1.17 el riesgo es reducido, pero sigue siendo mala practica. Si se baja la version, este bug se vuelve explotable.
Correccion recomendada
// CORRECTO: Usar Sysvar — Anchor valida la direccion automaticamente pub clock: Sysvar<'info, Clock>, // ✓ siempre el sysvar real // O mejor: usar Clock::get() directamente (sin cuenta) let clock = Clock::get()?;
building-secure-contracts/solana/sysvar_account_check
Solana 1.8.1 sysvar patch
Checklist de correccion
🚫 CPI Security (CRITICO)
Todos los CPI validan program ID antes de
invoke()Imposible usar program accounts controlados por usuario
Anchor: Usar
Program<'info, Token> en Withdraw🚫 PDA Security (CRITICO)
PDAs usan
find_program_address() o seeds + bumpBump almacenado y reutilizado, no suministrado por usuario
Anchor disponible para constraints de seeds
🚫 Signer Validation (CRITICO)
Cuentas de autoridad usan
Signer<'info>set_admin requiere firma del admin actualConstraint
has_one en operaciones admin⚠ Account Validation (ALTO)
Todas las cuentas verifican owner antes de deserializar
Usar
Account<'info, T> en lugar de AccountInfoAnchor framework disponible para types seguros
🔶 Sysvar Security (MEDIO)
Solana 1.17 (post 1.8.1) — sysvars bloqueados por OS
Usar
Sysvar<'info, Clock> o Clock::get()Eliminar AccountInfo de sysvars como buena practica
✅ Testing
Tests de rechazo de program ID incorrecto
Tests con bump no canonico (debe fallar)
Trail of Bits solana-lints activos en CI