Краудфандинг изменил подход проектов, инициатив и бизнеса к привлечению средств, позволив авторам напрямую взаимодействовать со сторонниками и обходить традиционные финансовые институты. Технология блокчейн улучшает эту модель, добавляя прозрачность, децентрализацию и неизменяемость данных, обеспечивая более справедливую систему как для авторов, так и для участников.
В этой статье мы рассмотрим создание смарт-контракта для краудфандинга на Solidity, предназначенного для EVM-совместимых блокчейнов, таких как Linea. Контракт позволит пользователям создавать кампании, безопасно вносить средства, выводить их при успешном достижении цели и запрашивать возврат, если кампания не достигла нужного результата. Благодаря автоматизации смарт-контрактов платформа устраняет посредников и обеспечивает подотчётность.
Изучив реализацию, функции и возможности контракта, разработчики получат ценные знания о создании децентрализованных краудфандинговых платформ. Независимо от того, являетесь ли вы энтузиастом блокчейна, разработчиком или предпринимателем, это руководство покажет, как смарт-контракты могут революционизировать краудфандинг, делая его более безопасным, эффективным и не требующим доверия к третьим сторонам.
Предварительные требования
Вы должны иметь базовое понимание Solidity.
На вашем компьютере должны быть установлены Nodejs и Foundry.
Обзор контракта
Этот контракт обеспечивает:
Создание кампании: Пользователи могут инициировать краудфандинговые кампании, устанавливая цель и продолжительность.
Взносы: Пользователи могут вносить средства в активные кампании.
Вывод средств: Авторы кампаний могут выводить средства, если цель по сбору достигнута после окончания кампании.
Возвраты: Участники могут вернуть свои взносы, если цель не достигнута в установленный срок.
Настройка проекта
Выполните команду ниже, чтобы инициализировать проект 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 и добавьте в него приведённый ниже код.
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);
}
}
Ключевые особенности кода
Структура кампании В контракте используется struct с именем Campaign для хранения данных кампании.
struct Campaign {
address creator;
uint256 goal;
uint256 deadline;
uint256 amountRaised;
bool isWithdrawn;
mapping(address \=\> uint256) contributions;
}
Счётчик кампаний Каждая кампания уникально идентифицируется инкрементируемым 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 и добавьте в него приведённый ниже код.
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/
PRIVATE_KEY=*************************************************
Добавьте приведённые ниже данные в 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.