# Solidity进修之路(一):初识合约及ERC20 **Published by:** [dalaoge.eth](https://paragraph.com/@dalaoge/) **Published on:** 2022-02-11 **URL:** https://paragraph.com/@dalaoge/solidity-erc20 ## Content 大家好,我是大老鸽。之前因为学习web3.js接触过以太坊智能合约方面的的东西,不过没有正式且系统地学习过Solidity这门语言。这次开个新坑,记录一下接下来一段时期内的学习经验和成果。其实这篇也算不上什么科普技术文章,主要目的是与大家交流和分享,与各位一起成长进步。相关概念关于区块链、智能合约、EVM相关概念以及前端工程化开发、Node.js等开发基础本文不再赘述,这里默认读者对这些概念都已经有一定的理解。 没接触过区块链这方面的知识,在阅读本文前可以先深度阅读一下登链汉化的Solidity官方文档-入门智能合约章节。这些基础概念非常重要,建议学习Solidity之前一定要好好消化一下。 如果之前从未接触过Javascript和Node.js,请先去认真学习这部分知识,因为用到的开发环境工具需要你对这方面的东西有一定的熟练度。开发环境准备Win10x64,Node.js版本16.13.2,智能合约的测试、部署工具我选择了Hardhat。Hardhat是一个编译、部署、测试和调试以太坊应用的开发环境。它可以帮助开发人员管理和自动化构建智能合约和DApps过程中固有的重复性任务,并围绕这一工作流程轻松引入更多功能。这意味着hardhat在最核心的地方是编译、运行和测试智能合约。 Hardhat内置了Hardhat网络,这是一个专为开发设计的本地以太坊网络。主要功能有Solidity调试,跟踪调用堆栈、console.log()和交易失败时的明确错误信息提示等。 Hardhat Runner是与Hardhat交互的CLI命令,是一个可扩展的任务运行器。它是围绕任务和插件的概念设计的。每次你从CLI运行Hardhat时,你都在运行一个任务。例如,npx hardhat compile运行的是内置的compile任务。任务可以调用其他任务,允许定义复杂的工作流程。用户和插件可以覆盖现有的任务,从而定制和扩展工作流程。 Hardhat的很多功能都来自于插件,而作为开发者,你可以自由选择想使用的插件。Hardhat不限制使用什么工具的,但它确实有一些内置的默认值。所有这些都可以覆盖编辑器我使用的是vscode(毕竟老前端嘛),安装Solidity扩展。 Solidity扩展的默认配置可能会导致导入模块时报错,在首选项里加入如下配置即可: "solidity.remappings": [ "hardhat/=/Project/contract_project/node_modules/hardhat", "@openzeppelin/=/Project/contract_project/node_modules/@openzeppelin" ], "solidity.defaultCompiler": "localNodeModule" 有时候你配置好了他偶尔也会报错,然后过一会又自己好了,不是很稳定,不过不影响正常用🐶 最后提供一下与本文相关的开发文档,遇到问题可以查阅:Hardhat中文文档、Solidity中文文档新建项目创建项目文件夹,初始化并安装hardhat: npm init npm i hardhat --save-dev 安装完毕后,运行: npx hardhat 选择第一项 "Create a basic sample project",一路回车即可。 命令运行完毕后会构建出一个基本的项目目录:contracts为合约文件夹,用于存档各种你写的sol文件script为脚本文件夹,里面可以存放各种自定义js脚本,比如合约部署脚本等等test为单元测试hardhat.config.js文件用来配置hardhat的基本信息和各种自动化任务,一般我们把它放在根目录关于hardhat.config.js配置项,可以参考官方文档 此时如果再次运行 npx hardhat,则会显示出所有可用的指令:最后不要忘了运行一下命令安装依赖: npm install --save-dev "hardhat@^2.8.3" "@nomiclabs/hardhat-waffle@^2.0.0" "ethereum-waffle@^3.0.0" "chai@^4.2.0" "@nomiclabs/hardhat-ethers@^2.0.0" "ethers@^5.0.0"第一个智能合约首先进入contracts文件夹,删掉自动生成的Greeter.sol,新建HelloWorld.sol。// SPDX-License-Identifier: GPL-3.0 //规定Solidity编译器版本,不同版本可能有不同的语法和特性 pragma solidity ^0.8.9; //引入hardhat自带的console模块,可以使用js中的console.log方法输入日志 import 'hardhat/console.sol'; //contract关键字用于创建合约,类似于标准OOP语言中的“类(Class)” contract HelloWorld { //声明一个字符串类型的message属性,public标识它可以在合约外部被读取 string public message; //构造函数,与标准OOP语言中的构造函数一样,在创建合约时(类似于类的实例化)执行一次 //memory关键字表示临时存储,不持久化保存数据(对应storage关键字) constructor(string memory _message) { message = _message; } //这里定义了一个可以被公开访问say方法,view代表可以读取状态变量但不可修修改 //在0.4.17版本以前的constant关键字等同于后来的view,后来constant关键字被废弃 function say() public view returns (string memory) { console.log('Hello World!'); return message; } } 注意:第一行的版权许可不要删掉! 上面实现了一个非常简单的智能合约,只定义了一个变量、一个方法和一个构造函数。 智能合约非常像标准面向对象语言中的”类“,合约中可以包括变量、方法、事件、结构体等,其也有继承(Solidity支持多重继承)、重写等特性。创建合约类似于将一个类实例化,此时会调用构造函数,初始化各种持久化的变量。执行完成后代码会部署到链上,这时候我们就可以与合约交互了。 Solidity规定一个合约中最多定义一个构造函数,方法的书写方式是function关键字+方法名(参数)在前,后面再写修饰符,有返回值的方法最后要加上returns关键字+返回值类型(说实话有点别扭...)。编译合约运行以下指令即可: npx hardhat compile 编译完成后会多出两个文件夹:artifacts和cache。artifacts存放编译好的工件文件、调试文件以及一些构建信息文件,cache里的solidity-files-cache.js则保存了一些编译合约文件时生成的基本信息。 我们这边要着重关注的是编译出来的工件文件:HelloWorld.json一个工件拥有部署和与合约交互所需的所有信息。这些信息与大多数工具兼容,包括Truffle的artifact格式。每个工件都由一个以下属性的 json 组成。contractName: 合约名称的字符串。abi:合约ABI的JSON描述。bytecode:一个0x前缀的未连接部署字节码的十六进制字符串。如果合约不可部署,则有0x字符串。deployedBytecode:一个0x前缀的十六进制字符串,表示未链接的运行时/部署的字节码。如果合约不可部署,则有0x字符串。linkReferences:字节码的链接参考对象由solc返回。如果合约不需要链接,该值包含一个空对象。deployedLinkReferences:已部署的字节码的链接参考对象由solc返回。如果合约不需要链接,该值包含一个空对象。hardhat在每次编译时会检查文件,如果距离上次编译没有任何改动的话则默认不会编译。如果要强行重新编译,请加上--force参数: npx hardhat compile --force测试合约进入test文件夹,删除里面的sample-test.js,新建test.js: Hardhat默认的测试框架是Waffle,这东西普通的开发者可能之前没听说过(包括我),但是这个框架是基于大名鼎鼎的mocha和chai开发出来的。const { expect } = require('chai') const { ethers } = require('hardhat') describe('合约基本测试', function () { it('调用say方法,日志应该输出"Hello, world!"', async () => { //返回一个HelloWorld合约的工厂promise const HelloWorld = await ethers.getContractFactory('HelloWorld') //开始部署合约并且返回实例化出来的合约对象的promise const hw = await HelloWorld.deploy('Hello, world!') //等待合约部署完毕 await hw.deployed() //hw可以调用合约的成员方法,上面这一系列操作非常类似标准OOP中的实例化操作 expect(await hw.say()).to.equal('Hello, world!') }) }) 运行命令: npx hardhat test测试通过🥳部署合约接下来我们准备部署合约。首先,进入 scripts文件夹,删除里面的文件,新建deploy.jsconst { ethers } = require('hardhat') const main = async () => { //其实部署方法起来跟之前的测试完全一样 const HelloWorld = await ethers.getContractFactory('HelloWorld') const hw = await HelloWorld.deploy('HelloWorld', 'HelloWorld') await hw.deployed() console.log('合约部署成功,地址:', hw.address) } main() .then(() => process.exit(0)) .catch(error => { console.error(error) process.exit(1) }) 接下来我们启动一下Hardhat Network,这是一个为开发而设计的本地以太坊网络。Hardhat Network是如何工作的?它在收到每笔交易后,立即按顺序出块,没有任何延迟。底层是基于 @ethereumjs/vm EVM 实现, 与ganache、Remix和Ethereum Studio 使用的相同EVM。支持以下的硬分叉:byzantiumconstantinoplepetersburgistanbulmuirGlacier打开一个新终端,运行命令: npx hardhat node它将启动Hardhat Network,并作为一个公开的JSON-RPC和WebSocket服务器。 然后,只要将钱包或应用程序连接到http://localhost:8545 。 如果你想把Hardhat连接到这个节点,你只需要使用--network localhost来运行命令。 Hardhat Network默认用此状态初始化:一个全新的区块链,只是有创世区块。220个账户,每个账户有10000个ETH,助记词为:"test test test test test test test test test test test junk" 在原来的终端运行命令(此命令会自动编译合约): npx hardhat run --network localhost scripts/deploy.jsFork 主网你可以启动一个Fork主网的Hardhat Network实例。 Fork主网意思是模拟具有与主网相同的状态的网络,但它将作为本地开发网络工作。 这样你就可以与部署的协议进行交互,并在本地测试复杂的交互。 要使用此功能,你需要连接到存档节点。 建议使用Alchemy本地测试网络上除了你自己部署的合约,没有其他任何东西。如果想拿主网上的一些现成的东西测试你还需要自己部署,繁琐不说还非常容易出错。本地分叉主网可以使你的本地测试网络模拟主网状态,让你拥有主网上的合约、账户、ERC20 代币等。 要使用Fork主网可以通过以下两种方法。 第一种是直接加--fork参数: npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/ 另一种是在hardhat.config.js进行配置,然后再启动节点,不需要加--fork参数:networks: { hardhat: { forking: { url: "https://eth-mainnet.alchemyapi.io/v2/<key>" } } } ERC20在学习部署了一个简单的智能合约之后,我们再来看一下ERC20合约规范。ERC20可以说是与币圈人关系最密切的一个规范。什么是 ERC-20? ERC-20 提供了一个同质化代币的标准,换句话说,每个代币与另一个代币(在类型和价值上)完全相同。 例如,一个 ERC-20 代币就像 ETH 一样,意味着一个代币会并永远会与其他代币一样。很多常见代币都是ERC20代币:我们来看一下ERC20的源码: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol 太长了我就不往文章里贴了,大家可以打开上面链接自行查看。 ERC20规范的代币合约,主要派生自IERC20和IERC20Metadata两个合约接口规范。 IERC20:pragma solidity ^0.8.9; interface IERC20 { //方法 //返回代币总供应量 function totalSupply() external view returns (uint256); //返回传入的account的余额 function balanceOf(address account) external view returns (uint256); //转账,从调用合约的地址发送value个代币到地址recipient,触发Transfer事件 function transfer(address recipient, uint256 amount) external returns (bool); //返回被允许从owner提取到spender余额。 function allowance(address owner, address spender) external view returns (uint256); //授权spender使用amount数量的代币,触发Approval事件 function approve(address spender, uint256 amount) external returns (bool); //从地址sender发送value个代币到地址recipient,触发Transfer事件。 function transferFrom( address sender, address recipient, uint256 amount ) external returns (bool); //事件 //转账时触发 event Transfer(address indexed from, address indexed to, uint256 value); //授权时触发 event Approval(address indexed owner, address indexed spender, uint256 value); } IERC20Metadata:pragma solidity ^0.8.0; import "../IERC20.sol"; interface IERC20Metadata is IERC20 { //返回token名字 function name() external view returns (string memory); //返回token符号 function symbol() external view returns (string memory); //返回token精度 function decimals() external view returns (uint8); } 上面的数据类型是接口,Solidity的接口与其他标准面向对象语言的接口差不多:接口类似于抽象合约,使用interface关键字创建,接口只能包含抽象函数,不能包含函数实现。以下是接口的关键特性:接口的函数只能是外部类型。接口不能有构造函数。接口不能有状态变量。接口可以包含enum、struct定义,可以使用interface_name.访问它们。除了实现上面的接口以外,在ERC20中还定义了如下几个私有属性: //余额,此值为一个映射 mapping(address => uint256) private _balances; //授权某地址使用某地址多少token的映射 mapping(address => mapping(address => uint256)) private _allowances; //最大供应量 uint256 private _totalSupply; //名字 string private _name; //符号 string private _symbol; Solidity中的映射可以理解成一个Map对象,mapping(键=>值) private 对象名字。键类型允许除映射、变长数组、合约、枚举、结构体外的几乎所有类型。值类型没有任何限制,可以为任何类型包括映射类型。可以通过键名来查询对应的值,例如查询0x0000地址的余额: _balances[0x00000] //返回10000 我们一般使用ERC20合约来自@openzeppelin包,这份合约还定义了_mint、_burn、increaseAllowance、decreaseAllowance等方法。顺便一提,@openzeppelin里的decimals返回的是18。 以下是OpenZeppelin的简单介绍:OpenZeppelin provides security products to build, automate, and operate decentralized applications. We also protect leading organizations by performing security audits on their systems and products.实现一个简单的ERC20代币在contracts文件夹里新建DLG.sol,我们来整一个大老鸽币🐶// SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; //引入ERC20合约 import "@openzeppelin/contracts/代币/ERC20/ERC20.sol"; //is关键字指继承,DLG派生于ERC20,可以访问基类所有的非私有成员,包括内部(internal)函数和状态变量 //顺便一提,Solidity支持多重继承(这玩意会产生很多复杂的问题,很恶心) contract DLG is ERC20{ //构造函数,需要传入代币名字和符号 //派生合约需要指定基类所有参数,一般来说可以在声明合约时直接传参。但是像是下面这种情况,基类构造函数的参数依赖于派生合约的,必须按如下写法继承:在构造函数后面加上基类+参数。 constructor (string memory name, string memory symbol) ERC20(name, symbol) { //创建合约时调用_mint方法,此方法会无中生有的生成指定数量的代币并发送给指定账户,这里总供应量为100万 //msg.sender指正在调用此合约的地址 //msg的属性很多,有兴趣可以去查查文档 //decimals()精度返回值默认为18,这边有兴趣可以自行查阅一下以太坊的单位 _mint(msg.sender, 1000000 * 10**uint(decimals())); } } 这样我们就完成了一个很代币合约,非常简单的几行。 然后我们再次使用npx hardhat run --network localhost scripts/deploy.js在本地网络节点(记得开启本地Hardhat本地网络!)部署一下合约。const hre = require('hardhat') const main = async () => { // Hardhat always runs the compile task when running scripts with its command // line interface. // // If this script is run directly using `node` you may want to call compile // manually to make sure everything is compiled // await hre.run('compile'); // We get the contract to deploy const contract = await hre.ethers.getContractFactory('DLG') const result = await contract.deploy('DLG', 'DLG') await result.deployed() console.log('DLG deployed to:', result.address) } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. main() .then(() => process.exit(0)) .catch(error => { console.error(error) process.exit(1) }) 然后根据部署成功后生成的合约地址将代币导入钱包:恭喜你,你已经完成了你的第一个ERC20代币!总结本文讲了一些Solidity中比较基础的东西,发散了一些方面但又没太深入,所以很多朋友可能看得比较一知半解。不过没关系,因为我自己也还在一知半解状态。后面的文章将会深入探讨Solidity的语言,也会带领大家编写一些更为复杂机制的合约。 由于笔者也是初学者,如果文中出现了谬误还望各位读者批评指正,也欢迎大家来与我交流,一起成长进步!来自爱你的DFarm Club~ 欢迎关注我的推特: https://twitter.com/KirisakiAria 邮箱:dalaoge@outlook.com ## Publication Information - [dalaoge.eth](https://paragraph.com/@dalaoge/): Publication homepage - [All Posts](https://paragraph.com/@dalaoge/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@dalaoge): Subscribe to updates - [Twitter](https://twitter.com/KirisakiAria): Follow on Twitter