Pular para o conteúdo

Crie e implante um contrato inteligente de crowdfunding descentralizado na Linea

Aprenda a criar e implantar um contrato inteligente de crowdfunding descentralizado na Linea.

9 min de leitura
Crie e implante um contrato inteligente de crowdfunding descentralizado na Linea

O crowdfunding transformou a forma como projetos, causas e empresas captam recursos, permitindo que criadores se conectem diretamente com apoiadores e contornem as instituições financeiras tradicionais. A tecnologia blockchain aprimora esse modelo ao adicionar transparência, descentralização e imutabilidade, garantindo um sistema mais justo tanto para criadores quanto para contribuidores.

Este artigo vai guiá-lo na construção de um Smart Contract de Crowdfunding baseado em Solidity, projetado para blockchains compatíveis com EVM, como a Linea. O contrato permitirá que usuários criem campanhas, contribuam com fundos de forma segura, realizem saques após atingir a meta com sucesso e solicitem reembolsos caso uma campanha não alcance seu objetivo. Ao aproveitar a automação de smart contracts, a plataforma elimina intermediários e garante a responsabilização.

Ao explorar a implementação, os recursos e a funcionalidade do contrato, os desenvolvedores obterão insights valiosos sobre como construir plataformas de crowdfunding descentralizadas. Seja você um entusiasta de blockchain, desenvolvedor ou empreendedor, este guia demonstrará como os smart contracts podem revolucionar o crowdfunding, tornando-o mais seguro, eficiente e trustless.

Pré-requisitos

  • Você deve ter um entendimento básico de Solidity.

  • Você deve ter o Nodejs e o Foundry instalados no seu computador.

Visão geral do contrato

Este contrato permite:

  1. Criação de campanhas: Os usuários podem iniciar campanhas de arrecadação definindo uma meta e uma duração.

  2. Contribuições: Os usuários podem contribuir com fundos para campanhas ativas.

  3. Saque de fundos: Os criadores de campanhas podem sacar os fundos caso a meta de arrecadação seja atingida após o prazo da campanha.

  4. Reembolsos: Os contribuidores podem recuperar suas contribuições caso a meta não seja atingida dentro do prazo.

Configuração do projeto

Execute o comando abaixo para iniciar um projeto Foundry. Utilizaremos o framework Foundry para construir o smart contract.

forge init crowdfunding

Abra a pasta crowdfunding no VS Code ou no seu editor de código favorito e exclua os arquivos scripts/counter.s.sol, src/counter.sol e test/counter.t.sol.

Instalar todas as dependências

forge install foundry-rs/forge-std --no-commit

Código completo do smart contract de crowdfunding

Crie um arquivo crowdfunding.sol na pasta src e adicione o código abaixo ao arquivo.

// SPDX-License-Identifier: MIT
  pragma solidity 0.8.18;

  contract Crowdfunding {
      struct Campaign {
          address creator;
          uint256 goal;
          uint256 deadline;
          uint256 amountRaised;
          bool isWithdrawn;
          mapping(address => uint256) contributions;
      }

      uint256 public campaignCount = 0;
      mapping(uint256 => Campaign) public campaigns;
      event CampaignCreated(uint256 campaignId, address creator, uint256 goal, uint256 deadline);
      event ContributionMade(uint256 campaignId, address contributor, uint256 amount);
      event FundsWithdrawn(uint256 campaignId, uint256 amount);
      event RefundIssued(uint256 campaignId, address contributor, uint256 amount);

      modifier campaignExists(uint256 campaignId) {
          require(campaignId < campaignCount, "Campaign does not exist");
          _;
      }

      modifier onlyCreator(uint256 campaignId) {
          require(msg.sender == campaigns[campaignId].creator, "Only the campaign creator can perform this action");
          _;
      }

      modifier beforeDeadline(uint256 campaignId) {
          require(block.timestamp <= campaigns[campaignId].deadline, "Campaign deadline has passed");
          _;
      }

      modifier afterDeadline(uint256 campaignId) {
          require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
          _;
      }

      // Create a new crowdfunding campaign
      function createCampaign(uint256 goal, uint256 duration) external {
          require(goal > 0, "Goal must be greater than zero");
          require(duration > 0, "Duration must be greater than zero");
          Campaign storage newCampaign = campaigns[campaignCount];
          newCampaign.creator = msg.sender;
          newCampaign.goal = goal;
          newCampaign.deadline = block.timestamp + duration;
          emit CampaignCreated(campaignCount, msg.sender, goal, newCampaign.deadline);
          campaignCount++;
      }

      // Contribute funds to a campaign
      function contribute(uint256 campaignId) external payable campaignExists(campaignId) beforeDeadline(campaignId) {
          require(msg.value > 0, "Contribution must be greater than zero");
          Campaign storage campaign = campaigns[campaignId];
          campaign.amountRaised += msg.value;
          campaign.contributions[msg.sender] += msg.value;
          emit ContributionMade(campaignId, msg.sender, msg.value);
      }

      // Withdraw funds if the campaign meets its goal
      function withdrawFunds(uint256 campaignId) external campaignExists(campaignId) onlyCreator(campaignId) afterDeadline(campaignId) {
          Campaign storage campaign = campaigns[campaignId];
          require(campaign.amountRaised >= campaign.goal, "Funding goal not reached");
          require(!campaign.isWithdrawn, "Funds already withdrawn");
          campaign.isWithdrawn = true;
          (bool sent, ) = campaign.creator.call{value: campaign.amountRaised}("");
          require(sent, "Failed to send Ether");
          emit FundsWithdrawn(campaignId, campaign.amountRaised);
      }

      // Request a refund if the campaign fails
      function requestRefund(uint256 campaignId) external campaignExists(campaignId) afterDeadline(campaignId) {
          Campaign storage campaign = campaigns[campaignId];
          require(campaign.amountRaised < campaign.goal, "Campaign succeeded, refunds not allowed");
          uint256 contribution = campaign.contributions[msg.sender];
          require(contribution > 0, "No contributions found for this campaign");
          campaign.contributions[msg.sender] = 0;
          (bool sent, ) = msg.sender.call{value: contribution}("");
          require(sent, "Failed to send Ether");
          emit RefundIssued(campaignId, msg.sender, contribution);
      }
  }
