在ETH网络上发布代币智能合约(在以太坊发行自己的币种)

Posted Aubrey-J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在ETH网络上发布代币智能合约(在以太坊发行自己的币种)相关的知识,希望对你有一定的参考价值。

在ETH网络上发布代币智能合约(在以太坊发行自己的币种)

以太坊官方开发文档:https://ethereum.org/zh/
以太坊小游戏代码学习:https://cryptozombies.io/

本文介绍了以下内容,文章比较长,涉及内容较多,如果对你有帮助,麻烦点赞、收藏,感谢支持

  • 如何搭建本地区块链智能合约开发环境
  • 如何编写代币合约、及代币合约的自定义方法
  • 智能合约测试、部署到开发网络、测试网络
  • 发布后调用合约
  • 相关代码会放到GitHub,链接在文章末尾

本文使用环境

  • Windows 10 x64
  • Node v12.14.0
  • Hardhat v2.6.6(npm安装依赖会自动安装最新,版本不影响使用)
  • MetaMask 浏览器插件(Google插件需要VPN,可以使用Edge,安装插件不要翻墙一样使用)
  • (非必须)使用Besu搭建的ETH私链(搭建方法参考我的另一篇文章:使用For Java的Hyperledger Besu部署以太坊的私有链),不搭建私链也可以,文章最后是在以太坊测试链练习发布,免去搭建私链的时间

一、环境搭建

以太坊开发环境(之一Hardhat):https://hardhat.org/
以太坊合约调用(之一Web3.js也可以编译、部署合约):https://web3js.readthedocs.io/

1.安装Node.js

Hardhat是依赖Node环境的,先安装Node.js

下载长期维护版,并进行安装即可(各系统的安装流程,环境变量配置此处不做介绍,非本文核心内容),安装完成后打开终端(cmd、PowerShell),如下测试node环境有效性,正常输出版本信息即可

node -v
v12.14.0

2.安装HardHat

(Windows版node环境)Hardhat环境搭建:

  1. 建立空文件夹,运行npm init,初始化一个npm项目,按照提示依次输入项目信息
npm init
  1. 初始化完成后,运行npm install --save-dev hardhat,稍后使用的时候使用npx hardhat的方式调用
npm install --save-dev hardhat
  1. 安装hardhat-waffle、hardhat-truffle5、hardhat-ethers、web3,这使得 Hardhat 与使用 Waffle 构建的测试兼容,并支持远程调用智能合约
npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-truffle5 @nomiclabs/hardhat-web3 web3
  1. 初始化一个hardhat项目
npx hardhat 

  1. 选择第一个创建简单的项目,这里会包含一些示例代码
  2. 创建完毕之后,再次运行npx hardhat即可获得帮助
  3. 开启挖抗模式,否则后面添加合约后,不会有新的块产出,就看不到效果,修改目录下hardhat.config.js文件的networks如下,其他配置可参考:HardHat网络参考
module.exports = {
  defaultNetwork: "hardhat",
  networks: {
    hardhat: {
	  // 在这种情况下,将在 3 到 6 秒的随机延迟后挖掘一个新块。
	  // 例如,第一个区块可以在 4 秒后开采,第二个区块在 5.5 秒后开采,依此类推。
      mining: {
        auto: false,
        interval: [3000, 6000]
      }
    }
  },
  solidity: "0.8.4",
};
  1. 启动本地开发网络
npx hardhat node

看到输出Account #1、Private Key等信息,说明本地网络启动成功,该网络每次启动都是新的,但创世用户都是固定的,仅用于开发使用,并非真正意义上的区块链网络,本地开发测试发布都需要依赖这个网络环境

  1. Hardhat Network将公开一个JSON-RPC接口。要使用它,请将您的钱包或应用程序连接到http://localhost:8545
  • 新建或导入MetaMask钱包账户,此文不介绍该内容,钱包信息注意妥善保管

  • 点击浏览器插件MetaMask,三个点,点击展开视图便于操作

  • 点击头像,选择设置

  • 添加本地开发网络

  • 保存后,头像旁就变为本地网络了,说明连接成功

  • 注意:这时的用户还是创建钱包的默认用户,并不是所连接网络上的用户,选择钱包的导入账户

  • 私钥,填写本地HardHat启动时,输出的任何一个Account下面的Private Key,点击导入后如图,才可以使用该账户进行后续操作,如果是私链,则使用私链中的用户,就是一定要使用对应网络中的用户,本地HardHat环境查看创世用户可以使用命令npm hardhat account

