Pressione ESC para fechar

Falhas de Reentrancy no Solidity pelo ChatGPT: 3 Exemplos

Sabe, quando eu passava as noites virado em hackathons... quer dizer, que noites o quê, a gente dormia era três horinhas debaixo dos racks de servidores, eu achava que qualquer analisador estático tiraria de letra as vulnerabilidades básicas em smart contracts. O que podia dar errado? Reentrancy (ataque de reentrância) é o clássico dos clássicos, todo mundo já falou sobre isso. O padrão Checks-Effects-Interactions, modificadores pra cá e pra lá... Aí chega um dev júnior na equipe alguns meses atrás e me solta essa com a maior marra: «Pô, o ChatGPT gerou um contrato de staking perfeito aqui pra mim, conferi tudo, tá seguro!»

Quase, mano. Por pouco eu não derrubei o café no teclado mecânico.

Vamos ser sinceros: as LLMs de hoje escrevem um Solidity bonitão. A sintaxe é impecável, os imports vêm redondinhos e o OpenZeppelin tá plugado seguindo as últimas modas. Mas quando o papo chega na lógica fina de distribuição de estados, a IA começa a mandar aquela certeza cega e teimosa — aquela confiança de quem não sabe nada mas tem certeza de tudo —, gerando umas brechas que depois, na hora da auditoria, limpam milhões de dólares da galera. Bora destrinchar três casos bem traiçoeiros onde o ChatGPT passa vergonha enquanto te garante que o teu código passaria numa auditoria da CertiK de olhos fechados.

Caso nº 1: Staking assíncrono com recompensas por bloco e chamada externa oculta

Cara, você pede pra IA escrever um contrato que distribui tokens com base no tempo que o cara passou com saldo no pool. O que o modelo faz? Ele taca o modificador nonReentrant em tudo que é canto onde vê transferência de fundos pro usuário. Só que ele esquece um detalhe arquitetural crucial.

Dá uma olhada nesse trecho que eu tirei de um code review real depois da cagada daquele júnior:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IERC20Mintable {
    function mint(address to, uint256 amount) external;
}
contract SecureStakingVault {
    IERC20Mintable public rewardToken;
    mapping(address => uint256) public balances;
    mapping(address => uint256) public lastUpdateBlock;
    
    uint256 public rewardRatePerBlock = 10 * 10**18;
    bool private locked;
    // Pronto, tá blindado! Quer mais o quê, seu noiado?
    modifier noReentry() {
        require(!locked, "LOCKED");
        locked = true;
        _;
        locked = false;
    }
    constructor(address _token) {
        rewardToken = IERC20Mintable(_token);
    }
    function stake() external payable {
        require(msg.value > 0, "Zero stake");
        balances[msg.sender] += msg.value;
        lastUpdateBlock[msg.sender] = block.number;
    }
    // A obra-prima do ChatGPT que parece 100% segura
    function harvestAndUnstake(uint256 _amount) external noReentry {
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        uint256 blocksPassed = block.number - lastUpdateBlock[msg.sender];
        uint256 reward = blocksPassed * rewardRatePerBlock * _amount / balances[msg.sender];
        // Atenção: a atualização de estado rola ANTES da chamada externa de mint
        balances[msg.sender] -= _amount;
        lastUpdateBlock[msg.sender] = block.number;
        // A emissão de recompensas dispara um construtor de token ou um hook, e aí... surpresa!
        rewardToken.mint(msg.sender, reward);
        (bool success, ) = msg.sender.call{value: _amount}("");
        require(success, "Transfer failed");
    }
}

Onde tá a pegadinha? O modelo acha firme que, como as variáveis de saldo foram zeradas antes do rewardToken.mint(), o ataque é impossível. Só que o rewardToken é um ERC-20 customizado, cujo contrato de mint traz um hook _beforeTokenTransfer ou um callback no estilo ERC-777 (caso o token seja mais complexo ou use proxy). Na hora que rola o mint, o fluxo de execução escapa pra um código de terceiros antes mesmo do ETH pingar na carteira do usuário, mas bem no meio da sessão ativa — quando o estado do pool já foi alterado de um jeito só e o reajuste de recompensas pros outros usuários foi pro espaço. É uma Reentrancy entre contratos de última geração, daquelas que os manuais tradicionais nem ousam mencionar.

