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 治理最佳实践
参数设置建议
| 参数 | 建议值 | 说明 |
|---|---|---|
| votingDelay | 1 天 | 给用户时间取消委托 |
| votingPeriod | 1 周 | 足够的投票时间 |
| quorum | 4% | 最低参与率 |
| proposalThreshold | 1% | 创建提案所需代币 |
| timelock | 2 天 | 执行延迟 |
安全考虑
防止闪电贷攻击:使用快照机制,确保投票权在提案创建前已持有。
防止鲸鱼垄断:设置提案阈值,限制大额持有者的权力。
时间锁保护:所有敏感操作都应通过时间锁执行。
紧急暂停:保留紧急情况下的暂停功能。
小结
DAO 是 Web3 组织形式的核心创新,OpenZeppelin Governor 提供了成熟、安全的实现方案。开发 DAO 时,需要仔细设计治理参数,平衡效率与安全,并确保用户有足够的时间参与决策。