分布式自治组织 the DAO 代码解析


  • qtum team

    分布式自治组织 the DAO 代码解析

    DAO 的全称是 Decentralized Autonomous Organization (去中心化的自治组织),可理解为完全由计算机代码控制运作的类似公司的实体。 the DAO 本质上是一个风险投资基金,通过以太坊将筹集到的资金锁定在智能合约中,每个参与众筹的人按照出资数额,获得相应的DAO代币(token),具有审查项目和投票表决的权利。投资议案由全体代币持有人投票表决,每个代币一票。如果议案得到需要的票数支持,相应的款项会划给该投资项目。投资项目的收益会按照一定规则回馈众筹参与人。

    The DAO 是区块链智能合约平台上一场伟大的实验,在2016年4月对外募资,27日内募集了1200万个以太币,价值1.32亿美元,是以太坊史上最大的一次众筹活动。尽管 the DAO 项目最终因递归调用BUG被黑客攻击利用而黯然落幕,但其中的思想仍值得我们学习。

    本文将带大家分析 The DAO 的设计理念和实现代码(基于1.0.1版本)

    1. DAO 中的资金是如何运转的

    DAO合约中的关键变量:

    // DAO合约继承自DAOInterface、Token、TokenCreation这三个合约,实际代码中的变量多定义在这三个合约中,为了便于理解整合到一起来了。
    
    contract DAO {
        // daoCreator 合约用于分裂时创建新Dao
        DAO_Creator public daoCreator;
        
        
        // 每个用户的Token数
        mapping (address => uint256) balances;
        
        // 总发行Token数
        uint256 public totalSupply;
        
        
        // The DAO众筹时超出 1wei:1Token 兑换率的以太会被存储到extraBalance合约账户中
        ManagedAccount public extraBalance;
        
        
        // 发起提议时需要缴纳的最小押金数
        uint public proposalDeposit;
        
        // 收到的提议押金总和
        uint sumOfProposalDeposits;
        
        
        // rewardAccount 合约账户中的资金用于向该Dao的Token持有者分红
        ManagedAccount public rewardAccount;
        // 该Dao向用户支付的资金情况
        mapping (address => uint) public paidOut;
        
        // rewardToken字典 记录着每个Dao投资出去的资金数
        mapping (address => uint) public rewardToken;
        // totalRewardToken 记录着主Dao和分裂出去的Dao总的投资数
        uint public totalRewardToken;
        
        // DAOrewardAccount 合约账户中的资金用于向主Dao和子Dao分红
        ManagedAccount public DAOrewardAccount;
        // DAOpaidOut 记录了向每个Dao已经支付的资金情况
        mapping (address => uint) public DAOpaidOut;
        
        // 所有的提议
        Proposal[] public proposals;
        
        // 投完票之后一直到提议结束,用户的Token会被锁定不得转移,blocked记录了处在锁定状态用户和对应的提议
        mapping (address => uint) public blocked;
        
        // 白名单,只有白名单中的地址才可以从DAO中转移资金出去
        mapping (address => bool) public allowedRecipients;
        
        .
        .
        .
        // 还有很多变量是关于一些配置信息、提议等的,太多了就不在此列举出来
    }
    

    • 在 Dao 部署到以太坊区块链之后,在规定的起始筹资阶段,任何人都可以通过向 Dao 智能合约地址发送以太币的方式来参与众筹,作为交换,代币会被创建用来代表是会员身份以及 Dao 一部分的所有权;这些代 币会被分配给众筹的参与者
    • 每一个代币的价格会随着时间而变化,众筹开始的前两周价格为1 wei,两周后每天上涨0.5,直到众筹结束前的第四天,代币价格固定为1.5 wei。
    • 为了避免有人在起始阶段以1wei买去大量代币,在预售结束后立即分离出一个Dao,按照1.5wei的价格来得到更多的以太币,所有比购买初始价格高的代币的以太币,会被发送到一个额外的帐户extraBalance。
    • 众筹完成,用户们投票决定要投资的提议,若提议获得大部分Token的支持,主账户里的Ether会被发送到提议指定的账户中。
    • 投资的项目盈利,回报以以太币的形式发送到 DAOrewardAccount 合约账户中
    • 用户想获得分红需要先产生一个提议将 DAOrewardAccount 中的以太币转移到 Dao 主账号中来,然后通过另一个提议将Dao主账号中的以太币转移到 rewardAccount 合约账户中,最后才可以通过持有 Token 的比例从 rewardAccount 中得到分红。
    • 如果用户不同意其他用户的投票,为了防止资金损失等情况,他可以选择分裂出去,在分裂之前投资所得收益他仍然能够得到应得的那一部分,分裂之后原先那个Dao的收益就与他无关了。

    2. 代码解析

    2.1 DAO初始化

    // DAO.sol
        function DAO(
            address _curator,
            DAO_Creator _daoCreator,
            uint _proposalDeposit,
            uint _minTokensToCreate,
            uint _closingTime,
            address _privateCreation
        ) TokenCreation(_minTokensToCreate, _closingTime, _privateCreation) {
            
            // 设置服务提供商
            curator = _curator;
            
            // 设置daoCreator
            daoCreator = _daoCreator;
            
            // 设置提议所需最小押金
            proposalDeposit = _proposalDeposit;
            
            // 新建 rewardAccount 合约账户,并把该 Dao 设置为他的owner
            rewardAccount = new ManagedAccount(address(this), false);
            
            // 新建 DAOrewardAccount 合约账户,并把该 Dao 设置为他的owner
            DAOrewardAccount = new ManagedAccount(address(this), false);
            
            // 确认 rewardAccount、DAOrewardAccount 创建成功
            if (address(rewardAccount) == 0)
                throw;
            if (address(DAOrewardAccount) == 0)
                throw;
            
            // 达到提议法定人数的最近时间
            lastTimeMinQuorumMet = now;
            
            // 这个数值和提议的最小法定人数有关,设置为5,意味着最小法定人数的最小值为20%的总人数
            minQuorumDivisor = 5;
            
            // 将提议数组的长度设置为1
            proposals.length = 1;
            
            // 将服务提供商和该DAO的地址加到白名单
            allowedRecipients[address(this)] = true;
            allowedRecipients[curator] = true;
        }
    

    在初始化过程中还调用了TokenCreation函数

    // TokenCreation.sol
        function TokenCreation(
            uint _minTokensToCreate,
            uint _closingTime,
            address _privateCreation) {
            
            // 设置众筹的截止时间
            closingTime = _closingTime;
            
            // 设置众筹的最小目标
            minTokensToCreate = _minTokensToCreate;
            
            // 如果设置了privateCreation,以为只有privateCreation才可以换取Token
            privateCreation = _privateCreation;
            
            // 新建 extraBalance 合约账户
            extraBalance = new ManagedAccount(address(this), true);
        }
    

    在DAO最新的代码(2017年5月)中,还增加了设置代码符号、名称、小数位的功能,使大家更方便发布创建自己的Token。

    2.2 购买Token

    在众筹期间,想DAO账户地址发送以太币就可以换取Token

    // DAO.sol
        function () returns (bool success) {
            
            if (now < closingTime + creationGracePeriod && msg.sender != address(extraBalance))
            // 在众筹期间内 以及 不为 extraBalance,进入兑换Token的流程
                return createTokenProxy(msg.sender);
            else
                // 在众筹时间外发送以太币的,会被 Dao 直接接收而不会返还 Token
                return receiveEther();
        }
    
        function receiveEther() returns (bool) {
            return true;
        }
    

    真正创建Token的部分:

    // TokenCreation.sol
        function createTokenProxy(address _tokenHolder) returns (bool success) {
            // 这个判断语句规定在众筹时间内、且发送了ether、且没有规定privateCreation或着是privateCreation的账户 才可以兑换Token
            if (now < closingTime && msg.value > 0
                && (privateCreation == 0 || privateCreation == msg.sender)) {
                
                // token的汇率 = 20/divisor()
                uint token = (msg.value * 20) / divisor();
                
                // 超出原始汇率部分的以太币存到extraBalance中
                extraBalance.call.value(msg.value - token)();
                
                // 更新*该用户的 token 总数
                balances[_tokenHolder] += token;
                
                // 更新token总发行数
                totalSupply += token;
                
                // 记录该用户发送的以太币总数量
                weiGiven[_tokenHolder] += msg.value;
                
                // 发出一个 CreatedToken 的event
                CreatedToken(_tokenHolder, token);
                
                // 如果已经达到最小众筹目标,isFueled置为真,发出 FuelingToDate event
                if (totalSupply >= minTokensToCreate && !isFueled) {
                    isFueled = true;
                    FuelingToDate(totalSupply);
                }
                return true;
            }
            throw;
        }
    

    Token的汇率: 20 / divisor();

    // TokenCreation.sol
        function divisor() constant returns (uint divisor) {
            if (closingTime - 2 weeks > now) {
                // 开头两周,1 token = 1 wei
                return 20;
            } else if (closingTime - 4 days > now) {
                // 随后10天,每天增加0.05
                return (20 + (now - (closingTime - 2 weeks)) / (1 days));
            } else {
                // 众筹结束前最后4天,价格锁定在 1 token = 1.5 wei
                return 30;
            }
        }
    

    2.3 如果没有达到最小众筹目标,退钱!

    // TokenCreation.sol
        function refund() noEther {
            // 如果在众筹截止时间后,仍没有成功
            if (now > closingTime && !isFueled) {
                
                if (extraBalance.balance >= extraBalance.accumulatedInput())
                    // 把extraBalance中的以太币都转到主账号中
                    extraBalance.payOut(address(this), extraBalance.accumulatedInput());
    
                // 执行退款操作,更新相关数值
                if (msg.sender.call.value(weiGiven[msg.sender])()) {
                    Refund(msg.sender, weiGiven[msg.sender]);
                    totalSupply -= balances[msg.sender];
                    balances[msg.sender] = 0;
                    weiGiven[msg.sender] = 0;
                }
            }
        }
    

    2.4 查询Token

    // Token.sol
        function balanceOf(address _owner) constant returns (uint256 balance) {
            // 用户的 Token 资产数都保存在balances变量中
            return balances[_owner];
        }
    

    2.5 Token 转账

    // Token.sol
        function transfer(address _to, uint256 _amount) noEther returns (bool success) {
            if (balances[msg.sender] >= _amount && _amount > 0) {
                // 在 balances 中将发送者的资产减小,接收者的资产增加,即可实现转账
                balances[msg.sender] -= _amount;
                balances[_to] += _amount;
                Transfer(msg.sender, _to, _amount);
                return true;
            } else {
               return false;
            }
        }
    

    2.6 发起提议

    先看一下代码中是如何定义提议的

    // DAO.sol
        struct Proposal {
            // 受益人
            address recipient;
            // 资金数
            uint amount;
            // 提议的描述
            string description;
            // 截止时间
            uint votingDeadline;
            // 是否开启的标志位
            bool open;
            // 是否被通过的标志位
            bool proposalPassed;
            // 提议的HASH
            bytes32 proposalHash;
            // 押金数
            uint proposalDeposit;
            // 是否要更换服务提供商
            bool newCurator;
            // 分裂DAO时候要用到的数据
            SplitData[] splitData;
            // 赞成票数
            uint yea;
            // 反对票数
            uint nay;
            // 记录投票者的投票情况
            mapping (address => bool) votedYes;
            mapping (address => bool) votedNo;
            // 提议的创建人
            address creator;
        }
    

    新建一个提议

    // DAO.sol
        function newProposal(
            address _recipient,
            uint _amount,
            string _description,
            bytes _transactionData,
            uint _debatingPeriod,
            bool _newCurator
        // 只有持有token的人才能发起提议
        ) onlyTokenholders returns (uint _proposalID) {
    
            // 检查参数
            if (_newCurator && (
                _amount != 0
                || _transactionData.length != 0
                || _recipient == curator
                || msg.value > 0
                || _debatingPeriod < minSplitDebatePeriod)) {
                throw;
            } else if (
                !_newCurator
                && (!isRecipientAllowed(_recipient) || (_debatingPeriod <  minProposalDebatePeriod))
            ) {
                throw;
            }
            
            // 提议的辩论时间不得超过两个月
            if (_debatingPeriod > 8 weeks)
                throw;
            
            // 众筹未完成不得发起新的提议
            if (!isFueled
                || now < closingTime
                || (msg.value < proposalDeposit && !_newCurator)) {
    
                throw;
            }
    
            if (now + _debatingPeriod < now) // 防止时间参数溢出
                throw;
    
            // 防止51%攻击
            if (msg.sender == address(this))
                throw;
            
            // 更新最新最小法定人数达标时间
            if (proposals.length == 1) // initial length is 1 (see constructor)
                lastTimeMinQuorumMet = now;
    
            _proposalID = proposals.length++;
            Proposal p = proposals[_proposalID];
            // recipient 提议受益人
            p.recipient = _recipient;
            // amount 众筹资金
            p.amount = _amount;
            // description 提议的描述
            p.description = _description;
            p.proposalHash = sha3(_recipient, _amount, _transactionData);
            // 投票截止时间
            p.votingDeadline = now + _debatingPeriod;
            p.open = true;
            //p.proposalPassed = False; // that's default
            // newCurator 在更换服务提供商的时候才用的上
            p.newCurator = _newCurator;
            if (_newCurator)
                p.splitData.length++;
            p.creator = msg.sender;
            // 提议的押金数
            p.proposalDeposit = msg.value;
            // 更新总接收押金数
            sumOfProposalDeposits += msg.value;
            // 发起一个ProposalAdded的event
            ProposalAdded(
                _proposalID,
                _recipient,
                _amount,
                _newCurator,
                _description
            );
        }
    
    

    参与投票

    // DAO.sol
        function vote(
            uint _proposalID,
            bool _supportsProposal
        // 有token的才可以投票
        ) onlyTokenholders noEther returns (uint _voteID) {
    
            Proposal p = proposals[_proposalID];
            
            // 已经投过的不可以再投、已经投过的不可以再投、超过投票时间不可以再投
            if (p.votedYes[msg.sender]
                || p.votedNo[msg.sender]
                || now >= p.votingDeadline) {
    
                throw;
            }
            
            // 更新双方投票情况
            if (_supportsProposal) {
                p.yea += balances[msg.sender];
                p.votedYes[msg.sender] = true;
            } else {
                p.nay += balances[msg.sender];
                p.votedNo[msg.sender] = true;
            }
            
            // 为了防止一token多投,投票之后会被记录到blocked中,被锁定的账户不可以转移token,直到提议结束才可以解锁
            if (blocked[msg.sender] == 0) {
                blocked[msg.sender] = _proposalID;
            } else if (p.votingDeadline > proposals[blocked[msg.sender]].votingDeadline) {
                // this proposal's voting deadline is further into the future than
                // the proposal that blocks the sender so make it the blocker
                blocked[msg.sender] = _proposalID;
            }
    
            Voted(_proposalID, _supportsProposal, msg.sender);
        }
    

    2.7 解锁

    // DAO.sol
        function unblockMe() returns (bool) {
            return isBlocked(msg.sender);
        }
        function isBlocked(address _account) internal returns (bool) {
            if (blocked[_account] == 0)
                return false;
            Proposal p = proposals[blocked[_account]];
            // 提议的截止日期到达之后才可以解锁
            if (now > p.votingDeadline) {
                blocked[_account] = 0;
                return false;
            } else {
                return true;
            }
        }
    

    2.8 提议投票通过,执行提议

    // DAO.sol
        function executeProposal(
            uint _proposalID,
            bytes _transactionData
        ) noEther returns (bool _success) {
    
            Proposal p = proposals[_proposalID];
            
            // 普通提议和更换服务提供商的提议有不同的等待时间
            uint waitPeriod = p.newCurator
                ? splitExecutionPeriod
                : executeProposalPeriod;
            
            // 如果时间过了 就其状态设置为关闭
            if (p.open && now > p.votingDeadline + waitPeriod) {
                closeProposal(_proposalID);
                return;
            }
    
            // 检查是否具备执行条件
            if (now < p.votingDeadline  // 有没有达到时间?
                // 是不是还在投票阶段?
                || !p.open
                // HASH匹不匹配?
                || p.proposalHash != sha3(p.recipient, p.amount, _transactionData)) {
    
                throw;
            }
    
            // 如果提议的受益人不在白名单中,关闭提议,发起人的押金也退回去
            // in order to free the deposit and allow unblocking of voters
            if (!isRecipientAllowed(p.recipient)) {
                closeProposal(_proposalID);
                p.creator.send(p.proposalDeposit);
                return;
            }
    
            bool proposalCheck = true;
    
            if (p.amount > actualBalance())
                proposalCheck = false;
    
            uint quorum = p.yea + p.nay;
    
            // 检查是否达到最小法定人数
            if (_transactionData.length >= 4 && _transactionData[0] == 0x68
                && _transactionData[1] == 0x37 && _transactionData[2] == 0xff
                && _transactionData[3] == 0x1e
                && quorum < minQuorum(actualBalance() + rewardToken[address(this)])) {
    
                    proposalCheck = false;
            }
    
            if (quorum >= minQuorum(p.amount)) {
                // 押金退回
                if (!p.creator.send(p.proposalDeposit))
                    throw;
                // 更新最新最小法定人数达标时间
                lastTimeMinQuorumMet = now;
                if (quorum > totalSupply / 5)
                    minQuorumDivisor = 5;
            }
    
            // 再次确认 人数是否达标、是否通过检查、赞成票是否多于反对票
            if (quorum >= minQuorum(p.amount) && p.yea > p.nay && proposalCheck) {
            
                // 给提议的受益人转移Ether
                if (!p.recipient.call.value(p.amount)(_transactionData))
                    throw;
                // 将提议设置为已通过
                p.proposalPassed = true;
                _success = true;
    
    
                if (p.recipient != address(this) && p.recipient != address(rewardAccount)
                    && p.recipient != address(DAOrewardAccount)
                    && p.recipient != address(extraBalance)
                    && p.recipient != address(curator)) {
                    // 更新 rewardToken 中该Dao总投资金额数
                    rewardToken[address(this)] += p.amount;
                    // 更新所有 Dao 的总投资数
                    totalRewardToken += p.amount;
                }
            }
            // 关闭提议
            closeProposal(_proposalID);
    
            // 发出 ProposalTallied event
            ProposalTallied(_proposalID, _success, quorum);
        }
    

    其中,关闭提议部分的代码为

        function closeProposal(uint _proposalID) internal {
            Proposal p = proposals[_proposalID];
            if (p.open)
                // 更新总押金数
                sumOfProposalDeposits -= p.proposalDeposit;
            // 状态设置为关闭
            p.open = false;
        }
    

    2.9 分裂 DAO

    分裂dao之前需要先创建一个提议,将newCurator设置为真,并指定recipient为新的服务提供商。
    想分裂出去的用户需要对这个提议投赞成票,然后在辩论期过后调用splitDAO函数。

    注意本次分析的是1.0.1版本的代码,也就是产生递归调用BUG的那个版本,请勿在生产环境中使用相同的代码。

    // DAO.sol
        function splitDAO(
            uint _proposalID, //提议id
            address _newCurator // 新的服务提供商地址
        ) noEther onlyTokenholders returns (bool _success) {
    
            Proposal p = proposals[_proposalID];
    
            // 参数检查
            if (now < p.votingDeadline  // 是否到截止日期了?
                // 是否过期了?
                || now > p.votingDeadline + splitExecutionPeriod
                // 新服务提供商的地址是否匹配?
                || p.recipient != _newCurator
                // 两次的服务提供商地址都不为空吧?
                || !p.newCurator
                // 投过赞成票了吗?
                || !p.votedYes[msg.sender]
                // 是不是还投过其他提议的票?
                || (blocked[msg.sender] != _proposalID && blocked[msg.sender] != 0) )  {
    
                throw;
            }
    
            // 如果新的dao还没有创建,创建新dao
            if (address(p.splitData[0].newDAO) == 0) {
            
                // 创建新Dao,并将其地址保存到提议的splitData中
                p.splitData[0].newDAO = createNewDAO(_newCurator);
                // 确认新Dao创建成功了,地址不为空
                if (address(p.splitData[0].newDAO) == 0)
                    throw;
                // 账户上的资金 比 总押金数少? 这种情况应该永远不会发生,但还是保险一点
                if (this.balance < sumOfProposalDeposits)
                    throw;
                // 更新splitData中的数据
                p.splitData[0].splitBalance = actualBalance();
                p.splitData[0].rewardToken = rewardToken[address(this)];
                p.splitData[0].totalSupply = totalSupply;
                p.proposalPassed = true;
            }
    
            // 计算该转移的以太数
            uint fundsToBeMoved =
                (balances[msg.sender] * p.splitData[0].splitBalance) /
                p.splitData[0].totalSupply;
            
            
            // 将原 Dao 中的以太转移到新 Dao 中, 不包括 extraceBalance 中的
            if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
                throw;
    
    
            // 在分裂前投资所得权益 相应地 由原Dao转移到新Dao
            uint rewardTokenToBeMoved =
                (balances[msg.sender] * p.splitData[0].rewardToken) /
                p.splitData[0].totalSupply;
    
            uint paidOutToBeMoved = DAOpaidOut[address(this)] * rewardTokenToBeMoved /
                rewardToken[address(this)];
            
            // 更新 rewardToken 中新Dao在分裂前的投资权益
            rewardToken[address(p.splitData[0].newDAO)] += rewardTokenToBeMoved;
            if (rewardToken[address(this)] < rewardTokenToBeMoved)
                throw;
            
            // 更新原Dao的投资权益
            rewardToken[address(this)] -= rewardTokenToBeMoved;
    
            DAOpaidOut[address(p.splitData[0].newDAO)] += paidOutToBeMoved;
            if (DAOpaidOut[address(this)] < paidOutToBeMoved)
                throw;
            DAOpaidOut[address(this)] -= paidOutToBeMoved;
    
            // Burn DAO Tokens ?
            Transfer(msg.sender, 0, balances[msg.sender]);
            
            // 该用户在分裂前应的收益仍会得到,这一句也是造成bug的关键代码
            withdrawRewardFor(msg.sender); // be nice, and get his rewards
            
            // 原Dao的总token发行数减少
            totalSupply -= balances[msg.sender];
            
            // 该用户在原 Dao 中的Token清零
            // Token清零应该在转账之前就执行,不应该这样做。
            balances[msg.sender] = 0;
            
            paidOut[msg.sender] = 0;
            return true;
        }
    

    withdrawRewardFor 的代码入下:

    // DAO.sol
        function withdrawRewardFor(address _account) noEther internal returns (bool _success) {
            if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])
                throw;
    
            uint reward =
                (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account];
    
            reward = rewardAccount.balance < reward ? rewardAccount.balance : reward;
            // 从rewardAccount中转移以太到用户账户
            if (!rewardAccount.payOut(_account, reward))
                throw;
            paidOut[_account] += reward;
            return true;
        }
    

    withdrawRewrdFor 调用了rewradAccunt的payOut函数来发送以太

    // ManagedAccount.sol
        function payOut(address _recipient, uint _amount) returns (bool) {
            if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
                throw;
                
            // 如果_recipient是一个合约账户,且定义了默认函数function () {}, 将会触发此函数
            // 而且这一句没有限制gas数,进一步给了漏洞可乘之机
            if (_recipient.call.value(_amount)()) {
            
                PayOut(_recipient, _amount);
                return true;
            } else {
                return false;
            }
        }
    

    如果我们创建一个钱包合约,并设置它的默认函数功能为调用 Dao 合约的 splitDAO 函数若干次,
    接着我们为这个钱包合约发起一个分裂 Dao 的提议,投票表决期过后,执行 splitDAO 。这时便会触发递归调用漏洞。
    此时的函数栈看起来就是这个样子,主 Dao 中的资金便会被黑客偷走。

        splitDao
          withdrawRewardFor
             payOut
                recipient.call.value()()
                   splitDao
                     withdrawRewardFor
                        payOut
                           recipient.call.value()()
                           ...递归下去
    

    2.10 获得收益(都被坏人偷走了,还有什么收益呀~)

    // DAO.sol
        function getMyReward() noEther returns (bool _success) {
            //  调用的还是withdrawRewardFor函数
            return withdrawRewardFor(msg.sender);
        }
    

    3. 参考资料


Log in to reply
 

Looks like your connection to QTUM was lost, please wait while we try to reconnect.