在Solidity中实现DAO:从概念到代码的全面剖析

今天我们要聊一个在区块链世界里超级火热的话题——DAO(去中心化自治组织,DecentralizedAutonomousOrganization)。DAO就像一个链上的“民主社区”,通过智能合约让成员共同决策、管理资金或资源,摆脱中心化控制。如果你玩过DeFi、NFT或者Web3项目,可能会听说

今天我们要聊一个在区块链世界里超级火热的话题——DAO(去中心化自治组织,Decentralized Autonomous Organization)。DAO就像一个链上的“民主社区”,通过智能合约让成员共同决策、管理资金或资源,摆脱中心化控制。如果你玩过DeFi、NFT或者Web3项目,可能会听说过Aragon、Moloch或者The DAO这些名字。DAO的核心是去中心化治理,成员通过投票决定提案,比如花钱、升级合约或调整规则。

DAO是什么?为什么需要它?

DAO是运行在区块链上的组织,通过智能合约自动执行规则和决策。它的核心思想是“代码即法律”,成员通过持有代币或投票权参与治理,共同决定组织的行为。DAO的典型特点包括:

  • 去中心化:没有单一控制者,所有决策由成员投票决定。
  • 透明性:所有规则和交易记录在链上,公开可查。
  • 自动化:智能合约自动执行投票结果,无需人工干预。
  • 灵活性:可以管理资金、NFT、协议参数等。

DAO的常见应用场景:

  • 资金管理:社区国库分配,比如资助项目或分红。
  • 协议治理:调整DeFi协议的参数(利率、费用等)。
  • 社区决策:NFT项目投票决定新功能或艺术方向。
  • 去中心化协作:开发者、艺术家或投资者的联合管理。

在Solidity中实现DAO,核心是设计投票机制、提案管理和资金分配。我们会通过一个实际例子——一个简单的DAO合约(SimpleDAO),逐步实现这些功能。

实现一个基础DAO合约

为了让大家快速上手,我们来写一个SimpleDAO合约,功能包括:

  • 成员通过持有代币(ERC20)获得投票权。
  • 成员可以提交提案(比如转账ETH)。
  • 成员投票支持或反对提案。
  • 提案达到通过条件后自动执行。
  • 支持查询提案和投票状态。

基础合约结构

先来看合约的框架,包含核心状态变量和初始化逻辑:

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

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract SimpleDAO {
    IERC20 public governanceToken;
    address public admin;
    uint public constant VOTING_PERIOD = 3 days;
    uint public constant MINIMUM_QUORUM = 100 ether; // 假设代币单位是wei
    uint public proposalCount;

    struct Proposal {
        uint id;
        address proposer;
        address to;
        uint value;
        bytes data;
        uint voteFor;
        uint voteAgainst;
        uint startTime;
        bool executed;
        mapping(address => bool) voted;
    }

    mapping(uint => Proposal) public proposals;
    mapping(address => bool) public isMember;

    event ProposalCreated(uint indexed proposalId, address indexed proposer, address to, uint value, bytes data);
    event Voted(uint indexed proposalId, address indexed voter, bool support, uint weight);
    event ProposalExecuted(uint indexed proposalId, bool success);
    event MemberAdded(address indexed member);
    event MemberRemoved(address indexed member);

    constructor(address _governanceToken) {
        governanceToken = IERC20(_governanceToken);
        admin = msg.sender;
    }
}

代码分析

  • 接口IERC20定义了治理代币的接口(查询余额、转账)。
  • 状态变量
    • governanceToken:治理代币合约地址。
    • admin:初始管理员,控制成员管理。
    • VOTING_PERIOD:投票持续时间(3天)。
    • MINIMUM_QUORUM:最低投票参与量(总票数需达到100代币)。
    • proposalCount:跟踪提案数量,生成唯一ID。
  • 结构体
    • Proposal:记录提案详情,包括ID、提议者、目标地址、金额、数据、投票数、开始时间、是否执行和投票记录。
  • 映射
    • proposals:用ID映射到提案。
    • isMember:记录谁是DAO成员。
  • 事件
    • 定义了创建提案、投票、执行提案、添加/移除成员的事件,方便前端监听。
  • 构造函数
    • 初始化治理代币地址和管理员。