Caso nº 2: Sequestro multifuncional via oráculos de liquidez

O veneno dos exploits modernos é que a reentrancy muitas vezes não mora dentro de uma única função, mas sim entre dois pontos de entrada totalmente diferentes que dividem o mesmo mapping. A IA adora fatiar a lógica em módulos "limpinhos", esquecendo completamente a ordem em que um invariante global é modificado.

  • Parâmetro de vulnerabilidade: Avaliação do analisador estático
  • Realidade dos fatos: Modificadores
  • Presentes (nonReentrant): Inúteis contra chamadas entre funções
  • Ordem das operações: Checks-Effects respeitados localmente
  • Equilíbrio global do protocolo quebrado: Reação do ChatGPT
  • «O código está totalmente protegido contra reentrancy»: Deixa passar manipulação cruzada de estado

Caso nº 3: Contratos com duração flutuante e cofres ERC-4626

O ChatGPT tem uma obsessão doentia com os templates padronizados do OpenZeppelin para cofres de rendimento (ERC-4626). Ele pega os exemplos prontos, mete a matemática personalizada dele pra calcular as shares e te entrega um código que compila de primeira. O problema é que a função de conversão de cotas em ativos (convertToAssets), quando chamada sob certas condições via contrato intermediário, abre brecha pra um retorno de controle multinível bem na hora de calcular a liquidez virtual.

Eu passei metade de uma madrugada dessas aí atrás de um bug na testnet, enquanto um bot simulador limpava toda a liquidez do pool seguindo exatamente esse roteiro. E a IA mandando: «Use escala de multiplicadores para maior precisão». No fim das contas, o que a gente ganha? Um belo de um read-only reentrancy clássico, onde um sistema externo cata o estado intermediário — e ainda não consolidado — de um cofre no exato momento em que rola um depósito.

Continuando nossa imersão nesse teatro do absurdo que os modelos de linguagem geram quando pedimos para eles escreverem um "smart contract simples e confiável".

Sabe o que é mais engraçado na vida de um ex-auditor de segurança? É ver desenvolvedores confiando cegamente nos comentários do próprio ChatGPT. A IA joga uma linha lá no topo tipo // Safe against reentrancy attacks thanks to Checks-Effects-Interactions — e pronto, o senso crítico do dev desliga na hora. Modo piloto automático ativado e bum: foguetório garantido na mainnet.

Vamos analisar outros dois exemplos pesadíssimos que as IAs insistem em considerar o padrão ouro de segurança, mas que na prática são estourados com a maior facilidade.

Caso nº 4: Arbitragem multi-etapas com reembolso via flash loan

Esse é o clássico que vive caindo em hackaton ou sendo passado para júnior: escrever um contrato que pega uma grana emprestada via flash loan, roda num DEX, cata o lucro, devolve o principal e embolsa a comissão no pool de liquidez. O que o ChatGPT faz? Ele coloca bonitinho umas checagens require no saldo logo no começo e no fim da transação, achando sinceramente que se o delta bater, o sistema está invulnerável.

Mas o diabo, como sempre, mora nos detalhes da chamada da função callback uniswapV2Call (ou de interfaces similares em outros protocolos).

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IUniswapV2Pair {
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}
contract AIArbitrageBotHolder {
    address private immutable owner;
    bool private unlocked = true;
    // A IA acha que essa trava basta para proteger toda a lógica
    modifier lock() {
        require(unlocked, "LOCKED");
        unlocked = false;
        _;
        unlocked = true;
    }
    constructor() {
        owner = msg.sender;
    }
    // O tal método "seguro" segundo a IA
    function executeFlashLoan(address _pair, uint256 _amountA, bytes calldata _data) external lock {
        // Pedindo liquidez para o pool
        IUniswapV2Pair(_pair).swap(_amountA, 0, address(this), _data);
    }
    // Callback para onde o pool devolve o controle
    function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
        // Erro feio: a checagem de msg.sender costuma ser esquecida pela IA para deixar o código "limpo"
        // E voilà, qualquer contrato malandro pode brotar aqui e estourar um loop de reentrância
        
        uint256 fee = amount0 * 3 / 997 + 1;
        uint256 repayment = amount0 + fee;
        // Lógica de arbitragem customizada gerada pelo ChatGPT
        _executeInternalArbitrage(amount0);
        // Devolvendo a grana para o pool
        // (Esquecendo totalmente que o state dos pools externos ainda não foi finalizado)
        bool success = IERC20(msg.sender).transfer(msg.sender, repayment);
        require(success, "Repayment failed");
    }
    function _internalArbitrage(uint256 amount) internal {
        // Stub para a lógica de swap complexa
    }
}

