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:
Criação de campanhas: Os usuários podem iniciar campanhas de arrecadação definindo uma meta e uma duração.
Contribuições: Os usuários podem contribuir com fundos para campanhas ativas.
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.
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.
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");
_;
}
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++;
}
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);
}
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);
}
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);
}
}
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;
uint256 goal;
uint256 deadline;
uint256 amountRaised;
bool isWithdrawn;
mapping(address \=\> uint256) contributions;
}
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;
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++;
}
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);
}
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");
_;
}
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");
_;
}
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);
}
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");
_;
}
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");
_;
}
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);
}
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);
Testes
Crie um arquivo crowdfunding.t.sol na pasta test e adicione o código abaixo ao arquivo.
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);
}
}
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/
PRIVATE_KEY=*************************************************
Adicione os detalhes abaixo ao foundry.toml
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
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
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.