Web3割韭菜的王牌“发币”到底是什么?教你基于ERC20开发发币DApp(涵盖前端、智能合约)

本文会讲解代币、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
复制代码

稍等片刻,部署成功。

image.png 将 DApp 部署到 Vercel

部署之前需要将合约地址配置到 vercel 的环境变量中。

由于之前我就配置好了 Vercel,而且这部分不是重点,就不展开讲了。

线上地址:www.webnext.cloud/

Github 源码地址:github.com/luzhenqian/…

后续我会更新一些其他 Web3 案例,也会放在这个仓库中。欢迎 star。

当然,在实际工作中,并不会真正从零实现一个 ERC20 的代币合约,通常会使用 OpenZepplin 这种库来一键发币。本文教学目的是以学习为主。

只懂得如何发行代币还不够,我们还需要知道如何推广币。推广币最简单的方式就是发布一个免费领币的网站,因为互联网上最不缺的就是羊毛党。

后面我也会写一篇文章介绍如何开发免费领币网站,也就是水龙头网站。

我们是一群立志改变世界的人。而 Web3 是未来世界一大变数,我们想帮助更多人了解并加入 Web3,如果你对 Web3 感兴趣,可以添加我的微信:LZQ20130415,邀你入群,一起沉淀、一起成长、一起拥抱未来。

猜你喜欢

转载自juejin.im/post/7186527890732679224
今日推荐