以太坊开发(十六)打造去中心化组织的投票系统

以太坊官方打造去中心化组织教程

本文基于作者阅读官方教程,根据理解而写,本身就有一些不太明白的地方需要解惑,只能作为一定的参考。另外如有疏漏请指正,感谢!

Decentralized Autonomous Organization 去中心化组织

这篇文章我们一起来打造一个投票智能合约。
边看代码边进行讲解。

基础版的投票合约

设置父类合约和接口

这部分的代码主要设置合约创建者为owner,并且提供替换owner的方法。定义了接收ether代币的方法。如果有疑问建议先阅读之前的文章。

区块链开发(二十)代币示例及讲解

区块链开发(二十一)众筹合约示例及讲解

代码如下:

contract owned {
    address public owner;

    function owned()  public {
        owner = msg.sender;
    }

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }

    function transferOwnership(address newOwner) onlyOwner  public {
        owner = newOwner;
    }
}

contract tokenRecipient {
    event receivedEther(address sender, uint amount);
    event receivedTokens(address _from, uint256 _value, address _token, bytes _extraData);

    function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public {
        Token t = Token(_token);
        require(t.transferFrom(_from, this, _value));
        receivedTokens(_from, _value, _token, _extraData);
    }

    function () payable  public {
        receivedEther(msg.sender, msg.value);
    }
}

interface Token {
    function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);
}

Congress合约

定义变量

  • 最小投票数:一个提案需要设置最小投票数。如果没有达到规定的投票数量,说明该提案是没有太大意义的。

  • 投票时间:设置一个允许投票的时间。到期后不允许投票。

  • 设定赞同票数的区间值:

    1. 一个投票可能是赞同或者反对。

    2. 如果想让一个提案通过,必须得到票数的50%加上这个设定的区间值。如果只要50%是赞同票就能通过,设为0就好了。如果要求全票通过,则设为members - 1。

    3. 提案的投票结果currentResult初始为0,在获得一张赞同票时,currentResult++,获得反对票时,currentResult--。一半赞同一半反对,currentResult最后为0。如果区间值设为0,意味该提案只需要半数人赞同便可通过。

    4. 假如设置赞同票数的区间值为2,共有10张投票,如果提案通过,则说明至少有7张是赞同票。因为需要currentResult大于2。

    // 最小的投票数
    uint public minimumQuorum;
    // 投票时间,以分钟为单位
    uint public debatingPeriodInMinutes;
    // 设定赞同票数的区间值
    int public majorityMargin;
    // 提案的数组
    Proposal[] public proposals;
    // 提案的个数
    uint public numProposals;
    // 成员id及地址
    mapping (address => uint) public memberId;
    // 成员的数组
    Member[] public members;

定义事件

    // 增加提案,传入提案id,受益人地址,价格,描述
    event ProposalAdded(uint proposalID, address recipient, uint amount, string description);

    // 投票,传入提案id,赞同/反对,投票人地址,陈述理由
    event Voted(uint proposalID, bool position, address voter, string justification);

    // 提案归档,传入提案id,结果,投票数,是否激活
    event ProposalTallied(uint proposalID, int result, uint quorum, bool active);

    // 设置某人是否为成员,传入地址,是否为组员
    event MembershipChanged(address member, bool isMember);

    // 改变规则,传入新的最小投票数,新的讨论时间,新的赞同票的区间值
    event ChangeOfRules(uint newMinimumQuorum, uint 
    newDebatingPeriodInMinutes, int newMajorityMargin);

定义结构体

    // 提案的结构体
    struct Proposal {
        // 受益人地址
        address recipient;
        // 金额
        uint amount;
        // 描述
        string description;
        // 投票截止时间
        uint votingDeadline;
        // 是否已执行
        bool executed;
        // 是否通过
        bool proposalPassed;
        // 得票数
        uint numberOfVotes;
        // 赞同票数
        int currentResult;
        // 哈希值
        bytes32 proposalHash;
        // 投票数组
        Vote[] votes;
        // 对应的地址是否已投票
        mapping (address => bool) voted;
    }

    // 成员结构体
    struct Member {
        // 地址
        address member;
        // 姓名
        string name;
        // 加入时间
        uint memberSince;
    }

    // 投票结构体
    struct Vote {
        // 支持还是反对
        bool inSupport;
        // 投票人地址
        address voter;
        // 理由描述
        string justification;
    }

修改器

增加一个判断当前合约调用者是否为成员的限制。

  // Modifier that allows only shareholders to vote and create new proposals
    // 限定了只有成员才可以投票及创建新提案
    modifier onlyMembers {
        require(memberId[msg.sender] != 0);
        _;
    }

构造函数

     /**
     * Constructor function
     * 构造函数
     */
    function Congress (

        // is the minimum amount of votes a proposal needs to have before it can be executed.
        // 设定提案被执行所需要的最少投票数
        uint minimumQuorumForProposals,

        // is the minimum amount of time (in minutes) that needs to pass before it can be executed.
        // 设定投票持续时间,如果时间到了之后没有通过,则提案不会被执行。以分钟为单位
        uint minutesForDebate,


        // A proposal passes if there are more than 50% of the votes plus the margin. Leave at 0 for simple majority, put it at the number of members - 1 to require an absolute consensus.
        // 如果想让一个提案通过,必须得到票数的50%加上这个设定的区间值。如果只要50%是赞同票就能通过,设为0就好了。如果要求全票通过,则设为members - 1。
        // 提案的投票结果currentResult初始为0,在获得一张赞同票时,currentResult++,获得反对票时,currentResult--。一半赞同一半反对,currentResult最后为0。如果区间值设为0,意味该提案只需要半数人赞同便可通过。
        // 假如设置赞同票数的区间值为2,共有10张投票,如果要想提案通过,则说明至少有7张是赞同票。
        // 设定赞同票数的区间值
        int marginOfVotesForMajority

    )  payable public {
        // 设定投票规则
        changeVotingRules(minimumQuorumForProposals, minutesForDebate, marginOfVotesForMajority);
        // It’s necessary to add an empty first member
        addMember(0, "");
        // and let's add the founder, to save a step later
        addMember(owner, 'founder');
    }

