創建並部署ERC20代幣

發表於 2022-03-11 18:39 作者: 登鏈社區

本文通過創建一個代幣深入講解 ERC20。

ERC20 代幣標准

第一個標准由 Fabian Vogelsteller 於 2015 年 11 月以 ethereum request for Comments(ERC)引入,它被自動分配到 GitHub 第 20 個議題,所以叫“ERC20 代幣”。目前絕大多數代幣都基於 ERC20 標准。ERC20 後來變成了以太坊改進提案 20(EIP-20),但是大部分仍然使用它最初的名字,ERC20。

ERC20 是一個同質化代幣標准,意思是不同的 ERC20 代幣是可互換的並且不具有獨特屬性。

ERC20 標准[2]爲實現代幣的合約定義了一個通用接口,這樣任何兼容的代幣都可以用同樣的方式訪問和使用。這個接口由許多必須在每次實現中都出現的函數構成,以及一些开發者可能添加的可選函數和屬性。

ERC20 需要的函數和事件

一個 ERC20 的代幣合約必須至少提供下面這些函數和事件:

totalSupply: 返回當前代幣總量,可以是一個固定值或者變量。

balanceOf:返回給定地址的代幣余額

transfer: 從執行轉账的地址余額中將指定數量的代幣轉移到指定地址。

transferFrom: 從一個账戶到另一個账戶,指定發送者,接收者和轉移的代幣數量。與approve結合使用。

approve: 指定一個被委托地址和委托代幣數量,被委托地址可以在不超過委托數量的前提下多次從委托账戶轉移代幣。

allowance: 給定一個所有者地址和一個被委托地址,返回被委托代幣余額。

Transfer: 在成功轉移(調用transfer或者transferFrom)後觸發的事件(即使轉移數量爲 0)。

Approval: 成功調用approve的事件日志。

ERC20 可選函數

name: 返回代幣的可讀名稱(如“US Dollars”)。

symbol: 返回代幣的可讀符號(如“USD”)。

decimals: 返回代幣數量的小數點位數。例如,如果decimals爲 2,表示小數點後 2 位。

ERC20 接口是用 Solidity 定義的。

下面是 Solidity 的 ERC20 接口規範:

contract ERC20 {
   function totalSupply() constant returns (uint theTotalSupply);
   function balanceOf(address _owner) constant returns (uint balance);
   function transfer(address _to, uint _value) returns (bool success);
   function transferFrom(address _from, address _to, uint _value) returns
      (bool success);
   function approve(address _spender, uint _value) returns (bool success);
   function allowance(address _owner, address _spender) constant returns
      (uint remaining);
   event Transfer(address indexed _from, address indexed _to, uint _value);
   event Approval(address indexed _owner, address indexed _spender, uint _value);
}

ERC20 數據結構

如果你檢查任何一個 ERC20 實現,你會發現它包含兩個數據結構,一個用來跟蹤余額(balance),另一個用來跟蹤委托代幣余額(allowance)。在 Solidity 中,都是用數據映射實現的。

第一個數據映射允許代幣合約跟蹤誰擁有代幣。每次交易都是從一個余額扣除同時在另一個余額增加:

mapping(address => uint256) balances;

第二個數據結構是委托代幣余額(allowance)的數據映射。正如我們將在下一節看到的,ERC20 代幣所有者可以讓一個被委托者花費自己余額中一定數量的代幣(allowance) 。

ERC20 合約用一個二維映射跟蹤委托代幣余額,其主鍵是代幣所有者的地址,映射到被委托地址和對應的委托代幣余額:

mapping (address => mapping (address => uint256)) public allowed;

ERC20 工作流程:“transfer” 和 “approve + transferFrom”

ERC20 代幣標准有兩個交易函數。你可能想知道爲什么。

ERC20 允許兩種不同的工作流程。第一種是一筆交易,使用transfer函數的的簡單流程。這個流程用於一個錢包發送代幣到另一個錢包。

