WEB3DEV

Cover image for Introdução ao Foundry: O contrato inteligente do Kickstarter
Panegali
Panegali

Posted on

Introdução ao Foundry: O contrato inteligente do Kickstarter

De acordo com a documentação oficial aqui, o Foundry é um conjunto de ferramentas de desenvolvimento de contratos inteligentes. Isso significa simplesmente que, com o Foundry, podemos gerenciar nossas dependências, compilar nossos projetos, executar testes extensivos, realizar implantações e interagir com a blockchain a partir da linha de comando, e tudo isso é feito com os Scripts do Solidity. Interessante, não é? Espere até você ouvir a melhor parte.

O Foundry é incrivelmente rápido (provavelmente a ferramenta de desenvolvimento de contratos inteligentes mais rápida no momento), escrito em Rust e também é a estrutura preferida dos engenheiros e auditores de segurança de contratos inteligentes.

Em artigos anteriores, escrevemos alguns contratos inteligentes muito básicos para nos ajudar a entender o Solidity, mas, a partir de agora, vamos resolver problemas reais e também resolvê-los como são feitos no mundo real usando ferramentas e tecnologias padrão. Escrever nossos contratos no Remix é ótimo, pois podemos testar e implantar os contratos com facilidade, mas o Remix tem suas limitações quando se trata de desenvolver aplicativos de nível de produção, e é por isso que estamos usando uma ferramenta mais adequada chamada Foundry. Também usarei o VS Code como meu editor de texto.

Ideia do projeto

Então, o que estamos construindo? Neste artigo, resolveremos o principal problema que assola o aplicativo Kickstarter. Para aqueles que não estão familiarizados com o que é o Kickstarter, trata-se de um lugar onde as pessoas que têm ideias para criar alguns produtos, mas precisam de financiamento, vão até lá e outras pessoas financiam essas ideias e participam dos procedimentos do produto (em poucas palavras). Você pode encontrar mais detalhes no site aqui.

Acredito que já podemos perceber os problemas que isso poderia trazer. Pessoas com más intenções podem, de fato, simplesmente acessar a plataforma alegando ter uma ideia de produto, reunir fundos e, em seguida, sair voando com todo o dinheiro. Como podemos resolver esse problema? Sabemos (sem dúvida) que a blockchain resolve o problema da confiança, no sentido de que não precisamos confiar em um terceiro antes de fazer transações com ele ou esperar que ele se comporte adequadamente. Portanto, a tecnologia blockchain seria uma ótima maneira de realmente resolver o problema de confiança que assola o aplicativo Kickstarter e é exatamente isso que este artigo pretende alcançar.

Iniciando

Agora que já tiramos as questões administrativas do caminho, vamos ver como instalar o Foundry e começar a escrever nossos contratos inteligentes.

A instalação do Foundry não é muito complicada. O processo de instalação, juntamente com a documentação, pode ser encontrado aqui. Os usuários do Windows talvez precisem considerar o uso do WSL para a instalação, mas o comando a ser executado é

curl -L https://foundry.paradigm.xyz | bash
Enter fullscreen mode Exit fullscreen mode

Isso instalará o FoundryUp e, em seguida, executar o FoundryUp instalará o Forge, Cast, Anvil e Chisel, que são as ferramentas que usaremos para todo o ciclo de desenvolvimento de nossa aplicação.

Eu instalei o Foundry e executei o comando FoundryUp da seguinte forma:

Comando Foundryup

Estarei trabalhando em um diretório vazio chamado "Kickstarter".

Como mencionado anteriormente, o comando FoundryUp instalou 4 ferramentas com propósitos diferentes para trabalhar com o Foundry. A primeira com a qual estaremos trabalhando é o Forge. Podemos executar o comando abaixo para inicializar um projeto Foundry básico:

forge init
Enter fullscreen mode Exit fullscreen mode

Isso cria algumas pastas e arquivos, como visto abaixo no VS Code:

Comando de inicialização do Forge

Por padrão, temos as pastas src, script e test, cada uma contendo um contrato inteligente relacionado ao contador. Vá em frente e exclua esses 3 arquivos e crie um novo arquivo Solidity na pasta src chamado "Kickstarter.sol". Não faremos nenhum teste neste artigo, pois isso será abordado extensivamente no meu próximo artigo. O único conteúdo no arquivo Kickstarter é apenas a configuração SPDF, a versão do Solidity e nossa declaração real de contrato chamada "Campaign".

