在 Linea 上构建并部署去中心化众筹智能合约

了解如何在 Linea 上构建和部署去中心化众筹智能合约。

9 分钟阅读
在 Linea 上构建并部署去中心化众筹智能合约

众筹改变了项目、事业和企业的融资方式,让创作者能够直接与支持者建立联系,绕过传统金融机构。区块链技术通过增加透明度、去中心化和不可篡改性来增强这一模式,为创作者和贡献者双方提供更公平的系统。

本文将指导您构建一个基于 Solidity 的众筹智能合约,专为 Linea 等 EVM 兼容区块链设计。该合约允许用户创建活动、安全地贡献资金、在成功达成目标后提取资金,以及在活动未达标时申请退款。通过利用智能合约自动化,该平台消除了中间商并强制执行问责制。

通过探索合约的实现、功能和特性,开发者将获得构建去中心化众筹平台的宝贵见解。无论您是区块链爱好者、开发者还是创业者,本指南都将展示智能合约如何通过使众筹更安全、更高效、更无需信任来彻底改变众筹行业。

前提条件

合约概述

本合约支持以下功能:

  1. 创建活动: 用户可以通过设置目标金额和持续时间来发起众筹活动。

  2. 贡献资金:用户可以向进行中的活动贡献资金。

  3. 提取资金: 活动创建者可在活动截止日期后达成筹款目标时提取资金。

  4. 退款:若活动在截止日期内未达成目标,贡献者可取回其贡献的资金。

项目设置

运行以下命令以初始化一个 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 文件,并将以下代码添加到该文件中。

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

代码核心功能

活动结构 合约使用名为 Campaignstruct 来存储活动详情。

   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 函数允许用户发起一个活动。它接受两个参数:goaldurationgoal 表示用户希望筹集的目标金额,而 duration 指定达成该目标所分配的时间(以秒为单位)。

该函数确保 goalduration 均大于零,以防止无效输入。活动详情随后存储在 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 文件,并将以下代码添加到该文件中。

// 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 创建一个活动,而 user2user3 各自通过 Foundry 的 deal 函数获得 10 个以太币。user2user3 各向该活动贡献 6 个以太币,合约正确更新了活动的总筹款金额。

test_withdrawCampaign 此测试验证活动创建者能否在活动达成筹款目标且截止日期已过后提取资金。在 user2user3 向活动贡献资金且经过 11 小时(通过 skip 模拟)后,user1 调用 withdrawFunds 提取筹集的资金。资金成功转移,合约余额反映了提取操作。

test_requestCampaign 此测试验证贡献者能否在活动未达成筹款目标时申请退款。在 user2user3 向活动贡献资金且经过 11 小时(通过 skip 模拟)后,user2user3 均调用 requestRefund。每位用户均成功收回了其贡献的以太币。

运行测试

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 上正式上线。

如需更多实践教程和区块链开发深度解析,请访问 MetaMask 开发者 YouTube 频道,并通过关注我们的 Twitter 获取最新动态。

AI 翻译。可能包含错误。请务必核实信息。

评价此翻译
  • Kingsley Okonkwo
    Kingsley Okonkwo

    Kingsley 是一位经认证的以太坊区块链开发者,现任 Consensys 技术写作工程师,专注于开发者工具领域。加入 Consensys 之前,Kingsley 曾在 GigsterBraintrust 等多个平台担任自由职业后端开发者。他拥有 5 年软件开发经验,熟练掌握 JavaScript(Node.js 和 React)、GoLang、SQL 及 Docker 等技术工具。Kingsley 擅长撰写分步教程和操作指南,善用最新 Web3 技术帮助开发者构建更优质的 dapp。他目前定居迪拜,业余时间喜欢打篮球和踢足球。

    阅读所有文章