Skip to content

Auditoria de segurança de contrato inteligente assistida por IA

Auditoria de segurança de contrato inteligente assistida por IA
16 de março de 2023

https://miro.medium.com/v2/resize:fit:720/format:webp/1*uuXfpsFppUApvfstaDbHlg.jpeg

Os contratos inteligentes desempenham um papel crucial no mundo em rápida evolução das finanças descentralizadas. Esses contratos autoexecutáveis ​​revolucionaram a forma como fazemos transações, oferecendo uma ampla gama de aplicações na blockchain Ethereum. No entanto, eles também apresentam potenciais riscos de segurança. Neste artigo, exploraremos como a IA pode ajudar na realização de auditorias de segurança para contratos inteligentes da Ethereum, fornecendo trechos de código, práticas recomendadas e outros insights valiosos.

Análise estática alimentada por Inteligência Artificial (IA)

Uma das técnicas mais populares para auditoria de segurança de contratos inteligentes é a análise estática. Ferramentas de análise estática alimentadas por IA podem verificar a base de código do contrato, identificando vulnerabilidades e fornecendo sugestões de correção.

Por exemplo, a popular ferramenta de análise de segurança baseada em IA, Mythril, pode ser usada para realizar análises estáticas em seus contratos inteligentes. Para usar o Mythril, primeiro instale-o:

$ pip install mythril

Em seguida, analise seu contrato inteligente:

$ myth analyze <contract.sol>

O Mythril fornecerá informações detalhadas sobre as vulnerabilidades encontradas, juntamente com sugestões sobre como corrigi-las.

Teste de Fuzz automatizado

O teste de fuzz é uma técnica essencial para descobrir vulnerabilidades em contratos inteligentes. Envolve fornecer informações aleatórias, inesperadas ou malformadas a um contrato para desencadear erros ou comportamento não intencional. A IA pode ser usada para gerar entradas de teste de forma inteligente, tornando o processo de teste de fuzz mais eficaz.

Uma dessas ferramentas é o Echidna, um fuzzer alimentado por IA para contratos inteligentes da Ethereum:

$ git clone https://github.com/crytic/echidna.git
$ cd echidna
$ stack install

Crie uma função de teste personalizada em seu contrato, assim:

$ echidna-test <contract.sol>

Testes unitários gerados por IA

Os testes unitários são essenciais para garantir a correção e a segurança dos contratos inteligentes. A IA pode ser usada para gerar testes unitários para seus contratos inteligentes, abrangendo diferentes casos e cenários extremos.

Ferramentas como o DeepTest podem gerar automaticamente casos de teste para seus contratos inteligentes. Para usar o DeepTest, instale-o via npm:

$ npm install -g deeptest

Em seguida, gere casos de teste para seu contrato inteligente:

$ deeptest <contract.sol>

O DeepTest gerará casos de teste que você pode usar para validar a funcionalidade e a segurança do seu contrato.

Melhores Práticas

Ao conduzir auditorias de segurança para contratos inteligentes da Ethereum, siga estas práticas recomendadas:

  • Use bibliotecas e estruturas de contratos inteligentes bem avaliadas, como o OpenZeppelin.
  • Mantenha seus contratos simples e modulares, facilitando a auditoria.
  • Aplique o princípio do menor privilégio, garantindo que as funções do contrato tenham as restrições de acesso apropriadas.
  • Incorpore casos de teste completos e bem documentados para validar o comportamento do seu contrato sob diferentes condições.
  • Atualize continuamente seu conhecimento sobre as melhores práticas de segurança e vulnerabilidades comuns da Ethereum.

Vamos dar um passo adiante com um teste

O contrato inteligente em Solidity a seguir demonstra um exemplo contendo vulnerabilidades como ataques de reentrância, front-running e erros de lógica simples. Este contrato é intencionalmente inseguro e não deve ser usado em um cenário do mundo real.

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract VulnerableContract {
    IERC20 public token;
    address payable public owner;
    mapping(address => uint256) public balances;

    constructor(IERC20 _token) {
        token = _token;
        owner = payable(msg.sender);
    }

    // Esta função é vulnerável a ataques de reentrância.
    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "Saldo insuficiente");

        // Vulnerabilidade à reentrância
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Falha na retirada");

        balances[msg.sender] -= amount;
    }

    // Esta função é vulnerável a front-running.
    function buyTokens(uint256 amount) external payable {
        uint256 price = getTokenPrice();
        require(msg.value >= price * amount, "Saldo insuficiente");

        // Vulnerabilidade ao Front-running
        token.transfer(msg.sender, amount);
    }

    // Esta função contém um simples erro de lógica.
    function getTokenPrice() public view returns (uint256) {
        // Erro de lógica: o preço deve ser calculado usando um oráculo válido ou uma fonte confiável.
        return 1 ether;
    }

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

    function() external payable {}
}

