iT邦幫忙

2023 iThome 鐵人賽

DAY 23
0
Web 3

從 區塊鏈 到 去中心化應用程式(DApp)系列 第 23

智能合約開發: 建立合約-區塊鏈上的群眾募資

  • 分享至 

  • xImage
  •  

建立合約-區塊鏈上的群眾募資

首先一樣先行討論 "群眾募資" 應用於 "智能合約" 的優點

  • 跨國性
    由於虛擬貨幣本身不具有國域 任何人均可以購買
    不會像是一般網路購物 限制於必須要有那些國家的銀行帳戶
  • 自動執行
    智能合約的自動執行 提供了保障 確保過程的合理性
  • 不可篡改性
    尤其對於股權型或是借貸型 擁有不可篡改的紀錄顯得格外重要
  • 去中心化
    資訊不受限於平台 避免平台對於資訊的濫用

因此使用智能合約進行群眾募資是有一定的優勢的

聽起來就很誘人吧~ 感覺是個不錯的應用。

接下來本篇就依照以下重點進行敘述:

  1. 專案目標規劃
  2. 智能合約撰寫
  3. 智能合約的部署
  4. 智能合約的互動

另外關於此篇的GitHub 專案位置
https://github.com/weiawesome/hardhat_tutorial

專案目標規劃

專案架構

平台

  • 提供給予提案者提案的機會,提案就為提案者建立基礎子合約
  • 提案者可以在平台更新自己的個人資訊(供贊助者查看)
  • 通過驗證後 啟動合約(基礎子合約 需激活才能使用)

專案

  • 一個平台可以有多個專案(屬於平台的子合約)
  • 可以放各種資訊(簡介、方案、種類、目標金額等等)
  • 提供贊助者 贊助的方式
  • 募款時間到時 自動處理募款金額

  • 平台上
    基本上是提供一個 可以放基本資訊與提案的位置
  • 專案上
    提案者成立自己的專案 整個募資過程就在自己的智能合約執行
    提案者對於自己的提案擁有相對的控制權

平台不會過多干涉 集資的過程 一切都由"智能合約"所控制

群眾募資 (眾籌) (CrowdFunding) 基本資訊

群眾募資是一種新興的募款方式
基本上它是結合了 團購 與 預購 的概念

透過群眾募資
既讓提案者擁有資金去研發、生產
也能讓提案者觀察市場聲量、偏好等等資訊

臺灣上 較人為熟知的例子像是 嘖嘖、挖貝 等等。

以分類來說

回饋方式

  1. 股權型
  2. 借貸型
  3. 回饋型
  4. 捐贈型

資金處理

  1. All Or Nothing(未達目標金額則全數退回給使用者)
  2. For Proposer(不管達標與否 都給提案者)

接下來合約的撰寫 會去實現群眾募資的效果

智能合約撰寫

平臺合約

// ./contracts/CrowdFunding.sol
/// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CrowdFunding {
    // 平台擁有者
    address public owner;

    // 提案事件
    event ProposalEvent(address indexed addr,address indexed contractAddress,string title);
    // 個人資料事件
    event UserInfoEvent(address indexed addr,string name,string info);

    // 建構子
    constructor(){
        owner=msg.sender;
    }

    // 提案
    function propose(string memory _title) public payable{
        // 提供基本資金用來建立子合約
        require(msg.value>0,"The proposal need a fundamental asset.");
        Proposal p=new Proposal{value: msg.value}(msg.sender,_title);
        // 新增提案事件
        emit ProposalEvent(msg.sender,address(p),_title);
    }

    // 激活子合約
    function activateProposal(address _contractAddr) public{
        Proposal proposal = Proposal(_contractAddr);
        proposal.activateProposal(msg.sender);
    }
    
    // 更新個人資料
    function updatePersonalInfo(string memory _name,string memory _info) public{
        emit UserInfoEvent(msg.sender,_name,_info);
    }
}

專案合約

合約名稱

// ./contracts/CrowdFunding.sol
contract Proposal{
}

專案分類

// 專案種類
enum ProposalType{
    LendingBased,
    EquityBased,
    DonationBased,
    RewardBased
}
// 專案內容分類
enum ProposalClassification{
    Social,
    Multimedia,
    Entertainment,
    Publishing,
    Lifestyle,
    Design,
    Technology,
    Leisure,
    Other
}
// 專案處理募款的方式
enum ProposalReturnSystem{
    AllOrNothing,
    ForProposer
}

合約物件