二、智能合约开发

所涉及的代码都需要有,因为ERC20是代币本身的规范,但是对代币合约的操作以及其他方法都是需要自己开发的,我这里增加了给用户发币等一些方法,为了良好的体验建议跟着往下走

EIPs/eip-20.md
点击链接查看以太坊官方发布的ERC20标准说明

ERC代表“Etuereum Request for Comment”。
ERC20标准定义了一些函数接口,规定了各个代币的基本功能,它可以快速发币,而且使用又方便,因此空投币和大部分基于以太坊合约的代币基本上就是利用ERC-20标准开发的。
如果简单理解的话就是,ERC20是以太坊的一种标准,所有符合这个标准的数字货币都可以成为ERC20代币,自然也就能存入支持以太币的钱包中了,但是这些货币的交易本质上还是在消耗以太坊的资源,所以交易费都还是以太币。
这里的标准其实就是一种智能合约模版,只要满足这个模版的代币都称之为ERC20代币。你可以理解为电脑主板上的内存插条,只要符合一定的尺寸和接线标准的内存品牌都可以使用。这就等于说以太坊订立了一个标准,其他代币就不需要另起炉灶,全部从头到尾再搞一遍,只要满足这个标准就可以在以太坊公链上运行了,极大降低了开发难度。很多代币就是只关注自己的业务逻辑,底层全部依赖以太坊。
 
转自:https://xw.qq.com/cmsid/20210512A03DDN00

  1. 先添加SafeMath.sol计算方法
    在contracts目录同级创建library目录,在library目录中创建文件SafeMath.sol,内容如下
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;

library SafeMath {
    function mul(uint256 a, uint256 b) internal pure returns (uint256){
        uint256 c = a * b;
        assert(a == 0 || c / a == b);
        return c;
    }
 
    function div(uint256 a, uint256 b) internal pure returns (uint256){
        assert(b > 0);
        uint256 c = a / b;
        return c;
    }
 
    function sub(uint256 a, uint256 b) internal pure returns (uint256){
        assert(b <= a);
        return a - b;
    }
 
    function add(uint256 a, uint256 b) internal pure returns (uint256){
        uint256 c = a + b;
        assert(c >= a);
        return c;
    }
}
  1. 添加ERC20.sol代币标准接口
    在contracts目录同级创建library目录,在library目录中创建文件ERC20.sol,内容如下
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;

interface ERC20 {
    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);
}
  1. 添加Roles.sol合约权限管理,授权用户列表添加、移出等
    在contracts目录同级创建library目录,在library目录中创建文件Roles.sol,内容如下
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;

/**
 * @title Roles
 * @dev Library for managing addresses assigned to a Role.
 */
library Roles {
  struct Role {
    mapping (address => bool) bearer;
  }

  /**
   * @dev give an account access to this role
   */
  function add(Role storage role, address account) internal {
    require(account != address(0));
    role.bearer[account] = true;
  }

  /**
   * @dev remove an account's access to this role
   */
  function remove(Role storage role, address account) internal {
    require(account != address(0));
    role.bearer[account] = false;
  }

  /**
   * @dev check if an account has this role
   * @return bool
   */
  function has(Role storage role, address account)
    internal
    view
    returns (bool)
  {
    require(account != address(0));
    return role.bearer[account];
  }
}
  1. 添加Ownable.sol代币所有者权限管理合约,针对业务,管理业务的授权用户列表,判断是否有权限,业务授权方法等
    在contracts目录同级创建library目录,在library目录中创建文件Ownable.sol,内容如下
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;

import "./Roles.sol";

contract Ownable {
  using Roles for Roles.Role;

  event MinterAdded(address indexed account);
  event MinterRemoved(address indexed account);

  Roles.Role private minters;

  constructor() {
    minters.add(msg.sender);
  }

  modifier onlyMinter() {
    require(isMinter(msg.sender));
    _;
  }

  function isMinter(address account) public view returns (bool) {
    return minters.has(account);
  }

  function addMinter(address account) public onlyMinter {
    minters.add(account);
    emit MinterAdded(account);
  }

  function renounceMinter() public {
    minters.remove(msg.sender);
  }

  function _removeMinter(address account) internal {
    minters.remove(account);
    emit MinterRemoved(account);
  }
}
  1. 查看contracts目录,您应该能够找到 Greeter.sol,这是hardhat默认的示例,不修改可以直接进行下一步编译进行测试;本文需要自己写一个代币的合约,依赖于上方创建的内容,如下是我自己根据ERC20规范写的代币合约,代币名称PET,文件名TokenPET.sol,实现ERC20,放在contracts目录下