執行轉账合約非常簡單。如果 Alice 想要發送 10 個代幣給 Bob,她的錢包會發送一筆交易到代幣合約的地址,調用transfer函數,並且參數爲 Bob 的地址和 10。代幣合約修改 Alice 的余額(-10)和 Bob 的余額(+10),然後發出一個Transfer事件。

第二種流程是兩筆交易,approve+transferFrom。這個流程允許代幣所有者將控制權委托給另一個地址。通常用於將控制權委托給一個分配代幣的合約,也可以被交易所使用。

例如,如果一個公司正在爲 ICO 發售代幣,他們可以委托一個衆籌合約地址來分發一定數量的代幣。這個衆籌合約可以通過transferFrom將代幣合約所有者的余額轉給每一個代幣买家,如下圖所示。

注意:首次代幣發行(ICO)是公司或者組織爲了籌集資金而出售代幣的衆籌機制。這個術語源自首次公开募股(IPO),這是上市公司在證券交易所向投資者出售股票的過程。與高度監管的 IPO 市場不同,ICO 是开放的、全球化的、混亂的。本文對 ICO 的示例和解釋並非對此類籌款活動的認可。

ERC20 workflow

ERC20 實現

雖然大約 30 行 Solidity 代碼就可以實現一個 ERC20 代幣,但大部分的實現都是更復雜的。這是爲了解決潛在的漏洞。EIP-20 標准提到兩種實現:

Consensys EIP20[3] —— 一種簡單且易讀的 ERC20 代幣的實現。OpenZeppelin StandardToken[4] —— 這個實現兼容 ERC20,並且有額外安全措施。它形成了 OpenZeppelin 庫的基礎,可以實現更復雜 ERC20 代幣,如籌款上限,拍賣,期權等功能。

發起自己的 ERC20 代幣

接下來我們創建並發起自己的代幣。下面的例子,將使用 Truffle 框架。假設你已經安裝了 Truffle,如果沒有安裝,請用 npm 安裝:

npm i truffle

假設我們的代幣叫“Mastering Ethereum Token”,我們用符號“MET”代表它。

注意:你可以在這裏[5]找到這個例子。

首先,我們創建並初始化一個 Truffle 項目目錄。運行下面 4 個命令並接受所有默認答案:

$ mkdir METoken
$ cd METoken
METoken $ truffle init
METoken $ npm init

你現在應該有下面的目錄結構了:

METoken/
+---- contracts
|   `---- Migrations.sol
+---- migrations
|   `---- 1_initial_migration.js
+---- package.json
+---- test
`---- truffle-config.js

編輯truffle-config.js配置文件,配置 Truffle 環境,或者復制下面的示例:

// Install dependencies:
// npm init
// npm install --save-dev dotenv truffle-wallet-provider ethereumjs-wallet
// Create .env in project root, with keys:
// ROPSTEN_PRIVATE_KEY="123abc"
// MAINNET_PRIVATE_KEY="123abc"
require('dotenv').config();
const Web3 = require("web3");
const web3 = new Web3();
const WalletProvider = require("truffle-wallet-provider");
const Wallet = require('ethereumjs-wallet');
var mainNetPrivateKey = new Buffer(process.env["MAINNET_PRIVATE_KEY"], "hex")
var mainNetWallet = Wallet.fromPrivateKey(mainNetPrivateKey);
var mainNetProvider = new WalletProvider(mainNetWallet, "https://mainnet.infura.io/");
var ropstenPrivateKey = new Buffer(process.env["ROPSTEN_PRIVATE_KEY"], "hex")
var ropstenWallet = Wallet.fromPrivateKey(ropstenPrivateKey);
var ropstenProvider = new WalletProvider(ropstenWallet, "https://ropsten.infura.io/");
module.exports = {
  networks: {
        dev: { // Whatever network our local node connects to
            network_id: "*", // Match any network id
            host: "localhost",
            port: 8545,
        },
        mainnet: {  // Provided by Infura, load keys in .env file
            network_id: "1",
            provider: mainNetProvider,
            gas: 4600000,
            gasPrice: web3.utils.toWei("20", "gwei"),
        },
        ropsten: { // Provided by Infura, load keys in .env file
            network_id: "3",
            provider: ropstenProvider,
            gas: 4600000,
            gasPrice: web3.utils.toWei("20", "gwei"),
        },
        kovan: {
            network_id: 42,
            host: "localhost", // parity --chain=kovan
            port: 8545,
            gas: 5000000
        },
        ganache: { // Ganache local test RPC blockchain
            network_id: "5777",
            host: "localhost",
            port: 7545,
            gas: 6721975,
        }
    }
};
-truffle-config.js-

如果你用示例truffle-config.js,記住在包含你的測試私鑰的METoken文件夾中創建一個.env文件,以便在以太坊公共測試網(如 Ropsten or Kovan)上部署和測試。你可以從 MetaMask 導出測試網私鑰。

這時,你的目錄應該是這樣的:

METoken/
+---- contracts
|   `---- Migrations.sol
+---- migrations
|   `---- 1_initial_migration.js
+---- package.json
+---- test
+---- truffle-config.js
`---- .env *new file*

