跳到主要内容

DAO 开发

DAO(Decentralized Autonomous Organization,去中心化自治组织)是 Web3 最重要的组织形式之一。它通过智能合约实现组织的治理规则,让代币持有者共同参与决策。本章将深入介绍 DAO 的开发原理和 OpenZeppelin Governor 的实践。

DAO 概述

什么是 DAO

DAO 是一种由智能合约管理的组织,其规则和决策过程编码在区块链上。与传统组织相比,DAO 具有以下特点:

去中心化:没有中心化的管理层,决策由代币持有者共同做出。

透明公开:所有提案、投票和执行记录都在链上公开可见。

代码即法律:组织规则由智能合约定义,自动执行,不可篡改。

全球参与:任何人都可以参与,无需地理限制。

DAO 的核心组件

┌─────────────────────────────────────────────────────────────┐
│ DAO 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 治理代币 │ │ 投票合约 │ │ 时间锁 │ │
│ │ Governance │ │ Governor │ │ Timelock │ │
│ │ Token │ │ │ │ │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ │ 投票权重 │ 执行延迟 │ │
│ └───────────────>│<───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 目标合约 │ │
│ │ Target │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

治理代币:代表投票权重的 ERC20 代币,持有者可以参与投票。

投票合约:管理提案创建、投票和执行的智能合约。

时间锁:在执行提案前等待一段时间,允许用户退出。

目标合约:DAO 实际控制的合约,如金库、协议参数等。

OpenZeppelin Governor

OpenZeppelin 提供了一套模块化的 Governor 合约系统,是开发 DAO 的标准选择。

治理代币

治理代币需要实现 ERC20Votes 扩展,以支持历史投票权快照:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes {
constructor(
string memory _name,
string memory _symbol,
uint256 _initialSupply
) ERC20(_name, _symbol) ERC20Permit(_name) {
_mint(msg.sender, _initialSupply * 10 ** decimals());
}

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

ERC20Votes 的作用

  • 记录历史投票权快照
  • 防止同一代币多次投票
  • 支持委托投票

委托投票

用户需要委托代币才能获得投票权:

contract VotingDelegation {
GovernanceToken public token;

function selfDelegate() external {
token.delegate(msg.sender);
}

function delegateTo(address _delegatee) external {
token.delegate(_delegatee);
}

function getVotes(address _account) external view returns (uint256) {
return token.getVotes(_account);
}

function getPastVotes(
address _account,
uint256 _blockNumber
) external view returns (uint256) {
return token.getPastVotes(_account, _blockNumber);
}
}

Governor 合约

完整的 Governor 合约实现:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
import "@openzeppelin/contracts/governance/TimelockController.sol";

contract MyGovernor is
Governor,
GovernorCountingSimple,
GovernorVotes,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
constructor(
IVotes _token,
TimelockController _timelock
)
Governor("MyGovernor")
GovernorVotes(_token)
GovernorVotesQuorumFraction(4)
GovernorTimelockControl(_timelock)
{}

function votingDelay() public pure override returns (uint256) {
return 7200;
}

function votingPeriod() public pure override returns (uint256) {
return 50400;
}

function proposalThreshold() public pure override returns (uint256) {
return 0;
}

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

时间锁控制器

时间锁提供执行延迟,保护用户:

contract TimelockSetup {
function deployTimelock(
address admin,
uint256 minDelay
) external returns (TimelockController) {
address[] memory proposers = new address[](1);
address[] memory executors = new address[](1);

proposers[0] = admin;
executors[0] = address(0);

TimelockController timelock = new TimelockController(
minDelay,
proposers,
executors,
admin
);

return timelock;
}
}

部署脚本

完整的 DAO 部署脚本:

const { ethers } = require("hardhat");

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

console.log("Deploying DAO with account:", deployer.address);

const minDelay = 2 * 24 * 60 * 60;
const proposers = [];
const executors = [ethers.ZeroAddress];

const TimelockController = await ethers.getContractFactory("TimelockController");
const timelock = await TimelockController.deploy(
minDelay,
proposers,
executors,
deployer.address
);
await timelock.waitForDeployment();
console.log("Timelock deployed to:", await timelock.getAddress());

const GovernanceToken = await ethers.getContractFactory("GovernanceToken");
const token = await GovernanceToken.deploy(
"Governance Token",
"GOV",
ethers.parseEther("1000000")
);
await token.waitForDeployment();
console.log("Token deployed to:", await token.getAddress());

const MyGovernor = await ethers.getContractFactory("MyGovernor");
const governor = await MyGovernor.deploy(
await token.getAddress(),
await timelock.getAddress()
);
await governor.waitForDeployment();
console.log("Governor deployed to:", await governor.getAddress());

const proposerRole = await timelock.PROPOSER_ROLE();
const executorRole = await timelock.EXECUTOR_ROLE();
const adminRole = await timelock.DEFAULT_ADMIN_ROLE();

await timelock.grantRole(proposerRole, await governor.getAddress());
await timelock.grantRole(executorRole, await governor.getAddress());
await timelock.revokeRole(adminRole, deployer.address);

console.log("DAO setup complete!");
}

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