Tá vendo a brecha? A IA taca o modificador lock no ponto de entrada externo executeFlashLoan, mas esquece completamente que a função de callback uniswapV2Call precisa ser isolada de forma rígida ou validar estritamente quem está chamando (msg.sender). Um invasor pode chamar esse callback direto do contrato dele no meio dos cálculos intermediários — quando os saldos do contrato wrapper ainda não estão consistentes — e drenar o collateral todinho. O ChatGPT nunca vai te avisar sobre isso; ele só vai mandar um emoji sorridente no chat e soltar: "Código otimizado para o padrão ERC".

Caso nº 5: Contratos de mint em lote para ERC-721 otimizados com cache

O mercado NFT atual quer gas baratinho. A IA saca isso rapidinho e começa a "otimizar" o código enfiando mapeamentos customizados de donos e contadores de cache direto na memória do contrato.

  • Métrica de auditoria: Veredito do ChatGPT — Status real da vulnerabilidade
  • Consumo de gás: Super baixo (Gas Optimized) — Conseguido quebrando o isolamento de state
  • Padrão de storage: Uso de structs locais em memória — Vulnerável a manipulações via hooks onERC721Received
  • Estabilidade: Passa nos testes padrão do Hardhat — Capota se pegar uma pilha funda de chamadas reentrantes

Quando um contrato manda um NFT via safeMint, o padrão exige chamar uma função de confirmação no contrato inteligente receptor. Se você faz um mint em lote (digamos, 10 tokens de uma vez) e atualiza o contador do saldo total depois de enviar cada token (ou num loop dentro de uma chamada externa), o invasor sequestra o controle logo na terceira iteração — hora em que o array interno de tokens ainda está pela metade, mas o contador de transações acha que o processo tá só começando. Resultado da brincadeira: colisão de index e emissão infinita de ativos do nada.

O que a gente, você e toda a indústria deve fazer diante disso?

Olha, eu mesmo codo todo santo dia, mas confiar cegamente em modelos gerativos em crypto é o equivalente a jogar roleta russa com a arma totalmente municiada. Se você usa IA para gerar protocolos, decore três regras de ouro:

  • Nunca confie em modificadores vindos de templates. Se a IA meteu um nonReentrant, isso só significa que ela conhece a palavra, mas faz ideia zero do contexto assíncrono da sua blockchain específica.
  • Sempre valide msg.sender em funções de callback. Qualquer interface como uniswapV2Call, onERC721Received ou tokensReceived tem que ser trancada com mais rigor que cofre de banco.
  • Escreva seus próprios testes de fuzzing no Foundry. Nenhum teste unitário estático vai achar uma reentrância cross-function tão bem quanto alguns milhões de rodadas aleatórias baseadas em invariantes.

Enquanto eu desabafo sobre essas vulnerabilidades, tem uma coisa que não sai da minha cabeça: a bem da verdade, nós mesmos deixamos o mercado relaxado. A gente acostumou a delegar tarefas rotineiras para a geração de código, esquecendo que o blockchain não perdoa erros de compilação de lógica. No desenvolvimento web tradicional, você pode dar um rollback no commit, subir um patch, pedir desculpas aos usuários por cinco minutos de downtime. No ecossistema EVM, seu bug fica gravado para sempre em um ledger imutável, como um monumento à negligência humana e ao triunfo da cegueira algorítmica.

Vamos fechar o resto do tema e dissecar mais um vetor nada óbvio que o ChatGPT gera com uma consistência assustadora.

Caso nº 6: Agregadores de liquidez e cotas virtuais em pools de stablecoin

Essa é a moda do momento entre os devs de DeFi: criar pools de swap customizadas com cálculo dinâmico de peso de tokens baseadas em curvas de produto constante. As redes neurais adoram essa matemática porque a internet está entupida de terabytes de código open source da Curve e do Uniswap v2. O modelo pega a fórmula, adapta para os seus requisitos, taca uma função addLiquidity e relata orgulhosamente que o produto está pronto para o deploy.

