NOVO: Helius adquire a Light Protocol
O Guia do Mochileiro para a Segurança de Programas Solana
Blog/Fundamentos

O Guia do Mochileiro para a Segurança de Programas Solana

Developer Experience Engineer0xIchigo no X0xIchigo no LinkedIn0xIchigo no GitHub
39 min de leitura
Índice

Este artigo foi escrito em coautoria com bl0ckpain, pesquisador de segurança e desenvolvedor de contratos inteligentes que trabalhou anteriormente com a Kudelski Security e a Halborn

Introdução

A segurança de programas Solana não se resume a impedir que hackers roubem os fundos de um projeto — ela também garante que um programa se comporte conforme o esperado, seguindo as especificações do projeto e as expectativas dos usuários. A segurança de programas Solana pode afetar o desempenho, a escalabilidade e a interoperabilidade de uma dApp. Portanto, antes de criar aplicações para usuários finais, os desenvolvedores precisam conhecer os possíveis vetores de ataque e as vulnerabilidades comuns.

Este artigo explora vulnerabilidades comuns que os desenvolvedores encontrarão ao criar programas Solana. Começamos com uma introdução à mentalidade dos invasores ao explorar programas Solana, abordando temas como o modelo de programação da Solana, como o design da Solana é inerentemente controlado por invasores, possíveis vetores de ataque e estratégias comuns de mitigação. Em seguida, abordamos diversas vulnerabilidades, explicando cada uma delas e apresentando exemplos de código inseguro e seguro quando aplicável. 

Este artigo destina-se a um público de nível intermediário ou avançado, pois pressupõe conhecimento do modelo de programação e do desenvolvimento de programas da Solana.

Este artigo não explicará o processo de criação de um programa nem conceitos específicos da Solana — nosso foco é examinar vulnerabilidades comuns e aprender a mitigá-las. Se você ainda não conhece a Solana, recomendamos a leitura destas publicações anteriores antes de continuar:

A Mentalidade do Invasor ao Explorar Programas Solana

O Modelo de Programação da Solana

O modelo de programação da Solana define o cenário de segurança das aplicações criadas em sua rede. Na Solana, as contas funcionam como contêineres de dados, de modo semelhante aos arquivos em um computador. Podemos separar as contas em dois tipos gerais: executáveis e não executáveis. Contas executáveis, ou programas, são contas capazes de executar código. Contas não executáveis são usadas para armazenar dados sem a capacidade de executar código, pois não armazenam código. Essa separação entre código e dados significa que os programas não têm estado — eles interagem com dados armazenados em outras contas, passadas por referência durante as transações.

A Solana É Controlada por Invasores

Uma transação especifica o programa que será chamado, uma lista de contas e um array de bytes com os dados da instrução. Esse modelo depende do programa para analisar e interpretar as contas e instruções fornecidas por determinada transação. Permitir que qualquer conta seja passada para a função de um programa concede aos invasores um controle significativo sobre os dados nos quais o programa operará. Entender o modelo de programação da Solana, inerentemente controlado por invasores, é essencial para desenvolver programas seguros.

Como um invasor pode passar qualquer conta para a função de um programa, a validação de dados se torna um pilar fundamental da segurança de programas Solana. Os desenvolvedores precisam garantir que seus programas consigam distinguir entre entradas legítimas e maliciosas. Isso inclui verificar a propriedade das contas, garantir que elas sejam do tipo esperado e confirmar se uma conta é signatária.

Possíveis Vetores de Ataque

O modelo de programação e o ambiente de execução exclusivos da Solana dão origem a vetores de ataque específicos. Entendê-los é essencial para que os desenvolvedores protejam seus programas contra possíveis explorações. Esses vetores de ataque incluem:

  • Erros de Lógica: falhas na lógica do programa podem ser manipuladas para causar comportamentos indesejados, como perda de ativos ou acesso não autorizado. Isso também inclui não implementar corretamente as especificações do projeto — se um programa afirma fazer x, ele deve fazer x e cumprir todas as suas particularidades
  • Falhas na Validação de Dados: uma validação inadequada dos dados de entrada pode permitir que invasores forneçam dados maliciosos e manipulem o estado ou a execução do programa
  • Problemas Específicos do Rust: apesar dos recursos de segurança do Rust, blocos de código inseguro, problemas de concorrência e panics podem introduzir vulnerabilidades
  • Vulnerabilidades de Controle de Acesso: não implementar corretamente as verificações de controle de acesso, como confirmar o proprietário de uma conta, pode permitir ações não autorizadas por agentes maliciosos
  • Erros Aritméticos e de Precisão: overflows/underflows e erros de precisão podem ser explorados para obter ganhos financeiros ou provocar falhas no programa
  • Problemas de Invocação Entre Programas (CPI): falhas no processamento de CPIs podem causar mudanças inesperadas de estado ou erros caso um programa chamado se comporte de maneira maliciosa ou inesperada
  • Uso Incorreto de Endereços Derivados de Programa (PDAs): gerar ou processar PDAs incorretamente pode criar vulnerabilidades nas quais invasores sequestram ou falsificam PDAs para obter acesso não autorizado ou manipular contas controladas pelo programa

A reentrância é inerentemente limitada na Solana devido ao seu modelo de execução. O runtime da Solana restringe as CPIs a uma profundidade máxima de quatro e impõe regras rígidas para contas, como permitir que apenas o proprietário de uma conta modifique seus dados. Essas restrições impedem ataques de reentrância ao limitar a autorrecursão direta e garantir que um programa não possa ser invocado involuntariamente em um estado intermediário.

Estratégias de Mitigação

Para mitigar esses possíveis ataques, os desenvolvedores devem combinar testes rigorosos, auditorias de código e adesão às práticas recomendadas:

  • Implemente uma validação abrangente das entradas e verificações de controle de acesso
  • Aproveite ao máximo o sistema de tipos e os recursos de segurança do Rust, evitando código inseguro, exceto quando necessário
  • Siga as práticas recomendadas de segurança da Solana e do Rust e mantenha-se atualizado sobre novos desenvolvimentos
  • Faça revisões internas de código e use ferramentas automatizadas para identificar vulnerabilidades comuns e erros de lógica durante o desenvolvimento do programa
  • Submeta sua base de código à auditoria de terceiros confiáveis, incluindo empresas de segurança e pesquisadores de segurança independentes
  • Crie uma plataforma de recompensas por bugs para seu programa a fim de incentivar a comunicação de vulnerabilidades, em vez de depender de hackers grey hat

As seções a seguir explorarão diferentes vulnerabilidades em ordem alfabética. Cada seção descreverá uma possível vulnerabilidade, explicará como mitigá-la e apresentará cenários de exemplo sempre que possível.

Correspondência de Dados de Contas

A Vulnerabilidade

A correspondência de dados de contas é uma vulnerabilidade que surge quando os desenvolvedores não verificam se os dados armazenados em uma conta correspondem ao conjunto de valores esperado. Sem as verificações adequadas de validação de dados, um programa pode operar inadvertidamente com contas incorretas ou substituídas de forma maliciosa. Essa vulnerabilidade é particularmente grave em cenários que envolvem verificações relacionadas a permissões.

Cenário de Exemplo

Considere um programa com recursos para gerenciar suas configurações administrativas. O programa inclui uma instrução para atualizar as configurações administrativas atuais, como sinalizadores de recursos ou parâmetros operacionais. A instrução precisa validar se a solicitação vem de um administrador autorizado. No entanto, o programa não verifica se a conta que solicita a alteração corresponde à conta do administrador armazenada nos dados de configuração:

Código
pub fn update_admin_settings(ctx: Context<UpdateAdminSettings>, new_settings: AdminSettings) -> Result<()> {
  ctx.accounts.config_data.settings = new_settings;
  
  Ok(())
}

#[derive(Accounts)]
pub struct UpdateAdminSettings<'info> {
  #[account(mut)]
  pub config_data: Account<'info, ConfigData>,
  pub admin: Signer<'info>,
}

#[account]
pub struct ConfigData {
  admin: Pubkey,
  settings: AdminSettings
}

Mitigação Recomendada

Para mitigar essa vulnerabilidade, os desenvolvedores podem implementar verificações explícitas que comparem as chaves das contas e os dados armazenados com os valores esperados. Por exemplo, verifique se a chave pública do depositante corresponde ao campo de proprietário da conta de token usada para o depósito:

Código
pub fn update_admin_settings(ctx: Context<UpdateAdminSettings>, new_settings: AdminSettings) -> Result<()> {
  if ctx.accounts.admin.key() != ctx.accounts.config_data.admin {
    return Err(ProgramError::Unauthorized);
  }  

  ctx.accounts.config_data.settings = new_settings;
  
  Ok(())
}

Os desenvolvedores também podem usar os atributos has_one e constraint do Anchor para aplicar verificações de validação de dados de forma declarativa. Usando o exemplo acima, poderíamos usar o atributo constraint para verificar se a chave pública do depositante e o proprietário da conta de token de depósito são equivalentes:

Código
pub struct UpdateAdminSettings<'info> {
  #[account(
    mut,
    constraint = config_data.admin == admin.key()
  )]
  pub config_data: Account<'info, ConfigData>,
  pub admin: Signer<'info>,
}

Realocação de Dados de Contas

A Vulnerabilidade

No Anchor, a função realloc fornecida pela struct AccountInfo introduz uma vulnerabilidade sutil relacionada ao gerenciamento de memória. Essa função permite realocar o tamanho dos dados de uma conta, o que pode ser útil para processar dados dinâmicos nos programas. No entanto, o uso incorreto de realloc pode gerar consequências indesejadas, incluindo o desperdício de unidades computacionais ou a possível exposição de dados obsoletos.