提案生命周期

创建提案

async function createProposal(governor, token, recipient, amount) {
const transferCalldata = token.interface.encodeFunctionData(
"transfer",
[recipient, amount]
);

const tx = await governor.propose(
[await token.getAddress()],
[0],
[transferCalldata],
"Proposal #1: Transfer tokens to team"
);

const receipt = await tx.wait();
const proposalCreatedEvent = receipt.logs.find(
log => log.fragment?.name === "ProposalCreated"
);

console.log("Proposal created with ID:", proposalCreatedEvent.args[0]);
return proposalCreatedEvent.args[0];
}

投票

async function castVote(governor, proposalId, support) {
const tx = await governor.castVote(proposalId, support);
const receipt = await tx.wait();

console.log("Vote cast:", receipt.hash);
}

async function castVoteWithReason(governor, proposalId, support, reason) {
const tx = await governor.castVoteWithReason(proposalId, support, reason);
const receipt = await tx.wait();

console.log("Vote cast with reason:", receipt.hash);
}

队列和执行

async function queueProposal(governor, targets, values, calldatas, description) {
const descriptionHash = ethers.id(description);

const tx = await governor.queue(
targets,
values,
calldatas,
descriptionHash
);

await tx.wait();
console.log("Proposal queued");
}

async function executeProposal(governor, targets, values, calldatas, description) {
const descriptionHash = ethers.id(description);

const tx = await governor.execute(
targets,
values,
calldatas,
descriptionHash
);

await tx.wait();
console.log("Proposal executed");
}

高级治理功能

时间戳模式

使用时间戳而非区块号进行投票:

contract TimestampGovernanceToken is ERC20, ERC20Permit, ERC20Votes {
constructor(
string memory _name,
string memory _symbol
) ERC20(_name, _symbol) ERC20Permit(_name) {}

function clock() public view override returns (uint48) {
return uint48(block.timestamp);
}

function CLOCK_MODE() public pure override returns (string memory) {
return "mode=timestamp";
}

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

投票权重快照

contract WeightedVoting {
struct Checkpoint {
uint32 fromBlock;
uint224 votes;
}

mapping(address => Checkpoint[]) private _checkpoints;

function getVotesAtBlock(
address account,
uint256 blockNumber
) public view returns (uint256) {
require(blockNumber < block.number, "Block not yet mined");

Checkpoint[] storage checkpoints = _checkpoints[account];

if (checkpoints.length == 0) {
return 0;
}

uint256 low = 0;
uint256 high = checkpoints.length - 1;

while (low < high) {
uint256 mid = (low + high + 1) / 2;
if (checkpoints[mid].fromBlock <= blockNumber) {
low = mid;
} else {
high = mid - 1;
}
}

return checkpoints[low].votes;
}
}

多签与 DAO 结合

contract MultiSigDAO is Governor {
uint256 public constant REQUIRED_SIGNATURES = 3;
mapping(uint256 => mapping(address => bool)) public hasSigned;
mapping(uint256 => uint256) public signatureCount;

function proposeWithSignatures(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description,
bytes[] memory signatures
) public returns (uint256) {
require(signatures.length >= REQUIRED_SIGNATURES, "Insufficient signatures");

uint256 proposalId = propose(targets, values, calldatas, description);

for (uint256 i = 0; i < signatures.length; i++) {
address signer = recoverSigner(proposalId, signatures[i]);
require(!hasSigned[proposalId][signer], "Already signed");
hasSigned[proposalId][signer] = true;
signatureCount[proposalId]++;
}

return proposalId;
}

function recoverSigner(
uint256 proposalId,
bytes memory signature
) internal pure returns (address) {
bytes32 hash = keccak256(abi.encodePacked(proposalId));
bytes32 ethSignedHash = keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
);

(bytes32 r, bytes32 s, uint8 v) = splitSignature(signature);
return ecrecover(ethSignedHash, v, r, s);
}

function splitSignature(
bytes memory sig
) internal pure returns (bytes32 r, bytes32 s, uint8 v) {
require(sig.length == 65, "Invalid signature length");

assembly {
r := mload(add(sig, 32))
s := mload(add(sig, 64))
v := byte(0, mload(add(sig, 96)))
}
}
}