新增组员

    /**
     * Add member 添加一个成员,传入成员地址和名称
     * 限定了只有owner才能调用此方法
     *
     * Make `targetMember` a member named `memberName`
     *
     * @param targetMember ethereum address to be added
     * @param memberName public name for that member
     */
    function addMember(address targetMember, string memberName) onlyOwner public {
        uint id = memberId[targetMember];

        // 如果是新成员,将memberId设为members数组长度
        if (id == 0) {
            memberId[targetMember] = members.length;
            id = members.length++;
        }

        // 无论是否为新成员还是已有成员,都重新设置地址加入时间及姓名
        members[id] = Member({member: targetMember, memberSince: now, name: memberName});
        MembershipChanged(targetMember, true);
    }

删除组员

    /**
     * Remove member 删除一个成员,传入成员地址
     * 限定了只有owner才能调用此方法
     *
     * @notice Remove membership from `targetMember`
     *
     * @param targetMember ethereum address to be removed
     */
    function removeMember(address targetMember) onlyOwner public {
        require(memberId[targetMember] != 0);

        for (uint i = memberId[targetMember]; i<members.length-1; i++){
            members[i] = members[i+1];
        }
        delete members[members.length-1];
        members.length--;
    }

改变投票规则

    /**
     * Change voting rules 改变投票规则
     *
     * Make so that proposals need to be discussed for at least `minutesForDebate/60` hours,
     * 保证一个提案至少需要讨论的时间为`minutesForDebate/60`小时

     * have at least `minimumQuorumForProposals` votes, and have 50% + `marginOfVotesForMajority` votes to be executed
     * 提案需要的最少得票数和得票中的指定赞成票数才可被执行

     *
     * @param minimumQuorumForProposals how many members must vote on a proposal for it to be executed
     * 提案被执行的最少得票数
     *
     * @param minutesForDebate the minimum amount of delay between when a proposal is made and when it can be executed
     * 提案的最少投票时间
     *
     * @param marginOfVotesForMajority the proposal needs to have 50% plus this number
     * 提案需要50%赞同票加上这个区间值才可通过
     *
     */
    function changeVotingRules(
        uint minimumQuorumForProposals,
        uint minutesForDebate,
        int marginOfVotesForMajority
    ) onlyOwner public {
        minimumQuorum = minimumQuorumForProposals;
        debatingPeriodInMinutes = minutesForDebate;
        majorityMargin = marginOfVotesForMajority;

        ChangeOfRules(minimumQuorum, debatingPeriodInMinutes, majorityMargin);
    }

新增提案

    /**
     * Add Proposal 增加提案
     *
     * Propose to send `weiAmount / 1e18` ether to `beneficiary` for `jobDescription`. `transactionBytecode ? Contains : Does not contain` code.
     *
     * @param beneficiary who to send the ether to
     * 受益人,如果提案顺利执行,可以获取到提案中的金额
     * @param weiAmount amount of ether to send, in wei
     * ether价格,单位是wei
     * @param jobDescription Description of job
     * 新提案的描述
     * @param transactionBytecode bytecode of transaction
     * 
     * 
     */
    function newProposal(
        address beneficiary,
        uint weiAmount,
        string jobDescription,
        bytes transactionBytecode
    )
        onlyMembers public
        returns (uint proposalID)
    {
        proposalID = proposals.length++;
        Proposal storage p = proposals[proposalID];
        p.recipient = beneficiary;
        p.amount = weiAmount;
        p.description = jobDescription;
        p.proposalHash = keccak256(beneficiary, weiAmount, transactionBytecode);
        p.votingDeadline = now + debatingPeriodInMinutes * 1 minutes;
        p.executed = false;
        p.proposalPassed = false;
        p.numberOfVotes = 0;
        ProposalAdded(proposalID, beneficiary, weiAmount, jobDescription);
        numProposals = proposalID+1;

        return proposalID;
    }

以ether为单位增加提案

    /**
     * Add proposal in Ether 以ether为单位增加提案
     *
     * Propose to send `etherAmount` ether to `beneficiary` for `jobDescription`. `transactionBytecode ? Contains : Does not contain` code.
     * This is a convenience function to use if the amount to be given is in round number of ether units.
     *
     * @param beneficiary who to send the ether to
     * @param etherAmount amount of ether to send
     * @param jobDescription Description of job
     * @param transactionBytecode bytecode of transaction
     */
    function newProposalInEther(
        address beneficiary,
        uint etherAmount,
        string jobDescription,
        bytes transactionBytecode
    )
        onlyMembers public
        returns (uint proposalID)
    {
        return newProposal(beneficiary, etherAmount * 1 ether, jobDescription, transactionBytecode);
    }  

检查是否指定提案的Hash值与传入的参数相等

    /**
     * Check if a proposal code matches
     * 检查是否指定提案的Hash值与传入的参数相等
     *
     * @param proposalNumber ID number of the proposal to query
     * @param beneficiary who to send the ether to
     * @param weiAmount amount of ether to send
     * @param transactionBytecode bytecode of transaction
     */
    function checkProposalCode(
        uint proposalNumber,
        address beneficiary,
        uint weiAmount,
        bytes transactionBytecode
    )
        constant public
        returns (bool codeChecksOut)
    {
        Proposal storage p = proposals[proposalNumber];
        return p.proposalHash == keccak256(beneficiary, weiAmount, transactionBytecode);
    }

