DAO 治理合约及提案执行实践

  • Sanji
  • 更新于 2024-03-17 22:35
  • 阅读 1362

上一篇我们详解了Compound治理源码,这篇我们来根据上面的治理逻辑完成简单的,完整的提案执行过程。

上一篇我们详解了 Compound治理源码, 这篇我们来根据上面的治理逻辑完成简单的,完整的提案执行过程。

实践

我们先创建一个我们自己的Dao,取名为JCDao(杰出Dao),用合约JCDao.sol来记录Dao中成员的身份

 mapping(address => bool) public Admin;
 mapping(address => bool) public whitelisted;

   function isDaoMember(address _address) public view returns(bool){
        return whitelisted[_address];
    }

    function setAdmin(address _addr) external onlyOwner{
        Admin[_addr] = true;
    }

    function isDaoAdmin(address _address) public view returns(bool){
        return Admin[_address];
    }

    function addMember(address _member) external{
        require(Admin[msg.sender]|| msg.sender == owner ,"You must be an admin&Owner to add a member");
        whitelisted[_member] = true;
    }

然后我们规定贡献ETH就可获得等量的token:

    function contribute() external payable{
        require(msg.value > 0,"You must contribute more than 0 ether");
        require(isDaoMember(msg.sender),"You must be a DAO member to contribute");
        contributions[msg.sender] += msg.value;
        JCToken(jcToken).mintDao(msg.sender,msg.value);

        emit Contribute(msg.sender,msg.value);
    }

设置timelock和一个withdraw方法,规定只有timelock能调用withdraw方法。

  function setTimeLock(address _timeLock) external onlyOwner{
        timeLock = _timeLock;
    }

    function withdraw(address to,uint256 amount) external{
        require(msg.sender == timeLock,"You must be the timeLock to call this function");
        payable(to).transfer(amount);
        emit Withdraw(to,amount);
    }

这就意味着只有通过治理,发起提案并通过才能调用withdraw方法,我们的目标就是完成这一流程。

然后我们来写我们的治理合约

同样的,一个代理合约,一个实现合约

我们在代理合约中设置admin modifier,只允许admin来设置实现合约:

    modifier onlyAdmin() {
        require(
            JCDao(dao).isDaoAdmin(msg.sender) || msg.sender == owner,
            "Governor:_setImplementation: admin&owner only"
        );
        _;
    }

    function setImplementation(address _implementation) public onlyAdmin{
        require(
            _implementation != address(0),
            "Governor:_setImplementation: invalid implementation address"
        );

        address oldImplementation = implementation;
        implementation = _implementation;

        emit NewImplementation(oldImplementation, implementation);
    }

然后开始写我们的实现合约:

我们将所需的event和一部分属性放在tool文件中:

这里的属性前面已经讲过,我在compound源码上进行了简化,以便理解

contract GovernorEvents {
    /// @notice Emitted when implementation is changed
    event NewImplementation(
        address oldImplementation,
        address newImplementation
    );

    /// @notice An event emitted when a proposal has been canceled
    event ProposalCanceled(uint id);

    /// @notice An event emitted when a proposal has been queued in the Timelock
    event ProposalQueued(uint id, uint eta);

    /// @notice An event emitted when a proposal has been executed in the Timelock
    event ProposalExecuted(uint id);

    event ProposalCreated(
        uint id,
        address proposer,
        address[] targets,
        uint[] values,
        string[] signatures,
        bytes[] calldatas,
        uint startBlock,
        uint endBlock,
        string description
    );

    event VoteCast(
        address indexed voter,
        uint proposalId,
        uint8 support,
        uint votes,
        string reason
    );
}

contract GovernImpV1 {
    address public admin;

    address public pendingAdmin;

    address public implementation;
}

contract GovernImpV2 is GovernImpV1 {
    /// @notice The delay before voting on a proposal may take place, once proposed, in blocks
    uint public votingDelay;

    /// @notice The duration of voting on a proposal, in blocks
    uint public votingPeriod;

    /// @notice The official record of all proposals ever proposed
    mapping(uint => Proposal) public proposals;

    /// @notice The latest proposal for each proposer
    mapping(address => uint) public latestProposalIds;

    /// @notice The total number of proposals
    uint public proposalCount;

    /// @notice The address of the Compound Protocol Timelock
    TimelockInterface public timelock;

    struct Proposal {
        uint id;
        address proposer;
        uint eta; //提案可用于执行的时间戳,在投票成功后设置
        address[] targets;
        uint[] values;
        string[] signatures;
        bytes[] calldatas;
        uint startBlock;
        uint endBlock;
        uint forVotes;
        uint againstVotes;
        uint abstainVotes;
        bool canceled;
        bool executed;
        mapping(address => Receipt) receipts;
    }

    /// @notice Ballot receipt record for a voter
    struct Receipt {
        bool hasVoted;
        uint8 support;
        uint96 votes;
    }

    enum ProposalState {
        Pending,
        Active, //在投票中
        Canceled,
        Defeated,
        Succeeded,
        Queued,
        Expired, //过期了
        Executed
    }
}