O método realloc tem dois parâmetros:

  • new_len: um usize que especifica o novo comprimento dos dados da conta
  • zero_init: um bool que determina se o novo espaço de memória deve ser inicializado com zeros

realloc é definido da seguinte forma:

Código
pub fn realloc(
    &self,
    new_len: usize,
    zero_init: bool
) -> Result<(), ProgramError>

A memória alocada para os dados da conta já é inicializada com zeros no ponto de entrada do programa. Isso significa que o novo espaço de memória já contém zeros quando os dados são realocados para um tamanho maior em uma única transação. Zerar novamente essa memória é desnecessário e aumenta o consumo de unidades computacionais. Por outro lado, realocar para um tamanho menor e depois retornar a um tamanho maior na mesma transação pode expor dados obsoletos se zero_init for false.

Cenário de Exemplo

Considere um programa de lista de tarefas dinâmica no qual os usuários podem adicionar, remover ou modificar itens em uma única transação. Esse programa precisa realocar dinamicamente o tamanho de seus dados com base nas ações do usuário:

Código
pub fn modify_todo_list(ctx: Context<ModifyTodoList>, modifications: Vec<TodoModification>) -> ProgramResult {
    // Logic to process modifications
    for modification in modifications {
        match modification {
            TodoModification::Add(entry) => {
                // Add logic
            },
            TodoModification::Remove(index) => {
                // Remove logic, potentially requiring data reallocation
            },
            TodoModification::Edit(index, new_entry) => {
                // Edit logic
            },
        }
    }

    // Reallocation logic to adjust the data size based on modifications
    let required_data_len = calculate_required_data_len(&modifications);
    ctx.accounts.todo_list_data.realloc(required_data_len, false)?;

    Ok(())
}

#[derive(Accounts)]
pub struct ModifyTodoList<'info> {
    #[account(mut)]
    todo_list_data: AccountInfo<'info>,
    // Other relevant accounts
}

Nesse cenário, a função modify_todo_list pode realocar to_do_list_data várias vezes para acomodar o tamanho exigido pelas modificações. Se o tamanho dos dados for reduzido para remover um item da lista de tarefas e depois aumentado novamente para adicionar novos itens na mesma transação, definir zero_init como false poderá expor dados obsoletos.

Mitigação Recomendada

Para mitigar esse problema, é essencial usar o parâmetro zero_init com cuidado:

  • Defina zero_init como true ao aumentar o tamanho dos dados após uma redução anterior na mesma chamada de transação. Isso garante que todo novo espaço de memória seja inicializado com zeros, impedindo a exposição de dados obsoletos
  • Defina zero_init como false ao aumentar o tamanho dos dados sem uma redução anterior na mesma chamada de transação, pois a memória já estará inicializada com zeros

Em vez de realocar dados para atender a requisitos específicos de tamanho, os desenvolvedores devem usar Tabelas de Consulta de Endereços (ALTs). As ALTs permitem compactar os dados de uma transação ao armazenar até 256 endereços em uma única conta on-chain. Cada endereço da tabela pode então ser referenciado por um índice de 1 byte, reduzindo significativamente os dados necessários para referências de endereços em determinada transação. As ALTs são muito mais úteis em cenários que exigem interações dinâmicas entre contas sem a necessidade de redimensionar a memória com frequência.

Recarregamento de Contas

A Vulnerabilidade

O recarregamento de contas é uma vulnerabilidade que surge quando os desenvolvedores não atualizam contas desserializadas após realizar uma CPI. O Anchor não atualiza automaticamente o estado das contas desserializadas depois de uma CPI. Isso pode levar a cenários em que a lógica do programa opera com dados obsoletos, causando erros de lógica ou cálculos incorretos.

Cenário de Exemplo

Considere um protocolo no qual os usuários podem fazer staking de tokens para receber recompensas ao longo do tempo. O programa que viabiliza esse processo inclui uma funcionalidade para atualizar as recompensas de staking de um usuário com base em determinadas condições ou gatilhos externos. As recompensas de um usuário são calculadas e atualizadas por meio de uma CPI para um programa de distribuição de recompensas. No entanto, o programa não atualiza a conta de staking original após a CPI para refletir o novo saldo de recompensas:

Código
pub fn update_rewards(ctx: Context<UpdateStakingRewards>, amount: u64) -> Result<()> {
    let staking_seeds = &[b"stake", ctx.accounts.staker.key().as_ref(), &[ctx.accounts.staking_account.bump]];

    let cpi_accounts = UpdateRewards {
        staking_account: ctx.accounts.staking_account.to_account_info(),
    };
    let cpi_program = ctx.accounts.rewards_distribution_program.to_account_info();
    let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, staking_seeds);

    rewards_distribution::cpi::update_rewards(cpi_ctx, amount)?;

    // Attempt to log the "updated" reward balance
    msg!("Rewards: {}", ctx.accounts.staking_account.rewards);
    
    // Logic that uses the stale ctx.accounts.staking_account.rewards

    Ok(())
}

#[derive(Accounts)]
pub struct UpdateStakingRewards<'info> {
    #[account(mut)]
    pub staker: Signer<'info>,
    #[account(
        mut,
        seeds = [b"stake", staker.key().as_ref()],
        bump,
    )]
    pub staking_account: Account<'info, StakingAccount>,
    pub rewards_distribution_program: Program<'info, RewardsDistribution>,
}

#[account]
pub struct StakingAccount {
    pub staker: Pubkey,
    pub stake_amount: u64,
    pub rewards: u64,
    pub bump: u8,
}

Nesse exemplo, a função update_rewards tenta atualizar as recompensas da conta de staking de um usuário por meio de uma chamada CPI para um programa de distribuição de recompensas. Inicialmente, o programa registra ctx.accounts.staking_account.rewards, ou seja, o saldo de recompensas, após a CPI e depois continua executando uma lógica que usa os dados obsoletos de ctx.accounts.staking_account.rewards. O problema é que o estado da conta de staking não é atualizado automaticamente após a CPI, por isso os dados estão obsoletos.

Mitigação Recomendada

Para mitigar esse problema, chame explicitamente o método reload do Anchor para recarregar uma conta específica do armazenamento. Recarregar uma conta após a CPI refletirá seu estado com precisão:

Código
pub fn update_rewards(ctx: Context<UpdateStakingRewards>, amount: u64) -> Result<()> {
    let staking_seeds = &[b"stake", ctx.accounts.staker.key().as_ref(), &[ctx.accounts.staking_account.bump]];

    let cpi_accounts = UpdateRewards {
        staking_account: ctx.accounts.staking_account.to_account_info(),
    };
    let cpi_program = ctx.accounts.rewards_distribution_program.to_account_info();
    let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, staking_seeds);

    rewards_distribution::cpi::update_rewards(cpi_ctx, amount)?;

    // Reload the staking account to reflect the updated reward balance
    ctx.accounts.staking_account.reload()?;

    // Log the updated reward balance
    msg!("Rewards: {}", ctx.accounts.staking_account.rewards);
    
    // Logic that uses ctx.accounts.staking_account.rewards

    Ok(())
}

CPI Arbitrária

A Vulnerabilidade

CPIs arbitrárias ocorrem quando um programa invoca outro programa sem verificar a identidade do programa de destino. Essa vulnerabilidade existe porque o runtime da Solana permite que qualquer programa chame outro programa se o chamador tiver o ID do programa chamado e seguir sua interface. Se um programa realizar CPIs com base em entradas do usuário sem validar o ID do programa chamado, ele poderá executar código em um programa controlado por um invasor.

Cenário de Exemplo

Considere um programa que distribui prêmios aos participantes com base em suas contribuições para um projeto. Depois de distribuir as recompensas, o programa registra os detalhes em um programa de livro-razão separado para fins de auditoria e acompanhamento. Presume-se que o programa de livro-razão seja confiável e forneça uma interface pública para acompanhar entradas específicas de programas autorizados. O programa inclui uma função para distribuir e registrar recompensas, que recebe o programa de livro-razão como uma conta. No entanto, a função não verifica o ledger_program fornecido antes de fazer uma CPI para ele:

Código
pub fn distribute_and_record_rewards(ctx: Context<DistributeAndRecord>, reward_amount: u64) -> ProgramResult {
    // Reward distribution logic

    let instruction = custom_ledger_program::instruction::record_transaction(
        &ctx.accounts.ledger_program.key(),
        &ctx.accounts.reward_account.key(),
        reward_amount,
    )?;

    invoke(
        &instruction,
        &[
            ctx.accounts.reward_account.clone(),
            ctx.accounts.ledger_program.clone(),
        ],
    )
}

#[derive(Accounts)]
pub struct DistributeAndRecord<'info> {
    reward_account: AccountInfo<'info>,
    ledger_program: AccountInfo<'info>,
}

Um invasor poderia explorar isso passando o ID de um programa malicioso como ledger_program, causando consequências indesejadas.

Mitigação Recomendada

Para se proteger contra esse problema, os desenvolvedores podem adicionar uma verificação que confirme a identidade do programa de livro-razão antes de realizar a CPI. Essa verificação garantiria que a chamada CPI fosse feita para o programa pretendido, impedindo CPIs arbitrárias:

Código
pub fn distribute_and_record_rewards(ctx: Context<DistributeAndRecord>, reward_amount: u64) -> ProgramResult {
    // Reward distribution logic

    // Verify the ledger_program is the expected custom ledger program
    if ctx.accounts.ledger_program.key() != &custom_ledger_program::ID {
        return Err(ProgramError::IncorrectProgramId.into())
    }
    
    let instruction = custom_ledger_program::instruction::record_transaction(
        &ctx.accounts.ledger_program.key(),
        &ctx.accounts.reward_account.key(),
        reward_amount,
    )?;

    invoke(
        &instruction,
        &[
            ctx.accounts.reward_account.clone(),
            ctx.accounts.ledger_program.clone(),
        ],
    )
}

#[derive(Accounts)]
pub struct DistributeAndRecord<'info> {
    reward_account: AccountInfo<'info>,
    ledger_program: AccountInfo<'info>,
}

Um programa pode ter um módulo CPI disponível publicamente se tiver sido escrito com o Anchor. Isso torna fácil e seguro invocar o programa a partir de outro programa Anchor. O módulo CPI do Anchor verifica automaticamente se o endereço do programa fornecido corresponde ao endereço armazenado no módulo. Como alternativa, inserir o endereço diretamente no código pode ser uma solução possível, em vez de pedir ao usuário que o forneça.

Funcionalidade de Transferência de Autoridade

A Vulnerabilidade

Os programas Solana costumam designar chaves públicas específicas como autoridades de funções críticas, como atualizar parâmetros do programa ou retirar fundos. No entanto, a impossibilidade de transferir essa autoridade para outro endereço pode gerar riscos significativos. Essa limitação se torna problemática em cenários como mudanças na equipe, vendas do protocolo ou comprometimento da autoridade.

Cenário de Exemplo

Considere um programa no qual uma autoridade administrativa global é responsável por definir parâmetros específicos do protocolo por meio de uma função set_params. O programa não inclui um mecanismo para alterar o administrador global:

Código
pub fn set_params(ctx: Context<SetParams>, /* parameters to be set */) -> Result<()> {
    require_keys_eq!(
        ctx.accounts.current_admin.key(),
        ctx.accounts.global_admin.authority,
    );

    // Logic to set parameters
}

Aqui, a autoridade é definida estaticamente, sem a possibilidade de atualizá-la para um novo endereço.

Mitigação Recomendada

Uma abordagem segura para mitigar esse problema é criar um processo de duas etapas para transferir a autoridade. Esse processo permitiria que a autoridade atual nomeasse uma nova pending_authority, que precisaria aceitar explicitamente a função. Isso não apenas disponibilizaria a funcionalidade de transferência de autoridade, mas também protegeria contra transferências acidentais ou tomadas de controle maliciosas. O fluxo seria o seguinte:

  • Nomeação pela Autoridade Atual: a autoridade atual nomearia uma nova pending_authority chamando nominate_new_authority, que define o campo pending_authority no estado do programa
  • Aceitação pela Nova Autoridade: a pending_authority nomeada chama accept_authority para assumir sua nova função, transferindo a autoridade atual para a pending_authority

Isso seria semelhante a:

Código
pub fn nominate_new_authority(ctx: Context<NominateAuthority>, new_authority: Pubkey) -> Result<()> {
    let state = &mut ctx.accounts.state;
    require_keys_eq!(
        state.authority,
        ctx.accounts.current_authority.key()
    );

    state.pending_authority = Some(new_authority);
    Ok(())
}

pub fn accept_authority(ctx: Context<AcceptAuthority>) -> Result<()> {
    let state = &mut ctx.accounts.state;
    require_keys_eq!(
        Some(ctx.accounts.new_authority.key()),
        state.pending_authority
    );

    state.authority = ctx.accounts.new_authority.key();
    state.pending_authority = None;
    Ok(())
}

#[derive(Accounts)]
pub struct NominateAuthority<'info> {
    #[account(
        mut,
        has_one = authority,
    )]
    pub state: Account<'info, ProgramState>,
    pub current_authority: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct AcceptAuthority<'info> {
    #[account(
        mut,
        constraint = state.pending_authority == Some(new_authority.key())
    )]
    pub state: Account<'info, ProgramState>,
    pub new_authority: Signer<'info>,
}

#[account]
pub struct ProgramState {
    pub authority: Pubkey,
    pub pending_authority: Option<Pubkey>,
    // Other relevant program state fields
}

Nesse exemplo, a estrutura da conta ProgramState mantém a authority atual e uma pending_authority opcional. O contexto NominateAuthority garante que a autoridade atual assine a transação, permitindo que ela nomeie uma nova autoridade. O contexto AcceptAuthority verifica se pending_authority corresponde ao signatário da transação, permitindo que ele aceite a função e se torne a nova autoridade. Essa configuração garante uma transição segura e controlada da autoridade dentro do programa.

Canonicalização do Bump Seed

A Vulnerabilidade

A canonicalização do bump seed refere-se ao uso do maior bump seed válido, ou seja, o bump canônico, ao derivar PDAs. Usar o bump canônico é uma maneira determinística e segura de encontrar um endereço a partir de um conjunto de seeds. Deixar de usar o bump canônico pode gerar vulnerabilidades, como permitir que agentes maliciosos criem ou manipulem PDAs que comprometam a lógica do programa ou a integridade dos dados.

Cenário de Exemplo

Considere um programa criado para gerar perfis de usuário exclusivos, cada um com um PDA associado derivado explicitamente por meio de create_program_address. O programa permite criar um perfil usando um bump fornecido pelo usuário. No entanto, isso é problemático porque introduz o risco de usar um bump não canônico:

Código
pub fn create_profile(ctx: Context<CreateProfile>, user_id: u64, attributes: Vec<u8>, bump: u8) -> Result<()> {
    // Explicitly derive the PDA using create_program_address and a user-provided bump
    let seeds: &[&[u8]] = &[b"profile", &user_id.to_le_bytes(),&[bump]];
    let (derived_address, _bump) = Pubkey::create_program_address(seeds, &ctx.program_id)?;

    if derived_address != ctx.accounts.profile.key() {
        return Err(ProgramError::InvalidSeeds);
    }

    let profile_pda = &mut ctx.accounts.profile;
    profile_pda.user_id = user_id;
    profile_pda.attributes = attributes;

    Ok(())
}

#[derive(Accounts)]
pub struct CreateProfile<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    /// The profile account, expected to be a PDA derived with the user_id and a user-provided bump seed
    #[account(mut)]
    pub profile: Account<'info, UserProfile>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct UserProfile {
    pub user_id: u64,
    pub attributes: Vec<u8>,
}

Nesse cenário, o programa deriva um PDA UserProfile usando create_program_address com seeds que incluem um bump fornecido pelo usuário. Usar um bump fornecido pelo usuário é problemático porque não garante o uso do bump canônico. Isso permitiria que um agente malicioso criasse vários PDAs com bumps diferentes para o mesmo ID de usuário.

Mitigação Recomendada

Para mitigar esse problema, podemos refatorar nosso exemplo para derivar PDAs usando find_program_address e validar explicitamente o bump seed:

Código
pub fn create_profile(ctx: Context<CreateProfile>, user_id: u64, attributes: Vec<u8>) -> Result<()> {
    // Securely derive the PDA using find_program_address to ensure the canonical bump is used
    let seeds: &[&[u8]] = &[b"profile", user_id.to_le_bytes()];
    let (derived_address, bump) = Pubkey::find_program_address(seeds, &ctx.program_id);

    // Store the canonical bump in the profile for future validations
    let profile_pda = &mut ctx.accounts.profile;
    profile_pda.user_id = user_id;
    profile_pda.attributes = attributes;
    profile_pda.bump = bump;

    Ok(())
}

#[derive(Accounts)]
#[instruction(user_id: u64)]
pub struct CreateProfile<'info> {
    #[account(
        init,
        payer = user,
        space = 8 + 1024 + 1,
        seeds = [b"profile", user_id.to_le_bytes().as_ref()],
        bump
    )]
    pub profile: Account<'info, UserProfile>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct UserProfile {
    pub user_id: u64,
    pub attributes: Vec<u8>,
    pub bump: u8,
}

Aqui, find_program_address é usado para derivar o PDA com o bump seed canônico, garantindo a criação determinística e segura do PDA. O bump canônico é armazenado na conta UserProfile, permitindo uma validação eficiente e segura em operações posteriores. Preferimos find_program_address a create_program_address porque este último cria um PDA válido sem procurar um bump seed. Como ele não procura um bump seed, pode retornar um erro de forma imprevisível para qualquer conjunto de seeds e, em geral, não é adequado para criar PDAs. find_program_address usará sempre o bump canônico ao criar um PDA. Isso ocorre porque ele percorre várias chamadas create_program_address, começando com um bump de 255 e reduzindo-o a cada iteração. Quando um endereço válido é encontrado, a função retorna o PDA derivado e o bump canônico usado para derivá-lo.

Encerramento de contas

A vulnerabilidade

Encerrar contas de forma inadequada em um programa pode causar várias vulnerabilidades, incluindo a possibilidade de contas "encerradas" serem reinicializadas ou usadas indevidamente. O problema surge quando uma conta não é marcada corretamente como encerrada ou quando sua reutilização em transações posteriores não é impedida. Essa falha pode permitir que agentes mal-intencionados explorem uma conta, resultando em ações ou acessos não autorizados no programa.

Exemplo de cenário

Considere um programa que permite aos usuários criar e encerrar contas de armazenamento de dados. O programa encerra uma conta transferindo seus lamports:

Código
pub fn close_account(ctx: Context<CloseAccount>) -> ProgramResult {
    let account = ctx.accounts.data_account.to_account_info();
    let destination = ctx.accounts.destination.to_account_info();

    **destination.lamports.borrow_mut() = destination
        .lamports()
        .checked_add(account.lamports())
        .unwrap();
    **account.lamports.borrow_mut() = 0;
    
    Ok(())
}

#[derive(Accounts)]
pub struct CloseAccount<'info> {
    #[account(mut)]
    pub data_account: Account<'info, Data>,
    #[account(mut)]
    pub destination: AccountInfo<'info>,
}

#[account]
pub struct Data {
    data: u64,
}

Isso é problemático porque o programa não zera os dados da conta nem a marca como encerrada. Apenas transferir os lamports restantes não encerra a conta.

Mitigação recomendada

Para mitigar esse problema, além de transferir todos os lamports, o programa também deve zerar os dados da conta e marcá-la com um discriminador (isto é, "CLOSED_ACCOUNT_DISCRIMINATOR"). O programa também deve implementar verificações para impedir que contas encerradas sejam reutilizadas em transações futuras:

Código
use anchor_lang::__private::CLOSED_ACCOUNT_DISCRIMINATOR;
use anchor_lang::prelude::*;
use std::io::Cursor;
use std::ops::DerefMut;

// Other code

pub fn close_account(ctx: Context<CloseAccount>) -> ProgramResult {
    let account = ctx.accounts.data_account.to_account_info();
    let destination = ctx.accounts.destination.to_account_info();

    **destination.lamports.borrow_mut() = destination
        .lamports()
        .checked_add(account.lamports())
        .unwrap();
    **account.lamports.borrow_mut() = 0;

    // Zero out the account data
    let mut data = account.try_borrow_mut_data()?;
    for byte in data.deref_mut().iter_mut() {
        *byte = 0;
    }

    // Mark the account as closed
    let dst: &mut [u8] = &mut data;
    let mut cursor = Cursor::new(dst);
    cursor.write_all(&CLOSED_ACCOUNT_DISCRIMINATOR).unwrap();

    Ok(())
}

pub fn force_defund(ctx: Context<ForceDefund>) -> ProgramResult {
    let account = &ctx.accounts.account;
    let data = account.try_borrow_data()?;

    if data.len() < 8 || data[0..8] != CLOSED_ACCOUNT_DISCRIMINATOR {
        return Err(ProgramError::InvalidAccountData);
    }

    let destination = ctx.accounts.destination.to_account_info();

    **destination.lamports.borrow_mut() = destination
        .lamports()
        .checked_add(account.lamports())
        .unwrap();
    **account.lamports.borrow_mut() = 0;

    Ok(())
}

#[derive(Accounts)]
pub struct ForceDefund<'info> {
    #[account(mut)]
    pub account: AccountInfo<'info>,
    #[account(mut)]
    pub destination: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct CloseAccount<'info> {
    #[account(mut)]
    pub data_account: Account<'info, Data>,
    #[account(mut)]
    pub destination: AccountInfo<'info>,
}

#[account]
pub struct Data {
    data: u64,
}

No entanto, zerar os dados e adicionar o discriminador de conta encerrada não é suficiente. Um usuário pode impedir que uma conta seja removida pela coleta de lixo reembolsando os lamports da conta antes do fim de uma instrução. Isso deixará a conta em um estado intermediário estranho, no qual ela não pode ser usada nem removida pela coleta de lixo. Por isso, adicionamos uma função force_defund para tratar esse caso extremo; agora qualquer pessoa pode retirar os fundos de contas encerradas.

O Anchor simplifica esse processo com a restrição #[account(close = destination)], automatizando o encerramento seguro de contas por meio da transferência de lamports, do zeramento dos dados e da definição do discriminador de conta encerrada, tudo em uma única operação.

Contas mutáveis duplicadas

A vulnerabilidade

Contas mutáveis duplicadas referem-se a um cenário em que a mesma conta é passada mais de uma vez como parâmetro mutável para uma instrução. Isso ocorre quando uma instrução exige duas contas mutáveis do mesmo tipo. Um agente mal-intencionado pode passar a mesma conta duas vezes, fazendo com que ela seja alterada de maneiras não previstas (por exemplo, sobrescrevendo dados). A gravidade dessa vulnerabilidade varia conforme o cenário específico.

Exemplo de cenário

Considere um programa criado para recompensar usuários com base na participação deles em determinada atividade on-chain. O programa tem uma instrução para atualizar o saldo de duas contas: uma conta de recompensa e uma conta de bônus. Um usuário deve receber uma recompensa padrão em uma conta e um possível bônus em outra, com base em critérios específicos predeterminados:

Código
pub fn distribute_rewards(ctx: Context<DistributeRewards>, reward_amount: u64, bonus_amount: u64) -> Result<()> {
    let reward_account = &mut ctx.accounts.reward_account;
    let bonus_reward = &mut ctx.accounts.bonus_account;

    // Intended to increment the reward and bonus accounts separately
    reward_account.balance += reward_amount;
    bonus_account.balance += bonus_amount;

    Ok(())
}

#[derive(Accounts)]
pub struct DistributeRewards<'info> {
    #[account(mut)]
    reward_account: Account<'info, RewardAccount>,
    #[account(mut)]
    bonus_account: Account<'info, RewardAccount>,
}

#[account]
pub struct RewardAccount {
    pub balance: u64,
}

Se um agente mal-intencionado passar a mesma conta como reward_account e bonus_account, o saldo da conta será atualizado incorretamente duas vezes.

Mitigação recomendada

Para mitigar esse problema, adicione uma verificação à lógica da instrução para confirmar que as chaves públicas das duas contas não são idênticas:

Código
pub fn distribute_rewards(ctx: Context<DistributeRewards>, reward_amount: u64, bonus_amount: u64) -> Result<()> {
    if ctx.accounts.reward_account.key() == ctx.accounts.bonus_account.key() {
        return Err(ProgramError::InvalidArgument.into())
    }
    
    let reward_account = &mut ctx.accounts.reward_account;
    let bonus_reward = &mut ctx.accounts.bonus_account;

    // Intended to increment the reward and bonus accounts separately
    reward_account.balance += reward_amount;
    bonus_account.balance += bonus_amount;

    Ok(())
}

Os desenvolvedores podem usar as restrições de conta do Anchor para adicionar uma verificação mais explícita à conta. Isso pode ser feito usando o atributo #[account] e a palavra-chave constraint:

Código
pub fn distribute_rewards(ctx: Context<DistributeRewards>, reward_amount: u64, bonus_amount: u64) -> Result<()> {
    let reward_account = &mut ctx.accounts.reward_account;
    let bonus_reward = &mut ctx.accounts.bonus_account;

    // Intended to increment the reward and bonus accounts separately
    reward_account.balance += reward_amount;
    bonus_account.balance += bonus_amount;

    Ok(())
}

#[derive(Accounts)]
pub struct DistributeRewards<'info> {
    #[account(
        mut,
        constraint = reward_account.key() != bonus_account.key()
    )]
    reward_account: Account<'info, RewardAccount>,
    #[account(mut)]
    bonus_account: Account<'info, RewardAccount>,
}

#[account]
pub struct RewardAccount {
    pub balance: u64,
}

Front-running

A vulnerabilidade

Com a crescente popularidade dos agregadores de transações, o front-running é uma preocupação que deve ser levada a sério pelos protocolos criados na Solana. Com a remoção da mempool da Jito, tratamos aqui o front-running como a capacidade de um agente mal-intencionado manipular valores esperados e reais por meio de transações cuidadosamente elaboradas. 

Exemplo de cenário

Imagine um protocolo que processa compras e lances de um produto, armazenando as informações de preço do vendedor em uma conta chamada SellInfo:

Código
#[derive(Accounts)]
pub struct SellProduct<'info> {
  product_listing: Account<'info, ProductListing>,
  sale_token_mint: Account<'info, Mint>,
  sale_token_destination: Account<'info, TokenAccount>,
  product_owner: Signer<'info>,
  purchaser_token_source: Account<'info, TokenAccount>,
  product: Account<info, Product>
}

#[derive(Accounts)]
pub struct PurchaseProduct<'info> {
  product_listing: Account<'info, ProductListing>,
  token_destination: Account<'info, TokenAccount>,
  token_source: Account<'info, TokenAccount>,
  buyer: Signer<'info>,
  product_account: Account<'info, Product>,
  token_mint_sale: Account<'info, Mint>,
}

#[account]
pub struct ProductListing {
  sale_price: u64,
  token_mint: Pubkey,
  destination_token_account: Pubkey,
  product_owner: Pubkey,
  product: Pubkey,
}

Para comprar um Product anunciado, um comprador deve fornecer a conta ProductListing relacionada ao produto desejado. Mas e se o vendedor puder alterar o sale_price do anúncio?

Código
pub fn change_sale_price(ctx: Context<ChangeSalePrice>, new_price: u64) -> Result<()> {...}

Isso criaria uma oportunidade de front-running para o vendedor, especialmente se a transação de compra não incluísse verificações de expected_price para garantir que o comprador não pague mais do que o esperado pelo produto desejado. Se o comprador enviar uma transação para comprar determinado Product, o vendedor poderá chamar change_sale_price e, usando a Jito, garantir que essa transação seja incluída antes da transação do comprador. Um vendedor mal-intencionado poderia alterar o preço na conta ProductListing para um valor exorbitante sem o conhecimento do comprador, obrigando-o a pagar muito mais do que o esperado pelo Product!

Mitigação recomendada

