最新消息:

The DAO漏洞分析

以太坊 justnode 548浏览

背景知识

The DAO:DAO 是Decentralized Autonomous Organization(分布式自治组织)的简称,the DAO是一个基于以太坊区块链平台的迄今为止世界上最大的众筹项目。其目的是让持有The DAO代币的参与者通过投票的方式共同决定被投资项目, 整个社区完全自制, 并且通过代码编写的智能合来实现。 于2016年5月28日完成众筹,共募集1150万以太币,在当时的价值达到1.49亿美元。

child DAO : 子DAO。 是指DAO中的Token持有者通过调用DAO智能合约中的split函数,创建的一个小型DAO智能合约。在创建过程中,持有者在原有DAO智能合约中的Token被销毁,存储在原DAO智能合约中对应的以太币被转移到新的DAO智能合约中。child DAO的设计是为了保护在DAO投票中处于弱势地位的Token持有者,通过创建child DAO给予他们一个小规模的可提议、投票以及分红的新的分布式自治组织的环境。

提币限制:The DAO在设计的时候规定必须等待27天才可以提取以太币。

创建chilid DAO: 首先提出一个分裂的提议,等待7天使其成熟。对分裂提议投赞成票的Token持有者有权利调用splitDAO创建 child DAO。

 

源码分析:

The DAO完整源码在这里

首先我们我分析splitDAO函数:

    function splitDAO(
        uint _proposalID,
        address _newCurator
    ) noEther onlyTokenholders returns (bool _success) {

       
        // If the new DAO doesn't exist yet, create the new DAO and store the
        // current split data
        if (address(p.splitData[0].newDAO) == 0) {
            p.splitData[0].newDAO = createNewDAO(_newCurator);
            // Call depth limit reached, etc.
            if (address(p.splitData[0].newDAO) == 0)
                throw;
            // should never happen
            if (this.balance < sumOfProposalDeposits)
                throw;
            p.splitData[0].splitBalance = actualBalance();
            p.splitData[0].rewardToken = rewardToken[address(this)];
            p.splitData[0].totalSupply = totalSupply;
            p.proposalPassed = true;
        }

        // Move ether and assign new Tokens
        uint fundsToBeMoved =
            (balances[msg.sender] * p.splitData[0].splitBalance) /
            p.splitData[0].totalSupply;
        if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
            throw;


        // Assign reward rights to new DAO
        uint rewardTokenToBeMoved =
            (balances[msg.sender] * p.splitData[0].rewardToken) /
            p.splitData[0].totalSupply;

        uint paidOutToBeMoved = DAOpaidOut[address(this)] * rewardTokenToBeMoved /
            rewardToken[address(this)];

        rewardToken[address(p.splitData[0].newDAO)] += rewardTokenToBeMoved;
        if (rewardToken[address(this)] < rewardTokenToBeMoved)
            throw;
        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]);
        withdrawRewardFor(msg.sender); // 存在风险的代码
        totalSupply -= balances[msg.sender];
        balances[msg.sender] = 0;
        paidOut[msg.sender] = 0;
        return true;
    }

以上代码中,先判断child DAO是否存在,如果不存在则创建。之后把原DAO账户中的以太币和代币进行转移。最后把原DAO账户中的以太币进行清零。在最后这一步中,先调用withdrawRewardFor函数,然后再做清零,问题就在这里,存在Race To Empty攻击的风险。

接下来我们来看看withdrawRewardFor函数的具体实现代码:

    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];
        if (!rewardAccount.payOut(_account, reward))
            throw;
        paidOut[_account] += reward;
        return true;
    }

在withdrawRewardFor函数中调用了rewardAccount.payOut函数,接下来继续跟进这个函数的实现:

function payOut(address _recipient, uint _amount) returns (bool) {
        if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
            throw;
        if (_recipient.call.value(_amount)()) {
            PayOut(_recipient, _amount);
            return true;
        } else {
            return false;
        }
    }

从payout的实现不难看出,使用call函数进行转账,并且没有gas限制。

 

发起攻击:

1.创建一个包含默认函数的合约,该默认函数用来递归调用withdrawRewardFor函数。

2.发起一个分裂的建议,等待7天成熟。

3.用创建的这个合约,调用splitDAO函数。该函数的调用栈如下所示:

    splitDao
      withdrawRewardFor  (第一次调用)
         payOut           
            recipient.call.value()()
               splitDao
                 withdrawRewardFor (第二次调用)
                    payOut
                       recipient.call.value()()

 

攻击思路:

我们创建一个合约,那么当合约接受转账时会触发回调函数。回调函数里再次调用withdrawBalance,这样就实现了在账户归零前多次进行撤资!

在此次The DAO事件中,黑客利用The DAO智能合约中split函数的漏洞,在The DAO Token被销毁前,多次转移以太币到Child DAO智能合约中,从而大规模盗取原The DAO智能合约中的以太币。

与本文相关的文章

  • 暂无相关文章!