iT邦幫忙

2023 iThome 鐵人賽

DAY 22
0
Web 3

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

智能合約開發: 建立合約-公平、公正的投票

  • 分享至 

  • xImage
  •  

建立合約-公平、公正的投票

在建立投票的智能合約前
首先對於使用智能合約來做為投票方式的優點

  • 去中心化 (不必額外信任 投票機構)
  • 自動化 且 快速 (時間到可以快速自行開票)
  • 不可竄改性、公正性 (程序公開透明)
  • 資金流向 透明 (避免弊端)

因此使用智能合約進行投票是有一定的優勢的

話不多說就來建立 一款 公平、公正的投票 智能合約吧!

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

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

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

專案目標規劃

這次期待做出一個合約 模擬真實選舉的投票

投票前

  • 候選人可以繳交保證金 並且登記候選
  • 候選人可以在開票前 做個人資料修正(姓名、政見)

投票時

  • 每個人都有票 而且一人一票 票票等值
  • 投票過程中 票型不公開

開票時

  • 自動宣布每個候選人的得票數
  • 當候選人票數達最低門檻或以上時,則返還保證金。
  • 選舉剩餘的金額,返還給選舉舉辦人(合約發布者)

查詢候選人

  • 直接查詢所有候選人
  • 根據地址查詢候選人
  • 根據名字查詢候選人

合約發布者

  • 隨時暫停選舉過程(禁止所有功能)
    避免合約有漏洞時,能暫時停住所有功能,減少損失
  • 隨時重新啟動選舉過程

智能合約撰寫

檔案位置: "./contracts/Election.sol"

基礎合約架構

// SPDX-License-Identifier: MIT
// Solidity 版本
pragma solidity ^0.8.0;

// 合約名稱
contract Election {
    
}

物件定義

// 候選人
struct Candidate {
    // 候選人的地址
    address addr;
    // 候選人名稱
    string name;
    // 候選人政見
    string manifesto;
    // 候選人保證金
    uint256 depositAmount;
    // 登記參選與否
    bool isRegistered;
}

// 選舉結果
struct VotesResult {
    // 候選人的地址
    address addr;
    // 候選人得票數
    uint votes;
}

變數設定

// 候選人們
mapping(address => Candidate) public candidates;
// 是否已投過票
mapping(address => bool) public hasVoted;
// 當前票形狀態
mapping(address => uint) public voteStatus;

// 合約擁有者
address payable public owner;
// 投票時間
uint public voteTime;
// 開票時間
uint public voteTallyingTime;
// 候選人們的地址
address[] public candidatesAddress;

// 最少返還保證金的投票數
uint private leastVoteCount;
// 保證金的金額
uint private depositCount;
合約是否為暫停狀態
bool private isSuspended;

事件規劃

// 登記為候選人紀錄
event RegisterRecord(address addr,string name,string manifesto,uint time);
// 候選人更改資訊紀錄
event UpdateInfoRecord(address addr,string name,string manifesto,uint time);
// 投票事實記錄
event VoteRecord(address addr,uint time);
// 提領金額紀錄
event DepositRecord(address addr,uint count,uint time);

建構子

constructor(uint _voteTime,uint _voteTallyingTime,uint _leastVoteCount,uint _depositCount) payable {
    require(
        _voteTallyingTime > _voteTime,
        "Vote tallying time should be after the vote time"
    );
    require(
        block.timestamp < _voteTime,
        "Vote time should be in the future"
    );

    voteTime = _voteTime;
    voteTallyingTime=_voteTallyingTime;
    leastVoteCount=_leastVoteCount;
    depositCount=_depositCount;
    owner = payable(msg.sender);
    isSuspended =false;
}
  • 基本檢查 關於投票時間與開票時間合理性
  • 將所有參數做初始化
  • 允許合約發布者提供金額進入合約作為基本資金

修飾函數

// 僅允許合約擁有者的操作
modifier onlyOwner() {
    require(msg.sender == owner, "Only owner can call this function");
    _;
}
// 檢查合約是否被合約擁有者暫停
modifier contractValid() {
    require(isSuspended ==false,"The election is Stopping");
    _;
}
// 投票前
modifier beforeVote(){
    require(block.timestamp<voteTime,"Error vote has been started!");
    _;
}
// 投票時間
modifier inVote(){
    require(block.timestamp>voteTime && block.timestamp<voteTallyingTime,"Now is not vote time!");
    _;
}
// 投票後(開票時間)
modifier afterVote(){
    require(block.timestamp>voteTallyingTime,"Now is not vote tallying time!");
    _;
}

投票前(登記參選與修改資訊)相關函式