// 專案提供的募資方案
struct Plan {
    // 方案名稱
    string title;
    // 方案內容
    string content;
    // 方案提供的數量
    uint quantity;
    // 方案的價格
    uint price;
    // 方案是否存在
    bool exist;
}

合約變數

// 平台擁有者
address public platformOwner;
// 專案擁有者
address payable public  owner;
// 專案名稱
string public proposalTitle;

// 專案細節介紹
string public info;
// 募款金額
uint public contractAmount;
// 目標金額
uint public goalAmount;
// 募款期限
uint public campaignDuration;
// 方案們
string[] public plansTitle;
mapping(string => Plan)  public plans;

// 各種分類
ProposalType public pt;
ProposalClassification public pc;
ProposalReturnSystem public pr;

// 提供贊助的紀錄(All Or Nothing 才需紀錄)
mapping(address => uint) public backRecord;
address[] public backers;

// 合約激活與否
bool public contractValid;

合約事件

// 捐贈事件
event BackEvent(address addr,string _title,uint amount);
// 專案結算 募款金額流向紀錄
event SettlementEvent(address addr,string _title,uint amount);

建構子

constructor(address _owner,string memory _title) payable{
    platformOwner=msg.sender;
    owner=payable(_owner);
    proposalTitle=_title;
    contractValid=false;
}

修飾函數

// 唯有合約擁有者可以訪問
modifier onlyOwner() {
    require(msg.sender == owner, "Only owner can call this function");
    _;
}
// 唯有合約激活可以訪問
modifier onlyContractValid() {
    require(contractValid, "The contract is invalid now");
    _;
}
// 唯有募款期間可以訪問
modifier inFundingPeriod(){
    require(block.timestamp<campaignDuration,"The crowd funding time is over.");
    _;
}
// 唯有募款結束可以訪問
modifier outFundingPeriod(){
    require(block.timestamp>campaignDuration,"The crowd funding time is not over.");
    _;
}

修改或新增資訊 相關函式

// 修改 專案資訊(介紹資訊)
function editInfo(string memory _info) public onlyOwner{
    info=_info;
}
// 新增 募款方案
function editPlan(string memory _title,string memory _content,uint _quantity,uint _price) public onlyOwner{
    require(plans[_title].exist==false,"The plan has been exist.");
    require(_price>0,"The price must be large then 0.");
    plansTitle.push(_title);
    plans[_title]=Plan(_title,_content,_quantity,_price,true);
}
// 設定 募款金額
function editGoalAmount(uint _amount) public onlyOwner{
    require(_amount>0,"The GoalAmount must be large than 0.");
    goalAmount=_amount;
}
// 設定 募款期限
function editCampaignDuration(uint _time) public onlyOwner{
    require(_time>block.timestamp,"The time must be in the future.");
    campaignDuration=_time;
}
// 設定專案種類
function editType(ProposalType _pt,ProposalClassification _pc,ProposalReturnSystem _pr) public onlyOwner{
    pt=_pt;
    pc=_pc;
    pr=_pr;
}

獲取資訊相關函式

// 獲取專案名稱
function getTitle() public view returns(string memory){
    return proposalTitle;
}
// 獲取專案資訊(介紹)
function getInfo() public view returns(string memory) {
    return info;
}
// 獲取專案可贊助的方案
function getPlans() public view returns(Plan[] memory) {
    Plan[] memory result = new Plan[](plansTitle.length);
    uint index = 0;

    for (uint i = 0; i < plansTitle.length; i++) {
        string memory _title=plansTitle[i];
        Plan storage plan = plans[_title];
        if (plan.exist==true){
            result[index]=plan;
            index++;
        }
    }
    return result;
}
// 獲取目標募款金額
function getGoalAmount() public view returns(uint) {
    return goalAmount;
}
// 獲取專案各項數據種類
function getType() public view returns(ProposalType,ProposalClassification,ProposalReturnSystem){
    return (pt,pc,pr);
}
// 獲取當前募款到的金額
function getAmount() public view returns(uint) {
    return contractAmount;
}
// 獲取募款期限
function getCampaignDuration() public view returns(uint) {
    return campaignDuration;
}

贊助專案

// 根據方案名稱進行贊助
function backPlan(string memory _title) public payable onlyContractValid inFundingPeriod{
    require(plans[_title].exist==true,"The plan is not exist.");
    require(plans[_title].quantity>0,"The plan is sold out.");
    require(msg.value==plans[_title].price,"The value is not equal to the plan's price.");
    if (pr==ProposalReturnSystem.AllOrNothing){
        if (backRecord[msg.sender]==0){
            backers.push(msg.sender);
        }
        backRecord[msg.sender]+=msg.value;
    }
    contractAmount+=msg.value;
    plans[_title].quantity-=1;
    emit BackEvent(msg.sender,_title,msg.value);
}

