콘텐츠로 건너뛰기

Linea에서 분산형 크라우드펀딩 스마트 계약 구축 및 배포

Linea에서 분산형 크라우드펀딩 스마트 컨트랙트를 빌드하고 배포하는 방법을 알아보세요.

9분 읽기
Linea에서 분산형 크라우드펀딩 스마트 계약 구축 및 배포

크라우드펀딩은 창작자가 지지자들과 직접 연결하고 기존 금융 기관을 우회할 수 있도록 함으로써 프로젝트, 사업, 단체가 자금을 조달하는 방식을 변화시켰습니다. 블록체인 기술은 투명성, 탈중앙화, 불변성을 더해 이 모델을 한층 강화하며, 창작자와 기여자 모두에게 더 공정한 시스템을 보장합니다.

이 글에서는 Linea와 같은 EVM 호환 블록체인을 위한 Solidity 기반 크라우드펀딩 스마트 컨트랙트를 구축하는 방법을 안내합니다. 이 컨트랙트를 통해 사용자는 캠페인을 생성하고, 안전하게 자금을 기여하며, 목표 달성 시 자금을 인출하고, 캠페인이 목표에 미치지 못할 경우 환불을 요청할 수 있습니다. 스마트 컨트랙트 자동화를 활용함으로써 플랫폼은 중개자를 제거하고 책임성을 강화합니다.

컨트랙트의 구현, 기능, 동작 방식을 살펴봄으로써 개발자들은 탈중앙화 크라우드펀딩 플랫폼 구축에 대한 귀중한 인사이트를 얻을 수 있습니다. 블록체인 열정가, 개발자, 창업자 누구에게나 이 가이드는 스마트 컨트랙트가 크라우드펀딩을 더 안전하고, 효율적이며, trustless하게 만들어 혁신할 수 있는 방법을 보여줄 것입니다.

사전 요구 사항

  • Solidity에 대한 기본적인 이해가 있어야 합니다.

  • PC에 Nodejs와 Foundry가 설치되어 있어야 합니다.

컨트랙트 개요

이 컨트랙트는 다음을 가능하게 합니다:

  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);
      }
  }
복사

코드의 주요 기능

캠페인 구조체 컨트랙트는 캠페인 세부 정보를 저장하기 위해 Campaign이라는 struct를 사용합니다.

   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 모두 0보다 큰지 확인합니다. 캠페인 세부 정보는 Campaign struct에 저장되고 campaignId가 그에 따라 할당됩니다.

새로 생성된 캠페인이 고유한 campaignId를 갖고 이전 캠페인과 겹치지 않도록 campaignCount가 증가합니다.

   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라는 하나의 매개변수를 받습니다.

이 함수는 사용자가 0 금액을 기여하지 않도록 보장합니다. 캠페인의 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 modifier는 campaignId가 유효한지 확인하고 캠페인이 존재하지 않으면 오류를 발생시킵니다.

   modifier campaignExists(uint256 campaignId) {
           require(campaignId < campaignCount, "Campaign does not exist");
           _;
       }
복사

beforeDeadline modifier는 캠페인의 마감일이 지났는지 확인하고 지났다면 오류를 발생시킵니다.

   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는 캠페인의 마감일이 지났는지 확인하고 지나지 않았다면 오류를 발생시킵니다.

   modifier afterDeadline(uint256 campaignId) {
           require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
           _;
       }
복사

실패한 캠페인에 대한 환불

캠페인이 실패할 경우 기여자는 기여금을 인출할 수 있습니다. 이 함수는 각 캠페인을 고유하게 식별하는 campaignId라는 하나의 매개변수를 받습니다.

캠페인이 모금 목표를 달성하지 못했는지, 사용자가 기여를 했는지 확인합니다. 두 조건이 모두 충족되면 함수는 사용자의 기여금을 0으로 초기화하고 환불을 처리합니다.

    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이 캠페인을 생성하고, user2와 user3은 Foundry의 deal 함수를 사용하여 각각 10 ether를 받습니다. user2와 user3 모두 캠페인에 6 ether를 기여하며, 컨트랙트는 캠페인의 총 모금액을 올바르게 업데이트합니다.

test_withdrawCampaign 이 테스트는 캠페인이 모금 목표를 달성하고 마감일이 지난 후 캠페인 창작자가 자금을 인출할 수 있는지 확인합니다. user2와 user3이 캠페인에 기여하고 11시간이 경과한 후(skip을 사용하여 시뮬레이션), user1이 withdrawFunds를 호출하여 모금된 자금을 인출합니다. 자금이 성공적으로 전송되고 컨트랙트 잔액에 인출이 반영됩니다.

test_requestCampaign 이 테스트는 캠페인이 모금 목표를 달성하지 못한 경우 기여자가 환불을 요청할 수 있는지 확인합니다. user2와 user3이 캠페인에 기여하고 11시간이 경과한 후(skip을 사용하여 시뮬레이션), user2와 user3 모두 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에서 컨트랙트를 라이브로 만들기 위한 배포 및 검증 프로세스를 안내했습니다.

더 많은 실습 튜토리얼과 블록체인 개발에 대한 심층적인 내용은 MetaMask Developer YouTube 채널을 확인하시고, Twitter를 팔로우하여 최신 인사이트를 받아보세요.

AI가 번역했습니다. 오류가 있을 수 있습니다. 항상 정보를 확인하시기 바랍니다.

번역 평가하기

이 글의 작성자:

  • Kingsley Okonkwo

    공인 이더리움 블록체인 개발자인 Kingsley는 Consensys에서 테크니컬 라이터로 활동하며, 특히 개발자 도구에 중점을 두고 있습니다. Consensys에 합류하기 전, Kingsley는 Gigster와 Braintrust를 포함한 여러 플랫폼에서 프리랜서 백엔드 개발자로 활동했습니다. 5년간의 소프트웨어 개발 경험을 바탕으로, JavaScript(Node.js 및 React), GoLang, SQL, Docker 등의 도구에 능숙합니다. Kingsley는 최신 web3 기술을 활용해 단계별 튜토리얼과 사용 방법 가이드를 제작하는 데 탁월하며, 동료 개발자들이 더 나은 dapp을 구축할 수 있도록 돕고 있습니다. 현재 두바이에 거주하고 있는 Kingsley는 여가 시간에 농구와 축구를 즐깁니다.

    모든 글 읽기