需要注意的是,发行的数量需要相对token小数点来设置。
例如如果token的小数点是0位,你要发行1000个token,那么发行数量_totalSupply的值是1000。
但是如果token的小数点是18位,你要发行1000个token,那么发行数量_totalSupply的值是1000000000000000000000(1000后面加上18个0)。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
import { SafeMath } from "../library/SafeMath.sol";
import { ERC20 } from "../library/ERC20.sol";

/// @title PET 私有区块链代币合约
/// @author Aubrey
/// @notice Explain to an end user what this does
/// @dev Explain to a developer any extra details
contract TokenPET is ERC20 {

  // 变量
  string private _name = 'PETurrency';
  string private _symbol = 'PET';
  // 8表示将令牌数量除以100000000得到其用户表示。
  uint8 private _decimals = 8;
  uint256 private _totalSupply;
  uint256 constant private MAX_UINT256 = 2**256 - 1;
  uint256 private maxMintBlock = 0;

  // 使用 SafeMath 函数库
  using SafeMath for uint256;
  // 类比二维数组
  mapping (address => mapping (address => uint256)) private allowed;
  // 类比一维数组
  mapping (address => uint256) private balances;

  // 令牌的名称
  function name() public view returns (string memory) {
    return _name;
  }
  // 令牌的符号
  function symbol() public view returns (string memory) {
    return _symbol;
  }
  // 令牌使用的小数位数 - 例如8,表示将令牌数量除以100000000得到其用户表示。 
  function decimals() public view returns (uint8) {
    return _decimals;
  }
  // 返回总代币供应量
  function totalSupply() public override view returns (uint256) {
    return _totalSupply;
  }
  // 获取账户余额
  function balanceOf(address _owner) public override view returns (uint256 balance) {
    return balances[_owner];
  }
  // 给账户转账
  function transfer(address _to, uint256 _value) public override returns (bool success) {
    assert(0 < _value);
    require(balances[msg.sender] >= _value);
    require(_to != address(0));
    balances[msg.sender] = balances[msg.sender].sub(_value);
    balances[_to] = balances[_to].add(_value);
    emit Transfer(msg.sender, _to, _value);
    return true;
  }
  // 从账户转账到账户 
  function transferFrom(address _from, address _to, uint256 _value) public override returns (bool success) {
    uint256 _allowance = allowed[_from][msg.sender];
    assert (balances[_from] >= _value);
    assert (_allowance >= _value);
    assert (_value > 0);
    require(_to != address(0));
    balances[_from] = balances[_from].sub(_value);
    balances[_to] = balances[_to].add(_value);
    allowed[_from][msg.sender] = _allowance.sub(_value);
    emit Transfer(_from, _to, _value);
    return true;
  }
  // 允许 _spender 多次取回您的帐户,最高达 _value 金额; 如果再次调用此函数,它将用 _value 的当前值覆盖的 allowance 值。
  function approve(address _spender, uint256 _value) public override returns (bool success) {
    require(_spender != address(0));
    require((_value == 0) || (allowed[msg.sender][_spender] == 0));
    allowed[msg.sender][_spender] = _value;
    emit Approval(msg.sender, _spender, _value);
    return true;
  }
  //  返回 _spender 仍然被允许从 _owner 提取的金额。
  function allowance(address _owner, address _spender) public override view returns (uint256 remaining) {
    return allowed[_owner][_spender];
  }

  // 内部方法 给地址 _to 初始化数量 _amount 数量的 tokens,注意 onlyOwner 修饰,只有合约创建者才有权限分配 分配会增加可发行总代币量,如果代币总量为0也可以增量发行
  function _mint(address _to, uint256 _amount) internal {
      require(_to != address(0));
      _totalSupply = _totalSupply.add(_amount);
      balances[_to] = balances[_to].add(_amount);
      emit Transfer(address(0), _to, _amount);
  }
  // 内部方法 销毁一定数量的令牌
  function _burn(address account, uint256 amount) internal {
    require(account != address(0));
    require(amount <= balances[account]);

    _totalSupply = _totalSupply.sub(amount);
    balances[account] = balances[account].sub(amount);
    emit Transfer(account, address(0), amount);
  }
  // 内部方法 从津贴中销毁一定数量的令牌
  function _burnFrom(address account, uint256 amount) internal {
    require(amount <= allowed[account][msg.sender]);

    // Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
    // this function needs to emit an event with the updated approval.
    allowed[account][msg.sender] = allowed[account][msg.sender].sub(amount);
    _burn(account, amount);
  }

}