这个框架为DAO打下了基础,接下来实现核心功能。

添加和移除成员

只有持有治理代币的地址可以成为成员,由管理员管理:

modifier onlyAdmin() {
    require(msg.sender == admin, "Only admin can call this function");
    _;
}

function addMember(address _member) external onlyAdmin {
    require(_member != address(0), "Invalid member address");
    require(!isMember[_member], "Already a member");
    require(governanceToken.balanceOf(_member) > 0, "No governance tokens");

    isMember[_member] = true;
    emit MemberAdded(_member);
}

function removeMember(address _member) external onlyAdmin {
    require(isMember[_member], "Not a member");

    isMember[_member] = false;
    emit MemberRemoved(_member);
}

代码分析

  • 修饰符onlyAdmin限制只有管理员能调用。
  • 验证
    • addMember:确保地址有效、不是成员且持有代币。
    • removeMember:确保是现有成员。
  • 事件:触发MemberAddedMemberRemoved,记录成员变更。

提交提案

只有成员可以提交提案(比如转ETH):

modifier onlyMember() {
    require(isMember[msg.sender], "Not a member");
    require(governanceToken.balanceOf(msg.sender) > 0, "No governance tokens");
    _;
}

function submitProposal(address _to, uint _value, bytes memory _data) external onlyMember {
    uint proposalId = proposalCount++;
    Proposal storage proposal = proposals[proposalId];
    proposal.id = proposalId;
    proposal.proposer = msg.sender;
    proposal.to = _to;
    proposal.value = _value;
    proposal.data = _data;
    proposal.startTime = block.timestamp;
    proposal.executed = false;

    emit ProposalCreated(proposalId, msg.sender, _to, _value, _data);
}

代码分析

  • 修饰符onlyMember确保调用者是成员且持有代币。
  • 提案初始化
    • proposalCount++生成唯一ID。
    • 记录提议者、目标地址、金额、数据和开始时间。
  • 灵活性data字段支持调用其他合约(比如转ERC20代币)。
  • 事件:触发ProposalCreated,记录提案详情。

投票

成员根据代币持有量投票支持或反对:

function vote(uint _proposalId, bool _support) external onlyMember {
    Proposal storage proposal = proposals[_proposalId];
    require(block.timestamp <= proposal.startTime + VOTING_PERIOD, "Voting period ended");
    require(!proposal.voted[msg.sender], "Already voted");
    require(!proposal.executed, "Proposal already executed");

    uint weight = governanceToken.balanceOf(msg.sender);
    require(weight > 0, "No voting power");

    proposal.voted[msg.sender] = true;
    if (_support) {
        proposal.voteFor += weight;
    } else {
        proposal.voteAgainst += weight;
    }

    emit Voted(_proposalId, msg.sender, _support, weight);
}

代码分析

  • 验证
    • 确保投票在时间窗口内(VOTING_PERIOD)。
    • 确保未投票且提案未执行。
  • 投票权重:根据governanceToken.balanceOf计算投票权重。
  • 更新:记录投票状态,增加支持或反对票数。
  • 事件:触发Voted,记录投票详情。

注意:投票权重基于当前余额,可能导致“最后一秒投票”问题(稍后优化)。

执行提案

投票结束后,任何人可以调用executeProposal执行通过的提案:

function executeProposal(uint _proposalId) external {
    Proposal storage proposal = proposals[_proposalId];
    require(block.timestamp > proposal.startTime + VOTING_PERIOD, "Voting period not ended");
    require(!proposal.executed, "Proposal already executed");
    require(proposal.voteFor + proposal.voteAgainst >= MINIMUM_QUORUM, "Quorum not reached");
    require(proposal.voteFor > proposal.voteAgainst, "Proposal not approved");

    proposal.executed = true;
    (bool success, ) = proposal.to.call{value: proposal.value}(proposal.data);
    require(success, "Execution failed");

    emit ProposalExecuted(_proposalId, success);
}