继承完工具类后,增添必要属性:

在这里我们规定,每个提案获得的票数超过300 ether就可以通过

address public jcToken;
address public dao;

uint public quorumVotes = 300 ether; //每个提案通过所需的最少票数

初始化:

 function initialize(
        address _dao,
        address _timelock,
        address _token,
        uint _votingPeriod,
        uint _votingDelay
    ) public {
        require(
            address(timelock) == address(0),
            "GovernorBravo::initialize: can only initialize once"
        );
        require(
            _timelock != address(0),
            "GovernorBravo::initialize: invalid timelock address"
        );
        require(
            _token != address(0),
            "GovernorBravo::initialize: invalid comp address"
        );
        timelock = TimelockInterface(_timelock);
        jcToken = _token;
        votingPeriod = _votingPeriod;
        votingDelay = _votingDelay;
        dao = _dao;
    }

提案函数与源码大致相同,我们在这只实现一种提案方式:

 /**
     * @dev 提议
     * @param targets 目标合约地址
     * @param values 转账金额
     * @param calldatas 调用数据
     * @param description 提议描述
     * @return proposalId 提议ID
     */
    function propose(
        address[] memory targets,
        uint[] memory values,
        string[] memory signatures,
        bytes[] memory calldatas,
        string memory description
    ) external payable returns (uint proposalId) {
        return
            _proposeInternal(
                msg.sender,
                targets,
                values,
                signatures,
                calldatas,
                description
            );
    }

    function _proposeInternal(
        address proposer,
        address[] memory targets,
        uint[] memory values,
        string[] memory signatures,
        bytes[] memory calldatas,
        string memory description
    ) internal returns (uint) {
        // 在提交本次提案之前先判断上一个提案是否被处理:
        uint latestProposalId = latestProposalIds[proposer];
        if (latestProposalId != 0) {
            ProposalState proposersLatestProposalState = state(
                latestProposalId
            );
            //
            require(
                proposersLatestProposalState != ProposalState.Active,
                "GovernorBravo::proposeInternal: one live proposal per proposer, found an already active proposal"
            );
            require(
                proposersLatestProposalState != ProposalState.Pending,
                "GovernorBravo::proposeInternal: one live proposal per proposer, found an already pending proposal"
            );
        }

        uint startBlock = block.number + votingDelay;
        uint endBlock = startBlock + votingPeriod;
        proposalCount++;

        uint newProposalID = proposalCount;
        Proposal storage newProposal = proposals[newProposalID];

        newProposal.id = newProposalID;
        newProposal.proposer = proposer;
        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.abstainVotes = 0;
        newProposal.canceled = false;
        newProposal.executed = false;

        latestProposalIds[newProposal.proposer] = newProposal.id;

        emit ProposalCreated(
            newProposal.id,
            proposer,
            targets,
            values,
            signatures,
            calldatas,
            startBlock,
            endBlock,
            description
        );

        return newProposal.id;
    }

投票函数:

    /**
     * @dev 投票
     * @param proposalId 提议ID
     * @param support 支持或反对或中立:0,1,2
     */
    function castVote(uint proposalId, uint8 support) external {
        emit VoteCast(
            msg.sender,
            proposalId,
            support,
            _castVoteInternal(msg.sender, proposalId, support),
            ""
        );  
    }
    /**
     * @dev 投票internal
     * @param voter 投票人
     * @param proposalId 提议ID
     * @param support 反对或支持或中立:0,1,2
     */
    function _castVoteInternal(
        address voter,
        uint proposalId,
        uint8 support
    ) internal returns (uint256) {
        require(
            state(proposalId) == ProposalState.Active,
            "GovernorBravo::castVoteInternal: voting is closed"
        );
        require(
            support <= 2,
            "GovernorBravo::castVoteInternal: invalid vote type"
        );
        Proposal storage proposal = proposals[proposalId];
        Receipt storage receipt = proposal.receipts[voter];
        uint256 votes = IJCToken(jcToken).getPriorVotes(
            voter,
            proposal.startBlock
        );

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

        receipt.hasVoted = true;
        receipt.support = support;
        receipt.votes = uint96(votes);

        return votes;
    }