Por padrão, o VSCode não fornece intellisense para nossos arquivos Solidity, para isso, teremos que instalar uma extensão do marketplace. Eu prefiro a fornecida pela Nomic Foundation.

Lógica do Contrato

As especificações para nosso contrato seriam as seguintes:

  • Um gerente que está procurando financiamento poderá especificar a contribuição mínima para quem deseja financiar sua ideia.
  • Outros usuários podem contribuir para a ideia se atenderem ao valor mínimo especificado pelo gerente.
  • Os gerentes não recebem o dinheiro diretamente, em vez disso, ele é armazenado na blockchain.
  • Sempre que o gerente precisa de dinheiro para financiar uma parte específica do projeto, eles criarão uma solicitação de gastos (por exemplo, se a ideia do projeto for um carro, então o gerente pode precisar comprar motores, portanto, eles criam uma solicitação de gastos para isso).
  • A solicitação de gastos passará por aprovação pelos financiadores das ideias.
  • Se a maioria dos financiadores aprovar a solicitação de gastos, o valor é enviado para a conta de quem precisa receber o pagamento (por exemplo, quem vende os motores do carro).
  • Um financiador que não participa do processo de aprovação é considerado falso, ou seja, eles não aprovam a solicitação de gastos.

Observação: Em nenhum momento o dinheiro é enviado diretamente para o gerente. Dessa forma, os financiadores também podem ter uma visão clara e rastrear como seu dinheiro foi gasto.

Código Real do Contrato

Agora que entendemos o quadro geral para nosso contrato, vamos para a implementação real. Obviamente, a primeira coisa a fazer seria lidar com a lógica que ocorre quando o contrato é criado pelo gerente (ênfase em "criado pelo gerente").

Nós criamos uma função construtora que recebe um uint para o valor mínimo necessário para participar da campanha. Criamos 2 variáveis imutáveis chamadas i_manager e i_minimum_contribution e, em seguida, definimos a variável i_manager como quem criou o contrato e a i_minimum_contribution como o valor passado por eles. Por convenção, defini as 2 variáveis como privadas (isso ajuda a economizar mais gás) e criei funções getter para ler os valores que armazenaram.

A palavra-chave "immutable" pode ser nova para nós até agora, mas ela simplesmente significa que o valor armazenado nesta variável não será alterado. É semelhante à palavra-chave "constant", exceto que temos que definir o valor para uma variável constante imediatamente após ela ser inicializada.

Do meu artigo anterior, afirmei que qualquer variável declarada no nível superior do contrato é uma variável de armazenamento, mas isso não é o caso para variáveis imutáveis e constantes.

Variáveis imutáveis e constantes são armazenadas no bytecode do próprio contrato e não na blockchain. Dessa forma, elas reduzem significativamente o gás gasto ao ler seus valores.

Certo, seguindo em frente, precisaremos de uma maneira para que outras pessoas possam se juntar à campanha e fazer suas contribuições.

Criamos 2 novas variáveis de armazenamento, s_funders e s_fundersCount, para acompanhar os financiadores da campanha com o valor de suas contribuições. A função é marcada como "payable". Isso significa que ela pode aceitar pagamento (neste caso, ethers) e acessamos o valor usando a variável global "msg.value".

A seguir, trabalharemos na função "createSpendRequest".

Aqui, temos várias coisas acontecendo. Primeiro, criamos uma estrutura para o nosso tipo de solicitação e, em seguida, criamos uma função para um gerente criar a solicitação de gastos; exigimos que apenas o gerente possa criar solicitações. O destinatário passado como parâmetro na função "createSpendRequest" é definido como "payable", já que pretendemos enviar dinheiro para este endereço se a solicitação de gastos for aprovada pelos financiadores.

Observe como a variável "spendRequest" é definida como de armazenamento? Isso indica que estamos tentando alterar o valor real no índice especificado armazenado na variável "s_spendRequests".