代码分析

  • 验证
    • 确保投票时间结束。
    • 确保提案未执行。
    • 确保达到最低票数(MINIMUM_QUORUM)。
    • 确保支持票多于反对票。
  • 执行:用call执行提案,支持ETH转账或合约调用。
  • 安全:检查success确保调用成功。
  • 事件:触发ProposalExecuted

完整基础版代码

整合以上代码:

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

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract SimpleDAO {
    IERC20 public governanceToken;
    address public admin;
    uint public constant VOTING_PERIOD = 3 days;
    uint public constant MINIMUM_QUORUM = 100 ether;
    uint public proposalCount;

    struct Proposal {
        uint id;
        address proposer;
        address to;
        uint value;
        bytes data;
        uint voteFor;
        uint voteAgainst;
        uint startTime;
        bool executed;
        mapping(address => bool) voted;
    }

    mapping(uint => Proposal) public proposals;
    mapping(address => bool) public isMember;

    event ProposalCreated(uint indexed proposalId, address indexed proposer, address to, uint value, bytes data);
    event Voted(uint indexed proposalId, address indexed voter, bool support, uint weight);
    event ProposalExecuted(uint indexed proposalId, bool success);
    event MemberAdded(address indexed member);
    event MemberRemoved(address indexed member);

    modifier onlyAdmin() {
        require(msg.sender == admin, "Only admin can call this function");
        _;
    }

    modifier onlyMember() {
        require(isMember[msg.sender], "Not a member");
        require(governanceToken.balanceOf(msg.sender) > 0, "No governance tokens");
        _;
    }

    constructor(address _governanceToken) {
        governanceToken = IERC20(_governanceToken);
        admin = msg.sender;
    }

    function addMember(address _member) external onlyAdmin {
        require(_member != address(0), "Invalid member address");
        require(!isMember[_member], "Already a member");
        require(governanceToken.balanceOf(_member) > 0, "No governance tokens");

        isMember[_member] = true;
        emit MemberAdded(_member);
    }

    function removeMember(address _member) external onlyAdmin {
        require(isMember[_member], "Not a member");

        isMember[_member] = false;
        emit MemberRemoved(_member);
    }

    function submitProposal(address _to, uint _value, bytes memory _data) external onlyMember {
        uint proposalId = proposalCount++;
        Proposal storage proposal = proposals[proposalId];
        proposal.id = proposalId;
        proposal.proposer = msg.sender;
        proposal.to = _to;
        proposal.value = _value;
        proposal.data = _data;
        proposal.startTime = block.timestamp;
        proposal.executed = false;

        emit ProposalCreated(proposalId, msg.sender, _to, _value, _data);
    }

    function vote(uint _proposalId, bool _support) external onlyMember {
        Proposal storage proposal = proposals[_proposalId];
        require(block.timestamp <= proposal.startTime + VOTING_PERIOD, "Voting period ended");
        require(!proposal.voted[msg.sender], "Already voted");
        require(!proposal.executed, "Proposal already executed");

        uint weight = governanceToken.balanceOf(msg.sender);
        require(weight > 0, "No voting power");

        proposal.voted[msg.sender] = true;
        if (_support) {
            proposal.voteFor += weight;
        } else {
            proposal.voteAgainst += weight;
        }

        emit Voted(_proposalId, msg.sender, _support, weight);
    }

    function executeProposal(uint _proposalId) external {
        Proposal storage proposal = proposals[_proposalId];
        require(block.timestamp > proposal.startTime + VOTING_PERIOD, "Voting period not ended");
        require(!proposal.executed, "Proposal already executed");
        require(proposal.voteFor + proposal.voteAgainst >= MINIMUM_QUORUM, "Quorum not reached");
        require(proposal.voteFor > proposal.voteAgainst, "Proposal not approved");

        proposal.executed = true;
        (bool success, ) = proposal.to.call{value: proposal.value}(proposal.data);
        require(success, "Execution failed");

        emit ProposalExecuted(_proposalId, success);
    }
}

这个版本已经能跑,但还有优化空间,接下来我们会加入高级功能和安全措施。

