本文基于作者阅读官方教程,根据理解而写,本身就有一些不太明白的地方需要解惑,只能作为一定的参考。另外如有疏漏请指正,感谢!
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合约
定义变量
最小投票数:一个提案需要设置最小投票数。如果没有达到规定的投票数量,说明该提案是没有太大意义的。
投票时间:设置一个允许投票的时间。到期后不允许投票。
-
设定赞同票数的区间值:
一个投票可能是赞同或者反对。
如果想让一个提案通过,必须得到票数的50%加上这个设定的区间值。如果只要50%是赞同票就能通过,设为0就好了。如果要求全票通过,则设为members - 1。
提案的投票结果currentResult初始为0,在获得一张赞同票时,currentResult++,获得反对票时,currentResult--。一半赞同一半反对,currentResult最后为0。如果区间值设为0,意味该提案只需要半数人赞同便可通过。
假如设置赞同票数的区间值为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
。beneficiary
中填入你的代币的地址。注意是代币的地址,不是之前受益人的地址。并且留意icon是否与你的代币的icon一致。Ether amount
留空。这里我们不发送ether而是发送代币。Job description
仍然填描述内容。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)中,其实这里的票数以及对应的赞同/反对比例,不一定是最终的结果。因为每个人都不断可以收到其他人发来的代币,或者发给其他人自己的代币,这意味着他的股份在不断变化,他的意见左右投票最终结果在加强或者弱化。有可能最后他的股份全部发送给了别人,那他投票时的意见应该一文不值。所以我们在其投票时,只记录了他的地址及意见。执行提案时,才来统计此人此时的比例。这意味着不到最后一刻(执行提案时),你永远不知道提案的投票结果是什么。