以太坊官方 Token 程式碼詳解

XFLY發表於2018-05-09

建議在閱讀本文前能對基礎的 Solidity 程式語言有一定的瞭解,因為這方面的資料還不多,所以直接去啃官方文件是最正確的選擇(你放心,目前只有英文版的,不過作者我在一些空餘時間正在翻譯該文件,希望能夠讓一些英文基礎不太好的讀者也能快速走上開發道路上 ?)。

pragma solidity ^0.4.16;
複製程式碼

這行程式碼是所有 Solidity 智慧合約的標配開頭,旨在告知編譯器我們編寫的智慧合約使用的 Solidity 語言的版本,防止將來版本的不可相容性錯誤。

interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; }
複製程式碼

這行程式碼宣告瞭一個介面 tokenRecipient,可以和繼承了該介面的其他合約進行相互呼叫,這是介面非常重要的特性,其中 interface 是宣告介面的關鍵字。介面內的函式都是未實現的,因為如何實現這些函式並不是它要關心的,可以理解為不同合約間之間的協議,大家共同遵守這個協議,但具體如何細化制定則由各自去實現。介面體內的就是“協議內容”,從程式碼角度看就是一個未實現的“空”函式。

contract TokenERC20 {}
複製程式碼

我們正式開始編寫智慧合約的主體部分了,它定義了一個叫 TokenERC20 的智慧合約。

string public name;
string public symbol;
uint8 public decimals = 18;
uint256 public totalSupply;
複製程式碼

我們分別宣告瞭 Token 的全稱、符號、最小單位、發行量,它們均被宣告成 public ,所以我們可以在部署合約的時候對它們進行指定。

mapping (address => uint256) public balanceOf;
複製程式碼

我們宣告瞭一個對映型別的變數 balanceOf,用於儲存每個賬戶中對應的餘額( Token 數量)。

mapping (address => mapping (address => uint256)) public allowance;
複製程式碼

該對映變數則用於儲存賬戶允許別人轉移自己的餘額數,簡單舉個例子就是我有一百萬用於慈善事業,我把這一百萬的使用權授權給了某慈善基金會,允許他們使用這筆錢(即把這筆錢轉移到收款人賬戶上),只要他們轉移的數目不超過我授權給他們的這一百萬,他們想怎麼轉就怎麼轉

event Transfer(address indexed from, address indexed to, uint256 value);
event Burn(address indexed from, uint256 value);
複製程式碼

這兩行程式碼是兩個事件,也是“空”函式,只需要宣告函式名稱和入參即可。事件唯一的作用就是當觸發該事件時,能夠將入參的這些資訊傳遞給客戶端,通知它們有事發生,至於是什麼事則由不同的事件來表明,而事情的詳情則由入參資訊來參考。

function TokenERC20(
    uint256 initialSupply,
    string tokenName,
    string tokenSymbol
) public {
    totalSupply = initialSupply * 10 ** uint256(decimals);
    balanceOf[msg.sender] = totalSupply;
    name = tokenName;
    symbol = tokenSymbol;
}
複製程式碼

該函式是建構函式,每個合約都有一個這樣的函式,且只會在部署合約時觸發一次,一般用於初始化一些變數,比如這個建構函式初始化了 Token 的發行量、全稱、符號。

其中 initialSupply * 10 ** uint256(decimals) 是進行單位換算,比如我們發行了100個 Token,但我們的最小單位是18,所以我們轉賬的時候可以傳送10∧-18個 Token,那麼我們在合約內進行轉賬統一用最小單位會好很多(其中的 ** 表示冪乘,也就是x的幾次方)。

然後我們通過 balanceOf[msg.sender] = totalSupply; 將全部 Token 都轉移到了部署合約的賬戶下,msg.sender 是一個全域性變數,表示當前呼叫者的賬戶地址。

function _transfer(address _from, address _to, uint _value) internal {}
複製程式碼

這個函式用來進行轉賬操作,是一個私有函式(通過使用關鍵字 internal),入參分別是打款人地址(_from)、收款人地址(_to)以及轉賬金額(_value)。下面我們緊接著分析下這個轉賬函式的內部實現:

require(_to != 0x0);
複製程式碼

首先我們來看下這個特殊的地址 0x0,可以理解成黑洞,凡是把 Token 轉移到這個地址的,都相當於被永久鎖定了,不屬於任何人了,或許只有上帝才能拿得回來吧?。

require 關鍵字表示要執行後面的程式碼則必須先通過該函式中的條件表示式,即只有當收款人地址不等於 0x0,才能執行接下來的轉賬操作,否則就丟擲異常。

require(balanceOf[_from] >= _value);
複製程式碼

我們大致也能猜出這行程式碼的意思了,要求打款人的餘額得大於他要打款的數額,通俗點就是你要打款100元,那首先你得拿得出這100元?。

require(balanceOf[_to] + _value > balanceOf[_to]);
複製程式碼

這行乍一看有點懵,這條件肯定成立啊,除非打款數目是個負數 ?,我們不能要求所有人都那麼誠實和遵守規矩,總會有那麼幾個調皮搗蛋鬼會耍點小心眼。作為程式,儘可能去考慮到所有的異常情況,並處理之。

uint previousBalances = balanceOf[_from] + balanceOf[_to];
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
Transfer(_from, _to, _value);
assert(balanceOf[_from] + balanceOf[_to] == previousBalances);
複製程式碼