投票

    /**
     * Log a vote for a proposal 进行投票
     *
     * Vote `supportsProposal? in support of : against` proposal #`proposalNumber`
     *
     * @param proposalNumber number of proposal 提案号
     * @param supportsProposal either in favor or against it 支持还是反对
     * @param justificationText optional justification text 陈述意见
     */
    function vote(
        uint proposalNumber,
        bool supportsProposal,
        string justificationText
    )
        onlyMembers public
        returns (uint voteID)
    {
        // Get the proposal 
        // 获取提案
        Proposal storage p = proposals[proposalNumber];  

       // 如果投票时间已过,退出
        require(now < p.votingDeadline);

        // If has already voted, cancel  
        // 如果已经投过票,退出      
        require(!p.voted[msg.sender]);  

        // Set this voter as having voted
        // 设置为已投票
        p.voted[msg.sender] = true;      

        // Increase the number of votes
        // 为此提案增加票数
        p.numberOfVotes++;       

        // If they support the proposal
        // 支持
        if (supportsProposal) {      

        // Increase score
        // 分数加1                   
            p.currentResult++;                          
        } else {             

        // If they don't
        // Decrease the score
        // 反对,分数减1
            p.currentResult--;                          
        }

        // Create a log of this event
        Voted(proposalNumber,  supportsProposal, msg.sender, justificationText);
        return p.numberOfVotes;
    }

执行提案

    /**
     * Finish vote 投票结束,执行提案
     *
     * Count the votes proposal #`proposalNumber` and execute it if approved
     * 清点某个提案的得票数,如果通过,执行此提案
     *
     * @param proposalNumber proposal number
     * @param transactionBytecode optional: if the transaction contained a bytecode, you need to send it
     * 可选参数。如果提案包含bytecode执行代码,需要执行此代码
     */
    function executeProposal(uint proposalNumber, bytes transactionBytecode) public {
        Proposal storage p = proposals[proposalNumber];

        // If it is past the voting deadline
        // 如果投票时间已过
        require(now > p.votingDeadline     

            // and it has not already been executed
            // 并且提案还未被执行                    
            && !p.executed     

            // and the supplied code matches the proposal
            // 并且传入的代码与提案中代码一致                                                    
            && p.proposalHash == keccak256(p.recipient, p.amount, transactionBytecode)  

            // and a minimum quorum has been reached...
            // 并且提案需要的投票数大于等于最小得票数
            && p.numberOfVotes >= minimumQuorum);                                  

        // ...then execute result

        if (p.currentResult > majorityMargin) {
            // Proposal passed; execute the transaction
            // 提案的结果大于赞同票区间值,提案通过,执行提案中的交易代码

            // Avoid recursive calling
            // 设置提案已经执行过了,以免递归执行
            p.executed = true; 

            require(p.recipient.call.value(p.amount)(transactionBytecode));

            p.proposalPassed = true;
        } else {
            // Proposal failed
            p.proposalPassed = false;
        }

        // Fire Events
        ProposalTallied(proposalNumber, p.currentResult, p.numberOfVotes, p.proposalPassed);
    }

全部代码

pragma solidity ^0.4.16;

contract owned {
    address public owner;

    function owned()  public {
        owner = msg.sender;
    }

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }

    function transferOwnership(address newOwner) onlyOwner  public {
        owner = newOwner;
    }
}

contract tokenRecipient {
    event receivedEther(address sender, uint amount);
    event receivedTokens(address _from, uint256 _value, address _token, bytes _extraData);

    function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public {
        Token t = Token(_token);
        require(t.transferFrom(_from, this, _value));
        receivedTokens(_from, _value, _token, _extraData);
    }

    function () payable  public {
        receivedEther(msg.sender, msg.value);
    }
}

interface Token {
    function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);
}