結算募款項目

// 根據類型不同進行不同的處理
function settleProposal() public outFundingPeriod{
    if (pr==ProposalReturnSystem.AllOrNothing){
        if (contractAmount>=goalAmount){
            uint remainAmount=address(this).balance;
            owner.transfer(remainAmount);
            emit SettlementEvent(owner,proposalTitle,remainAmount);
        } else{
            for (uint i=0;i<backers.length;i++){
                address payable addr=payable(backers[i]);
                addr.transfer(backRecord[backers[i]]);
                emit SettlementEvent(addr,proposalTitle,backRecord[backers[i]]);
                backRecord[backers[i]]=0;
            }
            uint remainAmount=address(this).balance;
            owner.transfer(remainAmount);
            emit SettlementEvent(owner,proposalTitle,remainAmount);
        }
    } else if (pr==ProposalReturnSystem.ForProposer){
        uint remainAmount=address(this).balance;
        owner.transfer(remainAmount);
        emit SettlementEvent(owner,proposalTitle,remainAmount);
    }
    contractValid=false;
}

激活專案合約

// 做基本的檢查 最後再激活合約
function activateProposal(address addr) public{
    require(addr==owner,"Only owner can activate the proposal");
    require((bytes(info)).length!=0,"The information can't be empty.");
    require(plansTitle.length!=0,"The plans can't be empty.");
    require(goalAmount!=0,"The goal amount can't be 0.");
    require(campaignDuration>block.timestamp,"The campaignDuration can't be in the past.");
    contractValid=true;
}

智能合約的部署

// ./scripts/crowdFunding/deploy.ts
import { ethers } from "hardhat";

async function main() {
  const lock = await ethers.deployContract("CrowdFunding");

  await lock.waitForDeployment();

  console.log(
    `The CrowdFunding platform is deployed to ${lock.target}`
  );
}
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

部署指令

# 部署 群眾募資 智能合約
npx hardhat run .\scripts\crowdFunding\deploy.ts --network ganache

輸出結果 與 Ganache 變化

  • 表示成功建立合約 並且顯示合約部署位置

  • 確實新增了一筆合約 而且與上述輸出結果吻合

智能合約的互動

在互動上 分為三個環節講述

  1. 平台互動
  2. 提案者的互動
  3. 贊助者的互動

平台互動

1. 提案

import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {PlatformContractAddress} from "../parameters";