這幾行我們放在一起講,先講第一行和第五行。我們在做轉賬操作前,先記錄下他們的餘額總和,然後在進行轉賬操作後去檢驗是否他們的餘額總和與轉賬前仍相等。這是不是有點多此一舉啊,像是一句廢話 ?。這樣做主要是保證程式的實際執行結果與預期的必須一致。程式是人寫出來的,所以沒辦法去避免 Bug 的出現。通常使用 assert 是為了在配合使用一些靜態分析工具時方便定位出 bug 所在,因為如果這邊丟擲異常說明程式碼一定寫錯了。

assertrequire 功能上都是判斷條件表示式並在不滿足條件時丟擲異常。assert 只被用在內部錯誤的除錯上,是去檢驗那些具有不變性的結果(比如這邊轉賬前後的雙方餘額總和應該是不會變的)。而 require 是被用在能被外部合約呼叫的那些值上(比如這邊檢驗打款人的餘額是否充足等,這些資訊都是能被大家查閱的,是公開的)。

Transfer() 這行程式碼將會向區塊鏈上的全部客戶端廣播一個事件(比如這邊就是:大家注意啦!~打款人xxx向收款人xxx轉賬了xxx的錢),至於客戶端接收與否那就是客戶端自己的事了?。

多說一句,我們注意到這個方法是 internal,即外部不可呼叫。通常我們對於這些內部方法的取名上採取下劃線開頭的方式(在寫了很多很多行程式碼後,回頭看到這個方法你就很清楚這個方法是個內部方法,這是一條最佳實踐~)。

function transfer(address _to, uint256 _value) public {
    _transfer(msg.sender, _to, _value);
}
複製程式碼

這才是對外開放的轉賬方法,從入參上我們可以看到轉賬的打款人一定是呼叫該方法的賬戶。在方法內部通過呼叫內部方法 _transfer() 來執行轉賬操作。

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
    require(_value <= allowance[_from][msg.sender]);
    allowance[_from][msg.sender] -= _value;
    _transfer(_from, _to, _value);
    return true;
}
複製程式碼

這方法從入參和作用上看簡直可怕,任何人都能呼叫這個方法,而且打款人可以隨意指定(你錢多,我指定你為打款人,自己為收款人,瘋狂往自己賬戶轉錢)。當然我們不能讓這樣的事發生,我們從這個方法到底要做什麼來看待這個問題。

記住,我們的例子是官方例子,裡面所有的邏輯都是可修改可補充刪減的!

我們希望能把自己的一部分代理給其他人,讓他們去打理(類似銀行的理財產品,但沒有利息?,如果你覺得很雞肋,那麼可以修改這個方法,比如想要代理出去的這筆錢是定期存的,且能夠有利息的,那就增加程式碼去實現這部分需求就好啦!)。

要實現這個代理功能,我們只需要增加一個變數,這個變數儲存打款人賦予代理人擁有轉賬多少錢。也就是我們文章開頭解釋的那個 allowance 變數。

allowance[_from][msg.sender]_from 就是打款人,msg.sender 就是代理人,對映的值就是打理的總餘額。接下來的程式碼就很好理解了,首先我們需要代理人能打理的總餘額足夠充足(能支付本次轉賬金額),然後從打理總餘額中扣除,進行轉賬操作,返回成功。

function approve(address _spender, uint256 _value) public
    returns (bool success) {
    allowance[msg.sender][_spender] = _value;
    return true;
}
複製程式碼

要代理人能打理,首先得授權代理人,這方法就是做這件事。你希望誰代理你這筆錢,那麼就呼叫這個方法,輸入代理人的賬號和需要代理的金額就好了。

function approveAndCall(address _spender, uint256 _value, bytes _extraData)
    public
    returns (bool success) {
    tokenRecipient spender = tokenRecipient(_spender);
    if (approve(_spender, _value)) {
        spender.receiveApproval(msg.sender, _value, this, _extraData);
        return true;
    }
}
複製程式碼

基本功能和 approve() 方法一樣,但是會呼叫代理人的 receiveApproval() 方法(這可是在呼叫其他合約的方法呢),當然前提是得代理人合約中實現了這個方法。

要在合約中呼叫其他合約的公共方法(內部方法你當然沒許可權去呼叫的,別想得美),我們就需要例項化介面,傳入其他合約的地址,然後就可以呼叫介面中宣告的所有方法了(再說一遍,前提是其他合約實現了這個方法)。

function burn(uint256 _value) public returns (bool success) {
    require(balanceOf[msg.sender] >= _value);
    balanceOf[msg.sender] -= _value;
    totalSupply -= _value;
    Burn(msg.sender, _value);
    return true;
}
複製程式碼

我的地盤我做主,同樣的,我們賦予你“燒錢”的權利?。一旦你呼叫了這個方法,那麼這筆錢就消失了,比轉到 0x0 黑洞地址還可怕。第一行,你要燒的錢得是你拿得出的;第二行,從你餘額里扣除;第三行,我們 Token 的總髮行量相應減少;第四行,釋出燒錢通知(全網都知道我燒了錢,想想也是裝逼的不行啊?);第五行,返回成功,燒錢成功!

function burnFrom(address _from, uint256 _value) public returns (bool success) {
    require(balanceOf[_from] >= _value);
    require(_value <= allowance[_from][msg.sender]);
    balanceOf[_from] -= _value;
    allowance[_from][msg.sender] -= _value;
    totalSupply -= _value;
    Burn(_from, _value);
    return true;
}
複製程式碼

既然有代理,那麼代理人就有“燒別人錢”的權力了!


官方的 Token 程式碼講解就到這裡結束,我們可以根據官方的例子改造成我們想要的功能 Token,都是可程式設計的,所以想象空間很大~

最後附上官方完整程式碼:TokenERC20 · GitHub

歡迎關注公眾號:『位元扣』,與我一起探索區塊鏈的世界。

相關文章