以太坊的官网https://ethereum.org/上只有4个主要部分:
钱包客户端下载、token创建、ICO发行、投票系统DAO。
但实际上ICO的发行需要token的创建和投票系统DAO的基础。
以ICO(在网站上被称作crowdsale)为目的,简要翻译一下官网内容,具体的图片就不贴了,可以自行对照官网阅读。
一、以太坊上的crowdsale(ICO)实现
以太坊在官网文档中介绍了如何使用solidity来实现所谓的“crowdsale”(其实就是ICO)。即在项目正式推出之前销售token,来为这个项目的开发筹集资金。买家在crowdsale买了token后,可以把token拿到公开市场上购买和出售,获得独立于应用程序自己的市场价值。总的来说,crowdsale就是先在以太坊的框架下,先创建自己的货币token,然后通过出售这种token融资。个人认为以太坊本身也就是一次大型的crowdsale,只不过以太坊成功在众多山寨币中脱颖而出,成为仅此于比特币的虚拟货币,相较其他林林总总的token更让人放心。
和众筹不一样的是,crowdsale能够让那些投资者很快将持有的token在数字货币交易所中进行交易。另外一个巨大的不同点,就是crowdsale的是建立在使用token作为凭证的基础上,同时发行者持有自己众筹项目的一部分股份。总的来说这个所谓的crowdsale还是ICO的一种好听的说法,我感觉官网的措辞也在刻意回避ICO这个概念。
Crowdsale虽然载体是货币,但是核心原则是和传统意义上的众筹是一样的:如果众筹失败,资金将会返还给赞助者;如果众筹成功,资金则交付给众筹发起人。同时,它应该满足以下的要求:首先,错过众筹deadline的人不能再投资进来;第二,已经投资的人不能退款,但是它可以把自己投资的凭证(token)转让给别人。
Crowdsale基础有两个:①token的发行和②自治组织DAO(其实就是多个人之间,公平地做出决策的系统,或者说投票系统)。Token的发行之前已经学过,具体功能也就是基本的转账和发行。而自治组织(DAO,Decentralized Autonomous Organization),最基本的功能就是通过投票决定提案是否通过的过程。
1. token的实现
简要介绍一下Token发行代码的功能。
n 设置初始token总量
n 用户之间的转账
n 中心化的用户资产的管理;通过挖矿难度对币值的管理。
n 类似股票交易的买入、卖出。
n PoW的设置。以太坊推荐使用“Casper”来替代PoW,Casper是一种PoS下注机制:要求验证人对共识结果进行下注。而共识结果又通过验证人的下注情况形成:验证人必须猜测其他人会赌哪个块胜出,同时也下注这个块。如果赌对了,他们就可以拿回保证金外加交易费用。PoW共识同样可以理解为是一个下注机制:矿工选择一个块基于它进行挖矿,也就是赌这个块会成为主链的一部分;如果赌对了,他可以收到奖励,而如果赌错了,他会损失电费。
2. 自治组织DAO的实现
自治系统的执行过程:
n 在一台机器A上部署好合约,在另外一台机器B上输入地址来观测这个合约。如果A发起了一个提案,比如要转一笔钱出去,那么B在自己的客户端可以选择同意或者不同意。
n 另外,如果试图用自己的token来进行交易,需要使用在两台机器上验证命令的hash来实现,因为原本的命令代码长度过长,全存到区块链上太浪费。
自治系统主要函数的解读:
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) public returns (bool success); } | 这部分对应自拟货币token的部分操作。
Owned: 存在一位中心化的管理员。
有些函数只有owner才能操作。
Owner的权限可以转移。
TokenRecipient: 管理转账的过程。 |
contract Congress is owned, tokenRecipient { // Contract Variables and events uint public minimumQuorum; uint public debatingPeriodInMinutes; int public majorityMargin; Proposal[] public proposals; uint public numProposals; mapping (address => uint) public memberId; Member[] public members;
event ProposalAdded(uint proposalID, address recipient, uint amount, string description); event Voted(uint proposalID, bool position, address voter, string justification); 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); _; } | Congress合约的变量定义和event。 一个合约相当于一个类。
这些Event帮助用户在客户端中用按钮的方式来快捷操作。
例如投票时只需要在voted菜单下选择是否同意就可以了。
proposal的结构体包含: 提案参与者, 人数, 提案描述, 截止时间 当前票数等等。
Member的结构体包括: Member的 地址, 名字, 加入时间。
Vote的结构体包括: 是否支持, 支持者的名字, Justification是可选项,投票者可以选择填写自己投票的原因。
|
function Congress ( uint minimumQuorumForProposals, uint minutesForDebate, 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'); }
| Congress的构造函数。
要定义的包括 提案参与人数, 达成共识所需的人数, 持续时间。,
|
function addMember(address targetMember, string memberName)onlyOwner public { uint id = memberId[targetMember]; if (id == 0) { memberId[targetMember] = members.length; id = members.length++; }
members[id] = Member({member: targetMember, memberSince: now, name: memberName}); MembershipChanged(targetMember, true); } | addMember()接受两个参数:新成员的地址和名字。将他们加到数组里。 |
removeMember(); | 删除成员的函数。
|
function changeVotingRules( uint minimumQuorumForProposals, uint minutesForDebate, int marginOfVotesForMajority ) onlyOwner public { minimumQuorum = minimumQuorumForProposals; debatingPeriodInMinutes = minutesForDebate; majorityMargin = marginOfVotesForMajority;
ChangeOfRules(minimumQuorum, debatingPeriodInMinutes, majorityMargin); }
| changeVotingRule改变投票规则。
输入3个参数: 参与表决的人数, 持续时间, 表决通过的目标人数。
直接修改即可。 |
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; }
| newProposal建立一个新提案。
内容很简单:是否要转移一定数量的以太币到某个账户中去?
这个提案的transactionBytecode为空。 |
function newProposalInEther(); | 和上一个函数作用一样, 只不过这次转账的单位是以太币, 上一个函数的单位是以太币的最小单位wei。 |
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); }
| checkProposalCode函数用来对比提案的hash是否相同。 |
function vote( uint proposalNumber, bool supportsProposal, string justificationText ) onlyMembers public returns (uint voteID) { Proposal storage p = proposals[proposalNumber]; // Get the proposal require(!p.voted[msg.sender]); // If has already voted, cancel p.voted[msg.sender] = true; // Set this voter as having voted p.numberOfVotes++; // Increase the number of votes if (supportsProposal) { // If they support the proposal p.currentResult++; // Increase score } else { // If they don't p.currentResult--; // Decrease the score } // Create a log of this event Voted(proposalNumber, supportsProposal, msg.sender, justificationText); return p.numberOfVotes; }
| Vote函数接受vote结构体中的三个参数,即 1. 提案代号, 2. 是否支持 3. 支持原因
如果支持的话,那么总支持人数++。
可以选择发送自己的支持(反对)原因。 |
function executeProposal(uint proposalNumber, bytes transactionBytecode) public { 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 == keccak256(p.recipient, p.amount, transactionBytecode) // and the supplied code matches the proposal && p.numberOfVotes >= minimumQuorum); // and a minimum quorum has been reached...
// ...then execute result
if (p.currentResult > majorityMargin) { // Proposal passed; execute the transaction
p.executed = true; // Avoid recursive calling 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); } } | executeProposal即最后的唱票环节。
如果 1. 时间在ddl之前 2. 没被强制取消 3. Hash对的上 4. 支持人数够
那么提案即通过。 |
以上就是一个最基本的投票过程了。实际上还可以做以下改进:
l 如果把自制token的符号设置为%,总量设置为100,那么就可以用这个权重来表示每个投票者的重要程度,比如按股权分配投票。
l 投票者可以更改自己的投票结果。
l 投票者可以到市场上出售,转让自己的投票权。
l 另外建一个contract,用来管理这个contract的owner——这样就解决了管理者权力太大的问题。
l 还可以把自己的投票权托付给另外一名投票者,他的选择就代表我的选择。
l 一个人也可以发起一个提案,但是要把时间作为能否执行一部分。也就是说,如果投票同意的人数越少,这个提案可能要几年之后才能通过;但是如果投票人数很多,几分钟之后便默认通过。
3. crowdsale的实现
Crowdsale要解决的主要问题:首先如何判断一个项目是否成功还是失败?根据之前的投票系统,可以使用统计票数类似的函数来判断是否达到了众筹目标,唯一的区别是,之前判断票数是不是够数,这次是判断钱到不到数。第二个问题是资金如何管理?要有监督者对已经筹集到的钱进行监督。这个监督者在之前是各个投票者,现在就是每个投资者,根据每个人的股份不同投票的占比不同。
首先,根据前面的两步创建一个token和一个DAO。Token的总量为100个,DAO提案的目标需要10个投票。Token的作用是给参与者凭证——当然token本身也可以作为奖励。通过DAO,众筹参与的过程其实就等同于向提案投票的过程。
之后就可以直接copy代码建立合约了。
pragma solidity ^0.4.16;
interface token { function transfer(address receiver, uint amount); } | Interface是一段提示contract内容的语句 。 |
contract Crowdsale { address public beneficiary; uint public fundingGoal; uint public amountRaised; uint public deadline; uint public price; token public tokenReward; mapping(address => uint256) public balanceOf; bool fundingGoalReached = false; bool crowdsaleClosed = false;
event GoalReached(address recipient, uint totalAmountRaised); event FundTransfer(address backer, uint amount, bool isContribution);
modifier afterDeadline() { if (now >= deadline) _; }
| 合约crowdsale。
主要变量包括: 众筹目标, 已筹得的钱, DDL, Token奖励等。 |
function Crowdsale( address ifSuccessfulSendTo, uint fundingGoalInEthers, uint durationInMinutes, uint etherCostOfEachToken, address addressOfTokenUsedAsReward ) { beneficiary = ifSuccessfulSendTo; fundingGoal = fundingGoalInEthers * 1 ether; deadline = now + durationInMinutes * 1 minutes; price = etherCostOfEachToken * 1 ether; tokenReward = token(addressOfTokenUsedAsReward); }
| 构造函数。接受四个参数: 1. 收款人,即如果众筹成功,收款人的地址。 2. 截止时间。 3. 每个token的售价。 4. 作为奖励的Token地址。
注意这其中的单位,这些单位都是编程语言预先定义好的神奇变量。 |
function () payable { require(!crowdsaleClosed); uint amount = msg.value; balanceOf[msg.sender] += amount; amountRaised += amount; tokenReward.transfer(msg.sender, amount / price); FundTransfer(msg.sender, amount, true); }
modifier afterDeadline() { if (now >= deadline) _; }
| 这个没有名字的函数是一个默认函数,每当有人来送钱就调用这个函数。
功能就是把送来的钱加到总额里。 |
function checkGoalReached() afterDeadline { if (amountRaised >= fundingGoal){ fundingGoalReached = true; GoalReached(beneficiary, amountRaised); } crowdsaleClosed = true; }
| checkGoalReached()检测有没有达到众筹目标。如果达到了,关闭众筹。 |
function safeWithdrawal() afterDeadline { if (!fundingGoalReached) { uint amount = balanceOf[msg.sender]; balanceOf[msg.sender] = 0; if (amount > 0) { if (msg.sender.send(amount)) { FundTransfer(msg.sender, amount, false); } else { balanceOf[msg.sender] = amount; } } }
if (fundingGoalReached && beneficiary == msg.sender) { if (beneficiary.send(amountRaised)) { FundTransfer(beneficiary, amountRaised, false); } else { //If we fail to send the funds to beneficiary, unlock funders balance fundingGoalReached = false; } } } } | safeWithdrawal() 如果目标没能达成,那么所有的款项都将退回资助者的账户中。
如果目标达成,但是钱无法打到发起人账户中,结果同样是退回资助者手中。 |
这就建立了最基本的众筹系统,此外还要进行一些改进:
l 可以在达到众筹目标之后不停止筹款,让更多想加入的人加入。
l 调整无名函数,这个函数每当有人来送钱的时候就自动调用一次,调整之后的函数通过监视捐款额来防止51%攻击。
l 根据以太币的block号而非自然时间来确定众筹的ddl。由于以太坊大约17秒产生一个区块,使用公式current_block_number+ duration_in_minutes * 60 / 17 + buffer来确定截止块号。
众筹成功之后,所有的参与者可以通过DAO来共同对发起人的行为进行决策。