以太坊的官網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來共同對發起人的行爲進行決策。