contract Congress is owned, tokenRecipient {

    // 定义变量和事件

    // 最小的投票数
    uint public minimumQuorum;
    // 投票时间,以分钟为单位
    uint public debatingPeriodInMinutes;
    // 设定赞同票数的区间值
    int public majorityMargin;
    // 提案的数组
    Proposal[] public proposals;
    // 提案的个数
    uint public numProposals;
    // 成员id及地址
    mapping (address => uint) public memberId;
    // 成员的数组
    Member[] public members;

    // 增加提案,传入提案id,受益人地址,价格,描述
    event ProposalAdded(uint proposalID, address recipient, uint amount, string description);

    // 投票,传入提案id,赞同/反对,投票人地址,陈述理由
    event Voted(uint proposalID, bool position, address voter, string justification);

    // 提案归档,传入提案id,结果,投票数,是否激活
    event ProposalTallied(uint proposalID, int result, uint quorum, bool active);

    // 设置某人是否为成员,传入地址,是否为组员
    event MembershipChanged(address member, bool isMember);

    // 改变规则,传入新的最小投票数,新的讨论时间,新的赞同票的区间值
    event ChangeOfRules(uint newMinimumQuorum, uint 
    newDebatingPeriodInMinutes, int newMajorityMargin);

    // 提案的结构体
    struct Proposal {
        // 受益人地址
        address recipient;
        // 金额
        uint amount;
        // 描述
        string description;
        // 投票截止时间
        uint votingDeadline;
        // 是否已执行
        bool executed;
        // 是否通过
        bool proposalPassed;
        // 得票数
        uint numberOfVotes;
        // 赞同票数
        int currentResult;
        // 哈希值
        bytes32 proposalHash;
        // 投票数组
        Vote[] votes;
        // 对应的地址是否已投票
        mapping (address => bool) voted;
    }

    // 成员结构体
    struct Member {
        // 地址
        address member;
        // 姓名
        string name;
        // 加入时间
        uint memberSince;
    }

    // 投票结构体
    struct Vote {
        // 支持还是反对
        bool inSupport;
        // 投票人地址
        address voter;
        // 理由描述
        string justification;
    }

    // Modifier that allows only shareholders to vote and create new proposals
    // 限定了只有成员才可以投票及创建新提案
    modifier onlyMembers {
        require(memberId[msg.sender] != 0);
        _;
    }

    /**
     * Constructor function
     * 构造函数
     */
    function Congress (

        // is the minimum amount of votes a proposal needs to have before it can be executed.
        // 设定提案被执行所需要的最少投票数
        uint minimumQuorumForProposals,

        // is the minimum amount of time (in minutes) that needs to pass before it can be executed.
        // 设定投票持续时间,如果时间到了之后没有通过,则提案不会被执行。以分钟为单位
        uint minutesForDebate,


        // A proposal passes if there are more than 50% of the votes plus the margin. Leave at 0 for simple majority, put it at the number of members - 1 to require an absolute consensus.
        // 如果想让一个提案通过,必须得到票数的50%加上这个设定的区间值。如果只要50%是赞同票就能通过,设为0就好了。如果要求全票通过,则设为members - 1。
        // 提案的投票结果currentResult初始为0,在获得一张赞同票时,currentResult++,获得反对票时,currentResult--。一半赞同一半反对,currentResult最后为0。如果区间值设为0,意味该提案只需要半数人赞同便可通过。
        // 假如设置赞同票数的区间值为2,共有10张投票,如果要想提案通过,则说明至少有7张是赞同票。
        // 设定赞同票数的区间值
        int marginOfVotesForMajority

    )  payable public {
        // 设定投票规则
        changeVotingRules(minimumQuorumForProposals, minutesForDebate, marginOfVotesForMajority);
        // It’s necessary to add an empty first member
        addMember(0, "");
        // and let's add the founder, to save a step later
        addMember(owner, 'founder');
    }

    /**
     * Add member 添加一个成员,传入成员地址和名称
     * 限定了只有owner才能调用此方法
     *
     * Make `targetMember` a member named `memberName`
     *
     * @param targetMember ethereum address to be added
     * @param memberName public name for that member
     */
    function addMember(address targetMember, string memberName) onlyOwner public {
        uint id = memberId[targetMember];

        // 如果是新成员,将memberId设为members数组长度
        if (id == 0) {
            memberId[targetMember] = members.length;
            id = members.length++;
        }

        // 无论是否为新成员还是已有成员,都重新设置地址加入时间及姓名
        members[id] = Member({member: targetMember, memberSince: now, name: memberName});
        MembershipChanged(targetMember, true);
    }

    /**
     * Remove member 删除一个成员,传入成员地址
     * 限定了只有owner才能调用此方法
     *
     * @notice Remove membership from `targetMember`
     *
     * @param targetMember ethereum address to be removed
     */
    function removeMember(address targetMember) onlyOwner public {
        require(memberId[targetMember] != 0);

        for (uint i = memberId[targetMember]; i<members.length-1; i++){
            members[i] = members[i+1];
        }
        delete members[members.length-1];
        members.length--;
    }

    /**
     * Change voting rules 改变投票规则
     *
     * Make so that proposals need to be discussed for at least `minutesForDebate/60` hours,
     * 保证一个提案至少需要讨论的时间为`minutesForDebate/60`小时

     * have at least `minimumQuorumForProposals` votes, and have 50% + `marginOfVotesForMajority` votes to be executed
     * 提案需要的最少得票数和得票中的指定赞成票数才可被执行

     *
     * @param minimumQuorumForProposals how many members must vote on a proposal for it to be executed
     * 提案被执行的最少得票数
     *
     * @param minutesForDebate the minimum amount of delay between when a proposal is made and when it can be executed
     * 提案的最少投票时间
     *
     * @param marginOfVotesForMajority the proposal needs to have 50% plus this number
     * 提案需要50%赞同票加上这个区间值才可通过
     *
     */
    function changeVotingRules(
        uint minimumQuorumForProposals,
        uint minutesForDebate,
        int marginOfVotesForMajority
    ) onlyOwner public {
        minimumQuorum = minimumQuorumForProposals;
        debatingPeriodInMinutes = minutesForDebate;
        majorityMargin = marginOfVotesForMajority;

        ChangeOfRules(minimumQuorum, debatingPeriodInMinutes, majorityMargin);
    }

    /**
     * Add Proposal 增加提案
     *
     * Propose to send `weiAmount / 1e18` ether to `beneficiary` for `jobDescription`. `transactionBytecode ? Contains : Does not contain` code.
     *
     * @param beneficiary who to send the ether to
     * 受益人,如果提案顺利执行,可以获取到提案中的金额
     * @param weiAmount amount of ether to send, in wei
     * ether价格,单位是wei
     * @param jobDescription Description of job
     * 新提案的描述
     * @param transactionBytecode bytecode of transaction
     * 
     * 
     */
    function newProposal(
        address beneficiary,
        uint weiAmount,
        string jobDescription,
        bytes transactionBytecode
    )
        onlyMembers public
        returns (uint proposalID)
    {
        proposalID = proposals.length++;
        Proposal storage p = proposals[proposalID];
        p.recipient = beneficiary;
        p.amount = weiAmount;
        p.description = jobDescription;
        p.proposalHash = keccak256(beneficiary, weiAmount, transactionBytecode);
        p.votingDeadline = now + debatingPeriodInMinutes * 1 minutes;
        p.executed = false;
        p.proposalPassed = false;
        p.numberOfVotes = 0;
        ProposalAdded(proposalID, beneficiary, weiAmount, jobDescription);
        numProposals = proposalID+1;

        return proposalID;
    }

    /**
     * Add proposal in Ether 以ether为单位增加提案
     *
     * Propose to send `etherAmount` ether to `beneficiary` for `jobDescription`. `transactionBytecode ? Contains : Does not contain` code.
     * This is a convenience function to use if the amount to be given is in round number of ether units.
     *
     * @param beneficiary who to send the ether to
     * @param etherAmount amount of ether to send
     * @param jobDescription Description of job
     * @param transactionBytecode bytecode of transaction
     */
    function newProposalInEther(
        address beneficiary,
        uint etherAmount,
        string jobDescription,
        bytes transactionBytecode
    )
        onlyMembers public
        returns (uint proposalID)
    {
        return newProposal(beneficiary, etherAmount * 1 ether, jobDescription, transactionBytecode);
    }

    /**
     * Check if a proposal code matches
     * 检查是否指定提案的Hash值与传入的参数相等
     *
     * @param proposalNumber ID number of the proposal to query
     * @param beneficiary who to send the ether to
     * @param weiAmount amount of ether to send
     * @param transactionBytecode bytecode of transaction
     */
    function checkProposalCode(
        uint proposalNumber,
        address beneficiary,
        uint weiAmount,
        bytes transactionBytecode
    )
        constant public
        returns (bool codeChecksOut)
    {
        Proposal storage p = proposals[proposalNumber];
        return p.proposalHash == keccak256(beneficiary, weiAmount, transactionBytecode);
    }

    /**
     * Log a vote for a proposal 进行投票
     *
     * Vote `supportsProposal? in support of : against` proposal #`proposalNumber`
     *
     * @param proposalNumber number of proposal 提案号
     * @param supportsProposal either in favor or against it 支持还是反对
     * @param justificationText optional justification text 陈述意见
     */
    function vote(
        uint proposalNumber,
        bool supportsProposal,
        string justificationText
    )
        onlyMembers public
        returns (uint voteID)
    {
        // Get the proposal 
        // 获取提案
        Proposal storage p = proposals[proposalNumber];  

         // 如果投票时间已过,退出
        require(now < p.votingDeadline);

        // If has already voted, cancel  
        // 如果已经投过票,退出      
        require(!p.voted[msg.sender]);  

        // Set this voter as having voted
        // 设置为已投票
        p.voted[msg.sender] = true;      

        // Increase the number of votes
        // 为此提案增加票数
        p.numberOfVotes++;       

        // If they support the proposal
        // 支持
        if (supportsProposal) {      

        // Increase score
        // 分数加1                   
            p.currentResult++;                          
        } else {             

        // If they don't
        // Decrease the score
        // 反对,分数减1
            p.currentResult--;                          
        }

        // Create a log of this event
        Voted(proposalNumber,  supportsProposal, msg.sender, justificationText);
        return p.numberOfVotes;
    }

    /**
     * Finish vote 投票结束,执行提案
     *
     * Count the votes proposal #`proposalNumber` and execute it if approved
     * 清点某个提案的得票数,如果通过,执行此提案
     *
     * @param proposalNumber proposal number
     * @param transactionBytecode optional: if the transaction contained a bytecode, you need to send it
     * 可选参数。如果提案包含bytecode执行代码,需要执行此代码
     */
    function executeProposal(uint proposalNumber, bytes transactionBytecode) public {
        Proposal storage p = proposals[proposalNumber];

        // If it is past the voting deadline
        // 如果投票时间已过
        require(now > p.votingDeadline     

            // and it has not already been executed
            // 并且提案还未被执行                    
            && !p.executed     

            // and the supplied code matches the proposal
            // 并且传入的代码与提案中代码一致                                                    
            && p.proposalHash == keccak256(p.recipient, p.amount, transactionBytecode)  

            // and a minimum quorum has been reached...
            // 并且提案需要的投票数大于等于最小得票数
            && p.numberOfVotes >= minimumQuorum);                                  

        // ...then execute result

        if (p.currentResult > majorityMargin) {
            // Proposal passed; execute the transaction
            // 提案的结果大于赞同票区间值,提案通过,执行提案中的交易代码

            // Avoid recursive calling
            // 设置提案已经执行过了,以免递归执行
            p.executed = true; 

            require(p.recipient.call.value(p.amount)(transactionBytecode));

            p.proposalPassed = true;
        } else {
            // Proposal failed
            p.proposalPassed = false;
        }

        // Fire Events
        ProposalTallied(proposalNumber, p.currentResult, p.numberOfVotes, p.proposalPassed);
    }
}

