Ir para o conteúdo

Ataque de reentrância

Um ataque de reentrância usa uma chamada externa ou callback para reentrar na lógica de um contrato enquanto o estado está inconsistente. Explica caminhos, defesas e limites da revisão.

Atualizado

Apenas para fins educacionais; não constitui recomendação de investimento. Uma falha de reentrância pode causar perda rápida e irreversível dos ativos do contrato, e nenhuma defesa ou auditoria isolada comprova que um contrato é seguro.

Resposta direta

Um ataque de reentrância pode ocorrer quando a lógica de um contrato faz uma chamada externa e o destinatário chama de volta antes que a execução original termine e restaure suas invariantes. Se saldos, cotas, permissões ou preços antigos continuarem visíveis, o callback pode repetir uma ação ou influenciar outra usando um estado inconsistente.

O callback não precisa reentrar na mesma função nem transferir moeda nativa. Hooks de tokens, callbacks de receptores NFT, chamadas a cofres ou estratégias e callbacks de empréstimos-relâmpago podem transferir o controle a código não confiável. A reentrância pode atravessar funções, contratos e módulos; a reentrância somente de leitura também pode fazer outro protocolo consumir um valor transitório.

Como funciona

Uma exploração típica segue esta sequência:

  1. O atacante entra em uma função que altera estado e passa nas verificações iniciais.
  2. O contrato vulnerável chama um endereço, token ou protocolo antes de concluir sua contabilidade.
  3. O código controlado pelo atacante entra no contrato original ou conectado enquanto o estado antigo continua visível.
  4. A chamada aninhada repete um efeito ou muda o estado compartilhado, e a execução retorna com uma invariante violada.

A transferência externa de controle pode ser explícita, como uma chamada de baixo nível, ou ficar oculta em transferência de tokens, hook do receptor, mint seguro, adaptador de estratégia ou interface de chamada arbitrária. Um revert normalmente desfaz a árvore de chamadas afetada, mas o atacante pode montar uma sequência aninhada bem-sucedida que retorna normalmente após extrair valor ou corromper a contabilidade.

As defesas devem ser combinadas:

  • Aplicar verificações-efeitos-interações: primeiro validar, depois registrar todos os efeitos internos relevantes e por último interagir externamente.
  • Usar uma proteção em todo ponto de entrada que compartilhe a invariante protegida, não apenas na função com a chamada evidente.
  • Preferir resgates iniciados pelo beneficiário quando apropriado e reduzir chamadas a tokens, receptores, hooks, proxies e integrações não confiáveis.
  • Definir invariantes entre contratos e testar callbacks maliciosos, caminhos entre funções, multicalls aninhadas e consumidores somente de leitura.
  • Combinar revisão da implementação com monitoramento, pausa e resposta a incidentes quando o projeto permitir.

Exemplo

Considere um cofre que envia valor e só zera o saldo do usuário depois da chamada:

function withdraw() external {
    uint256 amount = balances[msg.sender];
    require(amount > 0, "empty balance");

    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok, "transfer failed");

    balances[msg.sender] = 0;
}

O receptor obtém controle enquanto balances[msg.sender] ainda contém o valor antigo. Sua função receptora pode chamar withdraw() novamente, passar na mesma verificação e pedir outra transferência. Atualizar o saldo antes da chamada fecha essa janela; uma proteção pode rejeitar entrada aninhada. Nenhuma mudança comprova a segurança do sistema inteiro, pois outro ponto de entrada ou contrato conectado pode expor a mesma invariante incompleta.

Riscos

  • Uma proteção cobre uma função enquanto outra expõe o mesmo estado.
  • A ordem local está correta, mas uma invariante entre contratos permanece inconsistente durante o callback.
  • Um token, receptor, callback de empréstimo-relâmpago ou adaptador de estratégia executa código externo inesperadamente.
  • Uma função view publica preço ou taxa transitória que outro protocolo usa na mesma transação.
  • Uma atualização, módulo ou alteração no layout de armazenamento ignora ou corrompe o bloqueio original.
  • Os testes cobrem retirada recursiva, mas omitem caminhos entre funções, contratos e somente de leitura.

Para usuários, um selo de auditoria ou proteção contra reentrância evidencia um controle, não uma garantia. Atualizações, integrações e ações de emergência privilegiadas podem alterar a superfície de ataque. Limite aprovações e exposição, confira a implementação implantada e não presuma que perdas possam ser revertidas.

Equívocos comuns

  • Reentrância é apenas repetir uma função de retirada. Ela pode entrar em outra função ou contrato, ou expor dados inconsistentes a um leitor.
  • Só transferências de moeda nativa acionam callbacks. Padrões de tokens, hooks de receptores e integrações também podem executar código externo.
  • Uma proteção ou verificações-efeitos-interações torna o contrato seguro. Escopo, pontos de entrada compartilhados e invariantes entre contratos ainda exigem revisão.
  • Uma auditoria bem-sucedida elimina a reentrância. Auditorias têm escopo limitado; mudanças posteriores e integrações não revisadas podem criar novos caminhos.

Tópicos relacionados

Fontes

Navegação

Pesquisar na wiki...