# Compound学习——DAO治理

By [rbtree](https://paragraph.com/@rbtree) · 2022-09-28

---

Compound DAO治理投票的流程如下：

![](https://storage.googleapis.com/papyrus_images/f0f729893b8fba32d6470a6c28d8080e98c7ee043b58e29d4068b89f4bb842d3.png)

Compound有2个版本的治理合约，分别是Alpha和Bravo。治理合约和时间锁timelock配合使用，以提升治理的安全性。治理代币是Comp，用户持有Comp代币即拥有投票权，也可以将投票权委托给别人。

1 Comp代币
--------

Comp是Compound的治理代币，在DAO治理的提案、投票过程过都发挥着重要的作用。

Comp首先是一个ERC20代币，因此它像其他ERC20代币那样会有transfer、approve等功能，不过这里主要探讨和治理投票相关的部分。

### 1.1 投票权委托

首先看一下\_delegate函数，这个函数只是简单记录了一下委托关系，主要的投票权转移在\_moveDelegates。

    function _delegate(address delegator, address delegatee) internal {
        address currentDelegate = delegates[delegator];
        uint96 delegatorBalance = balances[delegator];
        delegates[delegator] = delegatee;
    
        emit DelegateChanged(delegator, currentDelegate, delegatee);
    
        _moveDelegates(currentDelegate, delegatee, delegatorBalance);
    }
    

在解释\_moveDelegates的代码之前，我们需要先了解一个结构——checkpoints。为什么这里要设计得这么复杂呢？因为在启动投票的时候，我们一般会指定一个快照时间，而不是在投票人投票的瞬间去查询它的投票权重，以防止有攻击者临时购买大量代币发起攻击。为了实现这个功能，我们不仅需要记录当下某个用户的投票权重，还需要记录用户的历史投票权重。

numCheckpoints和checkpoints就是用来记录历史投票权重的数据结构，每次用户的投票权重发生变化的时候（修改委托人或代币转移），都需要更新这个结构。

我们以用户user1来举例，最初的时候，ta的投票权重记录在checkpoints\[user1\]\[0\]中，此时numCheckpoints\[user1\]=1; 当user1的投票权重发生第一次变更时，新的投票权重会记录在checkpoints\[user1\]\[1\]，同时numCheckpoints\[user1\]会被更新为2.

因为我们在快照的时候一般会指定到具体区块，所以checkpoints中存储的不仅仅是当时的投票权重，还有当时的blocknumber。

下面的\_writeCheckpoint就是投票权重发生变化时会调用的函数。里面有一个if判断，如果if成立说明是同一个区块发生了多次投票权变更，那么直接以后一次的为准即可；如果if不成立则需要在map里增添一条记录，并把numCheckpoints增加1.

    /// @notice A checkpoint for marking number of votes from a given block
    struct Checkpoint {
        uint32 fromBlock;
        uint96 votes;
    }
    /// @notice A record of votes checkpoints for each account, by index
    mapping (address => mapping (uint32 => Checkpoint)) public checkpoints;
    /// @notice The number of checkpoints for each account
    .mapping (address => uint32) public numCheckpoints;
    
    function _writeCheckpoint(address delegatee, uint32 nCheckpoints, uint96 oldVotes, uint96 newVotes) internal {
          uint32 blockNumber = safe32(block.number, "Comp::_writeCheckpoint: block number exceeds 32 bits");
        
          if (nCheckpoints > 0 && checkpoints[delegatee][nCheckpoints - 1].fromBlock == blockNumber) {
              checkpoints[delegatee][nCheckpoints - 1].votes = newVotes;
          } else {
              checkpoints[delegatee][nCheckpoints] = Checkpoint(blockNumber, newVotes);
              numCheckpoints[delegatee] = nCheckpoints + 1;
          }
        
          emit DelegateVotesChanged(delegatee, oldVotes, newVotes);
    }
    

我们回到\_moveDelegates，在委托的时候，委托人的投票权重会减小，被委托人的投票权重会增大，因此需要同时处理两方。在处理的时候，要先从checkpoints查询上次更新后的投票权重RepOld，然后加/减本次的amount，再调用\_writeCheckpoint写入map。

这个函数不仅在委托变更的时候都会被调用，代币转移(transfer、transferfrom)也会调用。

    function _moveDelegates(address srcRep, address dstRep, uint96 amount) internal {
        if (srcRep != dstRep && amount > 0) {
            if (srcRep != address(0)) {
                uint32 srcRepNum = numCheckpoints[srcRep];
                uint96 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0;
                uint96 srcRepNew = sub96(srcRepOld, amount, "Comp::_moveVotes: vote amount underflows");
                _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew);
            }
    
            if (dstRep != address(0)) {
                uint32 dstRepNum = numCheckpoints[dstRep];
                uint96 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0;
                uint96 dstRepNew = add96(dstRepOld, amount, "Comp::_moveVotes: vote amount overflows");
                _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew);
            }
        }
    }
    

