众筹改变了项目、事业和企业的融资方式,让创作者能够直接与支持者建立联系,绕过传统金融机构。区块链技术通过增加透明度、去中心化和不可篡改性来增强这一模式,为创作者和贡献者双方提供更公平的系统。
本文将指导您构建一个基于 Solidity 的众筹智能合约,专为 Linea 等 EVM 兼容区块链设计。该合约允许用户创建活动、安全地贡献资金、在成功达成目标后提取资金,以及在活动未达标时申请退款。通过利用智能合约自动化,该平台消除了中间商并强制执行问责制。
通过探索合约的实现、功能和特性,开发者将获得构建去中心化众筹平台的宝贵见解。无论您是区块链爱好者、开发者还是创业者,本指南都将展示智能合约如何通过使众筹更安全、更高效、更无需信任来彻底改变众筹行业。
前提条件
您应具备 Solidity 的基础知识。
您的电脑上应已安装 Nodejs 和 Foundry。
合约概述
本合约支持以下功能:
创建活动: 用户可以通过设置目标金额和持续时间来发起众筹活动。
贡献资金:用户可以向进行中的活动贡献资金。
提取资金: 活动创建者可在活动截止日期后达成筹款目标时提取资金。
退款:若活动在截止日期内未达成目标,贡献者可取回其贡献的资金。
项目设置
运行以下命令以初始化一个 Foundry 项目。我们将使用 Foundry 框架来构建智能合约。
forge init crowdfunding
在 VS Code 或您喜欢的代码编辑器中打开 crowdfunding 文件夹,并删除 scripts/counter.s.sol、src/counter.sol 和 test/counter.t.sol。
安装所有依赖项
forge install foundry-rs/forge-std --no-commit
众筹智能合约完整代码
在 src 文件夹中创建一个 crowdfunding.sol 文件,并将以下代码添加到该文件中。
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);
}
}
代码核心功能
活动结构 合约使用名为 Campaign 的 struct 来存储活动详情。
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);
}
contribute 函数中的 campaignExists 修饰符用于验证 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);
测试
在 test 文件夹中创建一个 crowdfunding.t.sol 文件,并将以下代码添加到该文件中。
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 各自通过 Foundry 的 deal 函数获得 10 个以太币。user2 和 user3 各向该活动贡献 6 个以太币,合约正确更新了活动的总筹款金额。
test_withdrawCampaign
此测试验证活动创建者能否在活动达成筹款目标且截止日期已过后提取资金。在 user2 和 user3 向活动贡献资金且经过 11 小时(通过 skip 模拟)后,user1 调用 withdrawFunds 提取筹集的资金。资金成功转移,合约余额反映了提取操作。
test_requestCampaign
此测试验证贡献者能否在活动未达成筹款目标时申请退款。在 user2 和 user3 向活动贡献资金且经过 11 小时(通过 skip 模拟)后,user2 和 user3 均调用 requestRefund。每位用户均成功收回了其贡献的以太币。
运行测试
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 上正式上线。
如需更多实践教程和区块链开发深度解析,请访问 MetaMask 开发者 YouTube 频道,并通过关注我们的 Twitter 获取最新动态。