以太坊作為全球第二大公有鏈,其核心創(chuàng)新在于“智能合約”——一種運(yùn)行在區(qū)塊鏈上、自動(dòng)執(zhí)行預(yù)設(shè)規(guī)則的程序代碼,它無需中介信任,即可實(shí)現(xiàn)資產(chǎn)轉(zhuǎn)移、邏輯驗(yàn)證等復(fù)雜功能,是DeFi、NFT、DAO等應(yīng)用的技術(shù)基石,本文將以“簡(jiǎn)易投票系統(tǒng)”為例,帶你從零理解以太坊智能合約的編寫、部署與交互,完成一個(gè)可落地的Demo實(shí)踐。

智能合約:以太坊的“自動(dòng)執(zhí)行器”

智能合約本質(zhì)上是一段部署在以太坊虛擬機(jī)(EVM)上的代碼,它以“代碼即法律”的方式,在滿足預(yù)設(shè)條件時(shí)自動(dòng)觸發(fā)執(zhí)行,投票合約會(huì)在投票時(shí)間結(jié)束后自動(dòng)統(tǒng)計(jì)票數(shù),無需人工干預(yù);借貸合約會(huì)在抵押物價(jià)值不足時(shí)自動(dòng)清算,這種去中心化、不可篡改的特性,使其成為構(gòu)建可信應(yīng)用的核心工具。

開發(fā)環(huán)境準(zhǔn)備:三步搞定“合約工坊”

在編寫智能合約前,需完成以下環(huán)境搭建,以保障開發(fā)流程順暢:

安裝Node.js與npm

智能合約開發(fā)依賴Node.js環(huán)境,需確保版本≥16.0,從nodejs官網(wǎng)下載安裝后,通過終端運(yùn)行node -vnpm -v驗(yàn)證安裝成功。

配置Hardhat框架

Hardhat是當(dāng)前最流行的以太坊開發(fā)框架,支持合約編譯、測(cè)試、部署等全流程,在終端執(zhí)行以下命令初始化項(xiàng)目:

mkdir ethereum-contract-demo && cd ethereum-contract-demo  
npm init -y  
npm install --save-dev hardhat  
npx hardhat init  

按提示選擇“Create a JavaScript project”,并安裝依賴(如@nomicfoundation/hardhat-toolbox)。

安裝MetaMask錢包

MetaMask是瀏覽器端的以太坊錢包,用于管理賬戶、與合約交互,從MetaMask官網(wǎng)安裝瀏覽器插件,創(chuàng)建并備份好助記詞,確保錢包網(wǎng)絡(luò)切換至“本地開發(fā)網(wǎng)絡(luò)”(后續(xù)由Hardhat啟動(dòng))。

智能合約編寫:用Solidity實(shí)現(xiàn)“投票邏輯”

本文以“簡(jiǎn)易投票系統(tǒng)”為例,實(shí)現(xiàn)以下功能:

  • 合約部署者創(chuàng)建投票議題,設(shè)置投票選項(xiàng)和截止時(shí)間;
  • 地址可對(duì)指定選項(xiàng)投票(每人限一票);
  • 投票結(jié)束后,任何人可查詢最終票數(shù)。

創(chuàng)建合約文件

contracts目錄下新建Voting.sol,編寫如下代碼:

// SPDX-License-Identifier: MIT  
pragma solidity ^0.8.20;  
contract Voting {  
    // 投票選項(xiàng)結(jié)構(gòu)體  
    struct Option {  
        string name;  
        uint voteCount;  
    }  
    // 狀態(tài)變量  
    string public votingTopic;  
    uint public votingDeadline;  
    mapping(address => bool) public hasVoted;  
    Option[] public options;  
    // 事件:投票時(shí)觸發(fā),方便前端監(jiān)聽  
    event Voted(address voter, string option);  
    // 構(gòu)造函數(shù):部署時(shí)初始化投票議題和選項(xiàng)  
    constructor(string memory _topic, string[] memory _optionNames) {  
        votingTopic = _topic;  
        votingDeadline = block.timestamp + 1 days; // 投票截止時(shí)間:部署后1天  
        for (uint i = 0; i < _optionNames.length; i++) {  
            options.push(Option(_optionNames[i], 0));  
        }  
    }  
    // 投票函數(shù)  
    function vote(uint _optionIndex) public {  
        require(block.timestamp < votingDeadline, "Voting has ended");  
        require(!hasVoted[msg.sender], "You have already voted");  
        require(_optionIndex < options.length, "Invalid option index");  
        hasVoted[msg.sender] = true;  
        options[_optionIndex].voteCount++;  
        emit Voted(msg.sender, options[_optionIndex].name);  
    }  
    // 查詢票數(shù)  
    function getVoteCount(uint _optionIndex) public view returns (uint) {  
        require(_optionIndex < options.length, "Invalid option index");  
        return options[_optionIndex].voteCount;  
    }  
    // 獲取所有選項(xiàng)  
    function getOptions() public view returns (string[] memory, uint[] memory) {  
        string[] memory optionNames = new string[](options.length);  
        uint[] memory voteCounts = new uint[](options.length);  
        for (uint i = 0; i < options.length; i++) {  
            optionNames[i] = options[i].name;  
            voteCounts[i] = options[i].voteCount;  
        }  
        return (optionNames, voteCounts);  
    }  
}  

