{"content":{"title":"DAO 治理合约及提案执行实践","body":"上一篇我们详解了 [Compound治理源码](https://learnblockchain.cn/article/7643)， 这篇我们来根据上面的治理逻辑完成简单的，完整的提案执行过程。\r\n\r\n## 实践\r\n\r\n我们先创建一个我们自己的Dao，取名为JCDao（杰出Dao），用合约JCDao.sol来记录Dao中成员的身份\r\n\r\n```solidity\r\n mapping(address => bool) public Admin;\r\n mapping(address => bool) public whitelisted;\r\n \r\n   function isDaoMember(address _address) public view returns(bool){\r\n        return whitelisted[_address];\r\n    }\r\n\r\n    function setAdmin(address _addr) external onlyOwner{\r\n        Admin[_addr] = true;\r\n    }\r\n\r\n    function isDaoAdmin(address _address) public view returns(bool){\r\n        return Admin[_address];\r\n    }\r\n\r\n    function addMember(address _member) external{\r\n        require(Admin[msg.sender]|| msg.sender == owner ,\"You must be an admin&Owner to add a member\");\r\n        whitelisted[_member] = true;\r\n    }\r\n```\r\n\r\n然后我们规定贡献ETH就可获得等量的token：\r\n\r\n```solidity\r\n    function contribute() external payable{\r\n        require(msg.value > 0,\"You must contribute more than 0 ether\");\r\n        require(isDaoMember(msg.sender),\"You must be a DAO member to contribute\");\r\n        contributions[msg.sender] += msg.value;\r\n        JCToken(jcToken).mintDao(msg.sender,msg.value);\r\n        \r\n        emit Contribute(msg.sender,msg.value);\r\n    }\r\n```\r\n\r\n设置timelock和一个withdraw方法，规定只有timelock能调用withdraw方法。\r\n\r\n```solidity\r\n  function setTimeLock(address _timeLock) external onlyOwner{\r\n        timeLock = _timeLock;\r\n    }\r\n\r\n    function withdraw(address to,uint256 amount) external{\r\n        require(msg.sender == timeLock,\"You must be the timeLock to call this function\");\r\n        payable(to).transfer(amount);\r\n        emit Withdraw(to,amount);\r\n    }\r\n```\r\n\r\n这就意味着只有通过治理，发起提案并通过才能调用withdraw方法,我们的目标就是完成这一流程。\r\n\r\n然后我们来写我们的治理合约\r\n\r\n同样的，一个代理合约，一个实现合约\r\n\r\n我们在代理合约中设置admin modifier，只允许admin来设置实现合约：\r\n\r\n```solidity\r\n    modifier onlyAdmin() {\r\n        require(\r\n            JCDao(dao).isDaoAdmin(msg.sender) || msg.sender == owner,\r\n            \"Governor:_setImplementation: admin&owner only\"\r\n        );\r\n        _;\r\n    }\r\n\r\n    function setImplementation(address _implementation) public onlyAdmin{\r\n        require(\r\n            _implementation != address(0),\r\n            \"Governor:_setImplementation: invalid implementation address\"\r\n        );\r\n\r\n        address oldImplementation = implementation;\r\n        implementation = _implementation;\r\n\r\n        emit NewImplementation(oldImplementation, implementation);\r\n    }\r\n```\r\n\r\n然后开始写我们的实现合约：\r\n\r\n我们将所需的event和一部分属性放在tool文件中：\r\n\r\n这里的属性前面已经讲过，我在compound源码上进行了简化，以便理解\r\n\r\n```solidity\r\ncontract GovernorEvents {\r\n    /// @notice Emitted when implementation is changed\r\n    event NewImplementation(\r\n        address oldImplementation,\r\n        address newImplementation\r\n    );\r\n\r\n    /// @notice An event emitted when a proposal has been canceled\r\n    event ProposalCanceled(uint id);\r\n\r\n    /// @notice An event emitted when a proposal has been queued in the Timelock\r\n    event ProposalQueued(uint id, uint eta);\r\n\r\n    /// @notice An event emitted when a proposal has been executed in the Timelock\r\n    event ProposalExecuted(uint id);\r\n\r\n    event ProposalCreated(\r\n        uint id,\r\n        address proposer,\r\n        address[] targets,\r\n        uint[] values,\r\n        string[] signatures,\r\n        bytes[] calldatas,\r\n        uint startBlock,\r\n        uint endBlock,\r\n        string description\r\n    );\r\n\r\n    event VoteCast(\r\n        address indexed voter,\r\n        uint proposalId,\r\n        uint8 support,\r\n        uint votes,\r\n        string reason\r\n    );\r\n}\r\n\r\ncontract GovernImpV1 {\r\n    address public admin;\r\n\r\n    address public pendingAdmin;\r\n\r\n    address public implementation;\r\n}\r\n\r\ncontract GovernImpV2 is GovernImpV1 {\r\n    /// @notice The delay before voting on a proposal may take place, once proposed, in blocks\r\n    uint public votingDelay;\r\n\r\n    /// @notice The duration of voting on a proposal, in blocks\r\n    uint public votingPeriod;\r\n\r\n    /// @notice The official record of all proposals ever proposed\r\n    mapping(uint => Proposal) public proposals;\r\n\r\n    /// @notice The latest proposal for each proposer\r\n    mapping(address => uint) public latestProposalIds;\r\n\r\n    /// @notice The total number of proposals\r\n    uint public proposalCount;\r\n\r\n    /// @notice The address of the Compound Protocol Timelock\r\n    TimelockInterface public timelock;\r\n\r\n    struct Proposal {\r\n        uint id;\r\n        address proposer;\r\n        uint eta; //提案可用于执行的时间戳，在投票成功后设置\r\n        address[] targets;\r\n        uint[] values;\r\n        string[] signatures;\r\n        bytes[] calldatas;\r\n        uint startBlock;\r\n        uint endBlock;\r\n        uint forVotes;\r\n        uint againstVotes;\r\n        uint abstainVotes;\r\n        bool canceled;\r\n        bool executed;\r\n        mapping(address => Receipt) receipts;\r\n    }\r\n\r\n    /// @notice Ballot receipt record for a voter\r\n    struct Receipt {\r\n        bool hasVoted;\r\n        uint8 support;\r\n        uint96 votes;\r\n    }\r\n\r\n    enum ProposalState {\r\n        Pending,\r\n        Active, //在投票中\r\n        Canceled,\r\n        Defeated,\r\n        Succeeded,\r\n        Queued,\r\n        Expired, //过期了\r\n        Executed\r\n    }\r\n}\r\n```\r\n\r\n继承完工具类后，增添必要属性：\r\n\r\n在这里我们规定，每个提案获得的票数超过300 ether就可以通过\r\n\r\n```solidity\r\naddress public jcToken;\r\naddress public dao;\r\n\r\nuint public quorumVotes = 300 ether; //每个提案通过所需的最少票数\r\n```\r\n\r\n初始化：\r\n\r\n```solidity\r\n function initialize(\r\n        address _dao,\r\n        address _timelock,\r\n        address _token,\r\n        uint _votingPeriod,\r\n        uint _votingDelay\r\n    ) public {\r\n        require(\r\n            address(timelock) == address(0),\r\n            \"GovernorBravo::initialize: can only initialize once\"\r\n        );\r\n        require(\r\n            _timelock != address(0),\r\n            \"GovernorBravo::initialize: invalid timelock address\"\r\n        );\r\n        require(\r\n            _token != address(0),\r\n            \"GovernorBravo::initialize: invalid comp address\"\r\n        );\r\n        timelock = TimelockInterface(_timelock);\r\n        jcToken = _token;\r\n        votingPeriod = _votingPeriod;\r\n        votingDelay = _votingDelay;\r\n        dao = _dao;\r\n    }\r\n```\r\n\r\n提案函数与源码大致相同，我们在这只实现一种提案方式：\r\n\r\n```solidity\r\n /**\r\n     * @dev 提议\r\n     * @param targets 目标合约地址\r\n     * @param values 转账金额\r\n     * @param calldatas 调用数据\r\n     * @param description 提议描述\r\n     * @return proposalId 提议ID\r\n     */\r\n    function propose(\r\n        address[] memory targets,\r\n        uint[] memory values,\r\n        string[] memory signatures,\r\n        bytes[] memory calldatas,\r\n        string memory description\r\n    ) external payable returns (uint proposalId) {\r\n        return\r\n            _proposeInternal(\r\n                msg.sender,\r\n                targets,\r\n                values,\r\n                signatures,\r\n                calldatas,\r\n                description\r\n            );\r\n    }\r\n\r\n    function _proposeInternal(\r\n        address proposer,\r\n        address[] memory targets,\r\n        uint[] memory values,\r\n        string[] memory signatures,\r\n        bytes[] memory calldatas,\r\n        string memory description\r\n    ) internal returns (uint) {\r\n        // 在提交本次提案之前先判断上一个提案是否被处理：\r\n        uint latestProposalId = latestProposalIds[proposer];\r\n        if (latestProposalId != 0) {\r\n            ProposalState proposersLatestProposalState = state(\r\n                latestProposalId\r\n            );\r\n            //\r\n            require(\r\n                proposersLatestProposalState != ProposalState.Active,\r\n                \"GovernorBravo::proposeInternal: one live proposal per proposer, found an already active proposal\"\r\n            );\r\n            require(\r\n                proposersLatestProposalState != ProposalState.Pending,\r\n                \"GovernorBravo::proposeInternal: one live proposal per proposer, found an already pending proposal\"\r\n            );\r\n        }\r\n\r\n        uint startBlock = block.number + votingDelay;\r\n        uint endBlock = startBlock + votingPeriod;\r\n        proposalCount++;\r\n\r\n        uint newProposalID = proposalCount;\r\n        Proposal storage newProposal = proposals[newProposalID];\r\n    \r\n        newProposal.id = newProposalID;\r\n        newProposal.proposer = proposer;\r\n        newProposal.targets = targets;\r\n        newProposal.values = values;\r\n        newProposal.signatures = signatures;\r\n        newProposal.calldatas = calldatas;\r\n        newProposal.startBlock = startBlock;\r\n        newProposal.endBlock = endBlock;\r\n        newProposal.forVotes = 0;\r\n        newProposal.againstVotes = 0;\r\n        newProposal.abstainVotes = 0;\r\n        newProposal.canceled = false;\r\n        newProposal.executed = false;\r\n\r\n        latestProposalIds[newProposal.proposer] = newProposal.id;\r\n\r\n        emit ProposalCreated(\r\n            newProposal.id,\r\n            proposer,\r\n            targets,\r\n            values,\r\n            signatures,\r\n            calldatas,\r\n            startBlock,\r\n            endBlock,\r\n            description\r\n        );\r\n\r\n        return newProposal.id;\r\n    }\r\n```\r\n\r\n投票函数：\r\n\r\n```solidity\r\n    /**\r\n     * @dev 投票\r\n     * @param proposalId 提议ID\r\n     * @param support 支持或反对或中立：0，1，2\r\n     */\r\n    function castVote(uint proposalId, uint8 support) external {\r\n        emit VoteCast(\r\n            msg.sender,\r\n            proposalId,\r\n            support,\r\n            _castVoteInternal(msg.sender, proposalId, support),\r\n            \"\"\r\n        );  \r\n    }\r\n    /**\r\n     * @dev 投票internal\r\n     * @param voter 投票人\r\n     * @param proposalId 提议ID\r\n     * @param support 反对或支持或中立：0，1，2\r\n     */\r\n    function _castVoteInternal(\r\n        address voter,\r\n        uint proposalId,\r\n        uint8 support\r\n    ) internal returns (uint256) {\r\n        require(\r\n            state(proposalId) == ProposalState.Active,\r\n            \"GovernorBravo::castVoteInternal: voting is closed\"\r\n        );\r\n        require(\r\n            support <= 2,\r\n            \"GovernorBravo::castVoteInternal: invalid vote type\"\r\n        );\r\n        Proposal storage proposal = proposals[proposalId];\r\n        Receipt storage receipt = proposal.receipts[voter];\r\n        uint256 votes = IJCToken(jcToken).getPriorVotes(\r\n            voter,\r\n            proposal.startBlock\r\n        );\r\n\r\n        if (support == 0) {\r\n            proposal.againstVotes = proposal.againstVotes + votes;\r\n        } else if (support == 1) {\r\n            proposal.forVotes = proposal.forVotes + votes;\r\n        } else if (support == 2) {\r\n            proposal.abstainVotes = proposal.abstainVotes + votes;\r\n        }\r\n\r\n        receipt.hasVoted = true;\r\n        receipt.support = support;\r\n        receipt.votes = uint96(votes);\r\n\r\n        return votes;\r\n    }\r\n```\r\n\r\n入队函数，在这里为了后面的测试方便，我们都以区块高度作为时间度量（源码是时间戳）\r\n\r\n```solidity\r\n//提议被投票通过标准后可进入执行队列\r\n    function queue(uint proposalId) external {\r\n        require(\r\n            state(proposalId) == ProposalState.Succeeded,\r\n            \"GovernorBravo::queue: proposal can only be queued if it is succeeded\"\r\n        );\r\n        Proposal storage proposal = proposals[proposalId];\r\n        uint eta = block.number + timelock.delay();\r\n        console.log(\"queue eta:\",eta);\r\n        for (uint i = 0; i < proposal.targets.length; i++) {\r\n            _queueOrRevertInternal(\r\n                proposal.targets[i],\r\n                proposal.values[i],\r\n                proposal.signatures[i],\r\n                proposal.calldatas[i],\r\n                eta\r\n            );\r\n        }\r\n        proposal.eta = eta; //成功进入执行队列后，设置执行时间戳\r\n        emit ProposalQueued(proposalId, eta);\r\n    }\r\n\r\n    function _queueOrRevertInternal(\r\n        address target,\r\n        uint value,\r\n        string memory signature,\r\n        bytes memory data,\r\n        uint eta\r\n    ) internal {\r\n        require(\r\n            !timelock.queuedTransactions(\r\n                keccak256(abi.encode(target, value, signature, data, eta))\r\n            ),\r\n            \"GovernorBravo::queueOrRevertInternal: identical proposal action already queued at eta\"\r\n        );\r\n        timelock.queueTransaction(target, value, signature, data, eta);\r\n    }\r\n\r\n```\r\n\r\n执行函数：\r\n\r\n```solidity\r\n//执行提议\r\n    function execute(uint proposalId) external payable {\r\n        require(\r\n            state(proposalId) == ProposalState.Queued,\r\n            \"GovernorBravo::execute: proposal can only be executed if it is queued\"\r\n        );\r\n\r\n        Proposal storage proposal = proposals[proposalId];\r\n        proposal.executed = true;\r\n        // 执行提案中每一个动作。\r\n        for (uint i = 0; i < proposal.targets.length; i++) {\r\n             console.log(\"eta:\",proposal.eta);\r\n            timelock.executeTransaction(\r\n                proposal.targets[i],\r\n                proposal.values[i],\r\n                proposal.signatures[i],\r\n                proposal.calldatas[i],\r\n                proposal.eta\r\n            );\r\n           \r\n        }\r\n        emit ProposalExecuted(proposalId);\r\n    }\r\n```\r\n\r\nstate:\r\n\r\n```solidity\r\n    //获取提案的状态&根据投票结果得出提案是否通过\r\n    function state(uint proposalId) public view returns (ProposalState) {\r\n        Proposal storage proposal = proposals[proposalId];\r\n        if (proposal.canceled) {\r\n            return ProposalState.Canceled;\r\n        } else if (block.number <= proposal.startBlock) {\r\n            return ProposalState.Pending;\r\n        } else if (block.number <= proposal.endBlock) {\r\n            return ProposalState.Active;\r\n        } else if (\r\n            proposal.forVotes <= proposal.againstVotes ||\r\n            proposal.forVotes < quorumVotes\r\n        ) {\r\n            return ProposalState.Defeated;\r\n        } else if (proposal.eta == 0) {\r\n            return ProposalState.Succeeded;\r\n        } else if (proposal.executed) {\r\n            return ProposalState.Executed;\r\n        } else if (block.number >= proposal.eta + timelock.GRACE_PERIOD()) {\r\n            return ProposalState.Expired;\r\n        } else {\r\n            return ProposalState.Queued;\r\n        }\r\n    }\r\n```\r\n\r\n完成了治理合约，我们来写token：\r\n\r\n在这里我们直接继承openzeppelin的ERC20，省去了源码的transfer函数，\r\n\r\n增加一个mintDao函数，用于给贡献者发币：\r\n\r\n```solidity\r\n function mintDao(address account, uint256 amount) external onlyDao {\r\n        _mint(account, amount);\r\n    }\r\n```\r\n\r\n其余基于源码做了一些微调，读者可自行查看：\r\n\r\n```solidity\r\nfunction delegate(address delegatee) public {\r\n        return _delegate(msg.sender, delegatee);\r\n    }\r\n\r\n    function _delegate(address delegator, address delegatee) internal {\r\n        address currentDelegate = delegates[delegator];\r\n        uint256 delegatorBalance = balanceOf(delegator);\r\n        delegates[delegator] = delegatee;\r\n\r\n        emit DelegateChanged(delegator, currentDelegate, delegatee);\r\n\r\n        _moveDelegates(currentDelegate, delegatee, delegatorBalance);\r\n    }\r\n\r\n    function _moveDelegates(\r\n        address fromRep,\r\n        address toRep,\r\n        uint256 amount\r\n    ) internal {\r\n        if (fromRep != toRep && amount > 0) {\r\n            if (fromRep != address(0)) {\r\n                uint256 fromRepNum = numCheckpoints[fromRep];\r\n                uint256 fromRepOld = fromRepNum > 0\r\n                    ? checkpoints[fromRep][fromRepNum - 1].votes\r\n                    : 0;\r\n                uint256 fromRepNew = fromRepOld.sub(amount);\r\n                _writeCheckpoint(fromRep, fromRepNum, fromRepOld, fromRepNew);\r\n            }\r\n\r\n            if (toRep != address(0)) {\r\n                uint256 toRepNum = numCheckpoints[toRep];\r\n                uint256 toRepOld = toRepNum > 0\r\n                    ? checkpoints[toRep][toRepNum - 1].votes\r\n                    : 0;\r\n                uint256 toRepNew = amount.add(toRepOld);\r\n                _writeCheckpoint(toRep, toRepNum, toRepOld, toRepNew);\r\n            }\r\n        }\r\n    }\r\n\r\n    function _writeCheckpoint(\r\n        address delegatee,\r\n        uint256 nCheckpoints,\r\n        uint256 oldVotes,\r\n        uint256 newVotes\r\n    ) internal {\r\n        uint256 blockNumber =block.number;\r\n        if (\r\n            nCheckpoints > 0 &&\r\n            checkpoints[delegatee][nCheckpoints - 1].fromBlock ==  blockNumber\r\n        ) {\r\n            checkpoints[delegatee][nCheckpoints - 1].votes = newVotes;\r\n        } else {\r\n            checkpoints[delegatee][nCheckpoints] = Checkpoint(\r\n                blockNumber,\r\n                newVotes\r\n            );\r\n            numCheckpoints[delegatee] = nCheckpoints + 1;\r\n        }\r\n\r\n        emit DelegateVotesChanged(delegatee, oldVotes, newVotes);\r\n    }\r\n\r\n    //计票函数：\r\n    function getPriorVotes(\r\n        address account,\r\n        uint blockNumber\r\n    ) external view returns (uint256) {\r\n        require(\r\n            blockNumber < block.number,\r\n            \"Comp::getPriorVotes: not yet determined\"\r\n        );\r\n        uint256 nCheckpoints = numCheckpoints[account];\r\n        if (nCheckpoints == 0) {\r\n            return 0;\r\n        }\r\n\r\n        // First check most recent balance\r\n        if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) {\r\n            return checkpoints[account][nCheckpoints - 1].votes;\r\n        }\r\n\r\n        // Next check implicit zero balance\r\n        if (checkpoints[account][0].fromBlock > blockNumber) {\r\n            return 0;\r\n        }\r\n        \r\n        uint256 lower = 0;\r\n        uint256 upper = nCheckpoints - 1;\r\n        while (upper > lower) {\r\n            uint256 center = upper - (upper - lower) / 2; // ceil, avoiding overflow\r\n            Checkpoint memory cp = checkpoints[account][center];\r\n            if (cp.fromBlock == blockNumber) {\r\n                return cp.votes;\r\n            } else if (cp.fromBlock < blockNumber) {\r\n                lower = center;\r\n            } else {\r\n                upper = center - 1;\r\n            }\r\n        }\r\n        return checkpoints[account][lower].votes;\r\n    }\r\n\r\n    function getCurrentVotes(address account) external view returns (uint256) {\r\n        uint256 nCheckpoints = numCheckpoints[account];\r\n        return\r\n            nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0;\r\n    }\r\n```\r\n\r\n加上我们的时间锁合约：\r\n\r\n```solidity\r\ncontract TimeLock is TimelockInterface {\r\n    using SafeMath for uint;\r\n\r\n    address public admin;\r\n    address public pendingAdmin;\r\n\r\n    uint public delay;//在提案生效前的一段宽限期，可以选择不接受此提案而退出。\r\n    uint public constant GRACE_PERIOD = 10; \r\n\r\n    mapping(bytes32 => bool) public queuedTransactions;\r\n\r\n    event NewDelay(uint indexed newDelay);\r\n    event CancelTransaction(\r\n        bytes32 indexed txHash,\r\n        address indexed target,\r\n        uint value,\r\n        string signature,\r\n        bytes data,\r\n        uint eta\r\n    );\r\n    event ExecuteTransaction(\r\n        bytes32 indexed txHash,\r\n        address indexed target,\r\n        uint value,\r\n        string signature,\r\n        bytes data,\r\n        uint eta\r\n    );\r\n    event QueueTransaction(\r\n        bytes32 indexed txHash,\r\n        address indexed target,\r\n        uint value,\r\n        string signature,\r\n        bytes data,\r\n        uint eta\r\n    );\r\n\r\n    constructor(address _admin, uint _delay) public {\r\n        admin = _admin;\r\n        delay = _delay;\r\n    }\r\n\r\n    modifier onlyAdmin() {\r\n        require(\r\n            msg.sender == pendingAdmin,\r\n            \"Timelock::onlyAdmin: Call must come from pendingAdmin.\"\r\n        );\r\n        _;\r\n    }\r\n\r\n    //pendingAdmin一般为治理合约\r\n    function setAdmin(address _admin) public {\r\n        require(msg.sender == admin, \"call must by admin\");\r\n        require(\r\n            _admin != address(0),\r\n            \"Timelock::setAdmin: New admin cannot be the zero address.\"\r\n        );\r\n        pendingAdmin = _admin;\r\n    }\r\n\r\n    function setDelay(uint _delay) public onlyAdmin {\r\n        require(\r\n            msg.sender == address(this),\r\n            \"Timelock::setDelay: Call must come from Timelock.\"\r\n        );\r\n        delay = _delay;\r\n\r\n        emit NewDelay(delay);\r\n    }\r\n\r\n    //将在执行队列中的提案信息hash，并设此提案hash为true，代表此提案已入队列\r\n    function queueTransaction(\r\n        address target,\r\n        uint value,\r\n        string memory signature,\r\n        bytes memory data,\r\n        uint eta\r\n    ) public onlyAdmin returns (bytes32) {\r\n        require(\r\n            eta >= getBlockTimestamp().add(delay),\r\n            \"Timelock::queueTransaction: Estimated execution block must satisfy delay.\"\r\n        );\r\n\r\n        bytes32 txHash = keccak256(\r\n            abi.encode(target, value, signature, data, eta)\r\n        );\r\n        queuedTransactions[txHash] = true;\r\n\r\n        emit QueueTransaction(txHash, target, value, signature, data, eta);\r\n        return txHash;\r\n    }\r\n\r\n    //取消提案\r\n    function cancelTransaction(\r\n        address target,\r\n        uint value,\r\n        string memory signature,\r\n        bytes memory data,\r\n        uint eta\r\n    ) public onlyAdmin {\r\n        bytes32 txHash = keccak256(\r\n            abi.encode(target, value, signature, data, eta)\r\n        );\r\n        queuedTransactions[txHash] = false;\r\n\r\n        emit CancelTransaction(txHash, target, value, signature, data, eta);\r\n    }\r\n\r\n    //执行队列中的提案，这里一般是\r\n    function executeTransaction(\r\n        address target,\r\n        uint value,\r\n        string memory signature,\r\n        bytes memory data,\r\n        uint eta\r\n    ) public payable onlyAdmin returns (bytes memory) {\r\n        bytes32 txHash = keccak256(\r\n            abi.encode(target, value, signature, data, eta)\r\n        );\r\n        require(\r\n            queuedTransactions[txHash],\r\n            \"Timelock::executeTransaction: Transaction hasn't been queued.\"\r\n        );\r\n        require(\r\n            getBlockTimestamp() >= eta,\r\n            \"Timelock::executeTransaction: Transaction hasn't surpassed time lock.\"\r\n        );\r\n\r\n        queuedTransactions[txHash] = false;\r\n\r\n        bytes memory callData;\r\n        // 如果没有签名，直接调用data\r\n        if (bytes(signature).length == 0) {\r\n            callData = data;\r\n        } else {\r\n            // 如果有签名,将签名和data打包\r\n            callData = abi.encodePacked(\r\n                bytes4(keccak256(bytes(signature))),\r\n                data\r\n            );\r\n        }\r\n        // 执行提案中要执行的target合约交易\r\n        (bool success, bytes memory returnData) = target.call{value: value}(\r\n            callData\r\n        );\r\n        require(\r\n            success,\r\n            \"Timelock::executeTransaction: Transaction execution reverted.\"\r\n        );\r\n\r\n        emit ExecuteTransaction(txHash, target, value, signature, data, eta);\r\n\r\n        return returnData;\r\n    }\r\n\r\n    function getBlockTimestamp() internal view returns (uint) {\r\n        return block.number;\r\n    }\r\n}\r\n```\r\n\r\n最后，附上测试代码：简单的测试逻辑是否通畅\r\n\r\n我们发出一个提案：\r\n\r\nbytes memory dataP = abi.encodeWithSignature(\r\n                \"withdraw(address,uint256)\",\r\n                bob,\r\n                100 ether\r\n            );\r\n\r\n从JCDao合约给bob withdraw 100 个ETH\r\n\r\n```solidity\r\n//SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.24;\r\nimport {Test, console} from \"forge-std/Test.sol\";\r\n\r\nimport \"../src/JCDao/Dao.sol\";\r\nimport \"../src/JCDao/JCGovern.sol\";\r\nimport \"../src/JCDao/JCGovernImp.sol\";\r\nimport \"../src/JCDao/JCToken.sol\";\r\nimport \"../src/JCDao/TimeLock.sol\";\r\n\r\ncontract JCDaoTest is Test {\r\n    address public owner;\r\n    address public alice;\r\n    address public bob;\r\n    address public david;\r\n    address public lucy;\r\n\r\n    address public delegatee1;\r\n    address public delegatee2;\r\n    address public delegatee3;\r\n\r\n    JCDao public dao;\r\n    JCGovern public govern;\r\n    JCGovernImp public governImp;\r\n    JCToken public token;\r\n    TimeLock public timelock;\r\n\r\n    function setUp() public {\r\n        owner = makeAddr(\"owner\");\r\n        alice = makeAddr(\"alice\");\r\n        bob = makeAddr(\"bob\");\r\n        david = makeAddr(\"david\");\r\n        lucy = makeAddr(\"lucy\");\r\n\r\n        delegatee1 = makeAddr(\"delegatee1\");\r\n        delegatee2 = makeAddr(\"delegatee2\");\r\n        delegatee3 = makeAddr(\"delegator3\");\r\n\r\n        deal(owner, 10000 ether);\r\n        deal(alice, 10000 ether);\r\n        deal(bob, 10000 ether);\r\n        deal(david, 10000 ether);\r\n        deal(lucy, 10000 ether);\r\n\r\n        vm.startPrank(owner);\r\n        {\r\n            token = new JCToken();\r\n            dao = new JCDao(owner, address(token));\r\n            governImp = new JCGovernImp();\r\n            timelock = new TimeLock(owner, 20);\r\n            govern = new JCGovern(\r\n                address(dao),\r\n                address(timelock),\r\n                address(token),\r\n                address(governImp),\r\n                20,\r\n                10\r\n            );\r\n\r\n            token.setDao(address(dao));\r\n            timelock.setAdmin(address(govern));\r\n            dao.setTimeLock(address(timelock));\r\n            dao.setAdmin(alice);\r\n            dao.addMember(alice);\r\n            dao.addMember(bob);\r\n            dao.addMember(david);\r\n            dao.addMember(lucy);\r\n            dao.addMember(owner);\r\n\r\n            dao.contribute{value: 200 ether}();\r\n        }\r\n        vm.stopPrank();\r\n\r\n        vm.startPrank(alice);\r\n        {\r\n            dao.contribute{value: 100 ether}();\r\n        }\r\n        vm.stopPrank();\r\n        vm.startPrank(bob);\r\n        {\r\n            dao.contribute{value: 100 ether}();\r\n        }\r\n        vm.stopPrank();\r\n        vm.startPrank(lucy);\r\n        {\r\n            dao.contribute{value: 100 ether}();\r\n        }\r\n        vm.stopPrank();\r\n        vm.startPrank(david);\r\n        {\r\n            dao.contribute{value: 100 ether}();\r\n        }\r\n        vm.stopPrank();\r\n    }\r\n\r\n    address[] targets;\r\n    uint[] values;\r\n    string[] signatures;\r\n    bytes[] calldatas;\r\n    string description;\r\n\r\n    function test() public {\r\n        vm.startPrank(alice);\r\n        {\r\n            bytes memory dataP = abi.encodeWithSignature(\r\n                \"withdraw(address,uint256)\",\r\n                bob,\r\n                100 ether\r\n            );\r\n\r\n            targets.push(address(dao));\r\n            values.push(0);\r\n            signatures.push(\"\");\r\n            calldatas.push(dataP);\r\n            description = \"Test proposal\";\r\n\r\n            bytes memory dataG = abi.encodeWithSignature(\r\n                \"propose(address[],uint256[],string[],bytes[],string)\",\r\n                targets,\r\n                values,\r\n                signatures,\r\n                calldatas,\r\n                description\r\n            );\r\n            address(govern).call{value: 100}(dataG);\r\n\r\n            // 委托\r\n            token.delegate(delegatee1);\r\n        }\r\n        vm.stopPrank();\r\n\r\n        vm.roll(11);\r\n        //委托代理\r\n        vm.startPrank(bob);\r\n        {\r\n            token.delegate(delegatee1);\r\n        }\r\n        vm.stopPrank();\r\n\r\n        vm.startPrank(owner);\r\n        {\r\n            token.delegate(delegatee2);\r\n        }\r\n        vm.stopPrank();\r\n        vm.startPrank(david);\r\n        {\r\n            token.delegate(delegatee2);\r\n        }\r\n        vm.stopPrank();\r\n        vm.startPrank(lucy);\r\n        {\r\n            token.delegate(delegatee3);\r\n        }\r\n        vm.stopPrank();\r\n\r\n        vm.roll(21);\r\n\r\n        //代理投票\r\n        vm.startPrank(delegatee1);\r\n        {\r\n            bytes memory data = abi.encodeWithSignature(\r\n                \"castVote(uint256,uint8)\",\r\n                1,\r\n                0\r\n            );\r\n            address(govern).call(data);\r\n        }\r\n        vm.stopPrank();\r\n        vm.startPrank(delegatee2);\r\n        {\r\n            bytes memory data = abi.encodeWithSignature(\r\n                \"castVote(uint256,uint8)\",\r\n                1,\r\n                1\r\n            );\r\n            address(govern).call(data);\r\n        }\r\n        vm.stopPrank();\r\n        vm.startPrank(delegatee3);\r\n        {\r\n            bytes memory data = abi.encodeWithSignature(\r\n                \"castVote(uint256,uint8)\",\r\n                1,\r\n                2\r\n            );\r\n            address(govern).call(data);\r\n        }\r\n        vm.stopPrank();\r\n\r\n        vm.roll(32);\r\n\r\n        // 将提案加入执行队列（谁来执行都无所谓）\r\n        vm.startPrank(owner);\r\n        {\r\n            bytes memory data = abi.encodeWithSignature(\"queue(uint256)\", 1);\r\n            address(govern).call(data);\r\n        }\r\n        vm.stopPrank();\r\n\r\n        // 执行提案\r\n        //必须等20个区块的delay时间过了\r\n        vm.roll(53);\r\n        //先记录bob之前的余额：\r\n        uint beginBalance = bob.balance;\r\n        vm.startPrank(owner);\r\n        {\r\n            bytes memory data = abi.encodeWithSignature(\"execute(uint256)\", 1);\r\n            address(govern).call{value:100}(data);\r\n        }\r\n        vm.stopPrank();\r\n\r\n        // 检查提案是否成功执行\r\n        vm.roll(54);\r\n        assertEq(bob.balance,beginBalance+100 ether);\r\n        console.log(\"proposal success!\");\r\n    }\r\n}\r\n```\r\n\r\n可以看到，最后bob的余额增加了100个ETH ！\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/03/D4mi9J8t65f6b95614e14.png)\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/03/cNQvQOod65f6b9471d0b3.png)\r\n\r\n\r\n完整源码可见作者仓库\r\n\r\nhttps://github.com/TheLastHobbit/OpenSpace-Study/tree/main/day13/src/JCDao\r\n\r\n\r\n\r\n我是Sanji，他们都叫我山鸡，在校大学生，web3小学生，有交流或Hackathon组队意向都可私信\r\n\r\n个人微信：Z18382250961."},"author":{"user":"https://learnblockchain.cn/people/18203","address":null},"history":null,"timestamp":1710686124,"version":1}