Uma solução simples seria incluir verificações de expected_price no lado do comprador, impedindo-o de pagar mais do que o esperado pelo Product que deseja comprar:

Código
pub fn purchase_product(ctx: Context<PurchaseProduct>, expected_price: u64) -> Result<()> {
  assert!(ctx.accounts.product_listing.sale_price <= expected_price);
  ...
}

Inicialização insegura

Diferentemente dos contratos implantados na EVM, os programas da Solana não são implantados com um construtor para definir variáveis de estado. Em vez disso, eles são inicializados manualmente, normalmente por uma função chamada initialize ou algo semelhante. As funções de inicialização geralmente definem dados como a autoridade do programa ou criam contas que formam a base do programa implantado, como uma conta de estado central ou algo semelhante.

Como a função de inicialização é chamada manualmente, e não de forma automática durante a implantação do programa, essa instrução deve ser chamada por um endereço conhecido e controlado pela equipe de desenvolvimento do programa. Caso contrário, um invasor poderá antecipar a inicialização por meio de front-running e possivelmente configurar o programa usando contas sob seu controle.

Uma prática comum é usar a upgrade_authority do programa como o endereço autorizado a chamar a função initialize, caso o programa tenha uma autoridade de atualização.

Exemplo inseguro e como mitigá-lo

Código
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
  ctx.accounts.central_state.authority = authority.key();
  ...  
}

#[derive(Accounts)]
pub struct Initialize<'info> {
  authority: Signer<'info>,
  #[account(mut,
    init,
    payer = authority,
    space = CentralState::SIZE,
    seeds = [b"central_state"],
    bump
  )]
  central_state: Account<'info, CentralState>,
  ...
}

#[account]
pub struct CentralState {
  authority: Pubkey,
  ...
}

O exemplo acima é uma função initialize simplificada que define como autoridade de uma conta CentralState o chamador da instrução. No entanto, qualquer conta poderia chamar initialize! Como mencionado anteriormente, uma forma comum de proteger uma função de inicialização é usar a upgrade_authority do programa, conhecida no momento da implantação.

Veja abaixo um exemplo da documentação do Anchor, que usa constraint para garantir que somente a autoridade de atualização do programa possa chamar initialize:

Código
use anchor_lang::prelude::*;
use crate::program::MyProgram;

declare_id!("Cum9tTyj5HwcEiAmhgaS7Bbj4UczCwsucrCkxRECzM4e");

#[program]
pub mod my_program {
    use super::*;

    pub fn set_initial_admin(
        ctx: Context<SetInitialAdmin>,
        admin_key: Pubkey
    ) -> Result<()> {
        ctx.accounts.admin_settings.admin_key = admin_key;
        Ok(())
    }

    pub fn set_admin(...){...}

    pub fn set_settings(...){...}
}

#[account]
#[derive(Default, Debug)]
pub struct AdminSettings {
    admin_key: Pubkey
}

#[derive(Accounts)]
pub struct SetInitialAdmin<'info> {
    #[account(init, payer = authority, seeds = [b"admin"], bump)]
    pub admin_settings: Account<'info, AdminSettings>,
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(constraint = program.programdata_address()? == Some(program_data.key()))]
    pub program: Program<'info, MyProgram>,
    #[account(constraint = program_data.upgrade_authority_address == Some(authority.key()))]
    pub program_data: Account<'info, ProgramData>,
    pub system_program: Program<'info, System>,
}

Perda de precisão

A vulnerabilidade

A perda de precisão, embora possa parecer mínima, pode representar uma ameaça significativa para um programa. Ela pode causar cálculos incorretos, oportunidades de arbitragem e comportamentos inesperados do programa.

A perda de precisão em operações aritméticas é uma fonte comum de erros. Em programas da Solana, recomenda-se usar aritmética de ponto fixo sempre que possível. Isso ocorre porque os programas aceitam apenas um subconjunto limitado das operações de ponto flutuante do Rust. Se um programa tentar usar uma operação de ponto flutuante incompatível, o ambiente de execução retornará um erro de símbolo não resolvido. Além disso, as operações de ponto flutuante exigem mais instruções em comparação com suas equivalentes com números inteiros. O uso de aritmética de ponto fixo e a necessidade de processar com precisão grandes quantidades de tokens e valores fracionários podem agravar a perda de precisão.

Multiplicação após divisão

Embora a propriedade associativa seja válida para a maioria das operações matemáticas, sua aplicação na aritmética computacional pode causar uma perda inesperada de precisão. Um exemplo clássico ocorre ao realizar uma multiplicação depois de uma divisão, o que pode produzir resultados diferentes daqueles obtidos ao multiplicar antes de dividir. Por exemplo, considere as seguintes expressões: (a / c) * b e (a * b) / c. Matematicamente, essas expressões são associativas — elas deveriam produzir o mesmo resultado. No entanto, no contexto da Solana e da aritmética de ponto fixo, a ordem das operações importa muito. Realizar primeiro a divisão (a / c) pode causar perda de precisão se o quociente for arredondado para baixo antes de ser multiplicado por b. Isso pode gerar um resultado menor do que o esperado. Por outro lado, multiplicar (a * b) antes de dividir por c pode preservar uma parcela maior da precisão original. Essa diferença pode causar cálculos incorretos, comportamentos inesperados do programa e/ou oportunidades de arbitragem.

Funções aritméticas saturating_*

Embora as funções aritméticas saturating_* evitem overflow e underflow limitando os valores ao máximo ou mínimo possível, elas podem causar bugs sutis e perda de precisão caso esse limite seja atingido de forma inesperada. Isso ocorre quando a lógica do programa presume que a saturação, por si só, garantirá um resultado preciso e deixa de tratar a possível perda de precisão ou exatidão.

Por exemplo, imagine um programa criado para calcular e distribuir recompensas aos usuários com base na quantidade de tokens que eles negociam durante um período específico:

Código
pub fn calculate_reward(transaction_amount: u64, reward_multiplier: u64) -> u64 {
    transaction_amount.saturating_mul(reward_multiplier)
}

Considere um cenário em que transaction_amount seja de 100.000 tokens e reward_multiplier seja de 100 tokens por transação. Multiplicar os dois excederá o valor máximo que um u64 pode armazenar. Isso significa que o produto será limitado, causando uma perda substancial de precisão e recompensando o usuário abaixo do devido.

Erros de arredondamento

As operações de arredondamento são uma fonte comum de perda de precisão na programação. A escolha do método de arredondamento pode afetar significativamente a exatidão dos cálculos e o comportamento dos programas da Solana. A função try_round_u64() arredonda valores decimais para o número inteiro mais próximo. Arredondar para cima é problemático, pois pode inflar valores artificialmente e gerar discrepâncias entre os cálculos reais e esperados.

Considere um programa da Solana que converte garantias em liquidez com base nas condições do mercado. O programa usa try_round_u64() para arredondar o resultado de uma operação de divisão:

Código
pub fn collateral_to_liquidity(&self, collateral_amount: u64) -> Result<u64, ProgramError> {
    Decimal::from(collateral_amount)
        .try_div(self.0)?
        .try_round_u64()
}

Nesse cenário, o arredondamento para cima pode levar à emissão de mais tokens de liquidez do que o valor da garantia justifica. Agentes mal-intencionados podem explorar essa discrepância para realizar ataques de arbitragem e extrair valor do protocolo por meio de resultados de arredondamento manipulados a seu favor. Para mitigar o problema, use try_floor_u64 para arredondar para baixo até o número inteiro mais próximo. Essa abordagem minimiza o risco de inflar valores artificialmente e garante que o arredondamento não beneficie o usuário às custas do sistema. Como alternativa, implemente uma lógica para tratar cenários em que o arredondamento possa afetar explicitamente o resultado. Isso pode incluir a definição de limites específicos para decisões de arredondamento ou a aplicação de lógicas diferentes conforme a magnitude dos valores envolvidos.

Ausência de verificação de propriedade

A vulnerabilidade

As verificações de propriedade são essenciais para validar se o programa esperado é proprietário de uma conta envolvida em uma transação ou operação. As contas incluem um campo owner, que indica o programa com autoridade para gravar nos dados da conta. Esse campo garante que somente programas autorizados possam modificar o estado de uma conta. Além disso, ele é útil para garantir que as contas passadas para uma instrução pertençam ao programa esperado. A ausência de verificações de propriedade pode causar vulnerabilidades graves, incluindo transferências de fundos não autorizadas e a execução de operações privilegiadas.

Exemplo de cenário

Considere uma função de programa definida para permitir que somente administradores façam retiradas de um cofre. A função recebe uma conta de configuração, isto é, config, e usa seu campo admin para verificar se a chave pública da conta de administrador fornecida é igual à armazenada na conta config. No entanto, ela não verifica a propriedade da conta config, presumindo que seja confiável:

Código
pub fn admin_token_withdraw(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    // Account setup

    if config.admin != admin.pubkey() {
        return Err(ProgramError::InvalidAdminAccount)
    }

    // Transfer funds logic
}

Um agente mal-intencionado poderia explorar isso fornecendo uma conta config sob seu controle com um campo admin correspondente, enganando o programa para que execute a retirada.

Mitigação recomendada

Para mitigar esse problema, faça uma verificação de propriedade que valide o campo owner da conta:

Código
pub fn admin_token_withdraw(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    // Account setup

    if config.admin != admin.pubkey() {
        return Err(ProgramError::InvalidAdminAccount)
    }

    if config.owner != program_id {
        return Err(ProgramError::InvalidConfigAccount)
    }

    // Transfer funds logic
}

