編者按:本文系 登鏈科技CTO 熊麗兵 講師,在由掘金技術社區主辦,以太坊社區基金會、以太坊愛好者與 ConsenSys 協辦的《開發者的以太坊入門指南 | Jeth 第一期 - 北京場》 活動上的分享整理。Jeth 圍繞以太坊技術開發主題的系列線下活動。每期 Jeth 會邀請以太坊開發領域的優秀技術團隊和工程師在線下分享技術乾貨。旨在爲開發者提供線下技術交流互動機會,幫助開發者成長。前端
智能合約全棧介紹 - Howard | Jeth 第一期git
以太坊智能合約 + DApp 從入門到上線:來自前端工程師的實戰指南 - 王仕軍 | Jeth 第一期github
熊麗兵老師目前在登鏈科技擔任 CTO,是全網訪問量最大的區塊鏈技術博客《深刻淺出區塊鏈》博主,對底層公鏈技術,區塊鏈技術落地都有深刻研究。熊老師曾前後加入創新工場及獵豹移動,全面負責數款千萬級用戶開發及管理工做,2014年做爲技術合夥人參與建立酷吧時代科技,2016年起重心投入區塊鏈技術領域。編程
很高興參加掘金技術社區此次舉辦的《開發者的以太坊入門指南》活動,今天我帶來的分享主題是經過代幣和衆籌來介紹智能合約的開發。我先作一下自我介紹,我叫熊麗兵,應該有一些人看過個人博客《深刻淺出區塊鏈》,我如今在登鏈科技擔任 CTO。bash
我今天分享內容分爲圖上的四個部分,我是最後一個作分享的講師,相信在場的觀衆堅持到如今有點疲憊,可是個人內容很是實用,有不少人拿個人代碼已經籌了很多錢,或許個人代碼對你也有幫助,但願你們能認真聽。微信
代幣,幣其實就是錢,代幣能夠代替錢——這是我給代幣下的一個定義。從這個定義出發,無論是比特幣仍是以太幣都算代幣。咱們今天要講的這個代幣是基於以太坊的智能合約開發出來的。代幣不僅僅是能夠代替錢,還能夠代替不少的東西,能夠代替積分,也能夠代替一本書或一首歌,以上的均可以用以太坊智能合約來代替。前端工程師
智能合約就是以太坊上的程序,是代碼和數據(狀態)的集合。 智能合約跟人工智能的「智能」是沒有關係的,智能合約並不智能。智能合約最先是尼克薩博提出來的一個概念,是指把法律條文程序化,這個理念和以太坊上的程序很是相似,由於法律條文的執行不該該受到任何人的干涉與干擾;以太坊的程序也是同樣的,只要你寫好代碼之後沒有任何人能夠干擾以太坊程序的運行。數據結構
智能合約有不少編程語言,最多見、被官方推薦的是 Solidity 語言,這個語言的後綴是 .sol 。上圖是一個簡單的 Hello World,其中 contract
對應着合約,咱們能夠把它理解爲一個「類」,若是把 contract
變成 class
,它就是定義一個「類」了,這跟咱們寫別的語言定一個「類」有點類似。所以這個合約的名字就叫作 Hello World,做用是返回一個字符串,這是一個最簡單的智能合約。寫合約的時候不像其餘語言的有main
方法,Solidity 中是沒有的,每個函數都是單獨要用的。app
咱們再來看看如何來實現一個代幣,實現一個代幣最根本的是要維護一個賬本。這裏有一個簡單的賬本,尾號122賬戶裏面有餘額100,尾號123帳戶有120,若是咱們要執行從尾號122的帳戶轉帳10塊錢到尾號123的賬戶的時候,是否是從這個100裏面減掉10,從120裏面加上10。若是要實現這樣一個賬本應該怎麼作?你們想想,咱們是否是能夠把賬戶這部分(帳號)當成 key,把餘額當成 value,這就是 」鍵值對「,就是一個Map,或者叫字典。編程語言
Solidity裏面有一個數據結構叫做 mapping ,咱們能夠用這個關鍵字 mapping 這樣的結構來保存賬本信息,這裏 Key 是一個地址或一個帳號。 value 表示餘額。 另外,咱們要發幣的話要設置發行量。以及咱們須要有一個轉帳函數,從一個地址轉到另一個地址。
咱們看一下如何實現最簡單的代幣,咱們來看一下這個代幣的代碼,它只有15行。它定義了一個叫做My Token的合約,它用了mapping
鍵值對的數據結構,這個類型的鍵是address(地址類型),值是 uint 無符號型整數的。變量的名字叫做 balanceOf
,balance 在這裏指代餘額。那咱們這個合約裏面有兩個方法,一個叫作構造函數,另一個是轉賬的方法。構造函數來初始化發行量,最初的時候全部的發行的貨幣都在 owner 手裏,咱們經過msg.sender得到當前建立合約的人是誰。剛開始發行的時候,全部的代幣都在 owner 本身手裏,這個就像央行發行貨幣的時候,剛開始貨幣都在央行手裏是同樣的。
而後另一個方法就是transfer
,有兩個參數:接受者的地址,以及發送的金額。看看具體的實現,第一個用來判斷交易條件,即判斷他有沒有足夠的錢完成交易。第二步是作一個溢出的判斷,待會兒後面會有一個分析,就是目標這個賬戶加上這個餘額要大於他原來的賬戶。假如說目標賬戶的餘額接近這個存儲的上限,他加上一個值他可能會發生溢出,咱們這裏要判斷是否會出現這種狀況。
第三步和第四步就是作簡單的減法和加法,在原賬戶裏面減去或加上一個金額,綜上咱們經過這十幾行的代碼就實現了代幣。
咱們接下來看一下 ERC-20,咱們剛剛已經實現了這個代幣,爲何還要有 ERC-20?若是錢包要支持代幣的轉賬也好,獲取代幣的名字也罷,都須要有統一的名字。ERC-20 實際上是一個規範,你們能夠點開上方 GitHub 連接中查看規範的具體內容。 ERC-20 包含了名稱、發行量、統一的轉賬名稱、受權的函數名以及事件名。
pragma solidity 0.4.20;
contract ERC20Interface {
string public name;
string public symbol;
uint8 public decimals;
uint public totalSupply;
function transfer(address _to, uint256 _value) returns (bool success); function transferFrom(address _from, address _to, uint256 _value) returns (bool success); function approve(address _spender, uint256 _value) returns (bool success); function allowance(address _owner, address _spender) view returns (uint256 remaining); event Transfer(address indexed _from, address indexed _to, uint256 _value); event Approval(address indexed _owner, address indexed _spender, uint256 _value); } 複製代碼
這個是ERC20的一個接口文件,咱們來具體看一下它有哪些內容:
name
是咱們須要指定名字,好比說咱們要發生一個掘金Token,它的名字叫作掘金Token。
symbol
是 代幣的符號,如常見的 BTC、ETH 等。
decimal
是代幣最少交易的單位,它表示小數點的位數,若是最少能夠交易0.1個代幣的話,小數點位數就是1;假如最少交易一個代幣,就沒有小數點,那這個值就是0。
totalSupply
是指總髮行量。
下面幾個方法是用來進行轉賬的: transfer
指代轉帳的目標地址,它會提供一個返回值是否轉帳成功。
transferFrom
由被委託人調用,由被委託人轉移受權人的貨幣,
approve
是把權限委託給別人,括號裏寫的是被委託人的地址和被委託了多大的金額
allowance
能夠返回被受權委託的金額,委託人能夠查詢剩下的金額。
Transfer
和Approval
能夠監聽並記錄事件信息,當事件發生時你能夠獲得通知。
接下來演示具體的 ERC-20 代碼,由於ERC-20 Token的代碼有點長,咱們切換到remix看一下代碼:
contract ERC20 is ERC20Interface {
mapping (address => uint256) public balanceOf;
mapping (address => mapping (address => uint256)) internal allowed;
constructor() public {
totalSupply = 1000;
name = "JueJin Token";
symbol = "JJT";
decimals = 0;
balanceOf[msg.sender] = totalSupply;
}
function balanceOf(address _owner) view returns (uint256 balance) {
return balanceOf[_owner];
}
function transfer(address _to, uint _value) public returns (bool success) {
require(_to != address(0));
require(_value <= balanceOf[msg.sender]);
require(balanceOf[_to] + _value >= balanceOf[_to]);
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {
require(_to != address(0));
require(_value <= balanceOf[_from]);
require(_value <= allowed[_from][msg.sender]);
require(balanceOf[_to] + _value >= balanceOf[_to]);
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowed[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
}
function approve(address _spender, uint256 _value) returns (bool success) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
function allowance(address _owner, address _spender) view returns (uint256 remaining) {
return allowed[_owner][_spender];
}
}
複製代碼
這個就是標準的ERC-20的代碼,它首先 import
了一個接口文件,咱們經過 is
這個方式實現這樣一個接口,就像在 Java 裏面 extends
同樣。在這個合約裏面, balanceOf
用來定義每個地址所對應的餘額。allowed
中咱們剛剛講到標準裏面有兩個方法,一個是 approve 受權,另外一個是代理轉賬,進行這個過程的時候就須要判斷有沒有受權,咱們用 allowed
去作這個事情,它的 key 是地址,保存owner,value也是一個mapping,記錄被受權人及額度。
constructor() public {
totalSupply = 1000;
name = "JueJin Token";
symbol = "JJT";
decimals = 0;
balanceOf[msg.sender] = totalSupply;
}
function balanceOf(address _owner) view returns (uint256 balance) {
return balanceOf[_owner];
}
複製代碼
構造函數很簡單,對咱們剛剛指明的信息,好比說名字、總髮行量、符號等,對狀態的變量作一些初始化。那好比說我這個代幣的名字就叫作掘金Token,balanceOf
這個函數很簡單,它就是返回某一個賬號他有多少餘額。
function transfer(address _to, uint _value) public returns (bool success) {
require(_to != address(0));
require(_value <= balanceOf[msg.sender]);
require(balanceOf[_to] + _value >= balanceOf[_to]);
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {
require(_to != address(0));
require(_value <= balanceOf[_from]);
require(_value <= allowed[_from][msg.sender]);
require(balanceOf[_to] + _value >= balanceOf[_to]);
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowed[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
}
複製代碼
transfer
和咱們剛纔說的方法差很少,只多了一步,他發出了這樣一個事件,這個也是ERC20他標準裏面須要實現的,咱們在實現這樣一個轉賬的時候必需要把這個事件記錄下來。那 transferFrom
在轉出的時候再也不是由轉出的人而是由 From
這個地址發出的,可是 transferFrom
這個方法咱們須要作一個檢查,就是必需要有足夠受權的額度,當咱們執行了以後須要減掉必定的受權額度,其餘的地方都同樣。
function approve(address _spender, uint256 _value) returns (bool success) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
複製代碼
咱們再來看一下 approve
這段代碼,approve
這個方法我能夠受權給其餘人來操做我賬號下的代幣,這個參數是被受權人的地址和受權的額度。這個函數的實現其實就是對於咱們剛剛定義的一個狀態變量allowed
進行一個賦值,一樣這個方法也須要去發出這樣的一個事件,即我受權給誰了。
function allowance(address _owner, address _spender) view returns (uint256 remaining) {
return allowed[_owner][_spender];
}
複製代碼
最後咱們看看 allowance
,它就是返回受權額度。
以上就是ERC-20代幣的標準實現,有50多行的代碼,實現以後就是能夠簡單的部署一下。
咱們接着講衆籌,這個是我給衆籌的一個定義,就是(約定時間內)向公衆募資(約定數量),ICO 意思是首次代幣發行,首次代幣發行的時候實際上是向公衆募資以太幣,所以也是一個衆籌行爲,最出名的項目就是 EOS ,他們在將近一年的時間裏募集了721萬個 ETH。
那咱們再來看一下如何來實現一個衆籌。
首先要設定衆籌的時間,你不能無限期的衆籌;而後設定一個目標的金額,還有兌換的價格,由於咱們剛剛講ICO他實際上是用 ETH 買咱們本身的代幣,須要設定一個兌換的價格;此外就是受益人,當咱們衆籌完成以後誰可以來提取募集到的 ETH 。
實現一個以太和代幣的一個兌換,當咱們的合約收到別人打過來的 ETH 以後,咱們要給他轉對應的代幣給他,注意這樣的一個過程是被動觸發的。
實現提款/退款,衆籌目標完成後,受益人是能夠提款的,把全部的 ETH 給提走;若衆籌沒有完成,應該容許退款,固然這一步不是全部人可以作到的。
而後接着咱們仍是同樣,就是咱們來看一下 ICO 的代碼來了解如何實現衆籌。
pragma solidity ^0.4.16;
interface token {
function transfer(address receiver, uint amount) external ; } contract Ico {
address public beneficiary;
uint public fundingGoal;
uint public amountRaised;
uint public deadline;
uint public price;
token public tokenReward;
mapping(address => uint256) public balanceOf;
bool crowdsaleClosed = false;
event GoalReached(address recipient, uint totalAmountRaised);
event FundTransfer(address backer, uint amount, bool isContribution);
constructor (
uint fundingGoalInEthers,
uint durationInMinutes,
uint etherCostOfEachToken,
address addressOfTokenUsedAsReward
) public {
beneficiary = msg.sender;
fundingGoal = fundingGoalInEthers * 1 ether;
deadline = now + durationInMinutes * 1 minutes;
price = etherCostOfEachToken * 1 ether;
tokenReward = token(addressOfTokenUsedAsReward);
}
function () public payable {
require(!crowdsaleClosed);
uint amount = msg.value; // wei
balanceOf[msg.sender] += amount;
amountRaised += amount;
if (amount == 0) {
tokenReward.transfer(msg.sender, amount / price);
}
emit FundTransfer(msg.sender, amount, true);
}
modifier afterDeadline() {
if (now >= deadline) {
_;
}
}
function checkGoalReached() public afterDeadline {
if (amountRaised >= fundingGoal) {
emit GoalReached(beneficiary, amountRaised);
}
crowdsaleClosed = true;
}
function safeWithdrawal() public afterDeadline {
if (amountRaised < fundingGoal) {
uint amount = balanceOf[msg.sender];
balanceOf[msg.sender] = 0;
if (amount > 0) {
msg.sender.transfer(amount);
emit FundTransfer(msg.sender, amount, false);
}
}
if (fundingGoal <= amountRaised && beneficiary == msg.sender) {
beneficiary.transfer(amountRaised);
emit FundTransfer(beneficiary, amountRaised, false);
}
}
}
複製代碼
首先第一步是要設定相關的參數,這些參數是在構造的時候去作設計的,咱們看看有哪些參數。
beneficiary
是受益人;fundingGoal
是衆籌的目標;amountRaised
表示當前衆籌的總額;Deadline
是衆籌的截止日期;price
是兌換價格tokenReward
是所關聯的代幣,實際上咱們須要用代幣的一個地址去給他關聯起來,待會兒咱們看構造函數的時候就能夠看到;mapping(address → uint256)
是用來記錄每個參與的衆籌人投入了多少 ETH。bool crowdsaleClosed
判斷咱們的衆籌是否已經關閉了;event GoalReached
記錄衆籌完成的事件;event FundTransfer
記錄轉換的事件;constructor (
uint fundingGoalInEthers,
uint durationInMinutes,
uint etherCostOfEachToken,
address addressOfTokenUsedAsReward
) public {
beneficiary = msg.sender;
fundingGoal = fundingGoalInEthers * 1 ether;
deadline = now + durationInMinutes * 1 minutes;
price = etherCostOfEachToken * 1 ether;
tokenReward = token(addressOfTokenUsedAsReward);
}
複製代碼
構造函數就是咱們須要在建立合約的時候知道的幾個參數。
uint fundingGoalInEthers
目標總額;unit durationInMinutes
衆籌持續時間單位是分鐘,這個你們能夠隨意去調的。function () public payable {
require(!crowdsaleClosed);
uint amount = msg.value; // wei
balanceOf[msg.sender] += amount;
amountRaised += amount;
if (amount == 0) {
tokenReward.transfer(msg.sender, amount / price);
}
複製代碼
這個函數很奇怪,它沒有函數的名字,這個函數會在別人往這個合約打入 ETH 的時候他會被動觸發的。咱們經過用戶打入的 ETH 的金額,去換算咱們應該給他兌換多少代幣。 那首先咱們用msg.value
來得到打給用戶的以太幣的數量,記錄每個人從過去到如今一共打了多少 ETH 。
此外咱們還要有一個變量amountRaised
不停地把募集到的金額記錄下來,給用戶打入對應的代幣。 msg.sender
是打入以太幣的地址,tranfer
這個方法正是咱們剛剛寫ERC20實現的方法,咱們要用這個方法去給用戶發送咱們的代幣。
function safeWithdrawal() public afterDeadline {
if (amountRaised < fundingGoal) {
uint amount = balanceOf[msg.sender];
balanceOf[msg.sender] = 0;
if (amount > 0) {
msg.sender.transfer(amount);
emit FundTransfer(msg.sender, amount, false);
}
}
複製代碼
不管是用戶退款仍是受益人取ETH,咱們都須要提款,但必需要在衆籌結束以後才能夠提款,這就是afterDeadLine
的用處。afterDeadLine
就是一個函數修改器,有點像Python的裝飾器,它在函數執行的時候可先進行一些判斷,只有符合條件的狀況下才會去執行這樣的一個函數。那這裏必須符合的條件,就是當前的時間必須是在DeadLine以後。 回頭來看safeWithdrawal
的實現,咱們先要判斷當前募集到的總額是否小於目標的金額,若是是小於目標的金額表示衆籌失敗,失敗的話全部參與衆籌的人均可以把錢提走。這裏面咱們首先拿到用戶以前打入的以太幣數量,而後經過 API 提供的 transfer
方法,給某一個地址轉入對應的以太,經過這個方法能夠把用戶以前發過來的以太打回去。
if (fundingGoal <= amountRaised && beneficiary == msg.sender) {
beneficiary.transfer(amountRaised);
emit FundTransfer(beneficiary, amountRaised, false);
}
複製代碼
這裏還有另一個分支就是若是要募集到資金的總額他設定的目標,這樣受益人就能夠把全部的以太提走。固然這裏面還有一個條件,調用這個方法的人必須是當前的受益者,這個應該很好理解,不是說全部人均可以提款,而後咱們一樣是調用transfer 的方法把以太轉到受益人的地址名下,這樣就完成了一個ICO。整體上代碼也很少,只有七八十行。
amountRaised += amount;
if (amount == 0) {
tokenReward.transfer(msg.sender, 10);
} else {
tokenReward.transfer(msg.sender, amount / price);
}
複製代碼
假如說咱們要給每一個帳戶空投10個幣,代碼就能夠這麼寫,學會了就能夠發幣了。
totalSupply
,經過函數修改總供應量不就是增發了嗎,也就是日常說的挖礦了。美鏈前段時間炒得比較火,正是由於它這個溢出漏洞,咱們在這個連接中能夠了解發生漏洞時交易的狀況。在這筆交易的Token Transfer 裏面咱們能夠看到有巨大數量的幣轉移到了兩個不一樣的地址。實際上美鏈共發行70億個代幣,轉移的幣遠大於發行量,這筆交易形成憑空增發了不少代幣,這個漏洞出來了以後全部的交易所都已經關閉了美鏈的交易,當時美鏈應該是60多億市值,而後由於這個漏洞基本上直接歸零。
Function: batchTransfer(address[] _receivers, uint256 _value)
MethodID: 0x83f12fec
[0] :0000000000000000000000000000000000000000000000000000000000000040
[1] :8000000000000000000000000000000000000000000000000000000000000000
[2] :0000000000000000000000000000000000000000000000000000000000000002
[3] :000000000000000000000000b4d30cac5124b46c2df0cf3e3e1be05f42119033
[4] :0000000000000000000000000e823ffe018727585eaf5bc769fa80472f76c3d7
複製代碼
上方就是頁面 Input Data 欄中當時函數調用的狀況。這裏用的是批量轉賬的方法,這邊傳入一個地址,給全部這些地址作轉移對的金額。這個攻擊的交易把 value
設計得很是巧妙:他是8後面接了63個0,由於 uint 最大存儲上限是256位,換算成16進制恰好是64個字符。若是咱們對這個8後面接了63個0的數乘了2,咱們知道一個數乘一個2至關於向左移一位(16進制8是二進制1000),可是他只存了256位,溢出了以後就變成0。
咱們剛剛看到傳入的這個是8後面接了63個0,那這樣的話unit256 amount = unit256(cnt) * _value
中的value 乘以地址的個數的時候乘以了2以後,恰好amount就是0,這就致使這行代碼後面的全部檢測都會經過,他有一個判斷就是他原地址的餘額須要大於amount,那麼這裏溢出後amount是 0。
接下來咱們來看下半部分代碼中的條件,balance[msg.sender] = balances[msg.sender].sub(amount)
轉出了這我的他減去了金額減去了0,可是剩下的這兩個傳入地址須要加上8後面加63個0這樣一個代幣的金額,這樣的話就對這兩個地址,就是至關於平空增長了這麼多代幣,這個就是他溢出的漏洞。這個其實溢出的漏洞是這個合約裏面比較常見的一個漏洞,其實解決方法很簡單,就是這裏應該去引入SafeMath去作加法,咱們應該全部的算數運算方法都要用SafeMath避免去溢出這樣的一個漏洞。
我剛剛講這張合約的時候他有一部是能夠受權給其餘人轉賬,就是別人能夠表明我去轉賬,那 EDU 漏洞會在這種狀況下觸發,即在沒有通過受權的狀況下別人就能夠把錢轉走。
我想這個智能合約的做者沒有理解transferFrom
的意思,他忘了去用 allowed[_from][msg.sender] >= _value)
判斷轉賬的時候是否有足夠權限。其實即便他沒有加這一句,若是他要是引入了我剛剛講的SafeMath也能夠一樣避免這個問題。 每一次執行減法的時候,每一個 mappping 都有爲0的默認值。若是他要是引入了 SafeMath 的話,0減去一個值也會發生溢出。由於溢出有兩種狀況,一種是向上溢出,一個是向下溢出。allowed[_from][msg.sender]
的值是無符號型的整形,他若是是0去減去一個值的話,按照道理值是負數,可是這裏uint不保存負數,因此這個值減去以後會變成一個巨大的正整數,就發生了下溢出錯誤,可是程序依然沒有處理到。因此你不管你有多少代幣,別人均可以轉走。
代幣(Token) 項目的基礎,一個能夠交易的內容
區塊鏈思惟 沒法篡改的雙刃劍 OpenZeppelin/SafeMath
最後是一個簡單的總結:代幣是一個區塊鏈項目的基礎,它不僅僅是咱們看到交易的錢,還能夠代替不少的交易內容。咱們剛剛講到了一些漏洞,這就涉及到區塊鏈的思惟,在咱們平時開發的時候講究的是互聯網思惟,即快速迭代和不斷試錯;可是區塊鏈不能這樣作,區塊鏈有一個沒法修改的特色,這是把雙刃劍。你發佈以後沒有辦法輕易地修改,因此咱們在發佈智能合約的時候要很是謹慎,咱們要通過完善的設計還有細緻的測試。 推薦一個解決辦法是使用OpenZeppelin ,它把編寫智能合約最佳的實踐作成了一些庫,即輪子。咱們在編寫智能合約的時候儘可能不要本身去造輪子,而是用他們的代碼,由於他們的代碼通過不少審查。好比OpenZeppelin 就提供了一些 SafeMath 能避免咱們發送溢出。
以上就是今天的分享,謝謝你們。下方是個人微信二維碼,歡迎交流!