Crowdfunding hat die Art und Weise, wie Projekte, Initiativen und Unternehmen Mittel einwerben, grundlegend verändert – indem es Erstellern ermöglicht, direkt mit Unterstützern in Kontakt zu treten und traditionelle Finanzinstitutionen zu umgehen. Die Blockchain-Technologie verbessert dieses Modell durch mehr Transparenz, Dezentralisierung und Unveränderlichkeit und sorgt so für ein gerechteres System für Ersteller und Beitragende gleichermaßen.
Dieser Artikel führt dich durch die Entwicklung eines Solidity-basierten Crowdfunding-Smart-Contracts, der für EVM-kompatible Blockchains wie Linea konzipiert ist. Der Contract ermöglicht es Nutzern, Kampagnen zu erstellen, Mittel sicher beizutragen, Gelder nach erfolgreichem Erreichen des Ziels abzuheben und Rückerstattungen anzufordern, wenn eine Kampagne ihr Ziel verfehlt. Durch den Einsatz von Smart-Contract-Automatisierung eliminiert die Plattform Vermittler und stellt Rechenschaftspflicht sicher.
Durch die Untersuchung der Implementierung, der Funktionen und der Funktionalität des Contracts erhalten Entwickler wertvolle Einblicke in den Aufbau dezentraler Crowdfunding-Plattformen. Egal ob du Blockchain-Enthusiast, Entwickler oder Unternehmer bist – dieser Leitfaden zeigt, wie Smart Contracts das Crowdfunding revolutionieren können, indem sie es sicherer, effizienter und vertrauenslos machen.
Voraussetzungen
Du solltest über grundlegende Kenntnisse in Solidity verfügen.
Du solltest Nodejs und Foundry auf deinem PC installiert haben.
Überblick über den Contract
Dieser Contract ermöglicht:
Kampagnenerstellung: Nutzer können Fundraising-Kampagnen starten, indem sie ein Ziel und eine Laufzeit festlegen.
Beiträge: Nutzer können aktive Kampagnen finanziell unterstützen.
Mittelabhebung: Kampagnenersteller können Gelder abheben, wenn das Fundraising-Ziel nach Ablauf der Kampagnenfrist erreicht wurde.
Rückerstattungen: Beitragende können ihre Beiträge zurückfordern, wenn das Ziel nicht innerhalb der Frist erreicht wurde.
Projekteinrichtung
Führe den folgenden Befehl aus, um ein Foundry-Projekt zu initiieren. Wir verwenden das Foundry-Framework, um den Smart Contract zu entwickeln.
forge init crowdfunding
Öffne den Ordner „crowdfunding" in VS Code oder deinem bevorzugten Code-Editor und lösche die Dateien scripts/counter.s.sol, src/counter.sol und test/counter.t.sol.
Alle Abhängigkeiten installieren
forge install foundry-rs/forge-std --no-commit
Vollständiger Code des Crowdfunding-Smart-Contracts
Erstelle eine crowdfunding.sol-Datei im Ordner src und füge den folgenden Code in die Datei ein.
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);
}
}
Wichtige Funktionen des Codes
Campaign-Struktur Der Contract verwendet eine struct namens Campaign, um Kampagnendetails zu speichern.
struct Campaign {
address creator;
uint256 goal;
uint256 deadline;
uint256 amountRaised;
bool isWithdrawn;
mapping(address \=\> uint256) contributions;
}
Kampagnenanzahl Jede Kampagne wird durch eine inkrementelle campaignId eindeutig identifiziert, was die Skalierbarkeit gewährleistet und die Verwaltung mehrerer Kampagnen vereinfacht.
uint256 public campaignCount = 0;
mapping(uint256 => Campaign) public campaigns;
Kampagnenerstellung
Die Funktion createCampaign ermöglicht es einem Nutzer, eine Kampagne zu starten. Sie nimmt zwei Parameter entgegen: goal und duration. goal steht für den Zielbetrag, den der Nutzer einwerben möchte, während duration die Zeit in Sekunden angibt, die zum Erreichen dieses Ziels zur Verfügung steht.
Die Funktion stellt sicher, dass sowohl goal als auch duration größer als null sind, um ungültige Eingaben zu verhindern. Die Kampagnendetails werden dann in der Campaign-Struct gespeichert und die campaignId wird entsprechend zugewiesen.
Der campaignCount wird erhöht, um sicherzustellen, dass neu erstellte Kampagnen eindeutige campaignIds haben und sich nicht mit früheren Kampagnen überschneiden.
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++;
}
Beitragsmechanismus
Die Funktion contribute ermöglicht es Nutzern, aktive Kampagnen zu finanzieren. Sie nimmt einen Parameter entgegen, campaignId, der als eindeutiger Bezeichner für jede Kampagne dient und bestimmt, wohin der Beitrag fließt.
Die Funktion stellt sicher, dass Nutzer keinen Nullbetrag beitragen. Sie verfolgt den amountRaised für die Kampagne und erfasst den Gesamtbeitrag jedes Nutzers.
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);
}
Der Modifier campaignExists in der Funktion contribute prüft, ob die campaignId gültig ist, und gibt einen Fehler aus, wenn die Kampagne nicht existiert.
modifier campaignExists(uint256 campaignId) {
require(campaignId < campaignCount, "Campaign does not exist");
_;
}
Der Modifier beforeDeadline prüft, ob die Frist der Kampagne abgelaufen ist, und gibt einen Fehler aus, wenn dies der Fall ist.
modifier beforeDeadline(uint256 campaignId) {
require(block.timestamp <= campaigns[campaignId].deadline, "Campaign deadline has passed");
_;
}
Gelder abheben
Kampagnenersteller können Gelder über die Funktion withdrawFunds abheben, die einen Parameter entgegennimmt, campaignId, der als eindeutiger Bezeichner für jede Kampagne dient.
Nur der Kampagnenersteller kann Gelder abheben. Wenn ein anderer Nutzer dies versucht, gibt die Funktion einen Fehler aus.
Die Funktion stellt sicher, dass das Finanzierungsziel erreicht wurde und die Gelder noch nicht abgehoben wurden. Wenn das Ziel nicht erreicht wurde oder die Gelder bereits abgehoben wurden, gibt die Funktion einen Fehler aus.
Nach erfolgreicher Abhebung aktualisiert die Funktion den Status isWithdrawn auf true und überweist die Gelder an den Kampagnenersteller.
``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");
_;
}
Der Modifier afterDeadline stellt sicher, dass die Frist der Kampagne abgelaufen ist, und gibt einen Fehler aus, wenn dies nicht der Fall ist.
modifier afterDeadline(uint256 campaignId) {
require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
_;
}
Rückerstattungen bei gescheiterten Kampagnen
Beitragende können ihre Beiträge zurückfordern, wenn die Kampagne scheitert. Die Funktion nimmt einen Parameter entgegen, campaignId, der jede Kampagne eindeutig identifiziert.
Sie prüft, ob die Kampagne ihr Finanzierungsziel nicht erreicht hat und ob der Nutzer einen Beitrag geleistet hat. Wenn beide Bedingungen erfüllt sind, setzt die Funktion den Beitrag des Nutzers auf null zurück und verarbeitet die Rückerstattung.
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);
}
Events
Der Contract gibt Events für wichtige Aktionen aus:
CampaignCreated: Protokolliert Details einer neuen Kampagne.
ContributionMade: Protokolliert Beiträge zu einer Kampagne.
FundsWithdrawn: Protokolliert erfolgreiche Mittelabhebungen.
RefundIssued: Protokolliert an Beitragende ausgestellte Rückerstattungen.
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);
Tests
Erstelle eine Datei crowdfunding.t.sol im Ordner „test" und füge den folgenden Code in die Datei ein.
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);
}
}
Die Test-Suite stellt sicher, dass der Crowdfunding-Contract über seine Kernfunktionen hinweg korrekt funktioniert. Im Folgenden findest du eine Übersicht der Tests:
test_createCampaign
Dieser Test prüft, ob ein Nutzer erfolgreich eine Crowdfunding-Kampagne erstellen kann. Die Kampagne wird fehlerfrei erstellt, was bestätigt, dass die Funktion wie erwartet funktioniert.
test_contributeCampaign
Dieser Test prüft, ob mehrere Nutzer zu einer bestimmten Kampagne beitragen können. Im Test erstellt user1 eine Kampagne, während user2 und user3 jeweils 10 Ether über Foundrys deal-Funktion erhalten. Sowohl user2 als auch user3 tragen jeweils 6 Ether zur Kampagne bei, und der Contract aktualisiert den Gesamtbetrag der Kampagne korrekt.
test_withdrawCampaign
Dieser Test prüft, ob der Kampagnenersteller Gelder abheben kann, sobald die Kampagne ihr Finanzierungsziel erreicht hat und die Frist abgelaufen ist. Nachdem user2 und user3 zur Kampagne beigetragen haben und 11 Stunden vergangen sind (simuliert mit skip), ruft user1 withdrawFunds auf, um die eingeworbenen Gelder abzuheben. Die Gelder werden erfolgreich übertragen und das Guthaben des Contracts spiegelt die Abhebung wider.
test_requestCampaign
Dieser Test prüft, ob Beitragende Rückerstattungen anfordern können, wenn die Kampagne ihr Finanzierungsziel nicht erreicht. Nachdem user2 und user3 zur Kampagne beigetragen haben und 11 Stunden vergangen sind (simuliert mit skip), rufen sowohl user2 als auch user3 requestRefund auf. Jeder Nutzer erhält seinen beigetragenen Ether erfolgreich zurück.
Test ausführen
forge test
Deployment und Verifizierung auf Linea
Füge die erforderlichen Umgebungsvariablen zur .env-Datei hinzu und stelle sicher, dass sie sich auf der Stammebene des Projekts befindet. Diese Variablen speichern sensible Zugangsdaten, die für Blockchain-Interaktionen benötigt werden.
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/
PRIVATE_KEY=*************************************************
Füge die folgenden Details zur foundry.toml hinzu.
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Erstelle ein Makefile und füge die folgenden Details hinzu.
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
Deployment
make deploy
Verifizierung
make verify
Fazit
Dieser Leitfaden hat die Grundlagen des Aufbaus und der Bereitstellung eines dezentralen Crowdfunding-Smart-Contracts auf der Linea-Blockchain behandelt. Wir haben wichtige Contract-Funktionalitäten wie Kampagnenerstellung, Beiträge, Abhebungen und Rückerstattungen untersucht sowie das Schreiben und Ausführen von Tests zur Validierung des Contract-Verhaltens. Abschließend haben wir den Deployment- und Verifizierungsprozess durchlaufen, um den Contract auf Linea live zu schalten.
Für weitere praxisnahe Tutorials und tiefergehende Einblicke in die Blockchain-Entwicklung besuche den MetaMask Developer YouTube-Kanal und bleib mit unseren neuesten Erkenntnissen auf dem Laufenden, indem du uns auf Twitter folgst.