治理代币和ERC-20代币,根本不是一回事?揭开链上权力的真实面纱

  • 木西
  • 发布于 6小时前
  • 阅读 27

前言本文围绕治理代币展开多维度探究,首先对治理代币的核心概念、类型、功能及治理逻辑等核心内容进行系统性梳理,随后以HardhatV3为技术工具,通过具体的代码案例实操,完整呈现治理代币从开发环境搭建、智能合约编写,到测试、部署的全流程,助力开发者深化对治理代币的理解与应用;同时,针对开发者

前言

本文围绕治理代币展开多维度探究,首先对治理代币的核心概念、类型、功能及治理逻辑等核心内容进行系统性梳理,随后以 Hardhat V3 为技术工具,通过具体的代码案例实操,完整呈现治理代币从开发环境搭建、智能合约编写,到测试、部署的全流程,助力开发者深化对治理代币的理解与应用;同时,针对开发者在基于 Hardhat V3 进行治理代币开发、测试和部署过程中遇到的各类常见问题,逐一分析成因并提供切实可行的解决方案,形成理论梳理、技术实操与问题解决相结合的完整内容体系,为治理代币的开发实践提供全面参考。

一句话区分:治理代币 = ERC-20 代币 + 链上民主决策能力;ERC-20是"发币技术标准",治理代币是"代币功能场景"——前者定义规则,后者定义权力。

核心结论:所有治理代币都是 ERC-20 代币,但并非所有 ERC-20 都是治理代币

治理代币是 ERC-20 标准的超集——它在 ERC-20 基础功能上扩展了链上治理能力。就像智能手机 vs 普通手机,前者具备后者所有功能,并增加了智能特性。

功能与权利差异

普通代币

  • 仅作为价值载体
  • 用于支付、交易、质押等金融场景
  • 持有不影响项目决策

治理代币

  • 提案权:持有一定数量可发起提案(如 Compound 要求 100,000 COMP)
  • 投票权:对协议升级、参数调整、资金分配等进行表决
  • 委托权:将投票权委托给他人,同时保留代币所有权
  • 经济参与权:通常还能分享协议收益(如交易费分红)

    核心区别概览

    维度 普通代币 (ERC-20) 治理代币 (ERC-20Votes)
    主要用途 价值交换、支付、投资 参与链上治理、投票决策
    技术实现 基础 ERC-20 标准 ERC-20 + 投票/委托扩展
    持有者权利 仅财产权 财产权 + 治理权
    可转让性 自由流转 通常可转让,但投票权可委托
    激励机制 价值增长预期 参与治理奖励 + 价值增长
    使用场景 USDT(稳定币)、LINK(预言机) UNI(Uniswap)、AAVE(Aave)、COMP(Compound)

安全机制差异

风险类型 普通 ERC-20 治理代币
闪电贷攻击 无此风险 需设置 votingDelay(提案到投票的延迟)防止同区块攻击
投票权滥用 不适用 quorum(法定人数)和 proposalThreshold(提案门槛)
鲸鱼垄断 仅影响价格 可通过时间锁(Timelock)和委托机制缓解

智能合约开发、测试、部署

智能合约

1.治理代币合约

// 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);
    }
}

2.时间锁控制器合约

// 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) {}
}

3.治理合约

// 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();
    }
}

4.可治理的目标合约合约

// 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中进行如下配置

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 为工具,提供治理代币相关全套合约代码、配置方案,以及部署与完整提案流程的测试脚本,解决了合约编译体积过大等实操问题,为开发者提供可直接落地的治理代币开发方案,助力区块链项目去中心化治理的技术落地。

点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。