Um tipo comum de vulnerabilidade em smart contracts: Reentrância e como evitá‑la

Um tipo comum de vulnerabilidade em smart contracts: Reentrância e como evitá‑la

Os smart contracts são programas autoexecutáveis que rodam em blockchains como o Ethereum. Embora ofereçam transparência e automação, eles também trazem novos desafios de segurança. Entre as vulnerabilidades mais estudadas e exploradas pelos atacantes está a reentrância, um problema que já causou perdas de milhões de dólares, como no caso do DAO hack em 2016.

Este artigo aprofunda o conceito de reentrância, explica como ela funciona, apresenta exemplos práticos e, sobretudo, oferece um guia passo‑a‑passo para prevenir essa vulnerabilidade em seus contratos.

1. O que é reentrância?

Reentrância ocorre quando um contrato chama uma função externa que, por sua vez, chama de volta (re‑entra) a função original antes que o estado interno tenha sido completamente atualizado. Essa chamada recursiva permite que o invasor manipule o contrato, geralmente retirando fundos de forma indevida.

Em termos simples, imagine que o contrato A tem uma função withdraw() que envia Ether para o usuário e depois atualiza o saldo. Se o endereço do usuário for um contrato malicioso que tem um fallback function, ele pode chamar withdraw() novamente antes que o saldo seja reduzido, drenando o contrato.

2. Como a reentrância foi explorada na prática

O caso mais famoso foi o ataque ao DAO em 2016, que resultou em um roubo de cerca de 3,6 milhões de ETH. O atacante utilizou um contrato que chamava repetidamente a função de retirada enquanto o saldo ainda não havia sido atualizado, esgotando os fundos disponíveis.

Outro exemplo recente ocorreu em um contrato DeFi que permitia empréstimos flash. O invasor criou um contrato que, ao receber o empréstimo, chamava a função de retirada antes que o contrato registrasse o pagamento, gerando lucro ilícito.

3. Identificando pontos vulneráveis

Os desenvolvedores devem ficar atentos a três padrões que favorecem a reentrância:

  1. Chamadas externas antes da atualização de estado: sempre atualize variáveis críticas antes de enviar Ether ou chamar outro contrato.
  2. Uso de call() sem verificação adequada: call() é a forma mais baixa de interação e pode executar código arbitrário no contrato de destino.
  3. Falta de restrição de reentrância: não usar mecanismos como mutex ou reentrancy guard deixa o contrato vulnerável.

4. Boas práticas para prevenir a reentrância

A seguir, um checklist de segurança que você pode aplicar imediatamente:

Um tipo comum de vulnerabilidade em smart contracts - here safety
Fonte: Markus Winkler via Unsplash

4.1. Atualize o estado antes da chamada externa

Reordene o código para que todas as variáveis de controle sejam modificadas antes de enviar fundos. Exemplo:

// Código vulnerável
function withdraw(uint _amount) public {
    require(balances[msg.sender] >= _amount);
    msg.sender.call{value: _amount}(); // chamada externa
    balances[msg.sender] -= _amount; // atualização depois
}

// Código seguro
function withdraw(uint _amount) public {
    require(balances[msg.sender] >= _amount);
    balances[msg.sender] -= _amount; // atualização primeiro
    (bool success, ) = msg.sender.call{value: _amount}();
    require(success, "Transfer failed");
}

4.2. Use o padrão Checks‑Effects‑Interactions

Esse padrão recomenda: Check (verificar condições), Effect (alterar estado) e Interaction (interagir com outros contratos). Seguir essa ordem reduz drasticamente a chance de reentrância.

4.3. Utilize ReentrancyGuard da OpenZeppelin

A biblioteca OpenZeppelin oferece um contrato ReentrancyGuard que implementa um mutex simples. Basta herdar dele e marcar as funções críticas com o modificador nonReentrant:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract MyVault is ReentrancyGuard {
    mapping(address => uint) private balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint _amount) external nonReentrant {
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        balances[msg.sender] -= _amount;
        (bool success, ) = msg.sender.call{value: _amount}();
        require(success, "Transfer failed");
    }
}

4.4. Evite call() quando transfer() ou send() forem suficientes

Embora transfer() e send() imponham um limite de gás (2300), eles ainda podem ser descontinuados em futuras atualizações da EVM. Portanto, a abordagem recomendada hoje é usar call() com verificações explícitas de sucesso, combinada com ReentrancyGuard.

4.5. Limite o uso de fallback/receive functions

Contratos que não precisam receber Ether devem evitar implementar fallback ou receive functions que executam código complexo. Mantenha-as vazias ou apenas emitam eventos.

5. Ferramentas de auditoria e teste

Antes de lançar um contrato, use ferramentas automáticas para detectar padrões de reentrância:

  • Slither – analisador estático de código Solidity.
  • Echidna – fuzzer de propriedades para contratos.
  • Manticore – framework de análise simbólica.

Além disso, serviços de auditoria profissional, como ConsenSys Diligence, podem validar a segurança do seu código antes do deployment.

Um tipo comum de vulnerabilidade em smart contracts - additionally professional
Fonte: Jon Moore via Unsplash

6. Estudos de caso e lições aprendidas

Vamos analisar dois contratos reais que sofreram com reentrância e como corrigiram o problema.

6.1. DAO – O ataque histórico

O DAO não possuía nenhum mecanismo de proteção contra reentrância. A solução adotada pela comunidade foi um hard fork para reverter o estado e devolver os fundos aos investidores. Para saber mais sobre hard forks e seu impacto, confira o artigo Hard Fork: O que é, como funciona e seu impacto nas criptomoedas.

6.2. Um pool de liquidez DeFi vulnerável

Um contrato de pool permitia que usuários retirassem tokens sem atualizar o saldo interno. Após a exploração, a equipe implementou ReentrancyGuard e migrou para a versão 0.8.x do Solidity, que inclui verificações de overflow e recursos de segurança aprimorados.

7. Checklist rápido antes do deployment

  1. Aplicar o padrão Checks‑Effects‑Interactions.
  2. Usar ReentrancyGuard ou implementar mutex manual.
  3. Preferir call() com verificação de sucesso e limite de gás adequado.
  4. Executar análise estática com Slither e fuzzing com Echidna.
  5. Realizar auditoria externa ou revisão de pares.
  6. Documentar e comentar claramente todas as funções críticas.

Seguindo essas etapas, você reduz drasticamente o risco de ser vítima de um ataque de reentrância.

Conclusão

A reentrância continua sendo uma das vulnerabilidades mais perigosas em smart contracts, mas, felizmente, ela é totalmente evitável com boas práticas de desenvolvimento e auditoria rigorosa. Ao adotar o padrão Checks‑Effects‑Interactions, usar guardas de reentrância como as fornecidas pela OpenZeppelin e validar seu código com ferramentas de análise, você protege seu projeto e seus usuários contra perdas significativas.

Fique atento às atualizações da linguagem Solidity e às novas recomendações da comunidade. A segurança nunca é estática – ela evolui junto com a tecnologia.