Tutorial de Solidity: Construindo um Contrato Inteligente para Sistema de Doações

Neste tutorial, desenvolvemos um contrato inteligente simples que permite a usuários realizarem doações em ETH para um endereço de contrato, enquanto o proprietário do contrato pode extraer os fundos acumulados. O sistema inclui um valor mínimo de doação em dólares americanos e registra todas as transferências diretas realizadas para o endereço do contrato, independentemente de chamarem explicitamente a função de doação.

Arquitetura da Solução

A aplicação é composta por dois contratos principais: um biblioteca para conversão de preços e um contrato principle que gerencia as operações de doação e extração. Utilizamos oráculos Chainlink para obter preços atualizados do par ETH/USD em tempo real.

Código da Biblioteca de Conversão

A biblioteca ConversorPreco encapsula toda a lógica relacionada à obtenção do preço do Ethereum e sua conversão para dólares americanos. O desafio principal reside em manter a precisão decimais consistente com o padrão do Ethereum, que utiliza 18 casas decimais.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

library ConversorPreco {
    // Interface para obter dados do oráculo Chainlink
    function obterPreco() internal view returns (uint256) {
        // Endereço do feed de preços ETH/USD na rede Sepolia
        AggregatorV3Interface feedPreco = AggregatorV3Interface(
            0x694AA1769357215DE4FAC081bf1f309aDC325306
        );
        
        // extrai apenas o valor de preço da resposta
        (, int256 resposta, , , ) = feedPreco.latestRoundData();
        
        // Converte para formato de 18 decimais
        return uint256(resposta * 10000000000);
    }

    function obterTaxaConversao(uint256 quantidadeWei)
        internal
        view
        returns (uint256)
    {
        uint256 precoEth = obterPreco();
        uint256 valorEmUsd = (precoEth * quantidadeWei) / 1000000000000000000;
        return valorEmUsd;
    }
}

O método obterPreco() acessa o contrato do oráculo Chainlink para recuperar o preço atual do Ethereum em dólares. A resposta retornada possui 8 casas decimais, enquanto o padrão do Ethereum exige 18. Multiplicamos por 10^10 para realizar essa normalização, garantindo que cálculos subsequentes mantenham a precisão adequada.

A função obterTaxaConversao() recebe uma quantidade em wei e retorna o equivalente em dólares americanos. O cálculo envolve multiplicar o preço unitário pela quantidade e ajustar o resultado dividindo por 10^18, restaurando a escala de 18 decimais esperada.

Contrato Principal de Gerenciamento

O contrato ContractDoacoes implementa toda a lógica de negócio para receber doações, rastrear doadores e permitir extração de fundos pelo proprietário. Utilizamos mapeamentos e vetores para manter o registro de todas as transações.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "./ConversorPreco.sol";

error NaoProprietario();

contract ContractDoacoes {
    using ConversorPreco for uint256;

    mapping(address => uint256) private saldoDoadores;
    address[] private listaDoadores;
    address private proprietario;
    uint256 public constante MINIMO_USD = 50 * 10 ** 18;
    
    constructor() {
        proprietario = msg.sender;
    }

    function doar() public payable {
        uint256 valorConvertido = msg.value.obterTaxaConversao();
        require(valorConvertido >= MINIMO_USD, "Valor abaixo do minimo permitido!");
        
        saldoDoadores[msg.sender] += msg.value;
        listaDoadores.push(msg.sender);
    }
    
    function obterVersaoFeed() public view returns (uint256){
        AggregatorV3Interface feedPreco = AggregatorV3Interface(
            0x694AA1769357215DE4FAC081bf1f309aDC325306
        );
        return feedPreco.version();
    }
    
    modifier apenasProprietario() {
        if (msg.sender != proprietario) revert NaoProprietario();
        _;
    }
    
    function extraer() public apenasProprietario {
        // Zera o registro de saldo de cada doador
        for (uint256 indice = 0; indice < listaDoadores.length; indice++){
            address doador = listaDoadores[indice];
            saldoDoadores[doador] = 0;
        }
        
        // Reseta o vetor de doadores
        listaDoadores = new address[](0);
        
        // Transfere todos os fundos para o proprietário usando call
        (bool sucessoChamada, ) = payable(proprietario).call{
            value: address(this).balance
        }("");
        require(sucessoChamada, "Falha na transferencia de fundos");
    }

    fallback() external payable {
        doar();
    }

    receive() external payable {
        doar();
    }
}

Análise dos Componentes Principais

Estruturas de Dados

O mapeamento saldoDoadores armazena o valor acumulado doado por cada endereço, permitindo consultas rápidas sobre o histórico de contribuições individuais. O vetor listaDoadores mantém uma relação de todos os endereços que já realizou doações, facilitando iterações durante processos de auditoria ou extração.

Controle de Acesso

O modificador apenasProprietario() garante que funções sensíveis, como a extração de fundos, sejam executadas exclusivamente pelo endereço que realizou o deploy do contrato. A utilização de erros customizados através de revert NaoProprietario() representa uma prática moderna que otimiza o consumo de gas em transações que falham.

Mecanismo de Doação

A função doar() valida o valor mínimo de 50 dólares antes de registrar a transação. A verificação utiliza a biblioteca de conversão de preços para transformar o vallor em wei para seu equivalente em dólares, garantindo portabilidade do requisito mínimo independentemente da flutuação do mercado.

Processo de Extração

O método extrair() implementa um padrão robusto de transferência utilizando a instrução call. Esta abordagem é preferível aos métodos transfer e send por sua flexibilidade no tratamento de limitações de gas e compatibilidade com contratos que requerem execução adicional após o recebimento.

Funções Fallback e Recieve

As funções fallback() e receive() garantem que transferências diretas de ETH para o contrato, sem especificar dados ou chamando funções inexistentes, sejam tratadas como doações válidas. Quando msg.data está vazio, apenas receive() é acionada; para dados não vazios, fallback() assume o processamento.

Considerações de Implementação

A declaração do atributo proprietario como immutable é recomendada para casos onde o proprietário nunca mudará durante a vida útil do contrato, oferecendo otimizações de gas por permitir que o compilador trate o valor como constante após a construção. A escolha entre constante e mutável deve considerar os requisitos específicos da aplicação.

Tags: solidity smart-contract chainlink ethereum web3

Publicado em 8-22 03:12