部署

部署方法和之前一样。这里设置投票时间为5分钟,其他暂时不管,默认为0。

分享给他人

部署成功后,如果想将此合约分享给他人,你需要将合约地址以及JSON字符串复制给他。

复制地址

JSON字符串

观察合约

其他人可以通过在合约页面点击新增观察合约,然后输入合约地址及Json字符串进行观察。

使用合约

  • 在合约页面中,读取合约中的方法是免费的。它们只是读取区块上的信息。可以看到owner是创建合约的账号。

  • 右侧的写入合约由于要保存数据在区块上,所以会消耗一定的ether。

  • 在操作合约之前,先创建一个成员。注意执行方法的账号必须是创建合约的账号。

  • 这里没有一个用于显示成员的列表,但是你可以通过将某个账户地址放入读取合约中的方法Member id进行验证。

  • 如果你想让你的组织更加有公信力,可以尝试转入一些ether或者代币。

增加一个简单的提案:发送ether

  • 现在提交第一个提案到合约,在右侧选择函数列表中选择New Proposal

  • beneficiary中填写你想要发送ether的账户地址,然后在Wei Amount中填写你想要发送的数量。注意这里的单位是wei。然后在Job description中填写描述。Transaction bytecode暂时留空。执行后,可以看见左侧的Num proposals中数量变为1。如果提交多个提案,可以咋左侧的Proposals中填入提案编号来查看提案详情。

  • 在右侧选择vote函数进行投票。输入你想要投票的提案号,Supports proposal中勾选表示支持该提案,不选则表示反对。Justification text中可以填入描述。执行函数后投票成功。

  • 当投票时间已经截止,你可以选择executeProposal。如果提案只是简单的发送ether,transactionBytecode继续留空不填。点击执行后,注意看屏幕上显示的内容。
  • 出现数据无法被执行,说明executeProposal方法中有条件未被满足。根据方法中的代码,首先排查是否投票截止时间还未到。其次排查提案已被执行,通过合约界面的数据查看:
  • 投票截止时间已经过去了两小时。

  • Executed显示为NO。