警告

只能使用沒有在以太坊主網上持有資產的測試密鑰或者測試助記詞。切勿將真正持有資產的密鑰用於測試。

在我們的示例中,我們將導入 OpenZeppelin 庫,這個庫實現了一些重要的安全檢查並且容易擴展:

$ npm install openzeppelin-solidity@1.12.0
+ openzeppelin-solidity@1.12.0
added 1 package from 1 contributor and audited 2381 packages in 4.074s

openzeppelin-solidity包會在node_modules目錄下添加大約 250 個文件。OpenZeppelin 庫並不僅僅包含 ERC20 代幣,我們只會使用其中一小部分。

接下來,开始寫代幣合約。創建一個新文件,METoken.sol,並從下面復制示例[6]代碼。

pragma solidity ^0.4.21;
import 'openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol';
contract METoken is StandardToken {
    string public constant name = 'Mastering Ethereum Token';
    string public constant symbol = 'MET';
    uint8 public constant decimals = 2;
    uint constant _initial_supply = 2100000000;
    function METoken() public {
        totalSupply_ = _initial_supply;
        balances[msg.sender] = _initial_supply;
        emit Transfer(address(0), msg.sender, _initial_supply);
    }
}
-METoken.sol-

METoken.sol合約——實現 ERC20 代幣的 Solidity 合約,非常簡單,因爲它從 OpenZeppelin 庫繼承了所有功能。

示例 1. METoken.sol: 實現 ERC20 代幣的 Solidity 合約

這裏,我們定義了可選變量名,符號,和小數位數,也定義了一個_initial_supply變量——設爲 2100 萬個代幣,代幣數量可以細分到小數點後 2 位,也就是總共 21 億份。在合約的初始化函數(構造函數)中我們設置totalSupply等於_initial_supply,並且將所有_initial_supply全部分配給創建METoken合約的账戶(msg.sender)的余額。

現在我們用 truffle 來編譯 METoken 代碼:

$ truffle compile
Compiling ./contracts/METoken.sol...
Compiling ./contracts/Migrations.sol...
Compiling openzeppelin-solidity/contracts/math/SafeMath.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/BasicToken.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/ERC20.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/ERC20Basic.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol...

如你所見,truffle 編譯了 OpenZeppelin 庫的必要依賴。

接下來我們編寫一個遷移腳本來部署METoken合約。在METoken/migrations 文件夾中創建一個新文件2_deploy_contracts.js。復制下面示例[7]代碼:

var METoken = artifacts.require("METoken");
module.exports = function(deployer) {
  // Deploy the METoken contract as our only task
  deployer.deploy(METoken);
};
-2_deploy_contracts.js-