入队函数,在这里为了后面的测试方便,我们都以区块高度作为时间度量(源码是时间戳)

//提议被投票通过标准后可进入执行队列
    function queue(uint proposalId) external {
        require(
            state(proposalId) == ProposalState.Succeeded,
            "GovernorBravo::queue: proposal can only be queued if it is succeeded"
        );
        Proposal storage proposal = proposals[proposalId];
        uint eta = block.number + timelock.delay();
        console.log("queue eta:",eta);
        for (uint i = 0; i < proposal.targets.length; i++) {
            _queueOrRevertInternal(
                proposal.targets[i],
                proposal.values[i],
                proposal.signatures[i],
                proposal.calldatas[i],
                eta
            );
        }
        proposal.eta = eta; //成功进入执行队列后,设置执行时间戳
        emit ProposalQueued(proposalId, eta);
    }

    function _queueOrRevertInternal(
        address target,
        uint value,
        string memory signature,
        bytes memory data,
        uint eta
    ) internal {
        require(
            !timelock.queuedTransactions(
                keccak256(abi.encode(target, value, signature, data, eta))
            ),
            "GovernorBravo::queueOrRevertInternal: identical proposal action already queued at eta"
        );
        timelock.queueTransaction(target, value, signature, data, eta);
    }

执行函数:

//执行提议
    function execute(uint proposalId) external payable {
        require(
            state(proposalId) == ProposalState.Queued,
            "GovernorBravo::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++) {
             console.log("eta:",proposal.eta);
            timelock.executeTransaction(
                proposal.targets[i],
                proposal.values[i],
                proposal.signatures[i],
                proposal.calldatas[i],
                proposal.eta
            );

        }
        emit ProposalExecuted(proposalId);
    }

state:

    //获取提案的状态&根据投票结果得出提案是否通过
    function state(uint proposalId) public view returns (ProposalState) {
        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.number >= proposal.eta + timelock.GRACE_PERIOD()) {
            return ProposalState.Expired;
        } else {
            return ProposalState.Queued;
        }
    }

完成了治理合约,我们来写token:

在这里我们直接继承openzeppelin的ERC20,省去了源码的transfer函数,

增加一个mintDao函数,用于给贡献者发币:

 function mintDao(address account, uint256 amount) external onlyDao {
        _mint(account, amount);
    }

其余基于源码做了一些微调,读者可自行查看:

function delegate(address delegatee) public {
        return _delegate(msg.sender, delegatee);
    }

    function _delegate(address delegator, address delegatee) internal {
        address currentDelegate = delegates[delegator];
        uint256 delegatorBalance = balanceOf(delegator);
        delegates[delegator] = delegatee;

        emit DelegateChanged(delegator, currentDelegate, delegatee);

        _moveDelegates(currentDelegate, delegatee, delegatorBalance);
    }

    function _moveDelegates(
        address fromRep,
        address toRep,
        uint256 amount
    ) internal {
        if (fromRep != toRep && amount > 0) {
            if (fromRep != address(0)) {
                uint256 fromRepNum = numCheckpoints[fromRep];
                uint256 fromRepOld = fromRepNum > 0
                    ? checkpoints[fromRep][fromRepNum - 1].votes
                    : 0;
                uint256 fromRepNew = fromRepOld.sub(amount);
                _writeCheckpoint(fromRep, fromRepNum, fromRepOld, fromRepNew);
            }

            if (toRep != address(0)) {
                uint256 toRepNum = numCheckpoints[toRep];
                uint256 toRepOld = toRepNum > 0
                    ? checkpoints[toRep][toRepNum - 1].votes
                    : 0;
                uint256 toRepNew = amount.add(toRepOld);
                _writeCheckpoint(toRep, toRepNum, toRepOld, toRepNew);
            }
        }
    }

    function _writeCheckpoint(
        address delegatee,
        uint256 nCheckpoints,
        uint256 oldVotes,
        uint256 newVotes
    ) internal {
        uint256 blockNumber =block.number;
        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);
    }

    //计票函数:
    function getPriorVotes(
        address account,
        uint blockNumber
    ) external view returns (uint256) {
        require(
            blockNumber < block.number,
            "Comp::getPriorVotes: not yet determined"
        );
        uint256 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;
        }

        uint256 lower = 0;
        uint256 upper = nCheckpoints - 1;
        while (upper > lower) {
            uint256 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;
    }

    function getCurrentVotes(address account) external view returns (uint256) {
        uint256 nCheckpoints = numCheckpoints[account];
        return
            nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0;
    }