Na nossa função "approve", passamos o índice da solicitação de gastos como parâmetro e obtemos a solicitação nesse índice, armazenando-a em uma variável de armazenamento (novamente, indicando que ela alteraria o valor nesse índice). Em seguida, exigimos que o usuário que chama a função de aprovação tenha realmente contribuído com um valor válido para a campanha e também que não tenha votado inicialmente na solicitação de gastos. Em seguida, aumentamos a variável "approvalCount" e definimos o endereço do financiador como verdadeiro para mostrar que eles votaram.

Como precisamos de uma maneira de rastrear os financiadores que aprovaram uma solicitação de gastos, modificamos nossa estrutura de solicitação para incluir um mapeamento de aprovações como mostrado abaixo.

A última coisa a fazer seria finalizar a solicitação de gastos, e isso seria feito apenas pelo gerente, dependendo se a solicitação foi ou não aprovada pelos financiadores, é claro.

Aqui também exigimos que apenas o gerente possa chamar essa função (uma pequena modificação nisso em breve), obter a solicitação de gastos, exigir que mais da metade dos financiadores da campanha tenha realmente votado e também exigir que a solicitação de gastos não tenha sido concluída inicialmente.

Depois que todas as verificações acima forem aprovadas, transferimos o dinheiro (Eethers neste caso) para o destinatário e concluímos a solicitação de gastos.

Agora, a modificação que podemos fazer é remover o require (msg.sender == i_manager) e colocá-lo dentro de um modifier que podemos usar sempre que necessário. Isso nos ajuda a reduzir a repetição, já que estamos fazendo exatamente a mesma coisa nas funções createRequest e finalizeRequest.

Criamos nosso modificador conforme descrito acima e então os usamos nas funções createRequest e finalizeRequest da seguinte forma:


Ótimo, agora nosso contrato de campanha possui todos os requisitos necessários. Uma coisa a se observar, no entanto, é que todo o contrato que escrevemos até agora se aplica apenas a uma única campanha. Portanto, essencialmente, toda vez que alguém tem uma ideia que precisa de financiamento e decide usar nossa aplicação, teremos que implantar manualmente outra campanha.

Importando arquivos

Até agora, trabalhamos com apenas um arquivo Solidity que contém todo o nosso código, mas geralmente, precisaremos importar alguns outros contratos para a nossa aplicação e podemos fazer isso usando a palavra-chave import no Solidity.

Para resolver o problema de implantação manual mencionado anteriormente, uma maneira de lidar com isso é criar outro contrato que seria responsável por implantar nosso contrato de campanha (sim, um contrato pode implantar outro contrato). Então, apenas implantamos esse único contrato e sempre que alguém quiser usar nossa aplicação, esse contrato implanta um contrato de campanha para eles.

Dentro de nossa pasta src, vamos criar um arquivo Deploy.sol com o seguinte conteúdo:

Um contrato que cria outros contratos é chamado de fábrica. Portanto, nosso contrato DeployCampaign acima é uma fábrica.

Este contrato é bastante simples, pois temos apenas 2 funções. A primeira cria a campanha e passa o valor mínimo especificado, lançamos a campanha com a palavra-chave address e isso retorna o endereço onde o contrato foi implantado. Salvamos isso em uma variável de armazenamento para a matriz de endereços de todas as campanhas implantadas.

A segunda função simplesmente retorna todos os contratos implantados.

Observe a sintaxe usada para importar a campanha do arquivo Kickstarter.sol, já que estão no mesmo diretório.

Agora temos um problema. No contrato de campanha, estamos armazenando o endereço de quem cria o contrato na variável i_manager. O problema com nossa configuração atual é que agora o contrato DeployCampaign é realmente quem está criando o contrato de campanha, então o endereço armazenado na variável i_manager não será o endereço do gerente, mas sim será o endereço do contrato DeployCampaign. Ufa.

Quando chegarmos aos testes, veremos mais sobre isso.

Vamos modificar nosso contrato de campanha para que agora ele aceite não apenas a contribuição mínima, mas também o endereço de quem cria o contrato.

O construtor de campanha atualizado parece com isso:

enquanto o contrato DeployCampaign agora tem a seguinte aparência:

Agora está tudo pronto. O código completo para o Kickstarter e os arquivos de implementação já estão disponíveis:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.21;