O Anchor simplifica essa verificação com o tipo Account. Account<'info, T> é um wrapper de AccountInfo que verifica a propriedade pelo programa e desserializa os dados subjacentes em T, isto é, no tipo de conta especificado. Isso permite que os desenvolvedores usem Account<'info, T> para validar facilmente a propriedade da conta. Eles também podem usar o atributo #[account] para adicionar o trait Owner a uma conta. Esse trait define o endereço que deve ser proprietário da conta. Além disso, os desenvolvedores podem usar a restrição owner para definir o programa que deve ser proprietário de determinada conta, caso seja diferente daquele em execução no momento. Isso é útil, por exemplo, ao escrever uma instrução que espera que uma conta seja um PDA derivado de outro programa. A restrição owner é definida como #[account(owner = <expr>)], em que <expr> é uma expressão arbitrária.

Contas somente leitura

É igualmente importante verificar a validade das contas especificadas como somente leitura no contexto de execução de um programa. Isso é essencial porque um agente mal-intencionado pode passar contas com dados arbitrários ou especialmente elaborados em vez de contas legítimas. Isso pode causar comportamentos inesperados ou prejudiciais no programa. Os desenvolvedores ainda devem realizar verificações para garantir que as contas das quais um programa precisa ler sejam autênticas e não tenham sido adulteradas. Isso pode envolver a verificação do endereço da conta em relação a valores conhecidos ou a confirmação de que o proprietário da conta é o esperado, especialmente para sysvars, isto é, contas de sistema somente leitura, como Clock ou EpochSchedule. Acesse sysvars usando o método get(), que dispensa verificações manuais de endereço ou propriedade. Essa é uma abordagem mais segura para acessar essas contas; no entanto, nem todas as sysvars aceitam o método get(). Nesse caso, acesse-as usando o endereço público correspondente.

Ausência de verificação de assinatura

A vulnerabilidade

As transações são assinadas com a chave privada de uma carteira para garantir autenticação, integridade, não repúdio e autorização de uma transação específica por uma carteira específica. Ao exigir que as transações sejam assinadas com a chave privada do remetente, o ambiente de execução da Solana pode verificar se a conta correta inicia uma transação e se ela não foi adulterada. Esse mecanismo sustenta a natureza sem necessidade de confiança das redes descentralizadas. Sem essa verificação, qualquer conta que forneça a conta correta como argumento pode executar uma transação. Isso pode causar acesso não autorizado a informações, fundos ou funcionalidades privilegiadas. Essa vulnerabilidade surge quando não se valida se uma operação foi assinada pela chave privada da conta apropriada antes da execução de determinadas funcionalidades privilegiadas.

Exemplo de cenário

Considere a seguinte função:

Código
pub fn update_admin(program_id: &Pubkey, accounts &[AccountInfo]) -> ProgramResult {
    let account_iter = &mut accounts.iter();
    let config = ConfigAccount::unpack(next_account_info(account_iter)?)?;
    let admin = next_account_info(account_iter)?;
    let new_admin = next_account_info (account_iter)?;

    if admin.pubkey() != config.admin {
        return Err(ProgramError::InvalidAdminAccount);
    }

    config.admin = new_admin.pubkey();

    Ok(())
}

Essa função pretende atualizar o administrador do programa. Ela inclui uma verificação para garantir que o administrador atual inicie a operação, o que representa um bom controle de acesso. No entanto, a função não verifica se a chave privada do administrador atual assinou a transação. Assim, qualquer pessoa que chame essa função pode passar a conta admin correta de modo que admin.pubkey() = config.admin, independentemente de a conta que chama a função ser realmente a do administrador atual. Isso permite que um agente mal-intencionado execute a instrução passando sua própria conta como o novo administrador, ignorando diretamente a necessidade de autorização do administrador atual.

Mitigação recomendada

Os programas devem incluir verificações para confirmar que uma conta foi assinada pela carteira apropriada. Isso pode ser feito verificando o campo AccountInfo::is_signer das contas envolvidas na transação. O programa pode garantir que somente contas autorizadas executem determinadas ações verificando se a conta que realiza a operação privilegiada tem a flag is_signer definida como true.

O exemplo de código atualizado ficaria assim:

Código
pub fn update_admin(program_id: &Pubkey, accounts &[AccountInfo]) -> ProgramResult {
    let account_iter = &mut accounts.iter();
    let config = ConfigAccount::unpack(next_account_info(account_iter)?)?;
    let admin = next_account_info(account_iter)?;
    let new_admin = next_account_info (account_iter)?;

    if admin.pubkey() != config.admin {
        return Err(ProgramError::InvalidAdminAccount);
    }

    // Add in a check for the admin's signature
    if !admin.is_signer {
        return Err(ProgramError::NotSigner);
    }

    config.admin = new_admin.pubkey();

    Ok(())
}

O Anchor simplifica todo esse processo com o tipo de conta Signer<’info>.

Overflow e underflow

A vulnerabilidade

Um inteiro é um número sem componente fracionário. Rust armazena inteiros como variáveis de tamanho fixo. Essas variáveis são definidas pelo seu sinal (ou seja, com ou sem sinal) e pela quantidade de espaço que ocupam na memória. Por exemplo, o tipo u8 representa um inteiro sem sinal que ocupa 8 bits de espaço. Ele pode armazenar valores de 0 a 255. Armazenar um valor fora desse intervalo resultaria em overflow ou underflow de inteiro. Um overflow de inteiro ocorre quando uma variável excede sua capacidade máxima e retorna ao valor mínimo. Um underflow de inteiro ocorre quando uma variável fica abaixo da capacidade mínima e retorna ao valor máximo.

Rust inclui verificações de overflow e underflow de inteiros ao compilar no modo de depuração. Essas verificações fazem o programa entrar em panic durante a execução se essa condição for detectada. No entanto, Rust não inclui verificações que causam panic em caso de overflow ou underflow de inteiros ao compilar no modo de release com a flag --release. Esse comportamento pode introduzir vulnerabilidades sutis, pois o overflow ou underflow ocorre silenciosamente. O conjunto de ferramentas Berkeley Packet Filter (BPF) é parte essencial do ambiente de desenvolvimento da Solana, pois compila programas da Solana. O comando cargo build-bpf compila projetos Rust em bytecode BPF para implantação. O problema é que, por padrão, ele compila os programas no modo de release. Portanto, programas da Solana são vulneráveis a overflow e underflow de inteiros.

Cenário de exemplo

Um invasor pode explorar essa vulnerabilidade aproveitando o comportamento silencioso de overflow/underflow no modo de release, especialmente em funções que processam saldos de tokens. Veja o exemplo a seguir:

Código
pub fn process_instruction(
    _program_id: & Pubkey,
    accounts: [&AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let account = next_account_info(account_info_iter)?;

    let mut balance: u8 = account.data.borrow()[0];
    let tokens_to_subtract: u8 = 100;

    balance = balance - tokens_to_subtract;

    account.data.borrow_mut()[0] = balance;
    msg!("Updated balance to {}", balance);
    
    Ok(())
}

Para simplificar, essa função pressupõe que o saldo esteja armazenado no primeiro byte. Ela usa o saldo da conta e subtrai tokens_to_subtract. Se o saldo do usuário for menor que tokens_to_subtract, ocorrerá um underflow. Por exemplo, o saldo de um usuário com 10 tokens sofreria underflow e passaria para um total de 165 tokens

Mitigação recomendada

overflow-checks

A maneira mais fácil de mitigar essa vulnerabilidade é definir a chave overflow-checks como true no arquivo Cargo.toml do projeto. Assim, Rust adicionará verificações de overflow e underflow ao compilador. No entanto, adicionar essas verificações aumenta o custo computacional de uma transação. Nos casos em que for necessário otimizar o uso de recursos computacionais, pode ser mais vantajoso definir overflow-checks como false.

Aritmética checked_*

Use as funções aritméticas checked_* do Rust em cada tipo de inteiro para verificar estrategicamente a ocorrência de overflows e underflows em todo o programa. Essas funções retornarão None se ocorrer um overflow ou underflow. Isso permite que o programa trate o erro de forma adequada. Por exemplo, você pode refatorar o código anterior para:

Código
pub fn process_instruction(
    _program_id: & Pubkey,
    accounts: [&AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let account = next_account_info(account_info_iter)?;

    let mut balance: u8 = account.data.borrow()[0];
    let tokens_to_subtract: u8 = 100;

    match balance.checked_sub(tokens_to_subtract) {
        Some(new_balance) => {
            account.data.borrow_mut()[0] = new_balance;
            msg!("Updated balance to {}", new_balance);
        },
        None => {
            return Err(ProgramErrorr::InsufficientFunds);
        }
    }

    Ok(())
}

No exemplo revisado, checked_sub é usado para subtrair tokens_to_subtract de balance. Assim, se balance for suficiente para realizar a subtração, checked_sub retornará Some(new_balance). O programa continuará atualizando com segurança o saldo da conta e o registrará no log. No entanto, se a subtração resultar em underflow, checked_sub retornará None, o que pode ser tratado retornando um erro.

Macro Checked Math

Checked Math é uma macro procedural para alterar as propriedades de verificação de expressões matemáticas sem, na maioria dos casos, modificar essas expressões. O problema das funções aritméticas checked_* é a perda da notação matemática. Em vez disso, métodos trabalhosos como a.checked_add(b).unwrap() precisam ser usados no lugar de a + b. Por exemplo, se quisermos escrever (x * y) + z usando as funções aritméticas verificadas, escreveríamos x.checked_mul(y).unwrap().checked_add(z).unwrap().

Em vez disso, usando a macro Checked Math, a expressão ficaria assim:

Código
use checked_math::checked_math as cm;

cm!((x * y) + z).unwrap()

Isso é mais conveniente de escrever, preserva a notação matemática da expressão e requer apenas um .unwrap(). Isso ocorre porque a macro converte expressões matemáticas normais em uma expressão que retorna None se qualquer uma das etapas verificadas retornar None. Em caso de sucesso, Some(_) é retornado, por isso fazemos o unwrap da expressão no final.

Conversão de tipos

Da mesma forma, converter tipos de inteiro usando a palavra-chave as sem as verificações adequadas pode introduzir uma vulnerabilidade de overflow ou underflow de inteiro. Isso ocorre porque a conversão pode truncar ou estender valores de formas não intencionais. Ao converter um tipo de inteiro maior em um menor (por exemplo, u64 em u32), Rust truncará os bits mais altos do valor original que não couberem no tipo de destino. Isso é problemático quando o valor original excede o valor máximo que o tipo de destino pode armazenar. Ao converter um tipo de inteiro menor em um maior (por exemplo, i16 em i32), Rust estenderá o valor. Isso é simples para tipos sem sinal. No entanto, com inteiros com sinal, pode ocorrer uma extensão de sinal que introduza valores negativos não intencionais.

Mitigação recomendada

Use os métodos seguros de conversão de tipos do Rust para mitigar essa vulnerabilidade. Isso inclui métodos como try_from e from. O uso de try_from retorna um tipo Result, permitindo tratar explicitamente e de forma adequada os casos em que o valor não cabe no tipo de destino. O método from do Rust pode ser usado para uma conversão implícita e segura quando há garantia de que não haverá perda (por exemplo, de u8 para u32). Por exemplo, suponha que um programa precise converter com segurança uma quantidade de tokens do tipo u64 para o tipo u32 a fim de processá-la. Nesse caso, ele pode fazer o seguinte:

Código
pub fn convert_token_amount(amount: u64) -> Result<u32, ProgramError> {
    u32::try_from(amount).map_err(|_| ProgramError::InvalidArgument)
}

Neste exemplo, se amount exceder o valor máximo que um u32 pode armazenar (ou seja, 4 294 967 295), a conversão falhará e o programa retornará um erro. Isso evita a ocorrência de um possível overflow/underflow.

Compartilhamento de PDA

A vulnerabilidade

O compartilhamento de PDA é uma vulnerabilidade comum que ocorre quando o mesmo PDA é usado em vários domínios de autoridade ou funções. Isso pode permitir que um agente mal-intencionado acesse dados ou fundos que não lhe pertencem usando indevidamente PDAs como signatários sem que existam verificações adequadas de controle de acesso.

Cenário de exemplo

Considere um programa criado para facilitar o staking de tokens e a distribuição de recompensas. O programa usa um único PDA para transferir tokens para um determinado pool e sacar recompensas. O PDA é derivado usando uma seed estática (por exemplo, o nome do pool de staking), o que o torna comum a todas as operações:

Código
pub fn stake_tokens(ctx: Context<StakeTokens>, amount: u64) -> ProgramResult {
    // Logic to stake tokens
    Ok(())
}

pub fn withdraw_rewards(ctx: Context<WithdrawRewards>, amount: u64) -> ProgramResult {
    // Logic to withdraw rewards
    Ok(())
}

#[derive(Accounts)]
pub struct StakeTokens<'info> {
    #[account(
        mut,
        seeds = [b"staking_pool_pda"],
        bump
    )]
    staking_pool: AccountInfo<'info>,
    // Other staking-related accounts
}

#[derive(Accounts)]
pub struct WithdrawRewards<'info> {
    #[account(
        mut,
        seeds = [b"staking_pool_pda"],
        bump
    )]
    rewards_pool: AccountInfo<'info>,
    // Other rewards withdrawal-related accounts
}

Isso é problemático porque as funcionalidades de staking e saque de recompensas dependem do mesmo PDA derivado de staking_pool_pda. Isso pode permitir que usuários manipulem o contrato para sacar recompensas sem autorização ou manipular o staking.

Mitigação recomendada

Para mitigar essa vulnerabilidade, use PDAs distintos para funcionalidades diferentes. Garanta que cada PDA atenda a um contexto específico e seja derivado usando seeds exclusivas e específicas de cada operação:

Código
pub fn stake_tokens(ctx: Context<StakeTokens>, amount: u64) -> ProgramResult {
    // Logic to stake tokens
    Ok(())
}

pub fn withdraw_rewards(ctx: Context<WithdrawRewards>, amount: u64) -> ProgramResult {
    // Logic to withdraw rewards
    Ok(())
}

#[derive(Accounts)]
pub struct StakeTokens<'info> {
    #[account(
        mut,
        seeds = [b"staking_pool", &staking_pool.key().as_ref()],
        bump
    )]
    staking_pool: AccountInfo<'info>,
    // Other staking-related accounts
}

#[derive(Accounts)]
pub struct WithdrawRewards<'info> {
    #[account(
        mut,
        seeds = [b"rewards_pool", &rewards_pool.key().as_ref()],
        bump
    )]
    rewards_pool: AccountInfo<'info>,
    // Other rewards withdrawal-related accounts
}

No exemplo acima, os PDAs para fazer staking de tokens e sacar recompensas são derivados usando seeds distintas (staking_pool e rewards_pool, respectivamente), combinadas com a chave da conta específica. Isso garante que os PDAs estejam vinculados exclusivamente às funcionalidades pretendidas, mitigando o risco de ações não autorizadas.

Contas restantes

A vulnerabilidade

ctx.remaining_accounts permite passar para uma função contas adicionais que não foram inicialmente especificadas na struct Accounts. Isso dá mais flexibilidade ao desenvolvedor e permite lidar com cenários que exigem um número dinâmico de contas (ou seja, processar um número variável de usuários ou interagir com programas diferentes). No entanto, essa maior flexibilidade traz uma ressalva: as contas passadas por ctx.remaining_accounts não passam pela mesma validação aplicada às contas definidas na struct Accounts. Como ctx.remaining_accounts não valida as contas fornecidas, um agente mal-intencionado pode explorar isso passando contas com as quais o programa não deveria interagir, resultando em ações ou acessos não autorizados.

Cenário de exemplo

Considere um programa de recompensas que usa ctx.remaining_accounts para receber PDAs de usuários e calcular recompensas dinamicamente:

Código
pub fn calculate_rewards(ctx: Context<CalculateRewards>) -> Result<()> {
    let rewards_account = &ctx.accounts.rewards_account;
    let authority = &ctx.accounts.authority;

    // Iterate over accounts passed in via ctx.remaining_accounts
    for user_pda_info in ctx.remaining_accounts.iter() {
        // logic to check user activity and calculate rewards
    }

    // Logic to distribute calculated rewards

    Ok(())
}

#[derive(Accounts)]
pub struct CalculateRewards<'info> {
    #[account(mut)]
    pub rewards_account: Account<'info, RewardsAccount>,
    pub authority : Signer<'info>,
}

#[account]
pub struct RewardsAccount {
    pub total_rewards: u64,
    // Other relevant fields
}

O problema é que não há verificações explícitas para validar as contas passadas por ctx.remaining_accounts. Portanto, não há garantia de que somente contas válidas de usuários elegíveis sejam processadas no cálculo e na distribuição das recompensas. Assim, um agente mal-intencionado pode fornecer contas que não possui ou que ele mesmo criou para receber mais recompensas do que realmente deveria.

Mitigação recomendada

Para mitigar essa vulnerabilidade, os desenvolvedores devem verificar manualmente a validade de cada conta dentro da função. Isso inclui verificar o proprietário da conta para garantir que corresponda ao usuário esperado e validar todos os dados relevantes na conta. Ao incorporar essas verificações manuais, os desenvolvedores podem aproveitar a flexibilidade de ctx.remaining_acocunts e, ao mesmo tempo, mitigar o risco de acesso ou manipulação não autorizados.

Erros específicos do Rust

Rust é a lingua franca do desenvolvimento de programas na Solana. Desenvolver em Rust apresenta um conjunto específico de desafios e considerações, especialmente em relação a código inseguro e erros específicos do Rust. Entender as particularidades do Rust ajuda a desenvolver programas seguros, eficientes e confiáveis.

Rust inseguro

