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.