contract Campaign {
   address private immutable i_manager;
   uint private immutable i_minimum_contribution;
   mapping(address funder => uint amount) private s_funders;
   uint private s_fundersCount;

   struct Request {
       string description;
       uint value;
       address payable recipient;
       bool complete;
       uint approvalCount;
       mapping(address funder => bool approval) approvals;
   }

   uint private s_numRequests;
   mapping(uint index => Request spendRequest) private s_spendRequests;

   modifier require_owner() {
       require(msg.sender == i_manager);
       _;
   }

   constructor(uint minimum, address manager) {
       i_manager = manager;
       i_minimum_contribution = minimum;
   }

   function contribute() public payable {
       require(msg.value >= i_minimum_contribution);
       s_funders[msg.sender] += msg.value;
       s_fundersCount++;
   }

   function createSpendRequest(
       string memory description,
       uint value,
       address payable recipient
   ) public require_owner {
       Request storage spendRequest = s_spendRequests[s_numRequests++];
       spendRequest.description = description;
       spendRequest.value = value;
       spendRequest.recipient = recipient;
       spendRequest.complete = false;
       spendRequest.approvalCount = 0;
   }

   function approveRequest(uint index) public {
       Request storage request = s_spendRequests[index];

       require(s_funders[msg.sender] > 0);
       require(!request.approvals[msg.sender]);

       request.approvalCount++;
       request.approvals[msg.sender] = true;
   }

   function finalizeRequest(uint index) public require_owner {
       Request storage request = s_spendRequests[index];

       require(request.approvalCount > (s_fundersCount / 2));
       require(!request.complete);

       request.recipient.transfer(request.value);
       request.complete = true;
   }

   function getManager() public view returns (address) {
       return i_manager;
   }

   function getMinimumContribution() public view returns (uint) {
       return i_minimum_contribution;
   }

   function getfundersCount() public view returns (uint) {
       return s_fundersCount;
   }
}
Enter fullscreen mode Exit fullscreen mode
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.21;

import {Campaign} from "./Kickstarter.sol";

contract DeployCampaign {
   address[] private s_deployedCampaigns;

   function createCampaign(uint minimum) public {
       address campaign = address(new Campaign(minimum, msg.sender));
       s_deployedCampaigns.push(campaign);
   }

   function getDeployedCampaigns() public view returns (address[] memory) {
       return s_deployedCampaigns;
   }
}
Enter fullscreen mode Exit fullscreen mode

Compilando e implantando nossos contratos

Implantar nosso contrato localmente é muito fácil assim que tivermos a configuração do Foundry. Vimos brevemente como executar um comando com o forge, e há ainda alguns outros, mas por enquanto vamos inicializar uma blockchain local. Podemos fazer isso apenas executando o comando anvil assim

anvil
Enter fullscreen mode Exit fullscreen mode

Isso deve imprimir o seguinte no seu terminal

Por padrão (e semelhante ao Remix), o anvil tem algumas contas criadas para nós usarmos e essas contas já foram preenchidas antecipadamente, neste caso, 10.000 ethers cada. Também vemos nossas chaves privadas e nossa Mnemônica de conta.

Observação: A chave privada no índice 0 corresponde à conta no índice 0 e assim por diante.

Também vemos que nossa rede blockchain local está sendo executada na porta 8545. Para compilar e implantar nosso contrato usando a chave privada no índice 0, o comando a ser executado é

forge create DeployCampaign --rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
Enter fullscreen mode Exit fullscreen mode

O rpc-url especifica onde queremos implantar nosso contrato, neste caso, é nossa cadeia anvil em execução localmente, e a private-key especifica a conta (no índice 0) que está realmente fazendo a implantação. Deveríamos obter a saída conforme abaixo:

Observe como o implantador corresponde à conta no índice 0

Idealmente, a maneira como queremos implantar nossos contratos usando o Foundry seria escrever scripts de implantação, mas falaremos mais sobre isso no próximo artigo.

Bem, então, definitivamente cobrimos bastante neste artigo e colocamos as mãos na massa trabalhando com o Foundry e as ferramentas que ele fornece, mas ainda há mais a ser coberto em termos de implantação e teste, todos os quais serão abordados no próximo artigo.

Se você gostou deste artigo, por favor, dê um aplauso, compartilhe, comente e siga. Até a próxima.

Você pode encontrar o próximo artigo aqui.


Artigo escrito por Ifeoluwaolubo. Traduzido por Marcelo Panegali.

Top comments (0)