### 1.2 查询投票权重

我们需要查询某个用户在某个快照区块的投票权重。刚才说到checkpoints按照时间顺序记录了历史投票记录变更，如果我们把历史记录遍历一边，就可以确认它在某个区块的权重了。不过因为记录是有序的，所以我们不必线性查找，而是可以二分查找以提升效率。

    function getPriorVotes(address account, uint blockNumber) public view returns (uint96) {
        require(blockNumber < block.number, "Comp::getPriorVotes: not yet determined");
    
        uint32 nCheckpoints = numCheckpoints[account];
        if (nCheckpoints == 0) {
            return 0;
        }
    
        // First check most recent balance
        if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) {
            return checkpoints[account][nCheckpoints - 1].votes;
        }
    
        // Next check implicit zero balance
        if (checkpoints[account][0].fromBlock > blockNumber) {
            return 0;
        }
    
        uint32 lower = 0;
        uint32 upper = nCheckpoints - 1;
        while (upper > lower) {
            uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow
            Checkpoint memory cp = checkpoints[account][center];
            if (cp.fromBlock == blockNumber) {
                return cp.votes;
            } else if (cp.fromBlock < blockNumber) {
                lower = center;
            } else {
                upper = center - 1;
            }
        }
        return checkpoints[account][lower].votes;
    }
    

2 Timelock时间锁
-------------

当某个提案通过投票后，如果没有时间锁，意味着它可以立刻被执行，但这是有一定风险的。因为可能大部分治理代币持有者并不会积极参与每个提案，所以攻击者操纵代币执行恶意操纵的难度比大多数人想象的要低。增加时间锁机制正是为了尽可能避免这样的风险，这使得提案通过之后，大家还有一个时间窗口可以用来对恶意提案紧急叫停。

在之前的介绍Compound的借贷合约时，我们提到过很多次管理员账户。实际上管理员账户就是这个时间锁，一切修改借贷合约的行为最终都要通过这个时间锁来执行。