在部署到以太坊測試網之前,我們先啓動一個本地區塊鏈來測試。可以從命令行ganache-cli 或者圖形用戶界面來啓動ganache[8]區塊鏈。

ganache 啓動,我們就可以部署 METoken 合約並且看到是否一切正常:

$ truffle migrate --network ganache
Using network 'ganache'.
Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xb2e90a056dc6ad8e654683921fc613c796a03b89df6760ec1db1084ea4a084eb
  Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
  ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying METoken...
  ... 0xbe9290d59678b412e60ed6aefedb17364f4ad2977cfb2076b9b8ad415c5dc9f0
  METoken: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
  ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...

在 ganache 控制台,我們可以看到已經創建了四筆新的交易:

-ganache-

用 Truffle 控制台與 METoken 交互

我們可以通過 truffle 控制台在 ganache 區塊鏈上與合約交互。這是一個交互式 JavaScript 環境,提供了對 truffle 環境的訪問,並通過 web3 訪問區塊鏈。下面,我們將 truffle 控制台與 ganache 區塊鏈連接:

$ truffle console --network ganache
truffle(ganache)>

truffle(ganache)>表明我們已經連接到 ganache 區塊鏈,並且已經准備好輸入命令。truffle 控制台支持所有 truffle 命令,所以我們可以從控制台編譯和遷移。

我們已經運行了命令,所以讓我們直接進入到合約本身。METoken合約在 truffle 環境中就像一個 JavaScript 對象。在提示符的地方輸入 METoken,就會清除整個合約定義:

truffle(ganache)> METoken
{ [Function: TruffleContract]
  _static_methods:
[...]
currentProvider:
 HttpProvider {
   host: 'http://localhost:7545',
   timeout: 0,
   user: undefined,
   password: undefined,
   headers: undefined,
   send: [Function],
   sendAsync: [Function],
   _alreadyWrapped: true },
network_id: '5777' }

METoken對象也揭露了幾個屬性,如合約地址(因爲是用 migrate 命令部署的):

truffle(ganache)> METoken.address
'0x345ca3e014aaf5dca488057592ee47305d9b3e10'

如果我們想要與部署的合約交互,我們必須使用異步調用,以 JavaScript “promise” 的形式。用 deployed 函數來獲取合約實例,然後調用totalSupply函數:

truffle(ganache)> METoken.deployed().then(instance => instance.totalSupply())
BigNumber { s: 1, e: 9, c: [ 2100000000 ] }

接下來,讓我們用 ganache 創建的账戶來檢查 METoken 余額,並且發送一些 METoken 到另一個地址。首先,獲取账戶地址:

truffle(ganache)> let accounts
undefined
truffle(ganache)> web3.eth.getAccounts((err,res) => { accounts = res })
undefined
truffle(ganache)> accounts[0]
'0x627306090abab3a6e1400e9345bc60c78a8bef57'

账戶列表包含 ganache 創建的所有账戶,accounts[0]是部署METoken合約的账戶。它應該有METoken余額的,因爲我們的METoken構造函數將所有 token 給到了這個地址。我們檢查一下:

truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(accounts[0]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 9, c: [ 2100000000 ] }

最後,通過調用合約的transfer函數從accounts[0]轉移 1000.00 個 METoken 到accounts[1]

truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.transfer(accounts[1], 100000) })
undefined
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(accounts[0]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 9, c: [ 2099900000 ] }
undefined
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(accounts[1]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 5, c: [ 100000 ] }

提示:METoken 可以精確到小數點後 2 位,意思是 1 個 METoken 在合約中其實是 100 份。當我們轉移 1000 個 METoken 時,我們在調用transfer函數時指定的值是 100000

如你所見,在會話中,accounts[0]現在有 20,999,000 個 MET,accounts[1]有 1000 個 MET。

