CULTIVA IA
Security Audit Report
⚠ Confidencial
Auditoria de Vulnerabilidades Solana
Cliente: SolYield Finance
Programa: programs/solyield/src/lib.rs
Framework: Anchor 0.29 + Solana 1.17
Fecha: 15 Jun 2026
Analista: CULTIVA Security Agent
Critico
3
Alto
1
Medio
1
Total Hallazgos
5
Lineas analizadas
~120
Version Solana
1.17.x
Veredicto
NO APTO
para mainnet
🔍

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.

Anchor 0.29 Solana 1.17 Rust DeFi Vault SPL Token CPI Security PDA Validation
Resumen de riesgo
🚫
3
CRITICO
· Arbitrary CPI
· 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)
Hallazgos detallados
1
Arbitrary CPI — Programa de token no validado
Critico lib.rs:withdraw() L55-70
El usuario puede suministrar cualquier programa como token_program. Si pasa un programa malicioso, este recibirá una CPI con la firma del vault y podra drenar fondos.
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
Missing Signer Check — set_admin sin verificacion de firma
Critico lib.rs:set_admin() L20-28
La funcion set_admin acepta AccountInfo para el administrador sin comprobar is_signer, lo que permite a cualquier usuario tomar el control del protocolo.
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
Improper PDA Validation — bump suministrado por usuario
Critico lib.rs:deposit() L31-40
El bump del PDA se acepta como parametro de usuario y se usa con create_program_address en vez de find_program_address, permitiendo potenciales colisiones de direccion o suplantacion del vault.
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
Missing Ownership Check — deserializacion sin validar owner
Alto lib.rs:harvest() L43-50
El campo yield_config se deserializa sin verificar que el programa propietario de la cuenta sea el propio programa, permitiendo inyectar configuraciones maliciosas.
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
Sysvar Account Spoofing — clock pasado como AccountInfo
Medio lib.rs:check_clock() L53-58
El sysvar Clock se acepta como AccountInfo en lugar de Sysvar<'info, Clock>, permitiendo en versiones antiguas pasar un sysvar falso con timestamp manipulado.
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 + bump
Bump 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 actual
Constraint has_one en operaciones admin
⚠ Account Validation (ALTO)
Todas las cuentas verifican owner antes de deserializar
Usar Account<'info, T> en lugar de AccountInfo
Anchor 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