上方代码有三个内部方法,因为方法的操作内容比较敏感,所以控制为内部方法,然后使用权限控制调用是否允许,下面编写内部方法的对外调用接口

  1. 在contracts目录创建TokenPETMintable.sol代币合约文件,用来向外提供内部方法mint的鉴权调用,需要继承TokenPET、Ownable,根据Ownable的判断是否有权限调用内部方法或其他逻辑,之后的测试、部署都是基于该文件
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
import { TokenPET } from "../contracts/TokenPET.sol";
import { Ownable } from "../library/Ownable.sol";

contract TokenPETMintable is TokenPET, Ownable {
event MintingFinished();

  bool private _mintingFinished = false;

  modifier onlyBeforeMintingFinished() {
    require(!_mintingFinished);
    _;
  }

  /**
   * @return true if the minting is finished.
   */
  function mintingFinished() public view returns(bool) {
    return _mintingFinished;
  }

  /**
   * @dev Function to mint tokens
   * @param to The address that will receive the minted tokens.
   * @param amount The amount of tokens to mint.
   * @return A boolean that indicates if the operation was successful.
   */
  function mint(
    address to,
    uint256 amount
  )
    public
    onlyMinter
    onlyBeforeMintingFinished
    returns (bool)
  {
    _mint(to, amount);
    return true;
  }

  /**
   * @dev Function to stop minting new tokens.
   * @return True if the operation was successful.
   */
  function finishMinting()
    public
    onlyMinter
    onlyBeforeMintingFinished
    returns (bool)
  {
    _mintingFinished = true;
    emit MintingFinished();
    return true;
  }

}

三、智能合约编译

  1. 运行npx hardhat compile编译合同
npx hardhat compile
---------------------
# 编译成功最后输出如下
> Compilation finished successfully

四、智能合约测试

  1. 测试合约,在test目录下,能够找到sample-test.js,如下图所示,这是默认的测试代码示例;可以按照该格式写测试代码,调用合同中的方法并输出log进行测试合同是否正常

  1. PET代币合约测试代码如下
const { expect } = require("chai");

// 这里的PET是测试实例名
describe("PET", function() {
  // 测试描述
  it("Should return the new PET once it's changed", async function() {
    // 获取编译的智能合约TokenPET,这个名是sol合约文件的类名,就是contracts后面的名字,不是文件名
    const TokenPET = await ethers.getContractFactory("TokenPET");
    const pet = await TokenPET.deploy();
    await pet.deployed();

    // 断言判断
    expect(await pet.name()).to.equal("PETurrency");
    expect(await pet.symbol()).to.equal("PET");
  });
});

  1. 编写好测试脚本之后,运行npx hardhat test即可看到输出
npx hardhat test
-------------------------
# 输出如下说明成功
  PET
    √ Should return the new PET once it's changed (166ms)


  1 passing (901ms)

五、智能合约部署(本地开发网络)

  1. 测试通过之后,进行合同的部署,在scripts目录里面你会发现sample-script.js,这是发布合约的示例,如图所示;可以按照该格式进行部署脚本的编写

  1. 如下是,我针对自己添加的智能合约的部署脚本pet-deploy-script.js,hre.ethers.getContractFactory的参数是合同名,就是智能合约代码中contracts后的名,不是合约文件名
const hre 以上是关于在ETH网络上发布代币智能合约(在以太坊发行自己的币种)的主要内容,如果未能解决你的问题,请参考以下文章

ETH以太坊怎样进行一键发币?

手把手教你发行代币

如何在以太坊网络上发布自己的代币

基于以太坊发布属于自己的数字货币(代币)完整版

以太坊ERC20代币开发

什么是以太币/以太坊ETH?