其次再使用合约中的Check proposal code方法,输入新增提案的参数来查看是否hash值不一致:

方法返回YES,说明hash值没问题。

接着查看提案需要的投票数是否大于等于最终得票数:

提案需要的最少投票数是0票,而我们已经投过1票:

说明投票数没问题。

接着再看赞同票是否大于赞同票区间值:

提案设置的赞同票区间值是0票,而我们已经投过1张赞同票票:

说明赞同票数也符合条件。

所以这里不知道问题出在哪里导致方法无法执行。

增加一个复杂的提案:发送代币交易

现在来演示如何在新增提案时,附带一笔交易。之前新增提案时,Transaction bytecode一直留空,提案执行时并没有执行除了发送ether外的其他交易。

这里我们在新增提案时,附带一笔发送代币的交易信息。提案被执行时会向指定的账户发送指定数量的代币。

  • 在执行合约页面,复制数据中的内容。注意不要输入密码进行发送,这里只需要这笔交易的bytecode。
  • 接着回到我们的合约中,选择NewProposal

    1. beneficiary中填入你的代币的地址。注意是代币的地址,不是之前受益人的地址。并且留意icon是否与你的代币的icon一致。

    2. Ether amount留空。这里我们不发送ether而是发送代币。

    3. Job description仍然填描述内容。

    4. Transaction Bytecode填入刚才复制的内容。

在方法执行后,就可以到提案信息了。需要注意的是这里并没有显示transaction bytecode而是显示的Proposal hash。这是因为transaction bytecode可能会非常长,因此直接写入区块是一件非常奢侈的事情。所以这里对其进行了hash处理,对其长度进行了限制。

你可能会发现这里有一些安全漏洞:

  • 我怎么敢给一个看不见具体交易代码的提案投票呢?

  • 怎么阻止一个人使用不同的交易代码来执行一个已经通过的提案?

读取合约中的Check proposal code方法便在此时派上了用场。

  • 任何人都可以将方法参数放入这里,来确保它们和当前投票中的提案的参数是否一致。

  • 除非输入的代码与交易的bytecode的hash一致,否则提案不会被执行。

剩下的就和前面的例子一样,所有成员都可以进行投票。投票截止时间到了之后,某人可以执行此合约。唯一不同的是,这次执行提案的人必须提供之前提交的交易代码。也就是对执行人进行了限制,不是所有人都可以执行提案了。

根据股东的持股比例(权重)进行投票

在上面的例子中,我们采用的是类似邀请制的机制,邀请和禁止成员都是由一个中心化人物,类似总统,董事长,主席,来决定的。但是有一些缺陷:

  • 假如有人想要改变自己的主账号地址,应该怎么办?

  • 假如需要设置某人有更大权重的决定权,应该怎么办?

  • 假如实际上你想要在一个公开市场上交换、交易或分享成员资格,应该怎么办?

  • 假如你希望组织里的股东们能做出一致决定,应该怎么办?

我们会对合约进行一些修改,使用一种特殊的代币对它进行关联。创建一种初始值为100的代币,小数位是0,代币符号为百分比%。这个表示的是总的股份比例为100%,每个人的持股可能有多有少。比例高的意味着权重大,有更高的话语权。
如果希望保留小数位,可以为小数位加入几个0。然后部署代币合约,并复制代币地址。

股份制投票代码

pragma solidity ^0.4.16;

contract owned {
    address public owner;

    function owned() {
        owner = msg.sender;
    }

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }

    function transferOwnership(address newOwner) onlyOwner {
        owner = newOwner;
    }
}

contract tokenRecipient {
    event receivedEther(address sender, uint amount);
    event receivedTokens(address _from, uint256 _value, address _token, bytes _extraData);

    function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData){
        Token t = Token(_token);
        require(t.transferFrom(_from, this, _value));
        receivedTokens(_from, _value, _token, _extraData);
    }

    function () payable {
        receivedEther(msg.sender, msg.value);
    }
}

contract Token {
    mapping (address => uint256) public balanceOf;
    function transferFrom(address _from, address _to, uint256 _value) returns (bool success);
}

/**
 * The shareholder association contract itself
 */
