以太坊作為全球第二大公有鏈,其核心創(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 -v和npm -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
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ò)。