Copiar

Principais recursos do código

Estrutura da campanha O contrato utiliza uma struct chamada Campaign para armazenar os detalhes da campanha.

   struct Campaign {  
       address creator;            // Campaign creator  
       uint256 goal;               // Fundraising goal  
       uint256 deadline;           // Deadline to achieve the goal  
       uint256 amountRaised;       // Total contributions raised  
       bool isWithdrawn;           // Tracks if funds were withdrawn  
       mapping(address \=\> uint256) contributions; // Tracks contributions by users  
   }  
Copiar

Contagem de campanhas Cada campanha é identificada de forma única por um campaignId incremental, garantindo escalabilidade e simplificando o gerenciamento de múltiplas campanhas.

    uint256 public campaignCount = 0;
    mapping(uint256 => Campaign) public campaigns;
Copiar

Criação de campanha

A função createCampaign permite que um usuário inicie uma campanha. Ela recebe dois parâmetros: goal e duration. O goal representa o valor-alvo que o usuário deseja arrecadar, enquanto duration especifica o tempo em segundos disponível para atingir essa meta.

A função garante que tanto o goal quanto o duration sejam maiores que zero para evitar entradas inválidas. Os detalhes da campanha são então armazenados na struct Campaign, e o campaignId é atribuído de acordo.

O campaignCount é incrementado para garantir que as campanhas recém-criadas tenham campaignIds únicos e não se sobreponham a campanhas anteriores.

   function createCampaign(uint256 goal, uint256 duration) external {
       require(goal > 0, "Goal must be greater than zero");
       require(duration > 0, "Duration must be greater than zero");

       Campaign storage newCampaign = campaigns[campaignCount];
       newCampaign.creator = msg.sender;
       newCampaign.goal = goal;
       newCampaign.deadline = block.timestamp + duration;

       emit CampaignCreated(campaignCount, msg.sender, goal, newCampaign.deadline);
       campaignCount++;
   }
Copiar

Mecanismo de contribuição

A função contribute permite que os usuários financiem campanhas ativas. Ela recebe um parâmetro, campaignId, que serve como identificador único para cada campanha e determina para onde a contribuição será alocada.

A função garante que os usuários não contribuam com valor zero. Ela rastreia o amountRaised da campanha e registra a contribuição total feita por cada usuário.

  function contribute(uint256 campaignId) external payable campaignExists(campaignId) beforeDeadline(campaignId) {
       require(msg.value > 0, "Contribution must be greater than zero");

       Campaign storage campaign = campaigns[campaignId];
       campaign.amountRaised += msg.value;
       campaign.contributions[msg.sender] += msg.value;

       emit ContributionMade(campaignId, msg.sender, msg.value);
   }
Copiar

O modificador campaignExists na função contribute verifica se o campaignId é válido e lança um erro caso a campanha não exista.

   modifier campaignExists(uint256 campaignId) {
           require(campaignId < campaignCount, "Campaign does not exist");
           _;
       }
Copiar

O modificador beforeDeadline verifica se o prazo da campanha já passou e lança um erro caso tenha passado.

   modifier beforeDeadline(uint256 campaignId) {
           require(block.timestamp <= campaigns[campaignId].deadline, "Campaign deadline has passed");
           _;
       }
Copiar

Saque de fundos

Os criadores de campanhas podem sacar fundos utilizando a função withdrawFunds, que recebe um parâmetro, campaignId, servindo como identificador único para cada campanha.

