在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环境搭建:
- 建立空文件夹,运行npm init,初始化一个npm项目,按照提示依次输入项目信息
npm init
- 初始化完成后,运行npm install --save-dev hardhat,稍后使用的时候使用npx hardhat的方式调用
npm install --save-dev hardhat
- 安装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
- 初始化一个hardhat项目
npx hardhat
- 选择第一个创建简单的项目,这里会包含一些示例代码
- 创建完毕之后,再次运行npx hardhat即可获得帮助
- 开启挖抗模式,否则后面添加合约后,不会有新的块产出,就看不到效果,修改目录下
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",
};
- 启动本地开发网络
npx hardhat node
看到输出Account #1、Private Key等信息,说明本地网络启动成功,该网络每次启动都是新的,但创世用户都是固定的,仅用于开发使用,并非真正意义上的区块链网络,本地开发测试发布都需要依赖这个网络环境
- 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
- 先添加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;
}
}
- 添加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);
}
- 添加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];
}
}
- 添加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);
}
}
- 查看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);
}
}
上方代码有三个内部方法,因为方法的操作内容比较敏感,所以控制为内部方法,然后使用权限控制调用是否允许,下面编写内部方法的对外调用接口
- 在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;
}
}
三、智能合约编译
- 运行npx hardhat compile编译合同
npx hardhat compile
---------------------
# 编译成功最后输出如下
> Compilation finished successfully
四、智能合约测试
- 测试合约,在test目录下,能够找到sample-test.js,如下图所示,这是默认的测试代码示例;可以按照该格式写测试代码,调用合同中的方法并输出log进行测试合同是否正常
- 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");
});
});
- 编写好测试脚本之后,运行npx hardhat test即可看到输出
npx hardhat test
-------------------------
# 输出如下说明成功
PET
√ Should return the new PET once it's changed (166ms)
1 passing (901ms)
五、智能合约部署(本地开发网络)
- 测试通过之后,进行合同的部署,在scripts目录里面你会发现sample-script.js,这是发布合约的示例,如图所示;可以按照该格式进行部署脚本的编写
- 如下是,我针对自己添加的智能合约的部署脚本pet-deploy-script.js,
hre.ethers.getContractFactory
的参数是合同名,就是智能合约代码中contracts后的名,不是合约文件名
const hre 以上是关于在ETH网络上发布代币智能合约(在以太坊发行自己的币种)的主要内容,如果未能解决你的问题,请参考以下文章