// 登記參選
function registerCandidate(string memory _name, string memory _manifesto) public payable contractValid beforeVote{
    // 檢查是否登記過與保證金金額
    require(!candidates[msg.sender].isRegistered, "Candidate is already registered");
    require(msg.value == depositCount, "Error deposit amount");
    
    // 建立候選人資訊
    Candidate memory newCandidate = Candidate({
        addr : msg.sender,
        name: _name,
        manifesto: _manifesto,
        depositAmount: msg.value,
        isRegistered: true
    });
    
    // 新增候選人與提交登記參選事件
    candidates[msg.sender] = newCandidate;
    candidatesAddress.push(msg.sender);
    emit RegisterRecord(msg.sender,_name,_manifesto,block.timestamp);
}

// 修改候選人資訊
function updateCandidateInfo(string memory _name, string memory _manifesto) public contractValid beforeVote{
    // 檢查候選人是否已經登記
    require(candidates[msg.sender].isRegistered, "Candidate is not registered");
    
    // 更改候選人資料
    Candidate storage candidateToUpdate = candidates[msg.sender];
    candidateToUpdate.name = _name;
    candidateToUpdate.manifesto = _manifesto;
    
    // 提交更改資訊事件
    emit UpdateInfoRecord(msg.sender,_name,_manifesto,block.timestamp);

}

投票時(投票)相關函式

// 進行投票
function vote(address addr) public contractValid inVote{
    // 檢查是否已投過 與 被投票者是否為候選人
    require(hasVoted[msg.sender]==false,"The voter has been voted");
    require(candidates[addr].isRegistered==true,"The candidate is not exist");

    // 完成投票並且設定為已投票
    hasVoted[msg.sender]=true;
    voteStatus[addr]+=1;
    
    // 新增投票事件
    emit VoteRecord(msg.sender,block.timestamp);
}

開票時(公布得票數與返還保證金)相關函式

// 揭露投票結果
function revealVotes() public view contractValid afterVote returns (VotesResult[] memory) {
    VotesResult[] memory votesResult = new VotesResult[](candidatesAddress.length);

    for (uint i = 0; i < candidatesAddress.length; i++) {
        votesResult[i].addr=candidatesAddress[i];
        votesResult[i].votes=voteStatus[candidatesAddress[i]];
    }

    return votesResult;
}
// 保證金返還
function refundDeposit() public contractValid afterVote {
    require(address(this).balance>0,"The contract has no more balance!");
    for (uint i=0;i<candidatesAddress.length;i++){
        if (voteStatus[candidatesAddress[i]]>=leastVoteCount){
            address payable addr=payable(candidatesAddress[i]);
            addr.transfer(candidates[candidatesAddress[i]].depositAmount);
            emit DepositRecord(candidatesAddress[i],candidates[candidatesAddress[i]].depositAmount,block.timestamp);
            candidates[candidatesAddress[i]].depositAmount=0;
        }
    }
    uint amount=address(this).balance;
    owner.transfer(amount);
    emit DepositRecord(owner,amount,block.timestamp);
}

獲取候選人資訊

// 獲取所有候選人的資訊
function getCandidates() public view contractValid returns (Candidate[] memory) {
    Candidate[] memory result = new Candidate[](candidatesAddress.length);
    uint256 index = 0;

    for (uint256 i = 0; i < candidatesAddress.length; i++) {
        address addr = candidatesAddress[i];
        Candidate storage candidate = candidates[addr];
        if (candidate.isRegistered) {
            result[index] = candidate;
            index++;
        }
    }

    return result;
}

// 獲取指定候選人 藉由地址
function getSpecificCandidate(address addr) public view contractValid returns (Candidate memory){
    require(candidates[addr].isRegistered==true,"The candidate is not exist!");
    return candidates[addr];
}

// 獲取指定候選人 藉由名字 (可能候選人有重名 因此回傳選擇List)
function getSpecificCandidateByName(string memory _name) public view contractValid returns (Candidate[] memory) {
    Candidate[] memory result = new Candidate[](candidatesAddress.length);
    uint index = 0;

    for (uint i = 0; i < candidatesAddress.length; i++) {
        if (candidates[candidatesAddress[i]].isRegistered == true && keccak256(bytes(candidates[candidatesAddress[i]].name)) == keccak256(bytes(_name))) {
            address addr = candidatesAddress[i];
            result[index] = candidates[addr];
            index++;
        }
    }

    Candidate[] memory finalResult = new Candidate[](index);
    for (uint j = 0; j < index; j++) {
        finalResult[j] = result[j];
    }

    return finalResult;
}

合約擁有者暫停與繼續合約

// 合約擁有者暫停合約所有功能
function suspend() public onlyOwner {
    isSuspended=true;
}

// 合約擁有者繼續合約所有功能
function stopSuspend() public onlyOwner {
    isSuspended=false;
}

智能合約的部署

在合約部署上 一樣透過 TypeScript 撰寫腳本部署

