Перейти к содержимому

Создайте и разверните децентрализованный смарт-контракт краудфандинга на Linea

Узнайте, как создать и развернуть децентрализованный смарт-контракт краудфандинга на Linea.

9 мин чтения
Создайте и разверните децентрализованный смарт-контракт краудфандинга на Linea

Краудфандинг изменил подход проектов, инициатив и бизнеса к привлечению средств, позволив авторам напрямую взаимодействовать со сторонниками и обходить традиционные финансовые институты. Технология блокчейн улучшает эту модель, добавляя прозрачность, децентрализацию и неизменяемость данных, обеспечивая более справедливую систему как для авторов, так и для участников.

В этой статье мы рассмотрим создание смарт-контракта для краудфандинга на Solidity, предназначенного для EVM-совместимых блокчейнов, таких как Linea. Контракт позволит пользователям создавать кампании, безопасно вносить средства, выводить их при успешном достижении цели и запрашивать возврат, если кампания не достигла нужного результата. Благодаря автоматизации смарт-контрактов платформа устраняет посредников и обеспечивает подотчётность.

Изучив реализацию, функции и возможности контракта, разработчики получат ценные знания о создании децентрализованных краудфандинговых платформ. Независимо от того, являетесь ли вы энтузиастом блокчейна, разработчиком или предпринимателем, это руководство покажет, как смарт-контракты могут революционизировать краудфандинг, делая его более безопасным, эффективным и не требующим доверия к третьим сторонам.

Предварительные требования

  • Вы должны иметь базовое понимание Solidity.

  • На вашем компьютере должны быть установлены Nodejs и Foundry.

Обзор контракта

Этот контракт обеспечивает:

  1. Создание кампании: Пользователи могут инициировать краудфандинговые кампании, устанавливая цель и продолжительность.

  2. Взносы: Пользователи могут вносить средства в активные кампании.

  3. Вывод средств: Авторы кампаний могут выводить средства, если цель по сбору достигнута после окончания кампании.

  4. Возвраты: Участники могут вернуть свои взносы, если цель не достигнута в установленный срок.

Настройка проекта

Выполните команду ниже, чтобы инициализировать проект Foundry. Для создания смарт-контракта мы будем использовать фреймворк Foundry.

forge init crowdfunding

Откройте папку crowdfunding в VS Code или вашем любимом редакторе кода и удалите файлы scripts/counter.s.sol, src/counter.sol и test/counter.t.sol.

Установите все зависимости

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

Полный код смарт-контракта для краудфандинга

Создайте файл crowdfunding.sol в папке src и добавьте в него приведённый ниже код.

// 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);
      }
  }
Копировать

Ключевые особенности кода

Структура кампании В контракте используется struct с именем Campaign для хранения данных кампании.

   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  
   }  
Копировать

Счётчик кампаний Каждая кампания уникально идентифицируется инкрементируемым campaignId, что обеспечивает масштабируемость и упрощает управление несколькими кампаниями.

    uint256 public campaignCount = 0;
    mapping(uint256 => Campaign) public campaigns;
Копировать

Создание кампании

Функция createCampaign позволяет пользователю инициировать кампанию. Она принимает два параметра: goal и duration. Параметр goal представляет целевую сумму, которую пользователь хочет собрать, а duration задаёт время в секундах, отведённое на достижение этой цели.

Функция проверяет, что оба параметра — goal и duration — больше нуля, чтобы исключить некорректные значения. Данные кампании затем сохраняются в структуре Campaign, и соответствующим образом присваивается campaignId.

Значение campaignCount увеличивается, чтобы вновь созданные кампании имели уникальные campaignId и не пересекались с предыдущими.

   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 позволяет пользователям финансировать активные кампании. Она принимает один параметр — campaignId, который служит уникальным идентификатором каждой кампании и определяет, куда будет направлен взнос.

Функция проверяет, что пользователь не вносит нулевую сумму. Она отслеживает amountRaised для кампании и фиксирует общий взнос каждого пользователя.

  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);
   }
Копировать

Модификатор campaignExists в функции contribute проверяет, является ли campaignId действительным, и выдаёт ошибку, если кампания не существует.

   modifier campaignExists(uint256 campaignId) {
           require(campaignId < campaignCount, "Campaign does not exist");
           _;
       }
Копировать

Модификатор beforeDeadline проверяет, истёк ли дедлайн кампании, и выдаёт ошибку, если это произошло.

   modifier beforeDeadline(uint256 campaignId) {
           require(block.timestamp <= campaigns[campaignId].deadline, "Campaign deadline has passed");
           _;
       }
Копировать

Вывод средств

Авторы кампаний могут выводить средства с помощью функции withdrawFunds, которая принимает один параметр — campaignId, служащий уникальным идентификатором каждой кампании.