async function main() {
    const abi = require("../../../artifacts/contracts/CrowdFunding.sol/CrowdFunding.json").abi;

    const [deployer] = await ethers.getSigners();

    console.log("User's Address: ",deployer.address)

    const contractAddress = PlatformContractAddress;

    const contract = new Contract(contractAddress, abi, deployer);
    const amount = ethers.parseEther("10");
    const title="Tcweeei Proposal Title"

    await contract.propose(title,{ value: amount }).then(async (result) => {
        console.log("Result: ", result);
        console.log('Submit the proposal successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });

執行結果:

  • 漂亮 創建成功

2. 獲取提案的合約位置

import {ethers} from "hardhat";
import {PlatformContractAddress} from "../parameters";

async function main() {
    const [deployer]=await ethers.getSigners();

    const contractAddress = PlatformContractAddress
    const contractABI = require("../../../artifacts/contracts/CrowdFunding.sol/CrowdFunding.json").abi;

    const contract = new ethers.Contract(contractAddress, contractABI, deployer);

    const filter = contract.filters.ProposalEvent(deployer.address,null,null);
    const events = await contract.queryFilter(filter);

    for (let i = 0; i < events.length; i++) {
        // @ts-ignore
        console.log("Index: ",i," Value: ",events[i].args)
    }
}

main().then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

執行結果:

  • 確實獲得 標題 與剛創建相同的合約紀錄
    1. 提案者地址
    2. 專案的合約地址
    3. 專案名稱

3. 設定個人資料

import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {PlatformContractAddress} from "../parameters";

async function main() {
    const abi = require("../../../artifacts/contracts/CrowdFunding.sol/CrowdFunding.json").abi;

    const [deployer] = await ethers.getSigners();

    console.log("User's Address: ",deployer.address)

    const contractAddress = PlatformContractAddress;

    const contract = new Contract(contractAddress, abi, deployer);

    await contract.updatePersonalInfo("Tcweeei","I'm a handsome boy.").then(async (result) => {
        console.log("Result: ", result);
        console.log('Update the personal information successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });

執行結果:

4. 查詢個人資料

import {ethers} from "hardhat";
import {PlatformContractAddress} from "../parameters";

async function main() {
    const [deployer]=await ethers.getSigners();

    const contractAddress = PlatformContractAddress
    const contractABI = require(".././../../artifacts/contracts/CrowdFunding.sol/CrowdFunding.json").abi;

    const contract = new ethers.Contract(contractAddress, contractABI, deployer);

    const filter = contract.filters.UserInfoEvent(deployer.address,null,null);
    const events = await contract.queryFilter(filter);

    for (let i = 0; i < events.length; i++) {
        // @ts-ignore
        console.log("Index: ",i," Value: ",events[i].args)
    }
}

main().then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

執行結果:

  • 漂亮 與上述設定相同

5. 激活合約

import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {PlatformContractAddress, ProposalContractAddress} from "../parameters";

async function main() {
    const abi = require("../../../artifacts/contracts/CrowdFunding.sol/CrowdFunding.json").abi;

    const [deployer] = await ethers.getSigners();

    console.log("User's Address: ",deployer.address)

    const contractAddress = PlatformContractAddress;

    const contract = new Contract(contractAddress, abi, deployer);

    await contract.activateProposal(ProposalContractAddress).then(async (result) => {
        console.log("Result: ", result);
        console.log('Activate the proposal successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });

執行結果:

提案者的互動

1. 設定專案基本資訊

import { ethers } from 'hardhat';
import {Contract} from "ethers";
import { ProposalContractAddress} from "../parameters";

async function main() {
    const abi = require("../../../artifacts/contracts/CrowdFunding.sol/Proposal.json").abi;

    const [deployer] = await ethers.getSigners();

    console.log("User's Address: ",deployer.address)

    const contractAddress = ProposalContractAddress;

    const contract = new Contract(contractAddress, abi, deployer);
    
    // 設定專案資訊(簡介)

    const proposalInformation="This is the information ot introduction to the proposal.\nMade by Tcweeei.";

    await contract.editInfo(proposalInformation).then(async (result) => {
        console.log('Update the personal information successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
    
    // 設定目標金額
    
    const amount = ethers.parseEther("10");

    await contract.editGoalAmount(amount).then(async (result) => {
        console.log('Edit the goal amount with ',amount,'successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
    
    // 設定募款期限

    const currentTimestamp = Math.round(Date.now() / 1000);
    const campaignDuration=currentTimestamp+60*10;

    await contract.editCampaignDuration(campaignDuration).then(async (result) => {
        console.log('Update Campaign Duration with ',campaignDuration,' successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
    
    // 設定募款專案分類

    const pt=3; // RewardBased
    const pr=0; // Social
    const pc=1; // ForProposer

    await contract.editType(pt,pr,pc).then(async (result) => {
        console.log('Update the type successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
    
    // 設定募資的方案

    const title="方案一"
    const content="This is a plan need to 10 ETH and limit to 10.";
    const quantity=10
    const price = ethers.parseEther("10");

    await contract.editPlan(title,content,quantity,price).then(async (result) => {
        console.log('Add the plan with title ',title,' successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });

執行結果:

2. 結算

import { ethers } from 'hardhat';
import {Contract} from "ethers";
import { ProposalContractAddress} from "../parameters";

async function main() {
    const abi = require("../../../artifacts/contracts/CrowdFunding.sol/Proposal.json").abi;

    const [deployer] = await ethers.getSigners();

    console.log("User's Address: ",deployer.address)

    const contractAddress = ProposalContractAddress;

    const contract = new Contract(contractAddress, abi, deployer);

    await contract.settleProposal().then(async (result) => {
        console.log('Settle successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });

執行結果:

3. 觀察結算事件

import {ethers} from "hardhat";
import {PlatformContractAddress, ProposalContractAddress} from "../parameters";

async function main() {
    const [deployer]=await ethers.getSigners();

    const contractAddress = ProposalContractAddress
    const contractABI = require("../../../artifacts/contracts/CrowdFunding.sol/Proposal.json").abi;

    const contract = new ethers.Contract(contractAddress, contractABI, deployer);

    const filter = contract.filters.SettlementEvent();
    const events = await contract.queryFilter(filter);

    for (let i = 0; i < events.length; i++) {
        // @ts-ignore
        console.log("Index: ",i," Value: ",events[i].args)
    }
}

main().then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

執行結果:

  • 沒錯 因為集資成功 因此把所有金額都給提案者

贊助者的互動

1. 獲取專案資訊

import { ethers } from 'hardhat';
import {Contract} from "ethers";
import { ProposalContractAddress} from "../parameters";

async function main() {
    const abi = require("../../../artifacts/contracts/CrowdFunding.sol/Proposal.json").abi;

    const [deployer,user_with_index1] = await ethers.getSigners();

    console.log("User's Address: ",user_with_index1.address)

    const contractAddress = ProposalContractAddress;

    console.log("\nThe Proposal information:\n");

    const contract = new Contract(contractAddress, abi, user_with_index1);
    await contract.getTitle().then(async (result) => {
        console.log("Title: ", result);
    }).catch((e)=>{
        console.log("Error: ",e);
    })
    console.log("\n")
    await contract.getInfo().then(async (result) => {
        console.log("Information: ", result);
    }).catch((e)=>{
        console.log("Error: ",e);
    })
    await contract.getType().then(async (result) => {
        console.log("Type: ", result);
    }).catch((e)=>{
        console.log("Error: ",e);
    })
    console.log("\n");
    await contract.getPlans().then(async (result) => {
        console.log("Plans: ", result);
    }).catch((e)=>{
        console.log("Error: ",e);
    })
    console.log("\n");
    await contract.getCampaignDuration().then(async (result) => {
        console.log("CampaignDuration: ", result);
    }).catch((e)=>{
        console.log("Error: ",e);
    })
    console.log("\n");
    await contract.getAmount().then(async (result) => {
        console.log("CurrentAmount: ", result);
    }).catch((e)=>{
        console.log("Error: ",e);
    })
    await contract.getGoalAmount().then(async (result) => {
        console.log("GoalAmount: ", result);
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });

執行結果:

2. 贊助方案

import { ethers } from 'hardhat';
import {Contract} from "ethers";
import { ProposalContractAddress} from "../parameters";

async function main() {
    const abi = require("../../../artifacts/contracts/CrowdFunding.sol/Proposal.json").abi;

    const [deployer,user_with_index1] = await ethers.getSigners();

    console.log("User's Address: ",user_with_index1)

    const contractAddress = ProposalContractAddress;

    const contract = new Contract(contractAddress, abi, user_with_index1);

    const amount = ethers.parseEther("10");
    const title="方案一"

    await contract.backPlan(title,{value:amount}).then(async (result) => {
        console.log("Result: ", result);
        console.log('Back the plan successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });

執行結果:

3. 查詢贊助紀錄

import {ethers} from "hardhat";
import {PlatformContractAddress, ProposalContractAddress} from "../parameters";

async function main() {
    const [deployer]=await ethers.getSigners();

    const contractAddress = ProposalContractAddress
    const contractABI = require("../../../artifacts/contracts/CrowdFunding.sol/Proposal.json").abi;

    const contract = new ethers.Contract(contractAddress, contractABI, deployer);

    const filter = contract.filters.BackEvent();
    const events = await contract.queryFilter(filter);

    for (let i = 0; i < events.length; i++) {
        // @ts-ignore
        console.log("Index: ",i," Value: ",events[i].args)
    }
}

main().then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

執行結果:

從 Ganache 觀察:

  • 確實贊助了 10 ETH

結言

GitHub 專案位置: https://github.com/weiawesome/hardhat_tutorial

有感受到智能合約的優勢了嗎?

去中心化、自動化、公平公正、跨國性...

Q. 不過完全的去中心化 一定是優點嗎?

那不一定完全是優點
平台還是必須對提案的資訊內容做審查
不然會充斥詐騙 或是品質極差的提案

間接整個品牌的名譽就造就 非常糟的影響

最後的最後希望透過這篇能理解

  1. 甚麼是群眾募資
  2. 群眾募資使用智能合約的優點
  3. 如何撰寫智能合約
  4. 如何與智能合約互動

下回預告

想目前對於智能合約 如何部署撰寫 應該有基本觀念了

很能感受到去中心化的美好了吧~~~

講完合約 當然要來講如何應用囉!

終於終於要進入最後的篇章了

接下來就來建立一款 "去中心化應用程序(DApp)"

下回 "DApp建立: Web3.js、Ethers.js 介紹"


上一篇
智能合約開發: 建立合約-公平、公正的投票
下一篇
DApp建立: Web3.js、Ethers.js 介紹
系列文
從 區塊鏈 到 去中心化應用程式(DApp)30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言