contract Association is owned, tokenRecipient {

    uint public minimumQuorum;
    uint public debatingPeriodInMinutes;
    Proposal[] public proposals;
    uint public numProposals;
    Token public sharesTokenAddress;

    event ProposalAdded(uint proposalID, address recipient, uint amount, string description);
    event Voted(uint proposalID, bool position, address voter);
    event ProposalTallied(uint proposalID, uint result, uint quorum, bool active);
    event ChangeOfRules(uint newMinimumQuorum, uint newDebatingPeriodInMinutes, address newSharesTokenAddress);

    struct Proposal {
        address recipient;
        uint amount;
        string description;
        uint votingDeadline;
        bool executed;
        bool proposalPassed;
        uint numberOfVotes;
        bytes32 proposalHash;
        Vote[] votes;
        mapping (address => bool) voted;
    }

    struct Vote {
        bool inSupport;
        address voter;
    }

    // Modifier that allows only shareholders to vote and create new proposals
    // 需要持有代币才可进行投票(意味着需要有股份才可投票)
    modifier onlyShareholders {
        require(sharesTokenAddress.balanceOf(msg.sender) > 0);
        _;
    }

    /**
     * Constructor function
     *
     * First time setup
     */
    function Association(Token sharesAddress, uint minimumSharesToPassAVote, uint minutesForDebate) payable {
        changeVotingRules(sharesAddress, minimumSharesToPassAVote, minutesForDebate);
    }

    /**
     * Change voting rules
     *
     * Make so that proposals need to be discussed for at least `minutesForDebate/60` hours
     * and all voters combined must own more than `minimumSharesToPassAVote` shares of token `sharesAddress` to be executed
     *
     * @param sharesAddress token address
     * @param minimumSharesToPassAVote proposal can vote only if the sum of shares held by all voters exceed this number
     * @param minutesForDebate the minimum amount of delay between when a proposal is made and when it can be executed
     *
     *
     * minimumSharesToPassAVote 定义了如果提案执行需要的最少股份比例。
     *
     */
    function changeVotingRules(Token sharesAddress, uint minimumSharesToPassAVote, uint minutesForDebate) onlyOwner {
        sharesTokenAddress = Token(sharesAddress);
        if (minimumSharesToPassAVote == 0 ) minimumSharesToPassAVote = 1;
        minimumQuorum = minimumSharesToPassAVote;
        debatingPeriodInMinutes = minutesForDebate;
        ChangeOfRules(minimumQuorum, debatingPeriodInMinutes, sharesTokenAddress);
    }

    /**
     * Add Proposal
     *
     * Propose to send `weiAmount / 1e18` ether to `beneficiary` for `jobDescription`. `transactionBytecode ? Contains : Does not contain` code.
     *
     * @param beneficiary who to send the ether to
     * @param weiAmount amount of ether to send, in wei
     * @param jobDescription Description of job
     * @param transactionBytecode bytecode of transaction
     */
    function newProposal(
        address beneficiary,
        uint weiAmount,
        string jobDescription,
        bytes transactionBytecode
    )
        onlyShareholders
        returns (uint proposalID)
    {
        proposalID = proposals.length++;
        Proposal storage p = proposals[proposalID];
        p.recipient = beneficiary;
        p.amount = weiAmount;
        p.description = jobDescription;
        p.proposalHash = sha3(beneficiary, weiAmount, transactionBytecode);
        p.votingDeadline = now + debatingPeriodInMinutes * 1 minutes;
        p.executed = false;
        p.proposalPassed = false;
        p.numberOfVotes = 0;
        ProposalAdded(proposalID, beneficiary, weiAmount, jobDescription);
        numProposals = proposalID+1;

        return proposalID;
    }

    /**
     * Add proposal in Ether
     *
     * Propose to send `etherAmount` ether to `beneficiary` for `jobDescription`. `transactionBytecode ? Contains : Does not contain` code.
     * This is a convenience function to use if the amount to be given is in round number of ether units.
     *
     * @param beneficiary who to send the ether to
     * @param etherAmount amount of ether to send
     * @param jobDescription Description of job
     * @param transactionBytecode bytecode of transaction
     */
    function newProposalInEther(
        address beneficiary,
        uint etherAmount,
        string jobDescription,
        bytes transactionBytecode
    )
        onlyShareholders
        returns (uint proposalID)
    {
        return newProposal(beneficiary, etherAmount * 1 ether, jobDescription, transactionBytecode);
    }

    /**
     * Check if a proposal code matches
     *
     * @param proposalNumber ID number of the proposal to query
     * @param beneficiary who to send the ether to
     * @param weiAmount amount of ether to send
     * @param transactionBytecode bytecode of transaction
     */
    function checkProposalCode(
        uint proposalNumber,
        address beneficiary,
        uint weiAmount,
        bytes transactionBytecode
    )
        constant
        returns (bool codeChecksOut)
    {
        Proposal storage p = proposals[proposalNumber];
        return p.proposalHash == sha3(beneficiary, weiAmount, transactionBytecode);
    }

    /**
     * Log a vote for a proposal
     *
     * Vote `supportsProposal? in support of : against` proposal #`proposalNumber`
     *
     * @param proposalNumber number of proposal
     * @param supportsProposal either in favor or against it
     */
    function vote(
        uint proposalNumber,
        bool supportsProposal
    )
        onlyShareholders
        returns (uint voteID)
    {
        Proposal storage p = proposals[proposalNumber];
        
        require(now < p.votingDeadline);
        require(p.voted[msg.sender] != true);

        voteID = p.votes.length++;
        p.votes[voteID] = Vote({inSupport: supportsProposal, voter: msg.sender});
        p.voted[msg.sender] = true;
        p.numberOfVotes = voteID +1;
        Voted(proposalNumber,  supportsProposal, msg.sender);
        return voteID;
    }

    /**
     * Finish vote
     *
     * Count the votes proposal #`proposalNumber` and execute it if approved
     *
     * @param proposalNumber proposal number
     * @param transactionBytecode optional: if the transaction contained a bytecode, you need to send it
     */
    function executeProposal(uint proposalNumber, bytes transactionBytecode) {
        Proposal storage p = proposals[proposalNumber];

        require(now > p.votingDeadline                                             // If it is past the voting deadline
            && !p.executed                                                          // and it has not already been executed
            && p.proposalHash == sha3(p.recipient, p.amount, transactionBytecode)); // and the supplied code matches the proposal...


        // ...then tally the results
        // 统计投票结果
        // 计算投票人的股份比例总和
        // 计算赞同票数比例和反对票数比例
        uint quorum = 0;
        uint yea = 0;
        uint nay = 0;

        for (uint i = 0; i <  p.votes.length; ++i) {
            Vote storage v = p.votes[i];
            uint voteWeight = sharesTokenAddress.balanceOf(v.voter);
            quorum += voteWeight;
            if (v.inSupport) {
                yea += voteWeight;
            } else {
                nay += voteWeight;
            }
        }

        // Check if a minimum quorum has been reached
        // 投票者的股份比例总和必须达到执行提案的最低要求
        require(quorum >= minimumQuorum); 

        // 如果赞同票数比例大于反对票数比例,则执行提案
        if (yea > nay ) {
            // Proposal passed; execute the transaction

            p.executed = true;
            require(p.recipient.call.value(p.amount)(transactionBytecode));

            p.proposalPassed = true;
        } else {
            // Proposal failed
            p.proposalPassed = false;
        }

        // Fire Events
        ProposalTallied(proposalNumber, yea - nay, quorum, p.proposalPassed);
    }
}