[https://mirror.xyz/rbtree.eth/Phu4uodFP-xT1iLNLi5Jb4WWmMqTLZQrsNNR5ARZKz8](https://mirror.xyz/rbtree.eth/Phu4uodFP-xT1iLNLi5Jb4WWmMqTLZQrsNNR5ARZKz8)

### 2.1 Timelock的几个重要参数

admin：后面即将介绍的Compound治理（Governor）合约。

delay：投票通过之后，需要等待多久才可以执行。目前这个值的设定时2天。（这个数字可以在2-30天之间调整）。

GRACE\_PERIOD：过期时间。等待期完毕后，如果再超过14day，那么提案将不再能执行。

admin和dalay本身也是可以通过DAO治理投票修改的。

    uint public constant GRACE_PERIOD = 14 days;
    uint public constant MINIMUM_DELAY = 2 days;
    uint public constant MAXIMUM_DELAY = 30 days;
    
    address public admin;
    address public pendingAdmin;
    uint public delay;
    mapping (bytes32 => bool) public queuedTransactions;
    

### 2.2 queue

这个函数在投票通过之后可以被调用。它并没有很复杂的操作，只是简单地在queuedTransactions这个map里做了记录，包括target, value, signature, data, eta，前几个变量时执行提案需要的参数，最后一个时可以执行的时间（queue时间+2天）。

    function queueTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public returns (bytes32) {
        require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin.");
        require(eta >= getBlockTimestamp().add(delay), "Timelock::queueTransaction: Estimated execution block must satisfy delay.");
    
        bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
        queuedTransactions[txHash] = true;
    
        emit QueueTransaction(txHash, target, value, signature, data, eta);
        return txHash;
    }
    

### 2.3 cancel

取消queue。这部分判断逻辑在Governor合约中，后面会讲到，主要是为了防止有攻击者故意发起恶意提案。

    function cancelTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public {
        require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin.");
    
        bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
        queuedTransactions[txHash] = false;
    
        emit CancelTransaction(txHash, target, value, signature, data, eta);
    }
    

### 2.4 execute

提案执行。我们可以看到会判断当前时间是否在eta～eta+GRACE\_PERIOD之间，即提案通过后等待期完成，并且没有过期。真正的执行语句是target.call，这里使用low level call的方式完成了提案的执行。

因为timelock合约是最终执行提案的合约，所以compound协议的资产也都应该放在timelock合约中，以便提案可以方便地对资产做出处置。

    function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public payable returns (bytes memory) {
        require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin.");
    
        bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
        require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued.");
        require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock.");
        require(getBlockTimestamp() <= eta.add(GRACE_PERIOD), "Timelock::executeTransaction: Transaction is stale.");
    
        queuedTransactions[txHash] = false;
    
        bytes memory callData;
    
        if (bytes(signature).length == 0) {
            callData = data;
        } else {
            callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
        }
    
        // solium-disable-next-line security/no-call-value
        (bool success, bytes memory returnData) = target.call{value: value}(callData);
        require(success, "Timelock::executeTransaction: Transaction execution reverted.");
    
        emit ExecuteTransaction(txHash, target, value, signature, data, eta);
    
        return returnData;
    }
    

3 GovernorAlpha
---------------

这是早期版本的治理合约。

### 3.1 发起提案

观察函数的参数，我们可以发现propose的前4个参数都是数组，这是为了支持一个提案包含多笔交易，数组中的每个元素都对应一笔交易。

target: 目标交互合约

value: 交易的msg.value.

signature: 希望交互的函数签名

calldata：函数参数

description：提案描述

这个函数的逻辑非常简单，首先是判断提案人是否具备资格。阈值是proposalThreshold()，目前的设置是25000 Comp。

接下来判断了提案的执行函数是否合法，以及提案人是否同时提了多个提案。

最后是把提案的内容记录在了proposals之中，这里不仅记录了执行参数，还有和投票相关的参数，如投票状态forVotes、againstVotes，提案状态canceled、executed，投票的起止时间startBlock、endBlock。目前，votingDelay()大约是2天，votingPeriod()大约是3天，即发起提案后2天后可以开始投票，投票时间为3天。

    function propose(address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description) public returns (uint) {
        require(comp.getPriorVotes(msg.sender, sub256(block.number, 1)) > proposalThreshold(), "GovernorAlpha::propose: proposer votes below proposal threshold");
        require(targets.length == values.length && targets.length == signatures.length && targets.length == calldatas.length, "GovernorAlpha::propose: proposal function information arity mismatch");
        require(targets.length != 0, "GovernorAlpha::propose: must provide actions");
        require(targets.length <= proposalMaxOperations(), "GovernorAlpha::propose: too many actions");
    
        uint latestProposalId = latestProposalIds[msg.sender];
        if (latestProposalId != 0) {
          ProposalState proposersLatestProposalState = state(latestProposalId);
          require(proposersLatestProposalState != ProposalState.Active, "GovernorAlpha::propose: one live proposal per proposer, found an already active proposal");
          require(proposersLatestProposalState != ProposalState.Pending, "GovernorAlpha::propose: one live proposal per proposer, found an already pending proposal");
        }
    
        uint startBlock = add256(block.number, votingDelay());
        uint endBlock = add256(startBlock, votingPeriod());
    
        proposalCount++;
        uint proposalId = proposalCount;
        Proposal storage newProposal = proposals[proposalId];
        // This should never happen but add a check in case.
        require(newProposal.id == 0, "GovernorAlpha::propose: ProposalID collsion");
        newProposal.id = proposalId;
        newProposal.proposer = msg.sender;
        newProposal.eta = 0;
        newProposal.targets = targets;
        newProposal.values = values;
        newProposal.signatures = signatures;
        newProposal.calldatas = calldatas;
        newProposal.startBlock = startBlock;
        newProposal.endBlock = endBlock;
        newProposal.forVotes = 0;
        newProposal.againstVotes = 0;
        newProposal.canceled = false;
        newProposal.executed = false;
    
        latestProposalIds[newProposal.proposer] = newProposal.id;
    
        emit ProposalCreated(newProposal.id, msg.sender, targets, values, signatures, calldatas, startBlock, endBlock, description);
        return newProposal.id;
    }
    

### 3.2 投票

这里提供了两种方式，一种是自己发起交易投票，一种是签名后让别人帮自己发交易。后一种可以帮助小户节省gas。

    function castVote(uint proposalId, bool support) public;
    function castVoteBySig(uint proposalId, bool support, uint8 v, bytes32 r, bytes32 s) public;
    

投票函数的主要逻辑也很简单，在进行基本校验（判断提案是否处于投票期、投票人是否重复投票）之后，首先计算投票人的权重，然后更新proposal.forVotes或proposal.againstVotes的值。

    function _castVote(address voter, uint proposalId, bool support) internal {
        require(state(proposalId) == ProposalState.Active, "GovernorAlpha::_castVote: voting is closed");
        Proposal storage proposal = proposals[proposalId];
        Receipt storage receipt = proposal.receipts[voter];
        require(receipt.hasVoted == false, "GovernorAlpha::_castVote: voter already voted");
        uint96 votes = comp.getPriorVotes(voter, proposal.startBlock);
    
        if (support) {
            proposal.forVotes = add256(proposal.forVotes, votes);
        } else {
            proposal.againstVotes = add256(proposal.againstVotes, votes);
        }
    
        receipt.hasVoted = true;
        receipt.support = support;
        receipt.votes = votes;
    
        emit VoteCast(voter, proposalId, support, votes);
    }
    

### 3.3 排队

该函数在投票通过后可以执行。

在判断投票通过之后，会调用\_queueOrRevert，把提案中的每一个交易都通过 timelock.queueTransaction丢给时间锁函数，时间锁部分上一小节已经看过了。

    function queue(uint proposalId) public {
        require(state(proposalId) == ProposalState.Succeeded, "GovernorAlpha::queue: proposal can only be queued if it is succeeded");
        Proposal storage proposal = proposals[proposalId];
        uint eta = add256(block.timestamp, timelock.delay());
        for (uint i = 0; i < proposal.targets.length; i++) {
            _queueOrRevert(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], eta);
        }
        proposal.eta = eta;
        emit ProposalQueued(proposalId, eta);
    }
    
    function _queueOrRevert(address target, uint value, string memory signature, bytes memory data, uint eta) internal {
        require(!timelock.queuedTransactions(keccak256(abi.encode(target, value, signature, data, eta))), "GovernorAlpha::_queueOrRevert: proposal action already queued at eta");
        timelock.queueTransaction(target, value, signature, data, eta);
    }
    