-ganache-

向合約地址發送 ERC20 代幣

到目前爲止,我們已經創建了一個 ERC20 代幣並從一個账戶發送了一些代幣到另一個账戶。前面我們用來演示的账戶都是[外部账戶(external owned accouts)](https://learnblockchain.cn/article/320 "外部账戶(external owned accouts "外部账戶(external owned accouts)")"),意思是由私鑰[9]控制的账戶,不是一個合約。如果我們發送 MET 到一個合約地址又會發生什么呢?

首先,我們在測試環境部署另一個合約。這個例子,我們將直接用水龍頭合約Faucet.sol。將它復制到contracts 目錄下,這樣就把它添加到 METoken 項目下。現在目錄是這樣的:

METoken/
+---- contracts
|   +---- Faucet.sol
|   +---- METoken.sol
|   `---- Migrations.sol

還要再添加一個遷移,將FaucetMEToken分开部署:

var Faucet = artifacts.require("Faucet");
module.exports = function(deployer) {
  // Deploy the Faucet contract as our only task
  deployer.deploy(Faucet);
};

在 truffle 控制台編譯並遷移合約:

$ truffle console --network ganache
truffle(ganache)> compile
Compiling ./contracts/Faucet.sol...
Writing artifacts to ./build/contracts
truffle(ganache)> migrate
Using network 'ganache'.
Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0x89f6a7bd2a596829c60a483ec99665c7af71e68c77a417fab503c394fcd7a0c9
  Migrations: 0xa1ccce36fb823810e729dce293b75f40fb6ea9c9
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Replacing METoken...
  ... 0x28d0da26f48765f67e133e99dd275fac6a25fdfec6594060fd1a0e09a99b44ba
  METoken: 0x7d6bf9d5914d37bcba9d46df7107e71c59f3791f
Saving artifacts...
Running migration: 3_deploy_faucet.js
  Deploying Faucet...
  ... 0x6fbf283bcc97d7c52d92fd91f6ac02d565f5fded483a6a0f824f66edc6fa90c3
  Faucet: 0xb18a42e9468f7f1342fa3c329ec339f254bc7524
Saving artifacts...

贊,現在我們向 Faucet 合約發送 MET :

truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.transfer(Faucet.address, 100000) })
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(Faucet.address).then(console.log)})
truffle(ganache)> BigNumber { s: 1, e: 5, c: [ 100000 ] }

我們已經將 1000MET 轉給了 Faucet 合約。現在,我們要如何取出這些代幣呢?

記住,Faucet.sol是一個非常簡單的合約。它只有一個用來提取以太幣的函數withdraw,沒有用來提取 MET 的函數,或者任何其他 ERC20 代幣。如果我們用withdraw,它就會嘗試發送以太幣,但是因爲 Faucet 沒有以太幣余額,所以會失敗。

METoken 合約知道 Faucet 有余額,但是要轉移這些余額唯一的辦法就是讓 Faucet 合約調用METokentransfer函數。

下一步怎么辦,沒有辦法了。發送給 Faucet 的 MET 永遠卡住了。只有 Faucet 合約可以轉移代幣,但是 Faucet 合約沒有調用 ERC20 代幣合約的 transfer 函數的代碼。

或許你已經預料到這個問題了,也有可能,你沒有。事實上,數百名以太坊用戶意外的將各種代幣轉移到沒有 ERC20 功能的合約,據估計,這些代幣價值超過 250 萬美元(在寫這篇文章時),已經像上面的例子一樣永遠被卡住,永遠丟失了。

ERC20 代幣用戶在交易中無意丟失代幣的一個原因,是他們試圖將代幣轉移到一個交易所或者其他服務,以爲可以簡單的將代幣發送到從交易所網站上復制的以太坊地址,然而,很多交易所發布的接收地址其實是一個合約!這些合約只接收以太幣,而不是 ERC20 代幣,通常這些資金會被清掃到他們的“冷藏庫”或者其他中心化錢包。盡管很多警告說“不要將代幣發送到這個地址”,依然有很多代幣這樣丟失。

ERC20 代幣的問題

ERC20 標准的使用確實具有突破性,已經推出了上千種代幣,既有新功能的實現,又有如衆籌拍賣和 ICO 等各種資金籌集。然而,正如我們在前面向合約地址發送代幣時所看到的,它有一些潛在風險。

ERC 代幣一個不太明顯的問題,揭露了代幣和以太幣之間的細微差異。以太幣是通過以接收地址爲目標的交易進行轉移的,代幣轉移發生在代幣合約的狀態中,以代幣合約作爲目標,而不是接收者的地址。代幣合約跟蹤余額並觸發事件。在代幣轉移中,實際沒有交易發送給代幣接收者,接收者的地址只是被添加到代幣合約的映射。向一個地址發送以太幣的交易會改變地址狀態。轉移代幣到一個地址的交易只會改變代幣合約的狀態,而不是接收者地址的狀態。即使 ERC20 代幣的錢包也不會知道代幣余額,除非用戶特地添加一個代幣合約來“看”。一些錢包會“看”主流代幣合約,來檢查它們所控制的地址持有的余額,但是這僅限於現有 ERC20 合約的小部分。

事實上,用戶並不會想要跟蹤所有可能的 ERC20 代幣合約的所有余額。很多 ERC20 代幣更像是垃圾郵件,而不是可用的代幣。爲了吸引用戶,他們會自動爲有以太幣活躍的账戶創建余額。如果你有一個長期活躍的以太坊地址,尤其如果它是在預售中創建的,你就會發現它充滿了不知從哪裏冒出來的垃圾代幣。當然,這個地址並不是真的充滿了代幣,那只是有你的地址的代幣合約。只有在區塊瀏覽器看到這些代幣合約或者你的錢包查看你的地址時,你才會看到這些余額。

代幣的行爲方式與以太幣不同。以太幣是由 send 函數發送並且由合約中的 payable 函數或者外部地址接收。代幣是用只存在於 ERC20 合約中的transferapprovetransferFrom 函數發送,並且不會在接收合約觸發任何 payable 函數(至少在 ERC20 中)。代幣在功能上是像以太幣一樣的加密貨幣,但是他們的一些差異打破了這種幻想。

考慮另一個問題。要發送以太幣或者使用任何以太坊合約,你需要以太幣來支付 gas。發送代幣,你也需要以太幣。你不能用代幣爲交易支付 gas,並且代幣合約也不能爲你支付 gas。這可能會在不久的將來有所改變,但同時也會導致一些奇怪的用戶體驗。

原文:https://betterprogramming.pub/creating-erc20-token-on-ethereum-35e109dd96e0[10]

參考資料

[1]aisiji: https://learnblockchain.cn/people/3291

[2]ERC20標准: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md

[3]Consensys EIP20: https://github.com/ConsenSys/Tokens/blob/master/contracts/eip20/EIP20.sol

[4]OpenZeppelin StandardToken: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v1.12.0/contracts/token/ERC20/StandardToken.sol

[5]這裏: https://github.com/ac12644/METoken.git

[6]示例: https://github.com/ac12644/METoken/blob/main/contracts/METoken.sol

[7]示例: https://github.com/ac12644/METoken/blob/main/migrations/2_deploy_contracts.js

[8]ganache: https://learnblockchain.cn/article/3501

[9]私鑰: https://learnblockchain.cn/article/3624

[10]https://betterprogramming.pub/creating-erc20-token-on-ethereum-35e109dd96e0: https://betterprogramming.pub/creating-erc20-token-on-ethereum-35e109dd96e0

本文作者:aisiji[1]

標題:創建並部署ERC20代幣

地址:https://www.coinsdeep.com/article/13.html

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。

你可能還喜歡
熱門資訊