Lição: 082: Guardas de Reentrância
Nesta aula, iremos explorar o conceito de reentrância em contratos inteligentes e como nos defender contra isso utilizando guardas de reentrância. A reentrância é um vetor de ataque comum no Ethereum que pode permitir que usuários maliciosos explorem a forma como seus contratos gerenciam chamadas de função. Esta aula fornece uma visão geral da reentrância, suas implicações e exemplos práticos para ajudá-lo a proteger seus contratos inteligentes.
O que é Reentrância?
A reentrância ocorre quando uma função pode ser chamada novamente antes que sua execução anterior esteja completa. Isso pode levar a consequências indesejadas, especialmente em funções que modificam estados ou gerenciam transferências de ether. O exemplo mais famoso de um ataque de reentrância é o hack do DAO em 2016, onde os atacantes conseguiram retirar fundos repetidamente antes que o contrato tivesse a chance de atualizar seu estado, resultando em perdas significativas.
Compreendendo as Guardas de Reentrância
Para mitigar ataques de reentrância, você pode usar um padrão conhecido como guarda de reentrância. Uma guarda de reentrância impede que uma função seja invocada enquanto ainda está em execução. Isso é tipicamente alcançado usando uma variável booleana que rastreia se uma chamada de função está em andamento.
Implementação de uma Guarda de Reentrância
Aqui está como você pode implementar uma simples guarda de reentrância em um contrato inteligente Solidity:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ExemploReentrancia {
bool private bloqueado;
constructor() {
bloqueado = false;
}
modifier naoReentrante() {
require(!bloqueado, "GuardaDeReentrancia: chamada reentrante");
bloqueado = true;
_;
bloqueado = false;
}
mapping(address => uint) public saldos;
function depositar() public payable {
require(msg.value > 0, "Você deve enviar algum Ether");
saldos[msg.sender] += msg.value;
}
function retirar(uint _quantidade) public naoReentrante {
require(saldos[msg.sender] >= _quantidade, "Saldo insuficiente");
// Atualiza o estado antes de enviar Ether para evitar reentrância
saldos[msg.sender] -= _quantidade;
// Envia Ether
(bool sucesso, ) = msg.sender.call{value: _quantidade}("");
require(sucesso, "Transferência falhou");
}
}
Explicação do Código
-
Bloqueio Booleano: Definimos uma variável booleana privada
bloqueado
para rastrear se a função está sendo executada. -
Modifier: Criamos um modifier
naoReentrante
que verifica o estado debloqueado
. Se o estado fortrue
, a chamada é revertida com uma mensagem de erro. Se forfalse
, obloqueado
é definido comotrue
no início da execução e redefinido parafalse
antes de finalizar. -
Função de Depósito: Os usuários podem depositar ether, o que simplesmente adiciona o valor enviado ao seu saldo.
-
Função de Retirada: Antes de realizar qualquer transferência de ether, devemos garantir que o usuário tenha saldo suficiente e atualizar seu saldo em conformidade. A operação de envio real acontece após a atualização do saldo, reduzindo significativamente o risco de reentrância.
Melhores Práticas para Usar Guardas de Reentrância
- Ordem de Execução: Sempre atualize o estado do contrato (por exemplo, saldos) antes de fazer chamadas externas (por exemplo, transferindo ether).
- Chamadas Externas Mínimas: Minimize o número de chamadas de função externas em seu contrato para reduzir a superfície de ataque.
- Use Bibliotecas Estabelecidas: Sempre que possível, utilize bibliotecas bem auditadas, como a
ReentrancyGuard
do OpenZeppelin, para implementar essas proteções em vez de codificar suas próprias.
Usando a Guarda de Reentrância do OpenZeppelin
O OpenZeppelin fornece uma implementação robusta de uma guarda de reentrância que você pode facilmente integrar em seus contratos. Veja como você pode fazer isso:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract ExemploReentranciaOpenZeppelin é ReentrancyGuard {
mapping(address => uint) public saldos;
function depositar() public payable {
require(msg.value > 0, "Você deve enviar algum Ether");
saldos[msg.sender] += msg.value;
}
function retirar(uint _quantidade) public naoReentrante {
require(saldos[msg.sender] >= _quantidade, "Saldo insuficiente");
saldos[msg.sender] -= _quantidade;
(bool sucesso, ) = msg.sender.call{value: _quantidade}("");
require(sucesso, "Transferência falhou");
}
}
Conclusão
Os ataques de reentrância representam um risco significativo para contratos inteligentes no Ethereum. Ao usar guardas de reentrância, você pode mitigar efetivamente esses riscos e garantir que seu contrato opere de maneira segura. Sempre lembre-se de seguir as melhores práticas, atualizar o estado do contrato antes de fazer chamadas externas e aproveitar bibliotecas estabelecidas para aumentar a segurança de seus contratos inteligentes.