那么如何判断投票投过呢？我们看到判断提案状态的函数如下。

判断提案Defeated还是Succeeded的关键逻辑是：

proposal.forVotes <= proposal.againstVotes || proposal.forVotes < quorumVotes()

这意味着如果提案成功，需要有2个条件，赞成数>反对数并且赞成票数超过法定数。目前的法定数是400000 Comp。Comp的总量是10000000，目前流通量为7267152。

    function state(uint proposalId) public view returns (ProposalState) {
        require(proposalCount >= proposalId && proposalId > 0, "GovernorAlpha::state: invalid proposal id");
        Proposal storage proposal = proposals[proposalId];
        if (proposal.canceled) {
            return ProposalState.Canceled;
        } else if (block.number <= proposal.startBlock) {
            return ProposalState.Pending;
        } else if (block.number <= proposal.endBlock) {
            return ProposalState.Active;
        } else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes < quorumVotes()) {
            return ProposalState.Defeated;
        } else if (proposal.eta == 0) {
            return ProposalState.Succeeded;
        } else if (proposal.executed) {
            return ProposalState.Executed;
        } else if (block.timestamp >= add256(proposal.eta, timelock.GRACE_PERIOD())) {
            return ProposalState.Expired;
        } else {
            return ProposalState.Queued;
        }
    }
    