代码分析

代码和之前的很像,不同的是这次需要代币合约的地址,以便股东行使投票权。

我们定义了一个代币合约,提供一个balanceOf方法,以便获取股东的权重占比。

contract Token { mapping (address => uint256) public balanceOf; }

然后我们定义了一个类型为token的变量,将部署在区块链上的代币合约的地址指向它。在以太坊中,这是一种让合约之间交互的最简单的方式。

contract Association {
    token public sharesTokenAddress;
// ...
function Association(token sharesAddress, uint minimumSharesForVoting, uint minutesForDebate) {
        sharesTokenAddress = token(sharesAddress);

定义了如果提案执行需要的最少股份比例。

这里不再像之前需要最少投票数,而是根据投票人的股份比例总和来决定,看是否达到了提案执行的要求。

    /**
     * Change voting rules
     *
     * Make so that proposals need to be discussed for at least `minutesForDebate/60` hours
     * and all voters combined must own more than `minimumSharesToPassAVote` shares of token `sharesAddress` to be executed
     *
     * @param sharesAddress token address
     * @param minimumSharesToPassAVote proposal can vote only if the sum of shares held by all voters exceed this number
     * @param minutesForDebate the minimum amount of delay between when a proposal is made and when it can be executed
     *
     *
     * minimumSharesToPassAVote 定义了如果提案执行需要的最少股份比例。
     *
     */
    function changeVotingRules(Token sharesAddress, uint minimumSharesToPassAVote, uint minutesForDebate) onlyOwner {
        sharesTokenAddress = Token(sharesAddress);
        if (minimumSharesToPassAVote == 0 ) minimumSharesToPassAVote = 1;
        minimumQuorum = minimumSharesToPassAVote;
        debatingPeriodInMinutes = minutesForDebate;
        ChangeOfRules(minimumQuorum, debatingPeriodInMinutes, sharesTokenAddress);
    }

执行提案

首先检查投票人持股比例是否达标,其次统计赞同票数比例和反对票数比例。如果赞同票数比例大于反对票数比例,就执行提案。跟之前的投票系统相比,不再需要最低投票数和赞同票的区间值。

    /**
     * Finish vote
     *
     * Count the votes proposal #`proposalNumber` and execute it if approved
     *
     * @param proposalNumber proposal number
     * @param transactionBytecode optional: if the transaction contained a bytecode, you need to send it
     */
    function executeProposal(uint proposalNumber, bytes transactionBytecode) {
        Proposal storage p = proposals[proposalNumber];

        require(now > p.votingDeadline                                             // If it is past the voting deadline
            && !p.executed                                                          // and it has not already been executed
            && p.proposalHash == sha3(p.recipient, p.amount, transactionBytecode)); // and the supplied code matches the proposal...


        // ...then tally the results
        // 统计投票结果
        // 计算投票人的股份比例总和
        // 计算赞同票数比例和反对票数比例
        uint quorum = 0;
        uint yea = 0;
        uint nay = 0;

        for (uint i = 0; i <  p.votes.length; ++i) {
            Vote storage v = p.votes[i];
            uint voteWeight = sharesTokenAddress.balanceOf(v.voter);
            quorum += voteWeight;
            if (v.inSupport) {
                yea += voteWeight;
            } else {
                nay += voteWeight;
            }
        }

        // Check if a minimum quorum has been reached
        // 投票者的股份比例总和必须达到执行提案的最低要求
        require(quorum >= minimumQuorum); 

        // 如果赞同票数比例大于反对票数比例,则执行提案
        if (yea > nay ) {
            // Proposal passed; execute the transaction

            p.executed = true;
            require(p.recipient.call.value(p.amount)(transactionBytecode));

            p.proposalPassed = true;
        } else {
            // Proposal failed
            p.proposalPassed = false;
        }

        // Fire Events
        ProposalTallied(proposalNumber, yea - nay, quorum, p.proposalPassed);
    }
  • 赞同/反对票不再以票数决定,而是以持股 比例决定。假设一个持有51%的大股东投出赞同票,仍然大于持有49%股份的数个小股东投出的一致反对票。

  • 这里还有个需要注意的地方是,在投票阶段(vote)中,其实这里的票数以及对应的赞同/反对比例,不一定是最终的结果。因为每个人都不断可以收到其他人发来的代币,或者发给其他人自己的代币,这意味着他的股份在不断变化,他的意见左右投票最终结果在加强或者弱化。有可能最后他的股份全部发送给了别人,那他投票时的意见应该一文不值。所以我们在其投票时,只记录了他的地址及意见。执行提案时,才来统计此人此时的比例。这意味着不到最后一刻(执行提案时),你永远不知道提案的投票结果是什么。

未完待续... ...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容