檔案位置: "./script/election/deploy.ts"

import { ethers } from "hardhat";

async function main() {
  const currentTimestampInSeconds = Math.round(Date.now() / 1000);

  const voteTime = currentTimestampInSeconds+60*5;
  const voteTallyingTime = currentTimestampInSeconds+60*10;
  const leastVoteCount = 1;
  const basicFoundation = ethers.parseEther("10");


  const Contract = await ethers.getContractFactory("Election");
  const contract = await Contract.deploy(
      voteTime,
      voteTallyingTime,
      leastVoteCount,
      basicFoundation,
      { value: basicFoundation }
  );

  await contract.waitForDeployment();

  console.log(
      `Election with BasicFoundation: ${ethers.formatEther(basicFoundation)} ETH\nElection start in timestamp ${voteTime} vote tallying in timestamp ${voteTallyingTime} \nDeployed the contract to ${contract.target}`
  );
}

main()
    .then(() => process.exit(0))
    .catch(error => {
      console.error(error);
      process.exit(1);
    });
  • 投票環節: 5 分鐘候進入
  • 開票環節: 10 分鐘候進入
  • 保證金金額: 10 ETH
  • 基本資金: 10 ETH
  • 投票數基本門檻: 1 票

(確保 Ganache 開啟 與 Config 網路設定)

// ./hardhat.config.ts
const config: HardhatUserConfig = {
  solidity: "0.8.19",
  networks: {
    ganache: {
      url: "http://localhost:7545",
    },
  }
};

進行部署:

# 部署合約指令
npx hardhat run .\scripts\election\vote_before\register.ts --network ganache

輸出結果:

重新檢查 Ganache :

  • 帳戶資訊上:

    • 第一個帳戶確實花費了 10 ETH
  • 交易資訊上:

    • 確實新增了一筆合約 合約地址也與上方輸出結果符合

智能合約的互動

1. 登記候選