Esse contrato demonstra as seguintes vulnerabilidades:

  1. Ataque de reentrância: a função withdraw é vulnerável a ataques de reentrância porque envia Ether ao usuário antes de atualizar o saldo. Um invasor pode explorar essa vulnerabilidade criando uma função de fallback em seu contrato que chama withdraw novamente, drenando efetivamente o Ether do contrato.
  2. Front-running: a função buyTokens é vulnerável a ataques de front-running, uma vez que não possui nenhuma proteção contra manipulação de ordens de transação. Os invasores podem observar as transações pendentes e inserir suas próprias transações com preços de gás mais altos para comprar tokens antes de outros, potencialmente manipulando os preços dos tokens.
  3. Erro de lógica simples: a função getTokenPrice contém um erro de lógica, pois retorna um valor de Ether codificado em vez de usar um oráculo de preços confiável. Isso pode levar a preços incorretos dos tokens e a consequências indesejadas quando os usuários compram tokens.

Novamente, esse contrato é intencionalmente inseguro e não deve ser utilizado em nenhum cenário do mundo real. Serve como um exemplo de como não escrever um contrato inteligente e destaca a importância de realizar auditorias de segurança completas.

Aqui, irei guiá-lo através do processo de auditoria e correção do contrato inteligente usando as ferramentas de IA Mythril, Echidna e DeepTest, onde abordaremos os ataques de reentrância, de front-running e erros de lógica simples.

Auditoria com o Mythril

O Mythril é uma ferramenta popular de análise de segurança que realiza análises estáticas em seus contratos inteligentes. Primeiro, instale-o:

$ pip install mythril

Em seguida, analise seu contrato inteligente:

$ myth analyze <contract.sol>

O Mythril fornecerá informações detalhadas sobre as vulnerabilidades encontradas, juntamente com sugestões sobre como corrigi-las. No nosso caso, ele deve identificar a vulnerabilidade de ataque de reentrância na função withdraw.

Correção de ataque de reentrância

Para evitar ataques de reentrância, podemos usar o padrão Checks-Effects-Interactions (Verificação-Efeitos-Interações). Atualize o saldo antes de transferir os fundos:

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

    // Atualiza o saldo primeiro (padrão checks-effects-interactions)
    balances[msg.sender] -= amount;

    // Depois, transfere os fundos
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Falha na retirada");
}

Auditoria com o Equidna

O Echidna é um fuzzer alimentado por IA para contratos inteligentes da Ethereum. Primeiro, instale-o:

$ git clone https://github.com/crytic/echidna.git
$ cd echidna
$ stack install

Crie uma função de teste personalizada em seu contrato, assim:

pragma solidity ^0.8.0;

contract MyContract {
    // ...

    function echidna_test() public view returns (bool) {
        // Adicione sua lógica de teste aqui.
    }
}

Em seguida, execute o Echidna em seu contrato inteligente:

$ echidna-test <contract.sol>

O Echidna identificará vulnerabilidades e possíveis casos extremos que podem levar a problemas como front-running.

Correção do front-runnig

Para evitar o front-running, podemos usar um esquema de commit-reveal (compromisso-revelação) que dissocia a intenção de compra do token da compra real do token. Os usuários se comprometem a comprar tokens enviando um compromisso com hash e posteriormente revelam sua intenção, permitindo que o contrato processe a compra por ordem de compromisso.

// Define uma nova estrutura para compromissos de compra
struct Commitment {
    uint256 amount;
    uint256 price;
    uint256 nonce;
}

mapping(address => bytes32) public commitments;

function commit(uint256 amount, uint256 price, uint256 nonce) external {
    bytes32 hashedCommitment = keccak256(abi.encodePacked(msg.sender, amount, price, nonce));
    commitments[msg.sender] = hashedCommitment;
}

function reveal(uint256 amount, uint256 price, uint256 nonce) external payable {
    bytes32 hashedCommitment = keccak256(abi.encodePacked(msg.sender, amount, price, nonce));
    require(commitments[msg.sender] == hashedCommitment, "Compromisso inválido");

    uint256 totalPrice = price * amount;
    require(msg.value >= totalPrice, "Saldo insuficiente");

    // Transfere os tokens depois de verificar os compromissos e o pagamento.
token.transfer(msg.sender, amount);

    // Apaga o compromisso depois da compra bem sucedida de um token
    delete commitments[msg.sender];
}

Auditoria com o DeepTest

O DeepTest é uma ferramenta alimentada por IA que pode gerar automaticamente casos de teste para seus contratos inteligentes. Primeiro, instale-o via npm:

$ npm install -g deeptest

Em seguida, gere casos de teste para seu contrato inteligente:

$ deeptest <contract.sol>

O DeepTest irá gerar casos de teste que podem ajudá-lo a identificar problemas como o erro de lógica simples na função getTokenPrice.

Correção de erro de lógica simples

Para corrigir o erro de lógica na função getTokenPrice, você deve usar um oráculo de preços confiável, como a Chainlink, para obter um preço exato do token.

