Zum Inhalt springen

Einen dezentralen Crowdfunding-Smart-Contract auf Linea erstellen und deployen

Erfahre, wie du einen dezentralen Crowdfunding-Smart-Contract auf Linea erstellst und bereitstellst.

9 Min. Lesezeit
Einen dezentralen Crowdfunding-Smart-Contract auf Linea erstellen und deployen

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:

  1. Kampagnenerstellung: Nutzer können Fundraising-Kampagnen starten, indem sie ein Ziel und eine Laufzeit festlegen.

  2. Beiträge: Nutzer können aktive Kampagnen finanziell unterstützen.

  3. Mittelabhebung: Kampagnenersteller können Gelder abheben, wenn das Fundraising-Ziel nach Ablauf der Kampagnenfrist erreicht wurde.

  4. 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.

// 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);
      }
  }
Kopieren

Wichtige Funktionen des Codes

Campaign-Struktur Der Contract verwendet eine struct namens Campaign, um Kampagnendetails zu speichern.

   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  
   }  
Kopieren

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;
Kopieren

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++;
   }
Kopieren

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);
   }
Kopieren

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");
           _;
       }
Kopieren

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");
           _;
       }
Kopieren

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);
Kopieren

}

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");
         _;
     }
Kopieren

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");
           _;
       }
Kopieren

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);
       }
Kopieren

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);
Kopieren

Tests

Erstelle eine Datei crowdfunding.t.sol im Ordner „test" und füge den folgenden Code in die Datei ein.

// 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);
      }

  }
Kopieren

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/ // 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
Kopieren

Füge die folgenden Details zur foundry.toml hinzu.

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

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
Kopieren

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.

KI-übersetzt. Kann Fehler enthalten. Bitte überprüfen Sie die Informationen stets.

Bewerten Sie die Übersetzung

Dieser Artikel wurde verfasst von:

  • Kingsley Okonkwo

    Kingsley, zertifizierter Ethereum-Blockchain-Entwickler, ist als technischer Redakteur bei Consensys tätig und hat sich auf Entwickler-Tools spezialisiert. Vor seinem Einstieg bei Consensys arbeitete Kingsley als freiberuflicher Backend-Entwickler auf verschiedenen Plattformen, darunter Gigster und Braintrust. Mit 5 Jahren Erfahrung in der Softwareentwicklung beherrscht er Tools wie JavaScript (Node.js & React), GoLang, SQL und Docker. Kingsley zeichnet sich durch die Erstellung von Schritt-für-Schritt-Tutorials und Anleitungen aus und nutzt dabei die neuesten Web3-Technologien, um anderen Entwicklern beim Aufbau besserer Dapps zu helfen. Derzeit lebt Kingsley in Dubai und verbringt seine Freizeit gerne mit Basketball und Fußball.

    Alle Artikel lesen