Somente o criador da campanha pode sacar os fundos. Caso qualquer outro usuário tente fazê-lo, a função lançará um erro.

A função garante que a meta de arrecadação foi atingida e que os fundos ainda não foram sacados. Se a meta não for atingida ou os fundos já tiverem sido sacados, a função lançará um erro.

Após o saque bem-sucedido, a função atualiza o status isWithdrawn para true e transfere os fundos para o criador da campanha.

``Solidity function withdrawFunds(uint256 campaignId) external campaignExists(campaignId) onlyCreator(campaignId) afterDeadline(campaignId) { Campaign storage campaign = campaigns[campaignId]; require(campaign.amountRaised >= campaign.goal, "Funding goal not reached"); require(!campaign.isWithdrawn, "Funds already withdrawn");

   campaign.isWithdrawn = true;
   (bool sent, ) = campaign.creator.call{value: campaign.amountRaised}("");
   require(sent, "Failed to send Ether");

   emit FundsWithdrawn(campaignId, campaign.amountRaised);
Copiar

}

The `onlyCreator` modifier verifies whether the caller is the campaign creator and throws an error if they are not.

``Solidity
   modifier onlyCreator(uint256 campaignId) {
         require(msg.sender == campaigns[campaignId].creator, "Only the campaign creator can perform this action");
         _;
     }
Copiar

O modificador afterDeadline garante que o prazo da campanha já passou e lança um erro caso ainda não tenha passado.

   modifier afterDeadline(uint256 campaignId) {
           require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
           _;
       }
Copiar

Reembolsos para campanhas malsucedidas

Os contribuidores podem retirar suas contribuições caso a campanha falhe. A função recebe um parâmetro, campaignId, que identifica exclusivamente cada campanha.

Ela verifica se a campanha não atingiu sua meta de arrecadação e se o usuário fez uma contribuição. Se ambas as condições forem atendidas, a função redefine a contribuição do usuário para zero e processa o reembolso.

    function requestRefund(uint256 campaignId) external campaignExists(campaignId) afterDeadline(campaignId) {
           Campaign storage campaign = campaigns[campaignId];
           require(campaign.amountRaised < campaign.goal, "Campaign succeeded, refunds not allowed");
           uint256 contribution = campaign.contributions[msg.sender];
           require(contribution > 0, "No contributions found for this campaign");
           campaign.contributions[msg.sender] = 0;
           (bool sent, ) = msg.sender.call{value: contribution}("");
           require(sent, "Failed to send Ether");
           emit RefundIssued(campaignId, msg.sender, contribution);
       }
Copiar

Eventos O contrato emite eventos para ações principais:

  • CampaignCreated: Registra os detalhes de uma nova campanha.

  • ContributionMade: Registra contribuições para uma campanha.

  • FundsWithdrawn: Registra saques de fundos bem-sucedidos.

  • RefundIssued: Registra reembolsos emitidos para contribuidores.

       event CampaignCreated(uint256 campaignId, address creator, uint256 goal, uint256 deadline);
       event ContributionMade(uint256 campaignId, address contributor, uint256 amount);
       event FundsWithdrawn(uint256 campaignId, uint256 amount);
       event RefundIssued(uint256 campaignId, address contributor, uint256 amount);
Copiar

Testes

Crie um arquivo crowdfunding.t.sol na pasta test e adicione o código abaixo ao arquivo.

// SPDX-License-Identifier: UNLICENSED
  pragma solidity ^0.8.13;
  import {Test, console} from "forge-std/Test.sol";
  import {Crowdfunding} from "../src/Crowdfunding.sol";
  contract CounterTest is Test {
      Crowdfunding public crowd;
      address public user1 = makeAddr("user1");
      address public user2 = makeAddr("user2");
      address public user3 = makeAddr("user3");

      function setUp() public {
          crowd = new Crowdfunding();
      }

      function test_createCampaign() public {
          vm.startPrank(user1);
          crowd.createCampaign(10 ether, 10 hours);
      }

      function test_contributeCampaign() public {
          vm.startPrank(user1);
          crowd.createCampaign(10 ether, 10 hours);
          deal(user2, 10 ether);
          deal(user3, 10 ether);
          vm.startPrank(user2);
          crowd.contribute{value: 6 ether}(0);
           vm.startPrank(user3);
          crowd.contribute{value: 6 ether}(0);
      }

       function test_withdrawCampaign() public {
          vm.startPrank(user1);
          crowd.createCampaign(10 ether, 10 hours);
          deal(user2, 10 ether);
          deal(user3, 10 ether);
          vm.startPrank(user2);
          crowd.contribute{value: 6 ether}(0);
           vm.startPrank(user3);
          crowd.contribute{value: 6 ether}(0);
          skip(11 hours);
          console.log("b4", user1.balance);
          vm.startPrank(user1);
          crowd.withdrawFunds(0);
          console.log("after",user1.balance);
      }

      function test_requestCampaign() public {
          vm.startPrank(user1);
          crowd.createCampaign(10 ether, 10 hours);
          deal(user2, 10 ether);
          deal(user3, 10 ether);
          vm.startPrank(user2);
          crowd.contribute{value: 2 ether}(0);
           vm.startPrank(user3);
          crowd.contribute{value: 2 ether}(0);
          skip(11 hours);
          vm.startPrank(user2);
          crowd.requestRefund(0);
          vm.startPrank(user3);
          crowd.requestRefund(0);
          console.log("after",user2.balance);
          console.log("after",user3.balance);
      }

  }