代碼解析

  • 狀態(tài)變量votingTopic(議題)、votingDeadline(截止時(shí)間)、hasVoted(記錄投票地址)、options(投票選項(xiàng)數(shù)組);
  • 構(gòu)造函數(shù):部署時(shí)初始化議題和選項(xiàng),設(shè)置1天的投票時(shí)長(zhǎng);
  • vote函數(shù):核心投票邏輯,通過require校驗(yàn)投票時(shí)間、重復(fù)投票、選項(xiàng)有效性;
  • 事件Voted事件用于前端監(jiān)聽投票行為,提升交互體驗(yàn)。

合約編譯與測(cè)試:確保代碼“零錯(cuò)誤”

編譯合約

在終端執(zhí)行:

npx hardhat compile  

成功后,artifacts目錄會(huì)生成編譯后的ABI(應(yīng)用二進(jìn)制接口)和字節(jié)碼,這是合約與交互的“橋梁”。

編寫測(cè)試用例

test目錄下新建voting.test.js,使用Chai測(cè)試框架驗(yàn)證合約邏輯:

const { expect } = require("chai");  
const { ethers } = require("hardhat");  
describe("Voting Contract", function () {  
    let votingContract;  
    let owner, voter1, voter2;  
    beforeEach(async function () {  
        [owner, voter1, voter2] = await ethers.getSigners();  
        const Voting = await ethers.getContractFactory("Voting");  
        votingContract = await Voting.deploy("Best Crypto?", ["Bitcoin", "Ethereum"]);  
        await votingContract.waitForDeployment();  
    });  
    it("Should initialize with correct topic and options", async function () {  
        expect(await votingContract.votingTopic()).to.equal("Best Crypto?");  
        const [options, _] = await votingContract.getOptions();  
        expect(options).to.deep.equal(["Bitcoin", "Ethereum"]);  
    });  
    it("Should allow voting before deadline", async function () {  
        await votingContract.connect(voter1).vote(0); // 投票給Bitcoin  
        expect(await votingContract.getVoteCount(0)).to.equal(1);  
        expect(await votingContract.hasVoted(voter1.address)).to.equal(true);  
    });  
    it("Should reject duplicate votes", async function () {  
        await votingContract.connect(voter1).vote(0);  
        await expect(votingContract.connect(voter1).vot
隨機(jī)配圖
e(0)).to.be.revertedWith("You have already voted"); }); it("Should reject voting after deadline", async function () { // 快進(jìn)時(shí)間(Hardhat支持時(shí)間快進(jìn)) await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]); // 增加2天 await ethers.provider.send("evm_mine", []); await expect(votingContract.connect(voter1).vote(0)).to.be.revertedWith("Voting has ended"); }); });

運(yùn)行測(cè)試

執(zhí)行以下命令啟動(dòng)本地測(cè)試節(jié)點(diǎn)(默認(rèn)Hardhat Network)并運(yùn)行測(cè)試:

npx hardhat test  

若所有測(cè)試通過,說明合約邏輯正確,可進(jìn)入部署階段。

合約部署:讓代碼“上鏈運(yùn)行”

配置部署腳本

scripts目錄下新建deploy.js

async function main() {  
    const Voting = await ethers.getContractFactory("Voting");  
    const votingContract = await Voting.deploy("Best Crypto?", ["Bitcoin", "Ethereum"]);  
    await votingContract.waitForDeployment();  
    console.log("Voting contract deployed to:", votingContract.target);  
}  
main().catch((error) => {  
    console.error(error);  
    process.exitCode = 1;  
});  

部署到本地網(wǎng)絡(luò)

執(zhí)行部署命令:

npx hardhat run scripts/deploy.js --network localhost  

成功后,終端會(huì)輸出合約地址(如0x5FbDB2315678afecb367f032d93F642f64180aa3),此時(shí)合約已部署到Hardhat本地網(wǎng)絡(luò)。

驗(yàn)