加上我们的时间锁合约:

contract TimeLock is TimelockInterface {
    using SafeMath for uint;

    address public admin;
    address public pendingAdmin;

    uint public delay;//在提案生效前的一段宽限期,可以选择不接受此提案而退出。
    uint public constant GRACE_PERIOD = 10; 

    mapping(bytes32 => bool) public queuedTransactions;

    event NewDelay(uint indexed newDelay);
    event CancelTransaction(
        bytes32 indexed txHash,
        address indexed target,
        uint value,
        string signature,
        bytes data,
        uint eta
    );
    event ExecuteTransaction(
        bytes32 indexed txHash,
        address indexed target,
        uint value,
        string signature,
        bytes data,
        uint eta
    );
    event QueueTransaction(
        bytes32 indexed txHash,
        address indexed target,
        uint value,
        string signature,
        bytes data,
        uint eta
    );

    constructor(address _admin, uint _delay) public {
        admin = _admin;
        delay = _delay;
    }

    modifier onlyAdmin() {
        require(
            msg.sender == pendingAdmin,
            "Timelock::onlyAdmin: Call must come from pendingAdmin."
        );
        _;
    }

    //pendingAdmin一般为治理合约
    function setAdmin(address _admin) public {
        require(msg.sender == admin, "call must by admin");
        require(
            _admin != address(0),
            "Timelock::setAdmin: New admin cannot be the zero address."
        );
        pendingAdmin = _admin;
    }

    function setDelay(uint _delay) public onlyAdmin {
        require(
            msg.sender == address(this),
            "Timelock::setDelay: Call must come from Timelock."
        );
        delay = _delay;

        emit NewDelay(delay);
    }

    //将在执行队列中的提案信息hash,并设此提案hash为true,代表此提案已入队列
    function queueTransaction(
        address target,
        uint value,
        string memory signature,
        bytes memory data,
        uint eta
    ) public onlyAdmin returns (bytes32) {
        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;
    }

    //取消提案
    function cancelTransaction(
        address target,
        uint value,
        string memory signature,
        bytes memory data,
        uint eta
    ) public onlyAdmin {
        bytes32 txHash = keccak256(
            abi.encode(target, value, signature, data, eta)
        );
        queuedTransactions[txHash] = false;

        emit CancelTransaction(txHash, target, value, signature, data, eta);
    }

    //执行队列中的提案,这里一般是
    function executeTransaction(
        address target,
        uint value,
        string memory signature,
        bytes memory data,
        uint eta
    ) public payable onlyAdmin returns (bytes memory) {
        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."
        );

        queuedTransactions[txHash] = false;

        bytes memory callData;
        // 如果没有签名,直接调用data
        if (bytes(signature).length == 0) {
            callData = data;
        } else {
            // 如果有签名,将签名和data打包
            callData = abi.encodePacked(
                bytes4(keccak256(bytes(signature))),
                data
            );
        }
        // 执行提案中要执行的target合约交易
        (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;
    }

    function getBlockTimestamp() internal view returns (uint) {
        return block.number;
    }
}

最后,附上测试代码:简单的测试逻辑是否通畅

我们发出一个提案:

bytes memory dataP = abi.encodeWithSignature( "withdraw(address,uint256)", bob, 100 ether );

从JCDao合约给bob withdraw 100 个ETH

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";

import "../src/JCDao/Dao.sol";
import "../src/JCDao/JCGovern.sol";
import "../src/JCDao/JCGovernImp.sol";
import "../src/JCDao/JCToken.sol";
import "../src/JCDao/TimeLock.sol";