Copiar

O conjunto de testes garante que o contrato Crowdfunding funcione corretamente em seus recursos principais. Abaixo está um detalhamento dos testes:

test_createCampaign Este teste verifica se um usuário pode criar com sucesso uma campanha de crowdfunding. A campanha é criada sem erros, confirmando que a função funciona conforme o esperado.

test_contributeCampaign Este teste verifica se múltiplos usuários podem contribuir para uma campanha específica. No teste, user1 cria uma campanha, enquanto user2 e user3 recebem 10 ether cada usando a função deal do Foundry. Tanto user2 quanto user3 contribuem com 6 ether para a campanha, e o contrato atualiza corretamente o valor total arrecadado pela campanha.

test_withdrawCampaign Este teste verifica se o criador da campanha pode sacar os fundos assim que a campanha atingir sua meta de arrecadação e o prazo tiver passado. Após user2 e user3 contribuírem para a campanha e 11 horas terem se passado (simuladas usando skip), user1 chama withdrawFunds para sacar os fundos arrecadados. Os fundos são transferidos com sucesso e o saldo do contrato reflete o saque.

test_requestCampaign Este teste verifica se os contribuidores podem solicitar reembolsos caso a campanha não atinja sua meta de arrecadação. Após user2 e user3 contribuírem para a campanha e 11 horas terem se passado (simuladas usando skip), tanto user2 quanto user3 chamam requestRefund. Cada usuário recebe de volta com sucesso o ether que contribuiu.

Executar testes

forge test

Implantação e verificação na Linea

Adicione as variáveis de ambiente necessárias ao arquivo .env, garantindo que ele esteja posicionado no nível raiz do projeto. Essas variáveis armazenam credenciais sensíveis necessárias para interações com a blockchain.

LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/ // Infura RPC endpoint for connecting to Linea 
PRIVATE_KEY=************************************************* // Wallet private key for signing transactions LINEA_API_KEY=************************* // API key for accessing Linea services via Infura
Copiar

Adicione os detalhes abaixo ao foundry.toml

   [etherscan]
   linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Copiar

Crie um Makefile e adicione os detalhes abaixo.

   deploy:
       forge create src/Crowdfunding.sol:Crowdfunding --rpc-url $(LINEA_RPC_URL) --private-key $(PRIVATE_KEY)

   verify:; forge verify-contract --rpc-url $(LINEA_RPC_URL) --chain linea <contract address> src/Crowdfunding.sol:Crowdfunding
Copiar

Implantação

make deploy

Verificação

make verify

Conclusão

Este guia abordou os fundamentos da construção e implantação de um smart contract de crowdfunding descentralizado na blockchain Linea. Exploramos as principais funcionalidades do contrato, como criação de campanhas, contribuições, saques e reembolsos, além de escrever e executar testes para validar o comportamento do contrato. Por fim, percorremos o processo de implantação e verificação para colocar o contrato em funcionamento na Linea.

Para mais tutoriais práticos e análises aprofundadas sobre desenvolvimento em blockchain, confira o canal do YouTube do MetaMask Developer e fique por dentro das nossas últimas novidades nos seguindo no Twitter.

Traduzido por IA. Pode conter erros. Por favor, verifique sempre as informações.

Avalie a tradução

Este artigo foi escrito por:

  • Kingsley Okonkwo

    Kingsley, desenvolvedor certificado de Blockchain Ethereum, atua como redator técnico na Consensys, com foco especial em ferramentas para desenvolvedores. Antes de ingressar na Consensys, Kingsley trabalhou como desenvolvedor backend freelance em diversas plataformas, incluindo Gigster e Braintrust. Com 5 anos de experiência em desenvolvimento de software, ele domina ferramentas como JavaScript (Node.js e React), GoLang, SQL e Docker. Kingsley se destaca na criação de tutoriais passo a passo e guias práticos, utilizando o que há de mais moderno em tecnologia web3 para ajudar outros desenvolvedores a criar dapps melhores. Atualmente radicado em Dubai, Kingsley aproveita seu tempo livre jogando basquete e futebol.

    Ler todos os artigos