Só que o modelo não faz ideia de como funciona o cálculo assíncrono de saldos virtuais quando se chama tokens externos com taxa flutuante (tipo USDT ou tokens deflationary customizados com imposto por transferência).

Dá uma olhada nesse padrão que a IA considera 100% blindado:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface ITokenWithFee {
    function transferFrom(address sender, address recipient, amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}
contract AIWarpPool {
    mapping(address => uint256) public userShares;
    uint256 public totalShares;
    
    // A IA acha que se a gente calcula o saldo primeiro e depois grava no mapping, estamos seguros
    function deposit(address token, uint256 amount) external {
        uint256 balanceBefore = ITokenWithFee(token).balanceOf(address(this));
        
        // Chamada externa de transferência de token
        bool success = ITokenWithFee(token).transferFrom(msg.sender, address(this), amount);
        require(success, "Transfer failed");
        
        uint256 balanceAfter = ITokenWithFee(token).balanceOf(address(this));
        uint256 realReceived = balanceAfter - balanceBefore;
        // Emissão de shares com base nos fundos efetivamente recebidos
        uint256 shares = totalShares == 0 ? realReceived : (realReceived * totalShares) / balanceBefore;
        
        userShares[msg.sender] += shares;
        totalShares += shares;
    }
    function withdraw(uint256 shares) external {
        require(userShares[msg.sender] >= shares, "Not enough shares");
        
        uint256 amountToReturn = (shares * ITokenWithFee(msg.sender).balanceOf(address(this))) / totalShares;
        
        // Primeiro reduzimos o state...
        userShares[msg.sender] -= shares;
        totalShares -= shares;
        // ...e depois repassamos os fundos. O clássico Checks-Effects-Interactions!
        // Mas peraí...
        (bool success, ) = msg.sender.call{value: 0}(""); // Chamada externa condicional de transferência de token
        require(success, "Withdraw failed");
    }
}

Conseguiu ver a armadilha? A rede neural seguiu bonitinha a ordem de Checks-Effects-Interactions na função withdraw. Mas ela deixou completamente passar que o cálculo de amountToReturn está amarrado ao balanceOf(address(this)) atual no momento da chamada. Se um invasor criar um contrato wrapper que, ao receber o controle (ou via callback do token), inicia uma reentrancy no withdraw antes que o totalShares global mude em outro pool conectado ou por meio de uma chamada cross-contract de oráculo, ele calcula a proporção usando um valor desatualizado de liquidez.

O checklist definitivo para revisar o que a IA escreveu para você

Chega de bater cabeça achando que rede neural é mais esperta que você em arquitetura de sistemas distribuídos. Antes de jogar o código gerado em produção ou até mesmo em uma testnet valendo grana de verdade, passa esse pente-fino:

  • Isole qualquer função de callback como se tivesse um hacker sentado dentro dela com um exploit pronto na mão.
  • Inspecione cada chamada externa para ver se ela pode devolver o controle ao contrato no momento em que os invariantes globais ainda não bateram.
  • Esqueça a fé cega em modifiers — eles só protegem contra reentrancy direta na mesma função, mas são inúteis contra cadeias complexas entre contratos (cross-contract).

FAQ

O modelo depende de correspondência de padrões estáticos e falha ao rastrear alterações de estado assíncronas entre contratos, permitindo que invariantes globais fiquem inconsistentes antes que chamadas externas sejam executadas.

É obrigatório aplicar estritamente o padrão checks-effects-interactions em todos os módulos, isolar funções de callback internas e implementar fuzzing de invariantes automatizado via Foundry em vez de confiar cegamente em modificadores como nonReentrant.

Os grandes modelos de linguagem ignoram cálculos de saldos virtuais e atualizações prematuras de estado, permitindo que contratos maliciosos reentrem na lógica principal durante callbacks intermediários de tokens.
Oleg Filatov

As the Chief Technology Officer at EXMON Exchange, I focus on building secure, scalable crypto infrastructure and developing systems that protect user assets and privacy.

With over 15 years in cybersecurity, blockchain, and DevOps, I specialize in smart contract analysis, threat modeling, and secure system architecture.

At EXMON Academy, I share practical insights from real-world...

...

Deixe seu parecer

O seu endereço de e-mail não será publicado. Campos obrigatórios estão marcados *