// ./scripts/election/vote_before/register.ts
import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {ContractAddress} from "../parameters";

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

    const [deployer,user_with_index1,user_with_index2,user_with_index3,user_with_index4,user_with_index5] = await ethers.getSigners();

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

    const contractAddress = ContractAddress;

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

    const name = 'Tcweeei-Index-1';
    const manifesto = '候選人政見';

    const depositAmount = ethers.parseEther('10');
    await contract.registerCandidate(name, manifesto, { value: depositAmount }).then((result)=>{
        console.log("Result: ",result);
        console.log('Candidate registered successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });
  • 依序將候選人進行登記

執行指令

npx hardhat run .\scripts\election\vote_before\register.ts --network ganache

Ganache 結果分析

  • 確實造成很多呼叫合約

  • 合約發布者(第零個帳戶) 10 ETH 作為 合約基本資金
  • 候選人(第一個到第五個帳戶) 10 ETH 作為 保證金以參加選舉

2. 更改候選人資訊

// ./scripts/election/vote_before/update_information.ts
import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {ContractAddress} from "../parameters";

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

    const [deployer,user_with_index1,user_with_index2,user_with_index3,user_with_index4,user_with_index5] = await ethers.getSigners();

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

    const contractAddress = ContractAddress;

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

    const name = 'Super-Tcweeei';
    const manifesto = '候選人政見';

    await contract.updateCandidateInfo(name, manifesto).then((result)=>{
        console.log("Result: ",result);
        console.log('Candidate update successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });
  • 將第五個候選人改為 Super-Tcweeei

執行指令

npx hardhat run .\scripts\election\vote_before\update_information.ts --network ganache

3. 查詢候選人資料

查詢所有候選人資訊

// ./scripts/election/any_time/candidates.ts
import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {ContractAddress} from "../parameters";

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

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

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

    const contractAddress = ContractAddress;

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

    await contract.getCandidates().then((result)=>{
        console.log("Result: ",result);
        console.log('Get candidates successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

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

輸出結果:

  • 確實擁有五名候選人資訊

查詢指定候選人資訊藉由地址

// ./scripts/election/any_time/candidates_by_address.ts
import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {ContractAddress} from "../parameters";

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

    const [deployer,user_with_index1,user_with_index2,user_with_index3,user_with_index4,user_with_index5] = await ethers.getSigners();

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

    const contractAddress = ContractAddress;

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

    await contract.getSpecificCandidate(user_with_index1.address).then((result)=>{
        console.log("Result: ",result);
        console.log('Get candidates by address successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

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

查詢指定候選人資訊藉由名稱

// ./scripts/election/any_time/candidates_by_name.ts
import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {ContractAddress} from "../parameters";

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

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

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

    const CandidateName="Super-Tcweeei"

    const contractAddress = ContractAddress;

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

    await contract.getSpecificCandidateByName(CandidateName).then((result)=>{
        console.log("Result: ",result);
        console.log('Get candidates by name successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

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

4. 投票

// ./scripts/election/vote/vote.ts
import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {ContractAddress} from "../parameters";

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

    const [deployer,user_with_index1,user_with_index2,user_with_index3,user_with_index4,user_with_index5] = await ethers.getSigners();

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

    const contractAddress = ContractAddress;

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

    await contract.vote(user_with_index5).then((result)=>{
        console.log("Result: ",result);
        console.log('Vote successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

main()
    .then(() => process.exit(0))
    .catch(error => {
        console.error(error);
        process.exit(1);
    });
  • 可自行設定 誰投給誰

5. 查詢投票結果

// ./scripts/election/vote_after/reveal.ts
import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {ContractAddress} from "../parameters";

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

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

    const contractAddress = ContractAddress;

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

    await contract.revealVotes().then((result)=>{
        console.log("Result: ",result)
        console.log('Reveal votes successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

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

輸出結果:

  • 根據結果可以知道最後一位候選人得票最高

6. 返還保證金

// ./scripts/election/vote_after/refund_deposit.ts
import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {ContractAddress} from "../parameters";

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

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

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

    const contractAddress = ContractAddress;

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

    await contract.refundDeposit().then((result)=>{
        console.log("Result: ",result);
        console.log('Refund deposit successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

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

輸出結果:

Ganache 資訊:

  • 第一、二名候選人 未達最低門檻(1票) 沒收保證金
  • 第三、四、五名候選人 有達最低門檻(1票) 返還保證金
  • 第零個帳戶為合約發起人 獲得合約剩餘資金

7. 暫停與繼續合約

暫停合約

// ./scripts/election/admin/suspend.ts
import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {ContractAddress} from "../parameters";

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

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

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

    const contractAddress = ContractAddress;

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

    await contract.suspend().then((result)=>{
        console.log("Result: ",result);
        console.log('Suspended successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

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

檢查其他合約功能:

  • 確實被阻擋住 因為 "election is stopping"

繼續合約功能

// ./scripts/election/admin/continue.ts
import { ethers } from 'hardhat';
import {Contract} from "ethers";
import {ContractAddress} from "../parameters";

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

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

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

    const contractAddress = ContractAddress;

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

    await contract.stopSuspend().then((result)=>{
        console.log("Result: ",result);
        console.log('Stop suspended successfully');
    }).catch((e)=>{
        console.log("Error: ",e);
    })
}

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

輸出結果:

8. 檢查事件

登記參選事件

// ./scripts/election/any_time/events_register.ts
import {ethers} from "hardhat";
import {ContractAddress} from "../parameters";

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

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

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

    const filter = contract.filters.RegisterRecord();
    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);
    });

輸出結果:

  • 確實是一開始登記參選時的資料

候選人更改資訊事件

// ./scripts/election/any_time/events_update.ts
import {ethers} from "hardhat";
import {ContractAddress} from "../parameters";

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

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

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

    const filter = contract.filters.UpdateInfoRecord();
    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);
    });

輸出結果:

投票事件

// ./scripts/election/any_time/events_vote.ts
import {ethers} from "hardhat";
import {ContractAddress} from "../parameters";

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

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

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

    const filter = contract.filters.VoteRecord();
    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);
    });

輸出結果:

保證金返還事件

// ./scripts/election/any_time/events_deposit.ts
import {ethers} from "hardhat";
import {ContractAddress} from "../parameters";

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

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

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

    const filter = contract.filters.DepositRecord();
    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);
    });

輸出結果:

  • 確實如同預期
    • 第三、四、五候選人返還保證金 10ETH
    • 剩餘資金 30 ETH 返還給合約發起人

結言

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

開始真心感覺區塊鏈對於現實生活還是有正面影響了嗎?
(並不是都只有炒幣)

透過智能合約確實能達到許多效果
可以達到自動化、安全、去中心化等等效果

但其實像是種透過投票的合約
還能配合 零知識證明 可以更具有匿名且認證的效果

希望透過這篇能理解

  1. 智能合約 應用於 投票 的優缺點
  2. 如何撰寫一個投票的智能合約
  3. 如何部署智能合約
  4. 如何與智能合約互動

下回預告

剛結束關於 透過 智能合約 撰寫 完成投票的例子
是不是對於 智能合約 還躍躍欲試啊!!!

感覺這個還有須多優點呢! 感覺還能做點甚麼!

不錯不錯! 那就再來一回吧!

知道甚麼是群眾募資(眾籌)(Crowdfunding)

這如果尬上區塊鏈 可以長成甚麼樣子呢?

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


上一篇
智能合約開發: Hardhat 智能合約開發工具
下一篇
智能合約開發: 建立合約-區塊鏈上的群眾募資
系列文
從 區塊鏈 到 去中心化應用程式(DApp)30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言