DAO 金库管理

简单金库

contract DAOTreasury {
address public governor;

event Withdrawal(address indexed token, address indexed to, uint256 amount);
event Deposit(address indexed token, address indexed from, uint256 amount);

modifier onlyGovernor() {
require(msg.sender == governor, "Not governor");
_;
}

constructor(address _governor) {
governor = _governor;
}

function depositETH() external payable {
emit Deposit(address(0), msg.sender, msg.value);
}

function depositToken(
address token,
uint256 amount
) external {
IERC20(token).transferFrom(msg.sender, address(this), amount);
emit Deposit(token, msg.sender, amount);
}

function withdrawETH(
address to,
uint256 amount
) external onlyGovernor {
payable(to).transfer(amount);
emit Withdrawal(address(0), to, amount);
}

function withdrawToken(
address token,
address to,
uint256 amount
) external onlyGovernor {
IERC20(token).transfer(to, amount);
emit Withdrawal(token, to, amount);
}

function getBalance(address token) external view returns (uint256) {
if (token == address(0)) {
return address(this).balance;
}
return IERC20(token).balanceOf(address(this));
}
}

分期支付

contract VestingTreasury {
struct VestingSchedule {
address beneficiary;
uint256 totalAmount;
uint256 startTime;
uint256 duration;
uint256 released;
}

mapping(bytes32 => VestingSchedule) public vestingSchedules;

address public governor;

event VestingCreated(
bytes32 indexed scheduleId,
address indexed beneficiary,
uint256 totalAmount,
uint256 duration
);

event TokensReleased(
bytes32 indexed scheduleId,
address indexed beneficiary,
uint256 amount
);

constructor(address _governor) {
governor = _governor;
}

function createVestingSchedule(
address _beneficiary,
uint256 _totalAmount,
uint256 _duration
) external returns (bytes32) {
require(msg.sender == governor, "Not governor");

bytes32 scheduleId = keccak256(
abi.encodePacked(_beneficiary, block.timestamp, _totalAmount)
);

vestingSchedules[scheduleId] = VestingSchedule({
beneficiary: _beneficiary,
totalAmount: _totalAmount,
startTime: block.timestamp,
duration: _duration,
released: 0
});

emit VestingCreated(scheduleId, _beneficiary, _totalAmount, _duration);

return scheduleId;
}

function release(bytes32 _scheduleId) external {
VestingSchedule storage schedule = vestingSchedules[_scheduleId];

uint256 releasable = calculateReleasable(_scheduleId);
require(releasable > 0, "No tokens to release");

schedule.released += releasable;

payable(schedule.beneficiary).transfer(releasable);

emit TokensReleased(_scheduleId, schedule.beneficiary, releasable);
}

function calculateReleasable(
bytes32 _scheduleId
) public view returns (uint256) {
VestingSchedule storage schedule = vestingSchedules[_scheduleId];

uint256 vested = calculateVested(_scheduleId);
return vested - schedule.released;
}

function calculateVested(
bytes32 _scheduleId
) public view returns (uint256) {
VestingSchedule storage schedule = vestingSchedules[_scheduleId];

if (block.timestamp < schedule.startTime) {
return 0;
}

uint256 elapsed = block.timestamp - schedule.startTime;

if (elapsed >= schedule.duration) {
return schedule.totalAmount;
}

return (schedule.totalAmount * elapsed) / schedule.duration;
}

receive() external payable {}
}

DAO 治理最佳实践

参数设置建议

参数建议值说明
votingDelay1 天给用户时间取消委托
votingPeriod1 周足够的投票时间
quorum4%最低参与率
proposalThreshold1%创建提案所需代币
timelock2 天执行延迟

安全考虑

防止闪电贷攻击:使用快照机制,确保投票权在提案创建前已持有。

防止鲸鱼垄断:设置提案阈值,限制大额持有者的权力。

时间锁保护:所有敏感操作都应通过时间锁执行。

紧急暂停:保留紧急情况下的暂停功能。

小结

DAO 是 Web3 组织形式的核心创新,OpenZeppelin Governor 提供了成熟、安全的实现方案。开发 DAO 时,需要仔细设计治理参数,平衡效率与安全,并确保用户有足够的时间参与决策。

参考资料