Только автор кампании может выводить средства. Если это попытается сделать другой пользователь, функция выдаст ошибку.

Функция проверяет, что цель по сбору средств достигнута и что средства ещё не были выведены. Если цель не достигнута или средства уже выведены, функция выдаст ошибку.

При успешном выводе функция обновляет статус isWithdrawn на true и переводит средства автору кампании.

``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");
         _;
     }
Копировать

Модификатор afterDeadline проверяет, истёк ли дедлайн кампании, и выдаёт ошибку, если это ещё не произошло.

   modifier afterDeadline(uint256 campaignId) {
           require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
           _;
       }
Копировать

Возвраты для неудавшихся кампаний

Участники могут вернуть свои взносы, если кампания не достигла цели. Функция принимает один параметр — campaignId, который уникально идентифицирует каждую кампанию.

Она проверяет, что кампания не достигла своей цели по сбору средств и что пользователь сделал взнос. Если оба условия выполнены, функция обнуляет взнос пользователя и обрабатывает возврат.

    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);
       }
Копировать

События Контракт генерирует события для ключевых действий:

  • CampaignCreated: Фиксирует данные новой кампании.

  • ContributionMade: Фиксирует взносы в кампанию.

  • FundsWithdrawn: Фиксирует успешный вывод средств.

  • RefundIssued: Фиксирует возвраты, выданные участникам.

       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);
Копировать

Тесты

Создайте файл crowdfunding.t.sol в папке test и добавьте в него приведённый ниже код.

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

  }
Копировать

Набор тестов проверяет корректную работу контракта Crowdfunding по всем основным функциям. Ниже приведено описание тестов:

test_createCampaign Этот тест проверяет, что пользователь может успешно создать краудфандинговую кампанию. Кампания создаётся без ошибок, подтверждая корректную работу функции.

test_contributeCampaign Этот тест проверяет, что несколько пользователей могут вносить средства в конкретную кампанию. В тесте user1 создаёт кампанию, а user2 и user3 получают по 10 ether с помощью функции deal из Foundry. Оба пользователя — user2 и user3 — вносят по 6 ether в кампанию, и контракт корректно обновляет общую собранную сумму.

test_withdrawCampaign Этот тест проверяет, что автор кампании может вывести средства после достижения цели по сбору и истечения дедлайна. После того как user2 и user3 вносят средства и проходит 11 часов (смоделировано с помощью skip), user1 вызывает withdrawFunds для вывода собранных средств. Средства успешно переводятся, и баланс контракта отражает факт вывода.

test_requestCampaign Этот тест проверяет, что участники могут запросить возврат средств, если кампания не достигла своей цели. После того как user2 и user3 вносят средства и проходит 11 часов (смоделировано с помощью skip), оба пользователя вызывают requestRefund. Каждый пользователь успешно получает обратно внесённые ether.

Запуск тестов

forge test

Развёртывание и верификация в Linea

Добавьте необходимые переменные окружения в файл .env, убедившись, что он находится в корневой директории проекта. Эти переменные хранят конфиденциальные учётные данные, необходимые для взаимодействия с блокчейном.

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
Копировать

Добавьте приведённые ниже данные в foundry.toml

   [etherscan]
   linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Копировать

Создайте Makefile и добавьте в него приведённые ниже данные.

   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
Копировать

Развёртывание

make deploy

Верификация

make verify

Заключение

В этом руководстве рассмотрены основы создания и развёртывания децентрализованного смарт-контракта для краудфандинга в блокчейне Linea. Мы изучили ключевые функции контракта: создание кампаний, внесение взносов, вывод средств и возвраты, а также написали и запустили тесты для проверки поведения контракта. В завершение мы прошли через процесс развёртывания и верификации, чтобы запустить контракт в Linea.

Для получения практических руководств и углублённого изучения разработки на блокчейне посетите YouTube-канал MetaMask Developer и следите за нашими последними материалами в Twitter.

Переведено ИИ. Может содержать ошибки. Пожалуйста, всегда проверяйте информацию.

Оцените перевод

Автор статьи:

  • Kingsley Okonkwo

    Кингсли — сертифицированный разработчик блокчейна Ethereum, работающий техническим писателем в Consensys с особым акцентом на инструменты для разработчиков. До прихода в Consensys Кингсли работал фриланс-разработчиком бэкенда на нескольких платформах, включая Gigster и Braintrust. Имея за плечами 5 лет опыта в разработке программного обеспечения, он уверенно владеет такими инструментами, как JavaScript (Node.js и React), GoLang, SQL и Docker. Кингсли мастерски создаёт пошаговые руководства и практические гайды, используя новейшие технологии web3, чтобы помочь коллегам-разработчикам создавать более качественные dapps. В настоящее время проживая в Дубае, Кингсли в свободное время увлекается баскетболом и футболом.

    Читать все статьи