contract JCDaoTest is Test {
    address public owner;
    address public alice;
    address public bob;
    address public david;
    address public lucy;

    address public delegatee1;
    address public delegatee2;
    address public delegatee3;

    JCDao public dao;
    JCGovern public govern;
    JCGovernImp public governImp;
    JCToken public token;
    TimeLock public timelock;

    function setUp() public {
        owner = makeAddr("owner");
        alice = makeAddr("alice");
        bob = makeAddr("bob");
        david = makeAddr("david");
        lucy = makeAddr("lucy");

        delegatee1 = makeAddr("delegatee1");
        delegatee2 = makeAddr("delegatee2");
        delegatee3 = makeAddr("delegator3");

        deal(owner, 10000 ether);
        deal(alice, 10000 ether);
        deal(bob, 10000 ether);
        deal(david, 10000 ether);
        deal(lucy, 10000 ether);

        vm.startPrank(owner);
        {
            token = new JCToken();
            dao = new JCDao(owner, address(token));
            governImp = new JCGovernImp();
            timelock = new TimeLock(owner, 20);
            govern = new JCGovern(
                address(dao),
                address(timelock),
                address(token),
                address(governImp),
                20,
                10
            );

            token.setDao(address(dao));
            timelock.setAdmin(address(govern));
            dao.setTimeLock(address(timelock));
            dao.setAdmin(alice);
            dao.addMember(alice);
            dao.addMember(bob);
            dao.addMember(david);
            dao.addMember(lucy);
            dao.addMember(owner);

            dao.contribute{value: 200 ether}();
        }
        vm.stopPrank();

        vm.startPrank(alice);
        {
            dao.contribute{value: 100 ether}();
        }
        vm.stopPrank();
        vm.startPrank(bob);
        {
            dao.contribute{value: 100 ether}();
        }
        vm.stopPrank();
        vm.startPrank(lucy);
        {
            dao.contribute{value: 100 ether}();
        }
        vm.stopPrank();
        vm.startPrank(david);
        {
            dao.contribute{value: 100 ether}();
        }
        vm.stopPrank();
    }

    address[] targets;
    uint[] values;
    string[] signatures;
    bytes[] calldatas;
    string description;

    function test() public {
        vm.startPrank(alice);
        {
            bytes memory dataP = abi.encodeWithSignature(
                "withdraw(address,uint256)",
                bob,
                100 ether
            );

            targets.push(address(dao));
            values.push(0);
            signatures.push("");
            calldatas.push(dataP);
            description = "Test proposal";

            bytes memory dataG = abi.encodeWithSignature(
                "propose(address[],uint256[],string[],bytes[],string)",
                targets,
                values,
                signatures,
                calldatas,
                description
            );
            address(govern).call{value: 100}(dataG);

            // 委托
            token.delegate(delegatee1);
        }
        vm.stopPrank();

        vm.roll(11);
        //委托代理
        vm.startPrank(bob);
        {
            token.delegate(delegatee1);
        }
        vm.stopPrank();

        vm.startPrank(owner);
        {
            token.delegate(delegatee2);
        }
        vm.stopPrank();
        vm.startPrank(david);
        {
            token.delegate(delegatee2);
        }
        vm.stopPrank();
        vm.startPrank(lucy);
        {
            token.delegate(delegatee3);
        }
        vm.stopPrank();

        vm.roll(21);

        //代理投票
        vm.startPrank(delegatee1);
        {
            bytes memory data = abi.encodeWithSignature(
                "castVote(uint256,uint8)",
                1,
                0
            );
            address(govern).call(data);
        }
        vm.stopPrank();
        vm.startPrank(delegatee2);
        {
            bytes memory data = abi.encodeWithSignature(
                "castVote(uint256,uint8)",
                1,
                1
            );
            address(govern).call(data);
        }
        vm.stopPrank();
        vm.startPrank(delegatee3);
        {
            bytes memory data = abi.encodeWithSignature(
                "castVote(uint256,uint8)",
                1,
                2
            );
            address(govern).call(data);
        }
        vm.stopPrank();

        vm.roll(32);

        // 将提案加入执行队列(谁来执行都无所谓)
        vm.startPrank(owner);
        {
            bytes memory data = abi.encodeWithSignature("queue(uint256)", 1);
            address(govern).call(data);
        }
        vm.stopPrank();

        // 执行提案
        //必须等20个区块的delay时间过了
        vm.roll(53);
        //先记录bob之前的余额:
        uint beginBalance = bob.balance;
        vm.startPrank(owner);
        {
            bytes memory data = abi.encodeWithSignature("execute(uint256)", 1);
            address(govern).call{value:100}(data);
        }
        vm.stopPrank();

        // 检查提案是否成功执行
        vm.roll(54);
        assertEq(bob.balance,beginBalance+100 ether);
        console.log("proposal success!");
    }
}

可以看到,最后bob的余额增加了100个ETH !

image.png

image.png

完整源码可见作者仓库

https://github.com/TheLastHobbit/OpenSpace-Study/tree/main/day13/src/JCDao

我是Sanji,他们都叫我山鸡,在校大学生,web3小学生,有交流或Hackathon组队意向都可私信

个人微信:Z18382250961.

点赞 3
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Sanji
Sanji
0x9cC0...2BD2
华语web3黄埔军校S1成员,cuit区块链工程学生,Outrun初创成员,JCDao创始人,T 神亲授弟子,WWC创始人。