以太坊官方 Token 代碼詳解

建議在閱讀本文前能對基礎的 Solidity 編程語言有必定的瞭解,由於這方面的資料還很少,因此直接去啃官方文檔是最正確的選擇(你放心,目前只有英文版的,不過做者我在一些空餘時間正在翻譯該文檔,但願可以讓一些英文基礎不太好的讀者也能快速走上開發道路上 😆)。git

pragma solidity ^0.4.16;
複製代碼

這行代碼是全部 Solidity 智能合約的標配開頭,旨在告知編譯器咱們編寫的智能合約使用的 Solidity 語言的版本,防止未來版本的不可兼容性錯誤。github

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

這行代碼聲明瞭一個接口 tokenRecipient,能夠和繼承了該接口的其餘合約進行相互調用,這是接口很是重要的特性,其中 interface 是聲明接口的關鍵字。接口內的函數都是未實現的,由於如何實現這些函數並非它要關心的,能夠理解爲不一樣合約間之間的協議,你們共同遵照這個協議,但具體如何細化制定則由各自去實現。接口體內的就是「協議內容」,從代碼角度看就是一個未實現的「空」函數。編程

contract TokenERC20 {}
複製代碼

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

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

咱們分別聲明瞭 Token 的全稱、符號、最小單位、發行量,它們均被聲明成 public ,因此咱們能夠在部署合約的時候對它們進行指定。app

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的幾回方)。ui

而後咱們經過 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

歡迎關注公衆號:『比特扣』,與我一塊兒探索區塊鏈的世界。

相關文章
相關標籤/搜索