优化DAO实现

基础版功能齐全,但离生产环境还差一些。我们来优化以下方面:

  • 快照投票:防止“最后一秒投票”问题。
  • 提案类型:支持不同类型的提案(转账、参数调整、成员管理)。
  • 安全措施:防重入、权限控制。
  • 用户体验:查询提案和投票状态。
  • Gas优化:减少存储操作。

快照投票

当前投票基于实时代币余额,可能导致用户在投票截止前临时购买代币增加权重。我们改用快照机制,在提案创建时记录投票权重:

struct Proposal {
    uint id;
    address proposer;
    address to;
    uint value;
    bytes data;
    uint voteFor;
    uint voteAgainst;
    uint startTime;
    bool executed;
    mapping(address => bool) voted;
    mapping(address => uint) voteWeight;
}

function submitProposal(address _to, uint _value, bytes memory _data) external onlyMember {
    uint proposalId = proposalCount++;
    Proposal storage proposal = proposals[proposalId];
    proposal.id = proposalId;
    proposal.proposer = msg.sender;
    proposal.to = _to;
    proposal.value = _value;
    proposal.data = _data;
    proposal.startTime = block.timestamp;
    proposal.executed = false;
    proposal.voteWeight[msg.sender] = governanceToken.balanceOf(msg.sender);

    emit ProposalCreated(proposalId, msg.sender, _to, _value, _data);
}

function vote(uint _proposalId, bool _support) external onlyMember {
    Proposal storage proposal = proposals[_proposalId];
    require(block.timestamp <= proposal.startTime + VOTING_PERIOD, "Voting period ended");
    require(!proposal.voted[msg.sender], "Already voted");
    require(!proposal.executed, "Proposal already executed");

    uint weight = governanceToken.balanceOf(msg.sender);
    require(weight > 0, "No voting power");

    proposal.voted[msg.sender] = true;
    proposal.voteWeight[msg.sender] = weight;
    if (_support) {
        proposal.voteFor += weight;
    } else {
        proposal.voteAgainst += weight;
    }

    emit Voted(_proposalId, msg.sender, _support, weight);
}

分析

  • 快照:在submitProposal记录提议者的投票权重,vote使用当前余额但记录快照。
  • 公平性:防止临时购买代币影响投票。
  • Gas成本:增加voteWeight映射,略微增加存储成本。

支持多种提案类型

当前提案只支持转账,我们添加成员管理和参数调整提案:

enum ProposalType { Transfer, AddMember, RemoveMember, UpdateQuorum }

struct Proposal {
    uint id;
    address proposer;
    address to;
    uint value;
    bytes data;
    uint voteFor;
    uint voteAgainst;
    uint startTime;
    bool executed;
    ProposalType proposalType;
    mapping(address => bool) voted;
    mapping(address => uint) voteWeight;
}

function submitAddMemberProposal(address _newMember) external onlyMember {
    require(_newMember != address(0), "Invalid member address");
    require(!isMember[_newMember], "Already a member");

    uint proposalId = proposalCount++;
    Proposal storage proposal = proposals[proposalId];
    proposal.id = proposalId;
    proposal.proposer = msg.sender;
    proposal.to = address(this);
    proposal.value = 0;
    proposal.data = abi.encodeWithSignature("addMember(address)", _newMember);
    proposal.startTime = block.timestamp;
    proposal.executed = false;
    proposal.proposalType = ProposalType.AddMember;
    proposal.voteWeight[msg.sender] = governanceToken.balanceOf(msg.sender);

    emit ProposalCreated(proposalId, msg.sender, address(this), 0, proposal.data);
}

分析

  • 提案类型:用ProposalType区分转账、添加成员等。
  • 专用函数submitAddMemberProposal简化成员管理提案的创建。
  • 类似实现:移除成员、更新MINIMUM_QUORUM可类似实现。

安全措施

  • 防重入executeProposalcall在状态更新后执行,防止回调攻击。
  • 权限控制onlyAdminonlyMember确保操作合法。
  • 时间安全block.timestamp可能被矿工操纵,3天窗口降低风险。
  • 代币检查:每次投票验证代币余额,防止无权投票。

