本文会讲解代币、ERC20 的基本概念,并完成一个包含智能合约、前端、测试、部署的完整 DApp 设计与实现。
非常适合 Web3 初学者朋友进行学习。
文中用到的一些主要的工具、框架及技术:
- solidity:智能合约编程语言。
- MetaMask:加密钱包。
- truffle:智能合约开发套件。
- ganache:本地区块链。
- react、nextjs:前端UI库/框架。
- chakra、tailwindcss:组件库/CSS框架。
- wagmi、ethersjs:JS 与智能合约交互 SDK。
注意:文中不会涉及这些工具、框架、技术的基本安装及使用。
完成效果如下:
基本概念
什么是Token/代币/通证?
这里指的代币是以太坊平台的代币,其他平台的代币在概念上可能略有出入。
除了以太坊平台有代币的概念外,很多模仿以太坊的平台也都有代币的概念,比如 BSC 和 polygon 等。
代币,英文是 Token,顾名思义就是代表某种东西的货币。它可以表示任何东西:
像人民币一样的法币。
- 黄金。
- 石油。
- 股份资产。
- 游戏道具。
- 积分。
- 门票。
- 以及更多的东西…
代币唯一不能代表的东西就是 gas 费。
代币除了代币以外,还有另一个名字,叫做通证。所谓通证,也就是通用的证明。你持有这个通证,就可以证明你拥有某件东西或者某项权益。这和中国成立初期的粮票、布票是一个道理。
现在我们明白了,虽然它有三种称呼,但其实是一个意思,文中通称为代币。
在代码的角度上,代币就是一份智能合约,它负责提供查账、转账、记账等功能,没有什么特殊。
最后扩展一点儿小知识。
比特币和以太币是代币吗?可以说是,但和通常意义上的代币还是有些区别。我们一般会称呼为比特币和以太坊这种自己拥有区块链的代币为「币」。而除了比特币以外的币,我们还会称为「替代币」。没有自己的区块链,依赖其他区块链的币,我们称为「代币」。这些名词是每一个玩币的人都应该清楚的。
什么是 ERC、EIP 和 ERC20?
ERC 是 Ethereum Request for Comment 的缩写,也就是以太坊改进建议。提交 ERC 后,以太坊社区会对这个草案进行评估,最终会接受或者拒绝该建议。
如果接受的话,ERC 会被确认为 EIP。
EIP 是 Ethereum Improvement Proposals 的缩写,也就是被接纳的以太坊改进建议。
ERC 是按照时间顺序从 1 开始递增的,ERC 20 就是第 20 个建议。
在讲 ERC20 之前,我们先来看下发行代币过程中存在的问题。
我们上面讲过,代币就是智能合约,智能合约就是代码。虽然代币合约都是做查账、转账、记账这几件主要的事情的,但在没有规范约束的情况下,每种代币的实现可能都是不同的。
比如 Pig 币的转账函数是 t,参数顺序是余额、收款人;Cat 币的转账函数是 tr,参数顺序是收款人、余额。虽然各自都是没问题的,但那这样很多应用来集成他们就变得非常麻烦了,这会导致有多少种代币就要集成多少次。特别是交易所和钱包这类应用。
ERC20 是关于代币的建议,由以太坊联合创始人 Vitalik 在 2015 年 6 月提出。它是一个简单的接口,允许开发者在以太坊区块链上发行自己的代币,并可以与第三方应用集成。
EIP 20 的地址:eips.ethereum.org/EIPS/eip-20
既然是接口,那就是一种规范约束。所有人都应该按照这个接口去实现自己的代币合约。
如果你不按照这个规范实现你的代币合约,那么你的代币在集成到第三方应用时就会无法识别,比如在 MetaMask 中不能正常显示代币名称、余额等。
代币的价值很依赖流通性,如果你的代币不能通用、不能流通的话,那么基本上就失去了代币的价值。
所以发行代币,要按照 ERC20 接口去实现合约。
截至本文写作时间(2023/1/4),以太坊上的 ERC20 代币有将近 74 万种,BSC 上的 ERC20 代币有将近 300 万种。从这些数字上足以看出 ERC20 对促进代币发展的过程中提供的重要作用。
插一句,BSC 就是币安智能链,因为以太坊交易的 gas 费太贵,BSC 就模仿以太坊做了它们自己的平台,但 gas 相比以太坊少很多,所以吸引了很多用户,后面顺理成章地发展起来了。
ERC20 接口介绍
ERC20 接口规定了 9 个方法和 2 个事件。
方法:
function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
事件:
event Transfer(address indexed _from, address indexed _to, uint256 _value)
event Approval(address indexed _owner, address indexed _spender, uint256 _value)
我们详细讲解一下它们的作用。
- name:返回代币名称,比如我创建一个 Noah 币,那么就返回 “Noah”。
- symbol:返回代币符号,比如 Noah 币的符号是 NH,那么就返回 “NH”。也可以叫做代币代号。
- decimals:返回代币使用的小数点位数,代币会以代币数量除以这个值给用户展示。也就是精度。通常都会使用 18。
- totalSupply:返回代币发行总量,比如 Noah 币只发行 10000 个,那就返回 10000。
- balanceOf:返回指定账户的余额。
- transfer:将指定数量的代币转入指定的地址中,并需要触发 Transfer 事件。
- transferFrom:将指定数量的代币从一个指定的地址转到另一个指定的地址。这通常被称为提币,但依赖授权。
- approve:允许指定地址可以分多次从你的账户中提取代币,最多不超过指定的金额。并需要触发 Approval 事件,这也就是授权。
- allowance:查询指定地址给另一个指定地址的授权代币额度。
我们可以将它们分为三类,查询、转账、授权:
查询
查询类方法有:name、symbol、decimals、totalSupply、balanceOf。
其中 name、symbol 和 decimals 都是可选的,因为它们没有具体的功能。但建议是要全部实现。
totalSupply 和 balanceOf 分别是发行总量和查余额,很容易理解。
转账
转账方法有:transfer。
它可以将你账户上的代币转给另一个账户,也很容易理解。
授权
授权方法有:transferFrom、approve 和 allowance。
这三个函数可能比较难理解。
我再举个例子来详细讲一下授权这块内容。
假设我是一家游戏平台(游戏平台也是一个地址,和用户没区别),玩家张三完成了我的任务,我奖励他 50 个代币。但我并不会直接转账到他的账户,而是在授权账本上记下来,玩家张三可以使用我的 50 个代币。
李四是个游戏商人,贩卖游戏道具。张三要在李四手里买一个价值 30 个代币的道具,就可以使用平台的代币支付给李四。而这个代币可能也不会直接打到李四的账户上,和上面的玩法一样,我也在授权账本上记下,允许李四使用我的 30 个代币,同时把张三原来那 50 个代币的账户改为 20 个代币。
这就是授权的玩法,对应的实现就是:
游戏平台给玩家授权代币:approve。
查询玩家在游戏平台的授权代币余额:allowance。
玩家使用平台授权的代币进行交易:transferFrom。
当然这只是我举例的一个场景,授权的玩法可以应用在更多的场景中。
代码实现
实现 ERC20 接口的智能合约
实现 ERC20 是很多刚接触智能合约的小伙伴都需要学习的内容。
你可能需要使用 VSCode 或者 Remix 作为编辑器来写 Solidity 代码。这部分内容就不多讲了。
因为一些特殊的原因,这里我选择 VSCode。
我会使用 truffle 来创建项目。
它可以帮我们对合约进行编译和部署。
运行命令创建项目:
mkdir noth-token-contract
cd noth-token-contract
truffle init
创建 contracts/IERC20.sol 文件,定义 IERC20 接口。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint256);
function balanceOf(address _owner) external view returns (uint256 balance);
function transfer(address _to, uint256 _value) external returns (bool success);
function transferFrom( address _from, address _to, uint256 _value ) external returns (bool success);
function approve(address _spender, uint256 _value) external returns (bool success);
function allowance(address _owner, address _spender) external view returns (uint256 remaining);
event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(
address indexed _owner,
address indexed _spender,
uint256 _value
);
}
再创建 contracts/NoahToken.sol 文件,实现 IERC20 接口。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IERC20.sol";
contract NoahToken is IERC20 {
string private _name; // 代币名称
string private _symbol; // 代币代号
uint8 private _decimals; // 代币精度
uint256 private _totalSupply; // 代币发行总量
mapping(address => uint256) private _balances; // 账本
mapping(address => mapping(address => uint256)) private _allowance; // 授权记录
address public owner; // 合约发布者
constructor(
string memory _initName,
string memory _initSymbol,
uint8 _initDecimals,
uint256 _initTotalSupply
) {
// 发布合约时设置代币名称、代号、精度和发行总量
_name = _initName;
_symbol = _initSymbol;
_decimals = _initDecimals;
_totalSupply = _initTotalSupply;
owner = msg.sender;
// 在合约部署时把所有的代币发行给合约发布者
_balances[owner] = _initTotalSupply;
}
function name() external view override returns (string memory) {
return _name;
}
function symbol() external view override returns (string memory) {
return _symbol;
}
function decimals() external view override returns (uint8) {
return _decimals;
}
function totalSupply() external view override returns (uint256) {
return _totalSupply;
}
function balanceOf(address _owner) external view override returns (uint256 balance) {
return _balances[_owner];
}
function transfer(address _to, uint256 _value) external override returns (bool success) {
// 检查发送者余额是否足够
require(_balances[msg.sender] >= _value, "Insufficient balance");
// 扣除发送者余额
_balances[msg.sender] -= _value;
// 增加接收者余额
_balances[_to] += _value;
// 触发转账事件
emit Transfer(msg.sender, _to, _value);
return true;
}
function transferFrom( address _from, address _to, uint256 _value ) external override returns (bool success) {
// 检查发送者余额是否足够
require(_balances[_from] >= _value, "Insufficient balance");
// 检查授权额度是否足够
require(
_allowance[_from][msg.sender] >= _value,
"Insufficient allowance"
);
// 扣除发送者余额
_balances[_from] -= _value;
// 增加接收者余额
_balances[_to] += _value;
// 扣除授权额度
_allowance[_from][msg.sender] -= _value;
// 触发转账事件
emit Transfer(_from, _to, _value);
return true;
}
function approve(address _spender, uint256 _value) external override returns (bool success) {
// 设置授权额度
_allowance[msg.sender][_spender] = _value;
// 触发授权事件
emit Approval(msg.sender, _spender, _value);
return true;
}
function allowance(address _owner, address _spender) external view override returns (uint256 remaining) {
return _allowance[_owner][_spender];
}
}
具体代码的作用我都加到注释中了,就不再多赘述。
在合约编写完成之后,我们需要在本地进行测试、编译、部署。
使用 truffle 测试智能合约
truffle 支持通过代码对智能合约进行测试。目前支持 JavaScript 和 Solidity 两种语言,但 JavaScript 更灵活,也更流行。这里选择 JavaScript 进行测试。
创建 test/token.js 文件,该文件是测试文件。
测试 balanceOf 与 transfer 函数
truffle 使用 Mocha 和 Chai 这两个库作为断言库,但略有不同。
首先应该使用 contract 函数而不是 describe 函数。
contract 函数会传递一个默认参数,它会提供一组可用的账户。
const NoahToken = artifacts.require("NoahToken");
contract("Token", (accounts) => {
const [alice, bob] = accounts;
it("balanceOf", async () => {
// 发 Noah 币,发行 1024 个
const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '1024', { from: alice });
// 查看 alice 的余额是否是 1024
const result = await noahTokenInstance.balanceOf(alice);
assert.equal(result.valueOf().words[0], 1024, "1024 wasn't in alice");
});
it("transfer", async () => {
// 发 Noah 币,发行 1024 个
const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '1024', { from: alice });
// alice 将 1 个 Noah 币转给 bob
await noahTokenInstance.transfer(bob, 1, { from: alice });
// 查看 alice 的余额是否是 1023
let aliceBalanceResult = await noahTokenInstance.balanceOf(alice);
assert.equal(aliceBalanceResult.valueOf().words[0], 1023, "1023 wasn't in alice");
// 查看 bob 的余额是否是 1
let bobBalanceResult = await noahTokenInstance.balanceOf(bob);
assert.equal(bobBalanceResult.valueOf().words[0], 1, "1 wasn't in bob");
// bob 将 1 个 Noah 币转给 alice
await noahTokenInstance.transfer(alice, 1, { from: bob });
// 查看 alice 的余额是否是 1024
aliceBalanceResult = await noahTokenInstance.balanceOf(alice);
assert.equal(aliceBalanceResult.valueOf().words[0], 1024, "1024 wasn't in alice");
// 查看 bob 的余额是否是 0
bobBalanceResult = await noahTokenInstance.balanceOf(bob);
assert.equal(bobBalanceResult.valueOf().words[0], 0, "0 wasn't in bob");
});
});
代码中有详尽的注释,就不多赘述了。其他 function 也可以用这种方式进行测试。
编写完成后运行命令:
truffle test ./test/token.js
全部 pass 即可通过。
使用 ganache 本地部署智能合约
除了代码测试外,我们通常还需要将合约部署到开发环境,和前端代码进行集成联调。
我使用 ganache 部署智能合约,它会在本地运行一个区块链。
配置 truffle-config.js 和 migration 文件
首先在项目中对 truffle-config.js 文件进行修改,添加 development 环境的相关配置。
{
"network": {
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 7545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
}
}
同时创建一个 migrations/1_NoahToken_migration.js 文件,用于部署。
内容如下:
const NoahToken = artifacts.require("NoahToken");
module.exports = function (deployer) {
deployer.deploy(NoahToken, 'noah', 'NOAH', 18, '1024000000000000000000');
}
deployer.deploy 的第一个参数是合约,其余的参数是部署合约传递的参数。
部署到 ganache
最后运行 truffle 的编译部署脚本:
truffle migrate --network development --f 1
network 参数是指定的网络环境,truffle 会将合约部署到指定的网络。
f 参数是指定的部署文件名前缀,truffle 会从这个文件开始迁移。
稍等片刻就可以在 ganache 的 contracts 中就可以看到这个合约的地址了。
点进去就可以看到合约的详细信息。
不过需要注意,ganache 中数字是以十六进制形式进行展示,所以 decimals 和 totalSupply 和我们传入的十进制数字不匹配。
另一个注意事项是:ganache 中的 mapping 一直存在显示问题,永远都显示 0 items。这个 Bug 存在时间超过了一年。记得我刚用 Ganache 的时候,一度怀疑是我的合约写得有问题,在这个问题上折腾了一天,记忆犹新。遗憾的是,一年多过去了,ganache 还没有修复这个 Bug。
配置 MetaMask 网络
在测试之前,我们先要配置网络。
配置信息如下图所示:
在 MetaMask 中添加代币
接下来我们要把代币添加到 MetaMask 中。
在这一步就可以看到代币余额了。
不过本地的链在 MetaMask 中不能正常显示,部署到链上时是正常的。
实现前端 DApp
前端使用了很多库,我们先来安装这些库。
创建 Nextjs 项目
运行命令创建项目:
npx create-next-app
项目名看你的喜好;编程语言选择 TypeScript。
安装 wagmi 和 ethersjs
与智能合约交互的 SDK 使用 wagmi 和 ethersjs,安装依赖:
npm i wagmi ethers
安装 chakra
UI 组件库选择 chakra,安装依赖:
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion
安装配置 tailwindcss
CSS 框架选择 tailwindcss,安装依赖:
npm install -D tailwindcss postcss autoprefixer
初始化配置。
npx tailwindcss init -p
修改 tailwind.config.js 的内容。
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
修改 styles/globals.css 的内容。
@tailwind base;
@tailwind components;
@tailwind utilities;
这些配置比较烦琐,更多内容可以参考官方文档。
关闭 React 严格模式
nextjs 会默认打开 React 的严格模式,但我们用不到,需要关闭。
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
}
module.exports = nextConfig
创建 pages/noah-token.tsx 文件
这是我们代币的操作页面。
export default function NoahToken() {
return <div>hello, noah token!</div>
}
关闭服务端渲染
接下来我们需要配置 wagmi 的相关配置。但在那之前我们需要关闭 SSR。
nextjs 的页面默认都是开启 SSR 的,这会导致下面这个 Error。
触发这个 Error 的原因是服务端的 UI 和客户端不一致。
关闭服务端渲染很简单。
import dynamic from "next/dynamic";
export default dynamic(() => Promise.resolve(NoahToken), { ssr: false });
开发功能
代码分为几个组件:
- Profile:个人信息。
- Detail:代币信息。
- BalanceOf:查余额。
- Transfer:转账。
- Allowance:查授权余额。
- Approve:授权。
- TransferFrom:通过授权转账。
代码量较多,不在这里做更多分析。后续考虑单独写一篇专门介绍 wagmi 的文章。
可以参考 Github:github.com/luzhenqian/…
部署上线
将合约部署到 Goeril
Goeril 是目前最流行的测试网络之一,接下来我们会把 Noah 币部署到这个网络上。
要部署到 Goeril 首先需要在app.infura.io/ 上面创建一个项目。
然后可以获取到 API Key。
回到合约项目中。
安装两个包:
npm i @truffle/hdwallet-provider dotenv
dotenv 用于读取环境变量。
创建 .env 文件,写入以下内容:
PRIVATE_KEY="xxx"
PROJECT_ID="xxx"
回到 truffle-config.js 文件,添加 goerli 相关配置:
require('dotenv').config();
const { PRIVATE_KEY, PROJECT_ID } = process.env;
const HDWalletProvider = require('@truffle/hdwallet-provider');
module.exports = {
// ...
networks: {
// ...
goerli: {
provider: () => new HDWalletProvider(PRIVATE_KEY, `https://goerli.infura.io/v3/${PROJECT_ID}`),
network_id: 5, // Goerli's id
confirmations: 2, // # of confirmations to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
}
}
}
最后运行命令:
truffle migrate --network goerli --f 1
稍等片刻,部署成功。
将 DApp 部署到 Vercel
部署之前需要将合约地址配置到 vercel 的环境变量中。
由于之前我就配置好了 Vercel,而且这部分不是重点,就不展开讲了。
线上地址:www.webnext.cloud/
Github 源码地址:github.com/luzhenqian/…
后续我会更新一些其他 Web3 案例,也会放在这个仓库中。欢迎 star。
当然,在实际工作中,并不会真正从零实现一个 ERC20 的代币合约,通常会使用 OpenZepplin 这种库来一键发币。本文教学目的是以学习为主。
只懂得如何发行代币还不够,我们还需要知道如何推广币。推广币最简单的方式就是发布一个免费领币的网站,因为互联网上最不缺的就是羊毛党。
后面我也会写一篇文章介绍如何开发免费领币网站,也就是水龙头网站。
我们是一群立志改变世界的人。而 Web3 是未来世界一大变数,我们想帮助更多人了解并加入 Web3,如果你对 Web3 感兴趣,可以添加我的微信:LZQ20130415,邀你入群,一起沉淀、一起成长、一起拥抱未来。
今天的文章Web3割韭菜的王牌“发币”到底是什么?教你基于ERC20开发发币DApp(涵盖前端、智能合约)分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/15086.html