原文:Code Your Own Cryptocurrency on Ethereum (How to Build an ERC-20 Token and Crowd Sale website)
————————————分割线开始—————————————
区块链中有很多的术语,以下是我个人的一些使用习惯,供参考
token:中文大多叫代币,这里我统一用 Token 来表示
crowd sale:类似众筹销售?我这里用 集体拍卖 来表示
Ether:用 ETH 来表示
————————————分割线结束—————————————
大家好,我是来自 DAPP 大学的 Gregory。
今天我要向你介绍如何在以太坊区块链上编写属于你的加密货币并且出售它!我将会向你介绍如何创建属于你的基于以太坊智能合约ERC-20 Token,如何测试智能合约,如何部署智能合约到以太坊区块链上,并且介绍如何构建一个 ICO 网站并且将它部署到网络中。我也会介绍什么是 ERC-20 Token,以太坊 Token 是如何运行的,最开始的币是如何运行的。
你也可以访问我的视频教程
你可以在上面观看我的一个8小时的视频,如何构建 ERC-20 Token 以及 如何卖出去。在这个教程中我也会一步一步指导你。你可以在 GitHub 上下载该教程所有的代码资源。在我们开始构建 ERC-20 和 ICO 之前,我将先回答一些问题。
你也可以下载 8 小时的系列视频,所有视频内容都是免费的。
什么是ERC-20
以太坊区块链允许你创建你自己的加密货币或 Token,也可以购买 ETH 这是以太坊区块链自带的加密货币。ERC-20 仅仅只是一个标准,它指定了所有 Token 的行为,以便于它可以兼容它平台,比如加密数字货币交易所。
除了本文下面内容之外,你也可以观看下面这个视频来了解 ERC-20 Token 是如何运行。
那么它是如何运行的呢?现在让我们看下以太坊区块链是如何运行的。
以太坊是一种类似 btc 一样的区块链。以太坊会跟踪拥有 ETH 账户的余额。不像 btc,以太坊还是一个平台,允许你创建你自己的 Token,而不需要创建一个新的区块链。
让我们用一个例子来了解一个智能合约 ERC-20 Token 是如何运行的。我们需要创建一个名为“My Token”的 Token,简称“MTK”,该 Token 存量为 100,000,000(1亿)个。
首先,该 Token 的智能合约会记录该 Token 一些基础属性。例如它会记录 Token 的名称为 “My Token”,可以在加密货币交易所看到该 Token 的简称,也可以看到该 Token 的总量是多少。
同时也记录了谁拥有 “My Token” 并且拥有多少。
ERC-20 Token 和其它的加密数字货币一样,可以从一个账户转个另一个账号。
它们也可以在集体拍卖中购买,例如 ICO,这些我们将会在下一小结讨论。
它们也可以在加密货币交易所进行买卖。
ICO 是如何运行的
ERC-20 Token 有多种方式可以进行分发。一种很流行的方法就行举办一次集体拍卖,或者进行一次 ICO。集体拍卖是一家公司通过创建自己的 ERC-20 Token 为自己的业务筹集资金的一种方式,投资者可以使用 ETH 进行购买 Token。
无论什么时候发生集体拍卖,公司都会获得一笔流动的资金,这些都来自于用 ETH 支付的投资者们。以及保留在集体拍卖中销售出去的 ERC-20 Token。
为了参加集体拍卖,投资者需要有一个以太坊区块链账号进行连接。这个账号有钱包地址可以存储 ETH,也可以在集体拍卖中购买 ERC-20。
投资者必须访问与智能合约相关的众筹拍卖网站,智能合约规定了运行集体拍卖的所有规则。
无论一个投资者何时在集体拍卖网站购买 Token,他们都会从钱包中发送 ETH 到智能合约中。智能合约会把他购买的 Token 立即分配到他的钱包中。
智能合约设置了该 Token 在集体拍卖中的价格并且定义了集体拍卖的行为。
众筹拍卖有很多的类型。他们有多个层次或阶段,例如预先 ICO,ICO 和 ICO 奖励阶段。这些层中每一层都可能发生在不同的时间点并且都有不同的表现。
他们也有白名单来约束哪些投资者可以购买 Token。
他们也可以保留一定数量的 Token,这些 Token 不会在集体拍卖中出售。这些保留的 Token 通常是为每家公司特定的成员预留的,例如创始人和顾问。这些储备可以试固定的数量或者是百分比。
集体拍卖无论何时结束,最终都会由管理员来完成。无论何时发生这种情况,所有的储备 Token 都将会分发给何时的账户,并且集体拍卖网站也将正式结束。
ERC-20 中的 Token 是如何运行的
正如我前面所讲的那样,ERC-20 Token 是基于以太坊智能合约创造的。那么,什么是智能合约呢?
以太坊允许开发者使用智能合约编写应用程序运行在区块链上,它封装了所有程序的业务逻辑。它运行我们读取和写入数据到区块链中,也可以执行代码。编写智能合约的编程语言叫做 Solidity它看起来有点像 JavaScript。这是一种很成熟的编程语言,它允许我们做许多和 JavaScript 类似的事情,但是因为它的使用情况会有一点点的不同,正如我们将在本教程可以看到。
在 ERC-20 中,智能合约定义了 Token 运行的所有行为,并且可以追踪 Token 的所有者和账户余额。
ERC-20 是一个如何构建以太坊 Token 的 API 规范。这是被社区采纳的一种标准,运行 Token 支持各种各样的使用场景。我们需要构建遵循该标准的 Token,以便于它能被广泛的接受。如果我们不按照这种标准,我们也有很多烦那个是可以创建 Token,但是它们可能不兼容。
使用 ERC-20 标准确保该 Token 可以兼容以下情况(或者更多):
- 钱包转账——将 Token 从一个账户发送到另一个账户
- 可以在加密货币交易所购买或出售
- 像本教程演示的那样可以在 ICO 中购买
ERC-20 本质上是定义了一些智能合约一定会做出响应的接口。它指定了智能合约必须具有的结构和功能类型。它还提供了一些值得有的功能,但这些都是可选的。它定义了我们 Token 必须拥有的时间,例如转账事件。看,消费者可以订阅智能合约可以触发的事件,并且都是标准的,我们可以订阅这些事件,当 Token 出售时就会通知我们。
这里有一个例子实现了 ERC-20 标准的转账功能。它依靠智能合约来规定某人可以从钱包发送 ERC-20 Token 到另一个人的钱包中。
contract ERC20Token {
// ...
// function 声明式一个函数
// transfer 函数名称
// address _to, uint256 _value 参数类型
// public 函数作用域
// returns 返回值
// bool success 返回值类型
function transfer(address _to, uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value);
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
Transfer(msg.sender, _to, _value);
return true;
}
// ...
}
这个函数通过以下方式来实现 ERC-20 标准:
- 该函数存在
- 它接收正确的参数
- 如果用户转账没有足够的 Token 则会报错,例如 insufficient balance
- 将资产从发送者账户转移到接收者账户
- 它会触发卖事件
- 它会返回正确的值,例如 true
如果这一切还没有完全搞明白,请不要担心。在ERC-20 Token视频教程中我会一步一步构建,解释所有的细节。
你可以直接在以太坊改进建议 GitHub 仓库中阅读更多关于 ERC-20 Token 的标准。社区在这里讨论关于以太坊标准。我强烈建议你将这个仓库添加为书签,并且阅读所有的提交内容,这样你可以实时了解以太坊技术的发展和改变。
我还推荐这篇维基百科文章。
这是我们将要构建的
我们会建一个 ICO 的站点,将集体拍卖和区块链上的智能合约联系起来。在客户端中有一个表单用户可以从集体拍卖中购买 Token。它会显示集体拍卖进展,例如多少用户购买多少 Token,在集体拍卖中总共有多少数量可以购买。还会在“你的账户”中显示已连接到区块链的账号。
安装依赖
为了构建我们的 ERC-20 Token 和集体拍卖,首先我们需要安装一下依赖。
Node 包管理器(NPM)
我们首先要安装的是 Node Package Manager 或者是 NPM,这是 Node.js 附带的。如果你已经安装了,可以在终端输入一下命令进行查看:
$ node -v
Truffle 框架
下一个依赖的是 Truffle 框架,它允许我们在以太坊区块链上构建分散的应用程序。它提供了一整套工具让我们使用 Solidity 编程语言来编写智能合约。也可以让我们测试智能合约并且可以将它们部署到区块链上。它还为我们提供了一个开发客户端的地方。
你可以在命令行中使用 NPM 来安装 Truffle:
$ npm install -g truffle
Ganache
下一个依赖的是 Ganache,一个基于本地内存的区块链,你可从下载 Truffle 框架的网站安装 Ganache。它会给我们 10 个账号基于本地的以太坊区块链。每个账号准备了 100 个假的 ETH。
MetaMask
下一个依赖的是 Google Chrome 扩展 MetaMask。为了使用区块链,我们必须连接上它(这里我说的区块链指的是网络)。为了能使用以太坊区块链我们必须安装这个浏览器插件。这就是 MetaMask 的作用。我们也可以连接到我们本地以太坊区块链的个人账户和我们的智能合约进行互动。
这篇教程中我们会使用 Chrome 插件 MetaMask,如果你没有安装的话那么就需要安装这个浏览器插件。可以在谷歌浏览器扩展商店中找到并安装它。曾经安装过它,你需要确定下载你的插件列表是否存在。如果你安装了的话,你将会在浏览器右上方看到一个狐狸的图标。如果你在这里卡住了,可以参考视频进行操作。
语法高亮显示
这个依赖项是可选的,但还是推荐。我建议还是为 Solidity 编程语言安装下这个语法高亮插件。大多数文本编辑器和 IDE 都没有为 Solidity 提供现成的语法高亮,所以你需要安装它来支持这个一个功能。我使用的是 Sublime Text,并且我下载了以太坊包,它为 Solidity 提供了很棒的语法高亮。
ERC-20 Token 智能合约
现在我们已经安装好了所有依赖,让我们开始构建我们的 ERC-20 Token!这里是完整的 ERC-20 Token 智能合约 Solidity 代码:
pragma solidity ^0.4.2;
contract DappToken {
string public name = "DApp Token";
string public symbol = "DAPP";
string public standard = "DApp Token v1.0";
uint256 public totalSupply;
event Transfer(
address indexed _from,
address indexed _to,
uint256 _value
);
event Approval(
address indexed _owner,
address indexed _spender,
uint256 _value
);
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
function DappToken (uint256 _initialSupply) public {
balanceOf[msg.sender] = _initialSupply;
totalSupply = _initialSupply;
}
function transfer(address _to, uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value);
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
Transfer(msg.sender, _to, _value);
return true;
}
function approve(address _spender, uint256 _value) public returns (bool success) {
allowance[msg.sender][_spender] = _value;
Approval(msg.sender, _spender, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
require(_value <= balanceOf[_from]);
require(_value <= allowance[_from][msg.sender]);
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowance[_from][msg.sender] -= _value;
Transfer(_from, _to, _value);
return true;
}
}
让我们来看下智能合约做了哪些事情,是如何实现 ERC-20 标准的:
- 这里存储了 Token 的名称 string public name = “DApp Token”。
- 这里存储了 Token 在加密货币交易所的简称 string public symbol = “DAPP”。
- 这里存储了 Token 存在的总数 uint256 public totalSupply。
- 使用 Solidity 将每个账户和拥有 Token 的数量进行关联 mapping(address => uint256) public balanceOf。
- 实现了 transfer 函数允许用户发送 Token 到另一个账号。
- 实现了 approve 函数允许其它账户花费 Token,例如加密货币交易所。这更新 allowance 映射,可以查看账户可以花费多少。
- 这里实现了 transferFrom 允许其它账户转移 Token。
可以在这个视频看我如何一步一步构建这个智能合约。
你可以查看这个测试用例来了解智能合约是如何工作的。这些测试用例能确保智能合约的行为是我们所期待的。这个完整的测试套件可以帮助我们检查智能合约的所有行为:
var DappToken = artifacts.require("./DappToken.sol");
contract('DappToken', function(accounts) {
var tokenInstance;
it('initializes the contract with the correct values', function() {
return DappToken.deployed().then(function(instance) {
tokenInstance = instance;
return tokenInstance.name();
}).then(function(name) {
assert.equal(name, 'DApp Token', 'has the correct name');
return tokenInstance.symbol();
}).then(function(symbol) {
assert.equal(symbol, 'DAPP', 'has the correct symbol');
return tokenInstance.standard();
}).then(function(standard) {
assert.equal(standard, 'DApp Token v1.0', 'has the correct standard');
});
})
it('allocates the initial supply upon deployment', function() {
return DappToken.deployed().then(function(instance) {
tokenInstance = instance;
return tokenInstance.totalSupply();
}).then(function(totalSupply) {
assert.equal(totalSupply.toNumber(), 1000000, 'sets the total supply to 1,000,000');
return tokenInstance.balanceOf(accounts[0]);
}).then(function(adminBalance) {
assert.equal(adminBalance.toNumber(), 1000000, 'it allocates the initial supply to the admin account');
});
});
it('transfers token ownership', function() {
return DappToken.deployed().then(function(instance) {
tokenInstance = instance;
// Test `require` statement first by transferring something larger than the sender's balance
return tokenInstance.transfer.call(accounts[1], 99999999999999999999999);
}).then(assert.fail).catch(function(error) {
assert(error.message.indexOf('revert') >= 0, 'error message must contain revert');
return tokenInstance.transfer.call(accounts[1], 250000, { from: accounts[0] });
}).then(function(success) {
assert.equal(success, true, 'it returns true');
return tokenInstance.transfer(accounts[1], 250000, { from: accounts[0] });
}).then(function(receipt) {
assert.equal(receipt.logs.length, 1, 'triggers one event');
assert.equal(receipt.logs[0].event, 'Transfer', 'should be the "Transfer" event');
assert.equal(receipt.logs[0].args._from, accounts[0], 'logs the account the tokens are transferred from');
assert.equal(receipt.logs[0].args._to, accounts[1], 'logs the account the tokens are transferred to');
assert.equal(receipt.logs[0].args._value, 250000, 'logs the transfer amount');
return tokenInstance.balanceOf(accounts[1]);
}).then(function(balance) {
assert.equal(balance.toNumber(), 250000, 'adds the amount to the receiving account');
return tokenInstance.balanceOf(accounts[0]);
}).then(function(balance) {
assert.equal(balance.toNumber(), 750000, 'deducts the amount from the sending account');
});
});
it('approves tokens for delegated transfer', function() {
return DappToken.deployed().then(function(instance) {
tokenInstance = instance;
return tokenInstance.approve.call(accounts[1], 100);
}).then(function(success) {
assert.equal(success, true, 'it returns true');
return tokenInstance.approve(accounts[1], 100, { from: accounts[0] });
}).then(function(receipt) {
assert.equal(receipt.logs.length, 1, 'triggers one event');
assert.equal(receipt.logs[0].event, 'Approval', 'should be the "Approval" event');
assert.equal(receipt.logs[0].args._owner, accounts[0], 'logs the account the tokens are authorized by');
assert.equal(receipt.logs[0].args._spender, accounts[1], 'logs the account the tokens are authorized to');
assert.equal(receipt.logs[0].args._value, 100, 'logs the transfer amount');
return tokenInstance.allowance(accounts[0], accounts[1]);
}).then(function(allowance) {
assert.equal(allowance.toNumber(), 100, 'stores the allowance for delegated trasnfer');
});
});
it('handles delegated token transfers', function() {
return DappToken.deployed().then(function(instance) {
tokenInstance = instance;
fromAccount = accounts[2];
toAccount = accounts[3];
spendingAccount = accounts[4];
// Transfer some tokens to fromAccount
return tokenInstance.transfer(fromAccount, 100, { from: accounts[0] });
}).then(function(receipt) {
// Approve spendingAccount to spend 10 tokens form fromAccount
return tokenInstance.approve(spendingAccount, 10, { from: fromAccount });
}).then(function(receipt) {
// Try transferring something larger than the sender's balance
return tokenInstance.transferFrom(fromAccount, toAccount, 9999, { from: spendingAccount });
}).then(assert.fail).catch(function(error) {
assert(error.message.indexOf('revert') >= 0, 'cannot transfer value larger than balance');
// Try transferring something larger than the approved amount
return tokenInstance.transferFrom(fromAccount, toAccount, 20, { from: spendingAccount });
}).then(assert.fail).catch(function(error) {
assert(error.message.indexOf('revert') >= 0, 'cannot transfer value larger than approved amount');
return tokenInstance.transferFrom.call(fromAccount, toAccount, 10, { from: spendingAccount });
}).then(function(success) {
assert.equal(success, true);
return tokenInstance.transferFrom(fromAccount, toAccount, 10, { from: spendingAccount });
}).then(function(receipt) {
assert.equal(receipt.logs.length, 1, 'triggers one event');
assert.equal(receipt.logs[0].event, 'Transfer', 'should be the "Transfer" event');
assert.equal(receipt.logs[0].args._from, fromAccount, 'logs the account the tokens are transferred from');
assert.equal(receipt.logs[0].args._to, toAccount, 'logs the account the tokens are transferred to');
assert.equal(receipt.logs[0].args._value, 10, 'logs the transfer amount');
return tokenInstance.balanceOf(fromAccount);
}).then(function(balance) {
assert.equal(balance.toNumber(), 90, 'deducts the amount from the sending account');
return tokenInstance.balanceOf(toAccount);
}).then(function(balance) {
assert.equal(balance.toNumber(), 10, 'adds the amount from the receiving account');
return tokenInstance.allowance(fromAccount, spendingAccount);
}).then(function(allowance) {
assert.equal(allowance.toNumber(), 0, 'deducts the amount from the allowance');
});
});
});
你可以在 truffle 中使用这个命令运行这个测试:
$ truffle test
集体拍卖智能合约
现在我们可以构建集体拍卖智能合约了,运行投资者在 ICO 时购买 Token。这里是完整的集体拍卖智能合约 Solidity 代码:
pragma solidity ^0.4.2;
import "./DappToken.sol";
contract DappTokenSale {
address admin;
DappToken public tokenContract;
uint256 public tokenPrice;
uint256 public tokensSold;
event Sell(address _buyer, uint256 _amount);
function DappTokenSale(DappToken _tokenContract, uint256 _tokenPrice) public {
admin = msg.sender;
tokenContract = _tokenContract;
tokenPrice = _tokenPrice;
}
function multiply(uint x, uint y) internal pure returns (uint z) {
require(y == 0 || (z = x * y) / y == x);
}
function buyTokens(uint256 _numberOfTokens) public payable {
require(msg.value == multiply(_numberOfTokens, tokenPrice));
require(tokenContract.balanceOf(this) >= _numberOfTokens);
require(tokenContract.transfer(msg.sender, _numberOfTokens));
tokensSold += _numberOfTokens;
Sell(msg.sender, _numberOfTokens);
}
function endSale() public {
require(msg.sender == admin);
require(tokenContract.transfer(admin, tokenContract.balanceOf(this)));
// Just transfer the balance to the admin
admin.transfer(address(this).balance);
}
}
让我们看看这个智能合约做了什么事,以及集体拍卖有哪些功能:
- 为集体拍卖 address admin 存储管理员账号。
- 引用了 ERC-20 Token 智能合约 DappToken public tokenContract。
- 存储了 Token 的价格 uint256 public tokenPrice。
- 存储了 Token 已经出售的数量 uint256 public tokensSold。
- 实现了 sell 事件当卖出 Token 时消费者会受到通知。
- 实现了 buyToken 函数允许用户在集体拍卖中购买 Token。
- 实现了 endSale 函数允许管理员结束集体拍卖并收集出售过程中的 ETH 资金。
观看这个视频看我一步一步构建这个智能合约。
你可以查看这个测试用例来了解智能合约是如何工作的。这些测试用例能确保智能合约的行为是我们所期待的。这个完整的测试套件可以帮助我们检查智能合约的所有行为:
var DappToken = artifacts.require('./DappToken.sol');
var DappTokenSale = artifacts.require('./DappTokenSale.sol');
contract('DappTokenSale', function(accounts) {
var tokenInstance;
var tokenSaleInstance;
var admin = accounts[0];
var buyer = accounts[1];
var tokenPrice = 1000000000000000; // in wei
var tokensAvailable = 750000;
var numberOfTokens;
it('initializes the contract with the correct values', function() {
return DappTokenSale.deployed().then(function(instance) {
tokenSaleInstance = instance;
return tokenSaleInstance.address
}).then(function(address) {
assert.notEqual(address, 0x0, 'has contract address');
return tokenSaleInstance.tokenContract();
}).then(function(address) {
assert.notEqual(address, 0x0, 'has token contract address');
return tokenSaleInstance.tokenPrice();
}).then(function(price) {
assert.equal(price, tokenPrice, 'token price is correct');
});
});
it('facilitates token buying', function() {
return DappToken.deployed().then(function(instance) {
// Grab token instance first
tokenInstance = instance;
return DappTokenSale.deployed();
}).then(function(instance) {
// Then grab token sale instance
tokenSaleInstance = instance;
// Provision 75% of all tokens to the token sale
return tokenInstance.transfer(tokenSaleInstance.address, tokensAvailable, { from: admin })
}).then(function(receipt) {
numberOfTokens = 10;
return tokenSaleInstance.buyTokens(numberOfTokens, { from: buyer, value: numberOfTokens * tokenPrice })
}).then(function(receipt) {
assert.equal(receipt.logs.length, 1, 'triggers one event');
assert.equal(receipt.logs[0].event, 'Sell', 'should be the "Sell" event');
assert.equal(receipt.logs[0].args._buyer, buyer, 'logs the account that purchased the tokens');
assert.equal(receipt.logs[0].args._amount, numberOfTokens, 'logs the number of tokens purchased');
return tokenSaleInstance.tokensSold();
}).then(function(amount) {
assert.equal(amount.toNumber(), numberOfTokens, 'increments the number of tokens sold');
return tokenInstance.balanceOf(buyer);
}).then(function(balance) {
assert.equal(balance.toNumber(), numberOfTokens);
return tokenInstance.balanceOf(tokenSaleInstance.address);
}).then(function(balance) {
assert.equal(balance.toNumber(), tokensAvailable - numberOfTokens);
// Try to buy tokens different from the ether value
return tokenSaleInstance.buyTokens(numberOfTokens, { from: buyer, value: 1 });
}).then(assert.fail).catch(function(error) {
assert(error.message.indexOf('revert') >= 0, 'msg.value must equal number of tokens in wei');
return tokenSaleInstance.buyTokens(800000, { from: buyer, value: numberOfTokens * tokenPrice })
}).then(assert.fail).catch(function(error) {
assert(error.message.indexOf('revert') >= 0, 'cannot purchase more tokens than available');
});
});
it('ends token sale', function() {
return DappToken.deployed().then(function(instance) {
// Grab token instance first
tokenInstance = instance;
return DappTokenSale.deployed();
}).then(function(instance) {
// Then grab token sale instance
tokenSaleInstance = instance;
// Try to end sale from account other than the admin
return tokenSaleInstance.endSale({ from: buyer });
}).then(assert.fail).catch(function(error) {
assert(error.message.indexOf('revert' >= 0, 'must be admin to end sale'));
// End sale as admin
return tokenSaleInstance.endSale({ from: admin });
}).then(function(receipt) {
return tokenInstance.balanceOf(admin);
}).then(function(balance) {
assert.equal(balance.toNumber(), 999990, 'returns all unsold dapp tokens to admin');
// Check that the contract has no balance
balance = web3.eth.getBalance(tokenSaleInstance.address)
assert.equal(balance.toNumber(), 0);
});
});
});
祝贺你!🎉🎉🎉你已经成功构建了 ERC-20 Token 和集体拍卖以太坊智能合约!你可以查看该视频教程学习如何构建一个 ICO 网站和智能合约联系起来促进代币购买。它也介绍了如何一步一步构建智能合约。你可以在 GitHub 这里下载本教程所有的源码。