用户体验

添加查询函数:

function getProposal(uint _proposalId) external view returns (
    uint id,
    address proposer,
    address to,
    uint value,
    bytes memory data,
    uint voteFor,
    uint voteAgainst,
    uint startTime,
    bool executed,
    ProposalType proposalType
) {
    Proposal storage proposal = proposals[_proposalId];
    return (
        proposal.id,
        proposal.proposer,
        proposal.to,
        proposal.value,
        proposal.data,
        proposal.voteFor,
        proposal.voteAgainst,
        proposal.startTime,
        proposal.executed,
        proposal.proposalType
    );
}

function hasVoted(uint _proposalId, address _voter) external view returns (bool) {
    return proposals[_proposalId].voted[_voter];
}

分析

  • 查询getProposal返回提案详情,hasVoted检查投票状态。
  • 视图函数:不消耗Gas,适合前端调用。

Gas优化

  • 最小化存储voteWeight增加存储成本,可用事件记录投票权重减少存储。
  • 避免循环:投票和执行不遍历成员,降低Gas。
  • 批量投票:可添加批量投票函数,减少多次调用。

完整优化版代码

整合优化后的代码(部分省略重复功能):

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

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract SimpleDAO {
    IERC20 public governanceToken;
    address public admin;
    uint public constant VOTING_PERIOD = 3 days;
    uint public constant MINIMUM_QUORUM = 100 ether;
    uint public proposalCount;

    enum ProposalType { Transfer, AddMember, RemoveMember, UpdateQuorum }

    struct Proposal {
        uint id;
        address proposer;
        address to;
        uint value;
        bytes data;
        uint voteFor;
        uint voteAgainst;
        uint startTime;
        bool executed;
        ProposalType proposalType;
        mapping(address => bool) voted;
        mapping(address => uint) voteWeight;
    }

    mapping(uint => Proposal) public proposals;
    mapping(address => bool) public isMember;

    event ProposalCreated(uint indexed proposalId, address indexed proposer, address to, uint value, bytes data);
    event Voted(uint indexed proposalId, address indexed voter, bool support, uint weight);
    event ProposalExecuted(uint indexed proposalId, bool success);
    event MemberAdded(address indexed member);
    event MemberRemoved(address indexed member);

    modifier onlyAdmin() {
        require(msg.sender == admin, "Only admin can call this function");
        _;
    }

    modifier onlyMember() {
        require(isMember[msg.sender], "Not a member");
        require(governanceToken.balanceOf(msg.sender) > 0, "No governance tokens");
        _;
    }

    constructor(address _governanceToken) {
        governanceToken = IERC20(_governanceToken);
        admin = msg.sender;
    }

    function addMember(address _member) external onlyAdmin {
        require(_member != address(0), "Invalid member address");
        require(!isMember[_member], "Already a member");
        require(governanceToken.balanceOf(_member) > 0, "No governance tokens");

        isMember[_member] = true;
        emit MemberAdded(_member);
    }

    function removeMember(address _member) external onlyAdmin {
        require(isMember[_member], "Not a member");

        isMember[_member] = false;
        emit MemberRemoved(_member);
    }

    function submitProposal(address _to, uint _value, bytes memory _data) external onlyMember {
        uint proposalId = proposalCount++;
        Proposal storage proposal = proposals[proposalId];
        proposal.id = proposalId;
        proposal.proposer = msg.sender;
        proposal.to = _to;
        proposal.value = _value;
        proposal.data = _data;
        proposal.startTime = block.timestamp;
        proposal.executed = false;
        proposal.proposalType = ProposalType.Transfer;
        proposal.voteWeight[msg.sender] = governanceToken.balanceOf(msg.sender);

        emit ProposalCreated(proposalId, msg.sender, _to, _value, _data);
    }

    function submitAddMemberProposal(address _newMember) external onlyMember {
        require(_newMember != address(0), "Invalid member address");
        require(!isMember[_newMember], "Already a member");

        uint proposalId = proposalCount++;
        Proposal storage proposal = proposals[proposalId];
        proposal.id = proposalId;
        proposal.proposer = msg.sender;
        proposal.to = address(this);
        proposal.value = 0;
        proposal.data = abi.encodeWithSignature("addMember(address)", _newMember);
        proposal.startTime = block.timestamp;
        proposal.executed = false;
        proposal.proposalType = ProposalType.AddMember;
        proposal.voteWeight[msg.sender] = governanceToken.balanceOf(msg.sender);

        emit ProposalCreated(proposalId, msg.sender, address(this), 0, proposal.data);
    }

    function vote(uint _proposalId, bool _support) external onlyMember {
        Proposal storage proposal = proposals[_proposalId];
        require(block.timestamp <= proposal.startTime + VOTING_PERIOD, "Voting period ended");
        require(!proposal.voted[msg.sender], "Already voted");
        require(!proposal.executed, "Proposal already executed");

        uint weight = governanceToken.balanceOf(msg.sender);
        require(weight > 0, "No voting power");

        proposal.voted[msg.sender] = true;
        proposal.voteWeight[msg.sender] = weight;
        if (_support) {
            proposal.voteFor += weight;
        } else {
            proposal.voteAgainst += weight;
        }

        emit Voted(_proposalId, msg.sender, _support, weight);
    }

    function executeProposal(uint _proposalId) external {
        Proposal storage proposal = proposals[_proposalId];
        require(block.timestamp > proposal.startTime + VOTING_PERIOD, "Voting period not ended");
        require(!proposal.executed, "Proposal already executed");
        require(proposal.voteFor + proposal.voteAgainst >= MINIMUM_QUORUM, "Quorum not reached");
        require(proposal.voteFor > proposal.voteAgainst, "Proposal not approved");

        proposal.executed = true;
        (bool success, ) = proposal.to.call{value: proposal.value}(proposal.data);
        require(success, "Execution failed");

        emit ProposalExecuted(_proposalId, success);
    }

    function getProposal(uint _proposalId) external view returns (
        uint id,
        address proposer,
        address to,
        uint value,
        bytes memory data,
        uint voteFor,
        uint voteAgainst,
        uint startTime,
        bool executed,
        ProposalType proposalType
    ) {
        Proposal storage proposal = proposals[_proposalId];
        return (
            proposal.id,
            proposal.proposer,
            proposal.to,
            proposal.value,
            proposal.data,
            proposal.voteFor,
            proposal.voteAgainst,
            proposal.startTime,
            proposal.executed,
            proposal.proposalType
        );
    }

    function hasVoted(uint _proposalId, address _voter) external view returns (bool) {
        return proposals[_proposalId].voted[_voter];
    }

    receive() external payable {}
}