Rust é reconhecido por suas garantias de segurança de memória, obtidas por meio de um sistema rigoroso de propriedade e empréstimo. No entanto, essas garantias podem, às vezes, atrapalhar. Por isso, Rust oferece a palavra-chave unsafe para ignorar verificações de segurança. O Rust unsafe é usado em quatro contextos principais:

  • Funções inseguras: funções que executam operações capazes de violar as garantias de segurança do Rust devem ser marcadas com a palavra-chave unsafe. Por exemplo, unsafe fn dangerous_function() {}
  • Blocos inseguros: blocos de código nos quais operações inseguras são permitidas. Por exemplo, unsafe { // Unsafe operations }
  • Traits inseguras: traits que implicam determinadas invariantes que o compilador não consegue verificar. Por exemplo, unsafe trait BadTrait {}
  • Implementação de traits inseguras: as implementações de traits unsafe também devem ser marcadas como unsafe. Por exemplo, unsafe impl UnsafeTrait for UnsafeType {}

O Rust inseguro existe porque a análise estática é conservadora. Quando o compilador tenta determinar se o código mantém determinado conjunto de garantias, é melhor rejeitar alguns casos de código válido do que aceitar alguns casos de código inválido. Embora o código possa funcionar perfeitamente, o compilador Rust o rejeitará se não tiver informações suficientes para determinar com confiança se ele mantém as garantias de segurança do Rust. O código inseguro permite que os desenvolvedores ignorem essas verificações por sua própria conta e risco. Além disso, o hardware dos computadores é inerentemente inseguro. Os desenvolvedores precisam ter permissão para executar operações inseguras a fim de realizar programação de baixo nível com Rust.

Com a palavra-chave unsafe, os desenvolvedores podem:

  • Desreferenciar ponteiros brutos: permite acesso direto à memória por meio de ponteiros brutos que podem apontar para qualquer local da memória, o qual pode não conter dados válidos
  • Chamar funções inseguras: essas funções podem não cumprir as garantias de segurança do Rust e podem resultar em comportamento potencialmente indefinido
  • Acessar variáveis estáticas mutáveis: um estado global mutável pode causar condições de corrida de dados

A melhor forma de mitigar os riscos do Rust inseguro é minimizar o uso de blocos unsafe. Se o código unsafe for absolutamente necessário, seja qual for o motivo, garanta que ele esteja bem documentado, seja auditado regularmente e, se possível, esteja encapsulado em uma abstração segura que possa ser disponibilizada ao restante do programa.

Panics e gerenciamento de erros

Um panic ocorre quando um programa Rust encontra um erro irrecuperável e encerra sua execução. Panics são usados para erros inesperados que não deveriam ser capturados. No contexto de programas da Solana, um panic pode causar um comportamento inesperado, pois o runtime espera que os programas tratem os erros de forma adequada, sem falhar.

Quando ocorre um panic, Rust começa a desenrolar e limpar a pilha. Isso retorna um rastreamento de pilha com informações detalhadas sobre o erro. Essas informações podem revelar a um invasor detalhes sobre a estrutura de arquivos subjacente. Embora isso não se aplique diretamente aos programas da Solana, as dependências usadas por um programa podem estar vulneráveis a esse tipo de ataque. Mantenha as dependências atualizadas e use versões que não contenham vulnerabilidades conhecidas.

Cenários comuns de panic incluem:

  • Divisão por zero: Rust entrará em panic ao tentar dividir por zero. Portanto, sempre verifique se o divisor é zero antes de realizar uma divisão
  • Índice de array fora dos limites: acessar um array com um índice que exceda seus limites causará um panic. Para mitigar isso, use métodos que retornem um tipo Option (como get) para acessar elementos do array com segurança
  • Unwrap de valores None: chamar .unwrap() em um Option que contenha um valor None causará um panic. Sempre use correspondência de padrões ou métodos como unwrap_or, unwrap_or_else ou o operador ? em funções que retornam um Result

Para mitigar problemas associados a panics, é essencial evitar operações que causem panics, validar todas as entradas e condições que possam dar origem a operações problemáticas e usar os tipos Result e Option no tratamento de erros. Além disso, escrever testes abrangentes para o programa ajudará a identificar e corrigir possíveis cenários de panic antes da implantação.

Colisões de seeds

A vulnerabilidade

Colisões de seeds ocorrem quando diferentes entradas (ou seja, seeds e IDs de programa) usadas para gerar um PDA resultam no mesmo endereço de PDA. Isso é problemático quando PDAs são usados em um programa para diferentes finalidades, pois pode causar comportamentos inesperados, incluindo ataques de negação de serviço ou comprometimento total.

Cenário de exemplo

Considere um programa para uma plataforma descentralizada de votação em diferentes propostas e iniciativas. Cada sessão de votação de determinada proposta ou iniciativa é criada com um identificador exclusivo, e os usuários enviam votos. O programa usa PDAs tanto para as sessões de votação quanto para os votos individuais:

Código
// Creating a Voting Session PDA
#[derive(Accounts)]
#[instruction(session_id: String)]
pub struct CreateVotingSession<'info> {
    #[account(mut)]
    pub organizer: Signer<'info>,
    #[account(
        init,
        payer = organizer,
        space = 8 + Product::SIZE,
        seeds = [b"session", session_id.as_bytes()],
    )]
    pub voting_session: Account<'info, VotingSession>,
    pub system_program: Program<'info, System>,
}

// Submitting a Vote PDA
#[derive(Accounts)]
#[instruction(session_id: String)]
pub struct SubmitVote<'info> {
    #[account(mut)]
    pub voter: Signer<'info>,
    #[account(
        init,
        payer = voter,
        space = 8 + Vote::SIZE,
        seeds = [session_id.as_bytes(), voter.key().as_ref()]
    )]
    pub vote: Account<'info, Vote>,
    pub system_program: Program<'info, System>,
}

Nesse cenário, um invasor tentaria criar cuidadosamente uma sessão de votação que, ao ser combinada com a seed estática "session", resultasse em um PDA que, por coincidência, correspondesse ao PDA gerado para uma sessão de votação diferente. Criar deliberadamente um PDA que entre em conflito com o PDA de outra sessão de votação pode interromper as operações da plataforma. Isso poderia, por exemplo, impedir votos legítimos em propostas ou evitar que novas iniciativas fossem adicionadas à plataforma, já que o runtime da Solana não consegue distinguir entre os PDAs em colisão.

Mitigação recomendada

Para mitigar o risco de colisões de seeds, os desenvolvedores podem:

  • Usar prefixos exclusivos nas seeds de diferentes PDAs do mesmo programa. Essa abordagem ajudará a garantir que os PDAs permaneçam distintos
  • Usar identificadores exclusivos (por exemplo, timestamps, IDs de usuário e valores de nonce) para garantir que um PDA exclusivo seja gerado todas as vezes
  • Validar programaticamente que um PDA gerado não entre em colisão com PDAs existentes

Type cosplay

A vulnerabilidade

Type cosplay é uma vulnerabilidade em que um tipo de conta é representado incorretamente como outro devido à ausência de verificações de tipo durante a desserialização. Isso pode levar à execução de ações não autorizadas ou à corrupção de dados, pois o programa operaria com base em uma suposição incorreta sobre a função ou as permissões da conta. Sempre verifique explicitamente o tipo pretendido da conta durante a desserialização.

Cenário de exemplo

Considere um programa que gerencia o acesso a operações administrativas com base na função de um usuário. Cada conta de usuário inclui um discriminador de função para distinguir usuários comuns de administradores. O programa contém uma função para atualizar configurações administrativas, destinada apenas a administradores. No entanto, o programa não verifica o discriminador da conta e desserializa os dados da conta do usuário sem confirmar se a conta pertence a um administrador:

Código
pub fn update_admin_settings(ctx: Context<UpdateSettings>) -> ProgramResult {
    // Deserialize without checking the discriminator
    let user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();

    // Sensitive update logic

    msg!("Admin settings updated by: {}", user.authority)
    Ok(())
}

#[derive(Accounts)]
pub struct UpdateSettings<'info> {
    user: AccountInfo<'info>
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
    authority: Pubkey,
}

O problema é que update_admin_settings desserializa a conta de usuário fornecida sem verificar o discriminador de função da conta, em parte porque a struct User não possui um campo discriminador!

Mitigação recomendada

Para mitigar esse problema, os desenvolvedores podem introduzir um campo discriminador na struct User e verificá-lo durante o processo de desserialização:

Código
pub fn update_admin_settings(ctx: Context<UpdateSettings>) -> ProgramResult {
    let user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();

    // Verify the user's discriminator
    if user.discriminant != AccountDiscriminant::Admin {
        return Err(ProgramError::InvalidAccountData.into())
    }
    
    // Sensitive update logic

    msg!("Admin settings updated by: {}", user.authority)
    Ok(())
}

#[derive(Accounts)]
pub struct UpdateSettings<'info> {
    user: AccountInfo<'info>
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
    discriminant: AccountDiscriminant,
    authority: Pubkey,
}

#[derive(BorshSerialize, BorshDeserialize, PartialEq)]
pub enum AccountDiscriminant {
    Admin,
    // Other account types
}

Anchor simplifica a mitigação de vulnerabilidades de type cosplay ao gerenciar automaticamente os discriminadores dos tipos de conta. Isso é feito por meio do wrapper Account<'info, T>, com o qual Anchor garante a segurança de tipos verificando automaticamente o discriminador durante a desserialização. Isso permite que os desenvolvedores se concentrem mais na lógica de negócios do programa, em vez de implementar manualmente várias verificações de tipo.

Conclusão

A importância da segurança de programas não pode ser subestimada. Este artigo abordou diversas vulnerabilidades comuns, desde erros específicos do Rust até as complexidades do método realloc do Anchor. O caminho para dominar cada uma dessas vulnerabilidades e a segurança de programas em geral é contínuo e exige aprendizado, adaptação e colaboração constantes. Como desenvolvedores, nosso compromisso com a segurança não se limita a proteger ativos; trata-se de promover a confiança, garantir a integridade das nossas aplicações e contribuir para o crescimento e a estabilidade da Solana.

Se você leu até aqui, nosso muito obrigado, anon! Insira seu endereço de e-mail abaixo para não perder nenhuma novidade sobre a Solana. Quer se aprofundar? Confira os artigos mais recentes no blog da Helius e continue hoje mesmo sua jornada na Solana.

Recursos adicionais

Assine a Helius

Acompanhe as novidades mais recentes do desenvolvimento Solana e receba atualizações quando publicarmos

Imagem ampliada