### 3.4 执行

该函数在提案进入排队状态后才可以执行。会通过timelock.executeTransaction调用时间锁，时间锁的execute我们上一小节也已经说明，会判断时间是否超过2天，否则不会执行。

这个函数对发起者没有任何限制，即一旦Compound的提案通过并通过等待期，任何人都可以执行提案。

    function execute(uint proposalId) public payable {
        require(state(proposalId) == ProposalState.Queued, "GovernorAlpha::execute: proposal can only be executed if it is queued");
        Proposal storage proposal = proposals[proposalId];
        proposal.executed = true;
        for (uint i = 0; i < proposal.targets.length; i++) {
            timelock.executeTransaction{value: proposal.values[i]}(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta);
        }
        emit ProposalExecuted(proposalId);
    }
    

### 3.5 取消

提案发起之后、执行之前都可以取消。取消的条件需要满足以下条件中的任意一个：

comp.getPriorVotes(proposal.proposer, sub256(block.number, 1)) < proposalThreshold()，即提案人当前的投票权小于提案门槛。这应该是为了防止有攻击者短期购买大量Comp代币，发起恶意投票，然后立刻卖出代币的行为。

msg.sender == guardian，这个guardian时Compound项目的多签钱包。这可以看成是紧急情况下的紧急停止按钮。

如果上面的判断通过，会进入timelock的cancel函数。

    function cancel(uint proposalId) public {
        ProposalState state = state(proposalId);
        require(state != ProposalState.Executed, "GovernorAlpha::cancel: cannot cancel executed proposal");
    
        Proposal storage proposal = proposals[proposalId];
        require(msg.sender == guardian || comp.getPriorVotes(proposal.proposer, sub256(block.number, 1)) < proposalThreshold(), "GovernorAlpha::cancel: proposer above threshold");
    
        proposal.canceled = true;
        for (uint i = 0; i < proposal.targets.length; i++) {
            timelock.cancelTransaction(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta);
        }
    
        emit ProposalCanceled(proposalId);
    }
    

4 GovernorBravo
---------------

GovernorBravo是升级后的治理合约，和GovernorAlpha的功能大体类似，因此只介绍两者差异的部分。

### 4.1 可升级