分析

  • 功能完备:支持提案、投票、执行、成员管理和多种提案类型。
  • 安全:防重入、快照投票、时间限制。
  • 用户体验:事件和查询函数便于前端集成。
  • Gas优化:避免复杂循环,存储结构高效。

进阶功能:支持ERC20代币转账

DAO常需管理ERC20代币,我们添加专用提案:

function submitERC20TransferProposal(address _token, address _to, uint _amount) external onlyMember {
    uint proposalId = proposalCount++;
    Proposal storage proposal = proposals[proposalId];
    proposal.id = proposalId;
    proposal.proposer = msg.sender;
    proposal.to = _token;
    proposal.value = 0;
    proposal.data = abi.encodeWithSignature("transfer(address,uint256)", _to, _amount);
    proposal.startTime = block.timestamp;
    proposal.executed = false;
    proposal.proposalType = ProposalType.Transfer;
    proposal.voteWeight[msg.sender] = governanceToken.balanceOf(msg.sender);

    emit ProposalCreated(proposalId, msg.sender, _token, 0, proposal.data);
}

分析

  • ERC20支持:生成transfer调用的数据,支持任何ERC20代币。
  • 通用性:通过call执行,兼容标准ERC20合约。
  • 安全:需确保目标合约是可信的ERC20。
点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
天涯学馆
天涯学馆
0x9d6d...50d5
资深大厂程序员,12年开发经验,致力于探索前沿技术!