Primeiro, instale o pacote Chainlink:

npm install @chainlink/contracts

Em seguida, atualize o contrato para usar o oráculo de preços da Chainlink:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract FixedContract {
    IERC20 public token;
    address payable public owner;
    mapping(address => uint256) public balances;
    AggregatorV3Interface public priceOracle;

    constructor(IERC20 _token, AggregatorV3Interface _priceOracle) {
        token = _token;
        owner = payable(msg.sender);
        priceOracle = _priceOracle;
    }

    // ... (outras funções)

    // Substitui a função getTokenPrice anterior por isto:
    function getTokenPrice() public view returns (uint256) {
        (, int256 price, , , ) = priceOracle.latestRoundData();
        require(price > 0, "Preço de token inválido");
        return uint256(price);
    }
}

Nesta versão revisada do contrato inteligente, os ataques de reentrância são evitados seguindo o padrão de verificações-efeitos-interações, o front-running é mitigado usando um esquema de compromisso-revelação e o erro de lógica na função getTokenPrice é resolvido usando um oráculo de preços confiável. Ao implementar essas mudanças e aderir às melhores práticas, seu contrato inteligente ficará mais seguro.

É importante observar que nenhuma ferramenta automatizada pode garantir a segurança completa de um contrato inteligente. Embora ferramentas de IA como Mythril, Echidna e DeepTest possam ajudar a identificar vulnerabilidades, você também deve realizar revisões manuais de código e considerar a contratação de auditores profissionais para garantir o mais alto nível de segurança para seus contratos inteligentes.

Aqui está o contrato inteligente auditado completo com as vulnerabilidades identificadas corrigidas:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract AuditedContract {
    IERC20 public token;
    address payable public owner;
    mapping(address => uint256) public balances;
    AggregatorV3Interface public priceOracle;

    struct Commitment {
        uint256 amount;
        uint256 price;
        uint256 nonce;
    }

    mapping(address => bytes32) public commitments;

    constructor(IERC20 _token, AggregatorV3Interface _priceOracle) {
        token = _token;
        owner = payable(msg.sender);
        priceOracle = _priceOracle;
    }

    // Evita ataques de reentrância
    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "Saldo insuficiente");

        // Atualiza o saldo primeiro (padrão checks-effects-interactions)
        balances[msg.sender] -= amount;

        // Depois, transfere os fundos
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Falha na retirada");
    }

    // Evita ataques de front-running
    function commit(uint256 amount, uint256 price, uint256 nonce) external {
        bytes32 hashedCommitment = keccak256(abi.encodePacked(msg.sender, amount, price, nonce));
        commitments[msg.sender] = hashedCommitment;
    }

    function reveal(uint256 amount, uint256 price, uint256 nonce) external payable {
        bytes32 hashedCommitment = keccak256(abi.encodePacked(msg.sender, amount, price, nonce));
        require(commitments[msg.sender] == hashedCommitment, "Compromisso inválido");

        uint256 totalPrice = price * amount;
        require(msg.value >= totalPrice, "Saldo insuficiente");

        // Transfere os tokens depois de verificar o compromisso e o pagamento.
        token.transfer(msg.sender, amount);

        // Apaga o compromisso depois de uma compra de token bem sucedida.
        delete commitments[msg.sender];
    }

    // Usa um oráculo de preço confiável para corrigir o erro de lógica
    function getTokenPrice() public view returns (uint256) {
        (, int256 price, , , ) = priceOracle.latestRoundData();
        require(price > 0, "Preço de token inválido");
        return uint256(price);
    }

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

    receive() external payable {}
}

Este contrato inteligente aborda as vulnerabilidades discutidas anteriormente ao longo do artigo:

  1. Os ataques de reentrância são evitados seguindo o padrão verificação-efeitos-interações na função withdraw.
  2. Os ataques de front-running são mitigados usando um esquema de compromisso-revelação nas funções commit e reveal.
  3. O erro de lógica na função getTokenPrice é corrigido usando um oráculo de preços confiável, a Chainlink, para obter um preço exato de token.

Este contrato auditado é mais seguro, mas considere sempre realizar revisões manuais de código e consultar auditores profissionais para garantir o mais alto nível de segurança para seus contratos inteligentes.

Conclusão

A auditoria de segurança assistida por IA pode aumentar muito a segurança dos contratos inteligentes da Ethereum, oferecendo informações valiosas e recomendações para melhorias. Ao incorporar ferramentas alimentadas por IA, como Mythril, Echidna e DeepTest, em seu processo de auditoria, você pode identificar e resolver vulnerabilidades de maneira mais eficaz. Além disso, seguir as melhores práticas para o desenvolvimento de contratos inteligentes ajudará a garantir a segurança e o sucesso a longo prazo dos seus aplicativos descentralizados.

Esse artigo foi escrito por Javier Calderon Jr e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.