前言本文围绕治理代币展开多维度探究,首先对治理代币的核心概念、类型、功能及治理逻辑等核心内容进行系统性梳理,随后以HardhatV3为技术工具,通过具体的代码案例实操,完整呈现治理代币从开发环境搭建、智能合约编写,到测试、部署的全流程,助力开发者深化对治理代币的理解与应用;同时,针对开发者
本文围绕治理代币展开多维度探究,首先对治理代币的核心概念、类型、功能及治理逻辑等核心内容进行系统性梳理,随后以 Hardhat V3 为技术工具,通过具体的代码案例实操,完整呈现治理代币从开发环境搭建、智能合约编写,到测试、部署的全流程,助力开发者深化对治理代币的理解与应用;同时,针对开发者在基于 Hardhat V3 进行治理代币开发、测试和部署过程中遇到的各类常见问题,逐一分析成因并提供切实可行的解决方案,形成理论梳理、技术实操与问题解决相结合的完整内容体系,为治理代币的开发实践提供全面参考。
一句话区分:治理代币 = ERC-20 代币 + 链上民主决策能力;ERC-20是"发币技术标准",治理代币是"代币功能场景"——前者定义规则,后者定义权力。
核心结论:所有治理代币都是 ERC-20 代币,但并非所有 ERC-20 都是治理代币
治理代币是 ERC-20 标准的超集——它在 ERC-20 基础功能上扩展了链上治理能力。就像智能手机 vs 普通手机,前者具备后者所有功能,并增加了智能特性。
功能与权利差异
普通代币
| 维度 | 普通代币 (ERC-20) | 治理代币 (ERC-20Votes) |
|---|---|---|
| 主要用途 | 价值交换、支付、投资 | 参与链上治理、投票决策 |
| 技术实现 | 基础 ERC-20 标准 | ERC-20 + 投票/委托扩展 |
| 持有者权利 | 仅财产权 | 财产权 + 治理权 |
| 可转让性 | 自由流转 | 通常可转让,但投票权可委托 |
| 激励机制 | 价值增长预期 | 参与治理奖励 + 价值增长 |
| 使用场景 | USDT(稳定币)、LINK(预言机) | UNI(Uniswap)、AAVE(Aave)、COMP(Compound) |
| 风险类型 | 普通 ERC-20 | 治理代币 |
|---|---|---|
| 闪电贷攻击 | 无此风险 | 需设置 votingDelay(提案到投票的延迟)防止同区块攻击 |
| 投票权滥用 | 不适用 | 需 quorum(法定人数)和 proposalThreshold(提案门槛) |
| 鲸鱼垄断 | 仅影响价格 | 可通过时间锁(Timelock)和委托机制缓解 |
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.5.0
pragma solidity ^0.8.27;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract MyGovToken is ERC20, Ownable, ERC20Permit, ERC20Votes {
constructor(address initialOwner)
ERC20("MyGovToken", "MGTK")
Ownable(initialOwner)
ERC20Permit("MyGovToken")
{}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
// The following functions are overrides required by Solidity.
function _update(address from, address to, uint256 value)
internal
override(ERC20, ERC20Votes)
{
super._update(from, to, value);
}
function nonces(address owner)
public
view
override(ERC20Permit, Nonces)
returns (uint256)
{
return super.nonces(owner);
}
}
// contracts/MyTimelockController.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
contract MyTimelockController is TimelockController {
// minDelay: 最小延迟时间(秒)
// proposers: 可以发起提案的地址列表(通常只有治理合约)
// executors: 可以执行提案的地址列表(通常设为开放执行)
constructor(
uint256 minDelay,
address[] memory proposers,
address[] memory executors,
address admin
) TimelockController(minDelay, proposers, executors, admin) {}
}
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.5.0
pragma solidity ^0.8.27;
import {Governor} from "@openzeppelin/contracts/governance/Governor.sol";
import {
GovernorCountingSimple
} from "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import {
GovernorSettings
} from "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import {
GovernorTimelockControl
} from "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
import {
GovernorVotes
} from "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import {
GovernorVotesQuorumFraction
} from "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
import {
TimelockController
} from "@openzeppelin/contracts/governance/TimelockController.sol";
contract MyGovernor is
Governor,
GovernorSettings,
GovernorCountingSimple,
GovernorVotes,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
constructor(
IVotes _token,
TimelockController _timelock
)
Governor("MyGovernor")
GovernorSettings(7200 /* 1 day */, 50400 /* 1 week */, 0)
GovernorVotes(_token)
GovernorVotesQuorumFraction(4)
GovernorTimelockControl(_timelock)
{}
// The following functions are overrides required by Solidity.
function state(
uint256 proposalId
)
public
view
override(Governor, GovernorTimelockControl)
returns (ProposalState)
{
return super.state(proposalId);
}
function proposalNeedsQueuing(
uint256 proposalId
) public view override(Governor, GovernorTimelockControl) returns (bool) {
return super.proposalNeedsQueuing(proposalId);
}
function proposalThreshold()
public
view
override(Governor, GovernorSettings)
returns (uint256)
{
return super.proposalThreshold();
}
function _queueOperations(
uint256 proposalId,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint48) {
return
super._queueOperations(
proposalId,
targets,
values,
calldatas,
descriptionHash
);
}
function _executeOperations(
uint256 proposalId,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) {
super._executeOperations(
proposalId,
targets,
values,
calldatas,
descriptionHash
);
}
function _cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
return super._cancel(targets, values, calldatas, descriptionHash);
}
function _executor()
internal
view
override(Governor, GovernorTimelockControl)
returns (address)
{
return super._executor();
}
}
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
/**
* @dev Box 合约将被 DAO 治理
* 只有 Owner(即 TimelockController)可以调用 store 函数
*/
contract BoxVerlue is Ownable {
uint256 private _value;
constructor(address initialOwner) Ownable(initialOwner) {}
event ValueChanged(uint256 value);
function store(uint256 value) public onlyOwner {
_value = value;
emit ValueChanged(value);
}
function retrieve() public view returns (uint256) {
return _value;
}
}
# 编译指令
npx hardhat compile
# 注释:引用openzeppelin合约中的包导致治理合约包过大编译时会提示包过大的警示,同时也影响后续的部署和测试,需要在hardhat.config.ts中进行如下配置
import hardhatToolboxViemPlugin from "@nomicfoundation/hardhat-toolbox-viem";
import { configVariable, defineConfig } from "hardhat/config";
export default defineConfig({
plugins: [hardhatToolboxViemPlugin],
solidity: {
version: "0.8.28",
settings: {
optimizer: {
enabled: true,
runs: 1,// 对于治理合约,设置为1可最大化体积优化
},
viaIR: true,// 关键!启用中间表示优化,大幅减小程序体积
evmVersion: "cancun",// 保持你当前的EVM版本
},
},
networks: {
hardhatMainnet: {
type: "edr-simulated",
chainType: "l1",
},
hardhatOp: {
type: "edr-simulated",
chainType: "op",
},
sepolia: {
type: "http",
chainType: "l1",
url: configVariable("SEPOLIA_RPC_URL"),
accounts: [configVariable("SEPOLIA_PRIVATE_KEY")],
},
},
});
// scripts/deploy.ts
import { network, artifacts } from "hardhat";
import {parseEther} from "viem"
async function main() {
// 连接网络
const { viem } = await network.connect({ network: network.name });//指定网络进行链接
// 获取客户端
const [deployer] = await viem.getWalletClients();
const publicClient = await viem.getPublicClient();
// 部署治理代币
const GovTokenRegistry = await artifacts.readArtifact("MyGovToken");
// 部署(构造函数参数:initialOwner)
const GovTokenRegistryHash = await deployer.deployContract({
abi: GovTokenRegistry.abi,//获取abi
bytecode: GovTokenRegistry.bytecode,//硬编码
args: [deployer.account.address],//部署者地址作为初始治理者
});
// 等待确认并打印治理代币地址
const GovTokenRegistryReceipt = await publicClient.getTransactionReceipt({ hash: GovTokenRegistryHash });
console.log("治理代币合约地址:", GovTokenRegistryReceipt.contractAddress);
// 部署时间锁合约
const MyTimelockController = await artifacts.readArtifact("MyTimelockController");
const minDelay = 2 * 24 * 60 * 60;
const MyTimelockControllerHash = await deployer.deployContract({
abi: MyTimelockController.abi,//获取abi
bytecode: MyTimelockController.bytecode,//硬编码
args: [minDelay,[],[],deployer.account.address],//process.env.RECIPIENT, process.env.OWNER
});
// 等待确认并打印地址
const MyTimelockControllerReceipt = await publicClient.getTransactionReceipt({ hash: MyTimelockControllerHash });
console.log("时间锁合约地址:", await MyTimelockControllerReceipt.contractAddress);
//部署治理合约
console.log(MyTimelockControllerReceipt.contractAddress,GovTokenRegistryReceipt.contractAddress)
const MyGovernor = await artifacts.readArtifact("MyGovernor");
const MyGovernorHash = await deployer.deployContract({
abi: MyGovernor.abi,//获取abi
bytecode: MyGovernor.bytecode,//硬编码
args: [GovTokenRegistryReceipt.contractAddress,MyTimelockControllerReceipt.contractAddress],//治理代币地址,时间锁地址
});
// 等待确认并打印地址
const MyGovernorReceipt = await publicClient.getTransactionReceipt({ hash: MyGovernorHash });
console.log("治理合约地址:", await MyGovernorReceipt.contractAddress);
// 部署目标合约 Box
const BoxVerlue = await artifacts.readArtifact("BoxVerlue");
const BoxHash = await deployer.deployContract({
abi: BoxVerlue.abi,//获取abi
bytecode: BoxVerlue.bytecode,//硬编码
args: [deployer.account.address],//部署者地址作为初始治理者
});
// 等待确认并打印地址
const BoxReceipt = await publicClient.getTransactionReceipt({ hash: BoxHash });
console.log("Box合约地址:", await BoxReceipt.contractAddress);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
npx hardhat run ./scripts/xxx.ts
import assert from "node:assert/strict";
import { describe, it,beforeEach } from "node:test";
import { formatEther,parseEther,encodeFunctionData ,keccak256, toHex} from 'viem'
import { network } from "hardhat";
describe("MyGovToken", async function () {
let viem: any;
let publicClient: any;
let owner: any, user1: any, user2: any, user3: any;
let deployerAddress: string;
let MyGovToken: any;
let MyTimelockController: any;
let MyGovernor: any;
let BoxVerlue: any;
const minDelay = 2 * 24 * 60 * 60;
beforeEach (async function () {
const { viem } = await network.connect();
publicClient = await viem.getPublicClient();//创建一个公共客户端实例用于读取链上数据(无需私钥签名)。
[owner,user1,user2,user3] = await viem.getWalletClients();//获取第一个钱包客户端 写入联合交易
deployerAddress = owner.account.address;//钱包地址
MyGovToken = await viem.deployContract("MyGovToken",[deployerAddress]);//部署合约
MyTimelockController = await viem.deployContract("MyTimelockController",[minDelay,[],[],deployerAddress]);//部署合约
MyGovernor = await viem.deployContract("MyGovernor",[MyGovToken.address,MyTimelockController.address,]);//部署合约
BoxVerlue = await viem.deployContract("BoxVerlue",[deployerAddress]);//部署合约
console.log("MyGovToken地址",MyGovToken.address);
console.log("MyTimelockController地址",MyTimelockController.address);
console.log("MyGovernor地址",MyGovernor.address);
console.log("BoxVerlue地址",BoxVerlue.address);
});
it("完整提案流程", async function () {
// 1. 铸造治理代币并委托投票权
console.log("\n=== 1. 准备治理代币 ===");
const mintAmount = parseEther('10000');
await MyGovToken.write.mint([user1.account.address, mintAmount], {
account: owner.account
});
// 用户必须委托给自己才能获得投票权(OpenZeppelin 快照机制)
await MyGovToken.write.delegate([user1.account.address], {
account: user1.account
});
// 验证投票权重
const votes = await MyGovToken.read.getVotes([user1.account.address]);
console.log(`用户1投票权重: ${formatEther(votes)} GT`);
assert.equal(votes, mintAmount, "委托后应获得对应投票权重");
// 2. 配置时间锁权限
console.log("\n=== 2. 配置时间锁角色 ===");
const PROPOSER_ROLE = await MyTimelockController.read.PROPOSER_ROLE();
const EXECUTOR_ROLE = await MyTimelockController.read.EXECUTOR_ROLE();
// const TIMELOCK_ADMIN_ROLE = await MyTimelockController.read.TIMELOCK_ADMIN_ROLE();
// console.log("TIMELOCK_ADMIN_ROLE:",TIMELOCK_ADMIN_ROLE);
// 授予治理合约提案权(必须步骤)
await MyTimelockController.write.grantRole([
PROPOSER_ROLE,
MyGovernor.address
], { account: owner.account });
// 允许任何人执行提案(可设置为特定地址)
await MyTimelockController.write.grantRole([
EXECUTOR_ROLE,
"0x0000000000000000000000000000000000000000"
], { account: owner.account });
// 将 Box 合约所有权转移给时间锁(确保只有治理能调用)
await BoxVerlue.write.transferOwnership([MyTimelockController.address], {
account: owner.account
});
console.log("✅ 时间锁权限配置完成");
// 3. 创建提案
console.log("\n=== 3. 创建提案 ===");
const newValue = 42n;
const calldata = encodeFunctionData({
abi: BoxVerlue.abi, // 直接使用合约的 ABI
functionName: 'store',
args: [newValue]
});
const targets = [BoxVerlue.address];
const values = [0n]; // 无 ETH 转账
const calldatas = [calldata];
const description = "提案 #1: 在 Box 合约中存储值 42";
const proposeTx = await MyGovernor.write.propose(
[targets, values, calldatas, description],
{ account: user1.account }
);
// 等待交易确认并解析提案ID
const receipt = await publicClient.waitForTransactionReceipt({ hash: proposeTx });
const logs = await MyGovernor.getEvents.ProposalCreated({
fromBlock: receipt.blockNumber,
toBlock: receipt.blockNumber
});
const proposalId = logs[0].args.proposalId;
console.log(`✅ 提案已创建,ID: ${proposalId}`);
// 4. 等待投票延迟期(votingDelay)
console.log("\n=== 4. 等待投票延迟 ===");
const votingDelay = await MyGovernor.read.votingDelay();
// await network.provider.send("hardhat_mine", [
// `0x${votingDelay.toString(16)}`
// ]);
await publicClient.request({
method: "hardhat_mine",
params: [`0x${votingDelay.toString(16)}`]
});
let state = await MyGovernor.read.state([proposalId]);
console.log(`提案状态: ${state} (1=Active)`);
// assert.equal(state, 1n, "延迟后状态应为 Active");
// 5. 用户投票(1=支持)
console.log("\n=== 5. 投票阶段 ===");
await MyGovernor.write.castVote([
proposalId,
1n // 0=反对, 1=支持, 2=弃权
], { account: user1.account });
const hasVoted = await MyGovernor.read.hasVoted([proposalId, user1.account.address]);
assert.equal(hasVoted, true, "用户应已成功投票");
console.log("✅ 用户1已投支持票");
// 6. 等待投票期结束(votingPeriod)
console.log("\n=== 6. 等待投票期结束 ===");
const votingPeriod = await MyGovernor.read.votingPeriod();
// await network.provider.send("hardhat_mine", [
// `0x${votingPeriod.toString(16)}`
// ]);
await publicClient.request({
method: "hardhat_mine",
params: [`0x${votingPeriod.toString(16)}`]
});
state = await MyGovernor.read.state([proposalId]);
console.log(`提案状态: ${state} (4=Succeeded)`);
// assert.equal(state, 4n, "投票通过后状态应为 Succeeded");
// 7. 将提案加入时间锁队列
console.log("\n=== 7. 时间锁排队 ===");
// const descriptionHash = await publicClient.keccak256(
// new TextEncoder().encode(description)
// );
const descriptionHash = keccak256(toHex(description));
await MyGovernor.write.queue([
targets,
values,
calldatas,
descriptionHash
], { account: user1.account });
state = await MyGovernor.read.state([proposalId]);
console.log(`提案状态: ${state} (5=Queued)`);
// assert.equal(state, 5n, "排队后状态应为 Queued");
// 8. 等待时间锁延迟期(minDelay)
console.log("\n=== 8. 等待时间锁延迟 ===");
// await network.provider.send("evm_increaseTime", [minDelay + 100]);
// await network.provider.send("hardhat_mine", ["0x1"]);
await publicClient.request({
method: "evm_increaseTime",
params: [minDelay + 100]
});
await publicClient.request({
method: "hardhat_mine",
params: ["0x1"]
});
// 9. 执行提案
console.log("\n=== 9. 执行提案 ===");
await MyGovernor.write.execute([
targets,
values,
calldatas,
descriptionHash
], { account: user1.account });
state = await MyGovernor.read.state([proposalId]);
console.log(`提案状态: ${state} (7=Executed)`);
// assert.equal(state, 7n, "执行后状态应为 Executed");
// 10. 验证目标合约状态变更
console.log("\n=== 10. 验证执行结果 ===");
const storedValue = await BoxVerlue.read.retrieve();
console.log(`Box 合约存储值: ${storedValue}`);
assert.equal(storedValue, newValue, "Box值应被成功更新为 42");
console.log("\n✅ 完整提案流程测试通过!");
});
});
npx hardhat test ./test/xxx.ts
至此,本文围绕治理代币,构建了理论与实践结合的完整内容体系:先明确治理代币是 ERC-20 标准的超集,厘清其与普通 ERC-20 代币在功能、权利、安全机制上的核心差异;再以 Hardhat V3 为工具,提供治理代币相关全套合约代码、配置方案,以及部署与完整提案流程的测试脚本,解决了合约编译体积过大等实操问题,为开发者提供可直接落地的治理代币开发方案,助力区块链项目去中心化治理的技术落地。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!