和GovernorAlpha不可升级不同，GovernorBravo是一个可升级代理。

    contract GovernorBravoDelegator is GovernorBravoDelegatorStorage, GovernorBravoEvents {
        ......
        function _setImplementation(address implementation_) public {
            require(msg.sender == admin, "GovernorBravoDelegator::_setImplementation: admin only");
            require(implementation_ != address(0), "GovernorBravoDelegator::_setImplementation: invalid implementation address");
    
            address oldImplementation = implementation;
            implementation = implementation_;
    
            emit NewImplementation(oldImplementation, implementation);
        }
    
        fallback () external payable {
            // delegate all other functions to current implementation
            (bool success, ) = implementation.delegatecall(msg.data);
    
            assembly {
                  let free_mem_ptr := mload(0x40)
                  returndatacopy(free_mem_ptr, 0, returndatasize())
    
                  switch success
                  case 0 { revert(free_mem_ptr, returndatasize()) }
                  default { return(free_mem_ptr, returndatasize()) }
            }
        }
    }
    

这项修改之后，Compound升级治理合约更方便了。之前GovernorAlpha本身不可升级，如果像升级治理模块，需要重新部署，此时合约地址会改变，因此需要改变timelock的管理员。

[https://ethtx.info/mainnet/0x88c07d09327092bf912d7ce81846f75293d6d5d0d7e52d2f5f9eaaf23223b0e4/](https://ethtx.info/mainnet/0x88c07d09327092bf912d7ce81846f75293d6d5d0d7e52d2f5f9eaaf23223b0e4/)

![](https://storage.googleapis.com/papyrus_images/06b32e995101470412a20c1265d89e8868d3bb77b145472f3307b43709db90c8.png)

现在，升级的时候，timelock的管理员不再需要变更。

[https://ethtx.info/mainnet/0x8949a24cbea10567f28ddf6d77449d7d9d36240dc2ed0aa03dfb75fdf81bae72/](https://ethtx.info/mainnet/0x8949a24cbea10567f28ddf6d77449d7d9d36240dc2ed0aa03dfb75fdf81bae72/)

![](https://storage.googleapis.com/papyrus_images/b8948cb1fc5d3b4f317a5dcaa0001438ce403bbeb21ff57b0568701a14dcdc62.png)

不仅如此，如果你是一个Compound治理活跃用户，如果是alpha的升级，为了应对governor地址的修改，你投票的相关参数也需要修改。特别是如果用户使用托管钱包，可能还需要托管方支持相关修改，这也给用户带来了一些麻烦。

另外，在alpha版本，因为升级之后地址会变更，之前的提案计数会丢失，需要外多一些设置才能和之前的编号接上。而bravo版本，升级的时候合约地址不变、storage保持不变，因此各种计数会天然和之前接上。

### 4.2 提案，增加白名单

不仅是持有40000Comp投票权的人可以提案，白名单用户也可以提案。

    function propose(address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description) public returns (uint) {
            require(initialProposalId != 0, "GovernorBravo::propose: Governor Bravo not active");
            // Allow addresses above proposal threshold and whitelisted addresses to propose
            require(comp.getPriorVotes(msg.sender, sub256(block.number, 1)) > proposalThreshold || isWhitelisted(msg.sender), "GovernorBravo::propose: proposer votes below proposal threshold");
            ......
    }
    

### 4.3 投票时可增添reason

增加了投票时的reason字段。

    function castVoteWithReason(uint proposalId, uint8 support, string calldata reason) external {
        emit VoteCast(msg.sender, proposalId, support, castVoteInternal(msg.sender, proposalId, support), reason);
    }
    

### 4.4 投票增加abstain选项

alpla版本只有for和against两个选项，bravo增加abstain。判定提案通过的逻辑保持不变，依然是for>against且for超过法定数。

    if (support == 0) {
        proposal.againstVotes = add256(proposal.againstVotes, votes);
    } else if (support == 1) {
        proposal.forVotes = add256(proposal.forVotes, votes);
    } else if (support == 2) {
        proposal.abstainVotes = add256(proposal.abstainVotes, votes);
    }

---

*Originally published on [rbtree](https://paragraph.com/@rbtree/compound-dao)*
