Foundry教程:ERC-20代币智能合约从编写到部署全流程开发-CSDN博客
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
概述
如果你想获得更好的阅读体验请前往我的博客
本博客的内容主要分为以下四部分
一是Foundry的介绍与安装主要介绍为什么选择Foundry进行智能合约开发和安装过程中的各种官方文档中未提及的问题
二是智能合约的编写主要介绍如何使用Foundry初始化开发环境导入其他Solidity模块
三是智能合约的测试介绍Foundry中测试工具以及如何使用Solidity编写测试脚本以及输出Gas报告等内容
四是智能合约的部署介绍如何使用Anvil
构建本地测试环境并进行合约测试并介绍如何将合约部署至测试网络。
本文介绍的内容都会较为初级如果您是高级开发人员建议您直接阅读文档
Foundry的介绍与安装
介绍
在智能合约编写领域较为著名的智能合约编译和测试工作流为hardhathardhat使用npm包进行管理使用JavaScript
作为测试工作流。但其速度受限于JavaScript的性能总体而言较为缓慢且需要在开发流程中变换使用JavaScript与Solidity两种编程语言。Foundry改变了这一工作流。
首先Foundry使用Rust编写其编译Solidity智能合约的速度更快同时如果您使用Linux系统foundry的安装也会非常简单。
其次在开发流程中Foundry仅使用solidity
一种编程语言智能合约工程师可以仅使用solidity
完成智能合约编写、测试和部署。而且Foundry提供了一套完整的开发工具箱主要包括以下三部分:
- forge用于合约代码的编译和测试
- cast用于与智能合约进行交互包括处在各类网络中的智能合约
- anvil用于在本地构建区块链网络
最后Foundry在对导入包的管理时通过git submodule
进行管理可以随时同步更新。个人认为较npm的管理方式更加的优雅和可控。
如果您想更加全面的了解Foundry我个人推荐您去阅读一下Foundry仓库的README.md以及它的文档
Foundry的安装
Foundry开发流程中需要Git
工具由于此内容较为简单读者可自行查阅安装方法。
对于Windows用户而言我个人不建议直接使用官方文档给出的从头编译Foundry的方法该方法需要你安装一套完整的Rust开发环境而且编译过程中会出现大量的无关的编译产物(大概有600MB)且编译时间并不短在10代酷睿i7的CPU和16G内存下编译时间长达数分钟。
为了降低安装难度我更加推荐直接使用官方文档提供的一键脚本直接安装官方提供的编译产物。但此方法只适用于Linux系统。
如果你使用的Mac系统可以跳过下面对Linux系统的讨论直接运行后文给出的终端命令。
如果你使用的系统就是Linux请注意是否使用了最新的版本如Debian11
或Ubuntu 22.04
等版本。如果您使用Debian10
此类低版本系统会出现因缺少关键运行库而报错以致程序无法运行。经过询问开发人员得知官方是在Ubuntu 20.04
系统下进行的项目编译依赖部分较高版本运行库如果使用Debian10
等低版本系统会出现错误。
开发者认为兼容低版本Liunx系统会显著提高编译环境的复杂性在短期内官方不会兼容较低版本的Liunx。
如果你使用Windows我个人推荐安装较高版本Linux的虚拟环境后再安装Foundry。如果您使用Windows10及以上的版本您可以使用WSL虚拟环境。经过测试Ubuntu 22.04 LTS
的WSL版本是符合Foundry一键脚本运行条件的你可以在这里找到它的安装包并安装在你的Windows中。
如果你配置好了符合条件的Linux系统可以直接使用下面给出的命令一键安装Foundry:
curl -L https://foundry.paradigm.xyz | bash
运行完上述命令后在运行下列命令:
foundryup
最后可以通过一下命令检验是否安装成功:
forge -h
如果想了解更多关于安装的信息可以自行阅读官方给出的文档
如果后续需要更新
foundry
可以在此运行foundryup
命令运行后会自动更新当前foundry
智能合约的编写
初始化开发环境
在初始化开发环境前请确认你有以太坊钱包。由于下文存在导出以太坊账户私钥的敏感操作所以这里建议你重新创建一个用于代码开发的以太坊账户。
以太坊提供了测试网络供开发者使用。
在下文中我主要使用MetaMask作为钱包同时主要使用Goerli TestNet
。当然你的账户中需要一些测试用ETH可以前往这个水龙头获取。注意此水龙头要求您注册Alchemy
账号。
由于此文编写时
Ropsten TestNet
仍未废弃所以后文采用了此测试网络。
本节内容主要参考了官方教程的First Steps with Foundry
简单的来说就是使用以下命令初始化开发环境:
forge init ERC20Test
其中ERC20Test
可以更改为你想命名的项目名字。
接下来我们需要安装一些开发库以更加方便地编写代码逻辑此处我们将引入solmate
和Openzeppelin
两个开发库。前者是经过优化的且简单易读的智能合约开发库但就仅实现了部分ERC功能后者未经过优化但包含的内容较多。在此次开发过程中我们主要使用solmate
。
我们可以使用forge
工具非常简单的导入这两个库使用的命令如下:
forge install Rari-Capital/solmate Openzeppelin/openzeppelin-contracts
安装完成后的目录如下:
我个人推荐使用VSCode作为Solidity的编辑器一般来说只需要进行下述两步操作:
- 安装Solidity扩展插件
- 在项目目录中输入以下命令
forge remappings > remappings.txt
该命令将生成映射文件避免报错
如果你想获得更多信息可以参考官方文档给出的Integrating with VSCode中的内容
智能合约的编写
本节主要介绍Solidity智能合约的编写本节内容面向具有一定编程经验的开发者。如果你读者对本节的内容仍无法理解可以先行阅读以下材料
- solidity的官方文档
- Solidity by Example
- Solidity极简入门或称WTF Solidity
我们首先进行下述重命名:
src/Contract.sol
=>src/token.sol
script/Contract.s.sol
=>script/token.s.sol
test/Contract.t.sol
=>test/token.t.sol
对于Foundry来说
.s.sol
和.t.sol
均为功能性代码的后缀这两个后缀名虽然使用Solidity作为开发语言但作用不同于智能合约主要起辅助作用
本次编写的智能合约与ERC20有关。简单来说ERC20允许我们在以太坊中进行发币。本文介绍的智能合约将不仅仅涉及简单的发币功能还将增加代币与以太坊ETH互换的功能开发者提取互换费用的功能。
更加详细的来说本智能合约主要功能是用户需要向智能合约中转入ETH后获得代币开发者可以提取用户为获得代币而转移到智能合约中的ETH。
打开src/token.sol
写入以下内容或者前往此处直接下载代码。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "solmate/tokens/ERC20.sol";
import "openzeppelin-contracts/contracts/access/Ownable.sol";
error NoPayMintPrice();
error WithdrawTransfer();
error MaxSupply();
contract SDUFECoin is ERC20, Ownable {
uint256 public constant MINT_PRICE = 0.00000001 ether;
uint256 public constant MAX_SUPPLY = 1_000_000;
constructor (
string memory _name,
string memory _symbol,
uint8 _decimals
) ERC20 (_name, _symbol, _decimals) {}
function mintTo(address recipient) public payable {
if (msg.value < MINT_PRICE) {
revert NoPayMintPrice();
} else {
uint256 amount = msg.value / MINT_PRICE;
uint256 nowAmount = totalSupply + amount;
if (nowAmount <= MAX_SUPPLY) {
_mint(recipient, amount);
} else {
revert MaxSupply();
}
}
}
function withdrawPayments(address payable payee) external onlyOwner {
uint256 balance = address(this).balance;
(bool transferTx, ) = payee.call{value: balance}("");
if (!transferTx) {
revert WithdrawTransfer();
}
}
}
第1行代码表示该智能合约属于MIT开源协议;
第2行代码表示该智能合约要求Solidity版本大于0.8.13;
第4、5行代码表示该智能合约需要导入的库;
第7、8、9行代码声明了接下来需要使用的两个错误NoPayMintPrice
错误将出现在合约执行人未转入ETH而直接交换代币的情况下; WithdrawTransfer
发生在非智能合约创造者提取合约资金的情况下;MaxSupply
错误发生在代币发行量超过阈值。
第11行代码声明智能合约主体使用is
标识符表示该智能合约是对ERC20
和Ownable
继承前者主要包括ERC20中的各种核心实现(引用自solmate
)后者主要实现了权限控制(引用自Openzeppelin
)避免非合约创造者在合约内提取ETH。
第13行代码规定了交换价格为0.00000001 ether
该常量确定了1ETH兑换1个代币的量价关系。我们将在后文解释为什么使用0.00000001
作为比值;
第14行代码规定了代币总发行量为1_000_000_000
其为常量;
第14-20行为对代币基本属性的构造器其中_name
规定了代币名称; _symbol
规定了代币的缩写; _decimals
规定了代币的基数类似于ETH中的ether
单位。我们上文所定义的代币发行总量代表这发现10^10个单位代币如果你将所有代币铸造出来放在在MetaMask钱包中显示的数量为100个。换而言之MetaMask等钱包显示数量总是由铸造出的代币个数除以其基数。这也说明了为什么使用0.00000001 ether
作为最小铸造价格该价格可以保证你转入1eth将获得在钱包中显示为1的代币。此处与一般的智能合约不同我们并没有直接给出常量的内容在后文部署智能合约的时候我们会通过注入的方式规定变量名这极大方便了合约复用。
对于ETH的单位的详细说明可以参考这个网页
第22-34行规定了最为重要的铸造函数该函数接受一个变量recipient
即代币接受者同时通过payable
关键词也可接受转入的ETH。如果想获得更多关于public
和payable
的信息可以参考这篇中文教程。总体而言该函数可以接受一个规定的变量recipient
和一个隐含的变量即转入的ETH数量(通过msg.value
获得数值)。如果你想更加直观的理解该函数所接受的两个参数可以前往这里查看或参见下图:
对于铸造函数内的逻辑较为简单只需要注意revert
用于报错。而_mint
函数和totalSupply
变量实际来自solmate
读者可自行查询函数定义。总而言之_mint
函数是核心方法totalSupply
变量存储有当前的代币总发行量。该变量也可以直接在etherscan中查阅或参见下图:
该函数的在第一个if判断中实现了规避交换价格低于最低价格的交易第二个if实现了判断当前总发行量是否超标的逻辑。
本智能合约为实现
_burn
函数该函数的主要作用是燃烧代币即减少代币数量可用于通货紧缩的经济模型。
第36-42行实现了提取合约内ETH的功能参数payee
是提取地址该合约通过external
关键词对onlyOwner
进行了扩展而onlyOwner
的主要作用是检查调用者是否为合约指定的Owner
默认为合约创建者当然也可以通过Ownable.sol
中实现的transferOwnership
函数更改合约的Owner
。第37行代码可以获得该合约内ETH的总量第38行通过一个底层函数call
实现资金转移并将转移的结果赋值给transferTx
。如果该值为false
则证明调用失败。
针对于call
的具体信息可以参考中文文档
call
是一个底层函数存在一定的安全问题但可以减少转移时的gas耗费。该函数在未来可能会被弃用。如果想知道比较安全的提取资金的方式可以参考中文Solidity文档中的讨论或者参考著名NFT交易所Opensea
给出的示例
在此处的回调中由于我们严格指定了合约调用人而且不存在复杂的回调问题所以使用call
函数是合理的。而且通过onlyOwner
实现了所谓检查-生效-交互模式。
智能合约的测试
编写测试脚本
为了确保智能合约的安全和有效我们会对智能合约进行全面的测试。在下文中我们将使用solidity
编写测试文件全面覆盖每一个函数。为方便读者阅读首先给出全文代码。
为方便讨论我们先给出测试文件的框架:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/token.sol";
contract tokenTest is Test {
SDUFECoin private token;
using stdStorage for StdStorage;
address internal constant receiver = address(1);
function setUp() public {
token = new SDUFECoin("SDUFECoinTest", "SDCT", 8);
}
//后文给出的函数全部位于此处
}
上述代码给出了各种初始化设置首先导入了需要测试的token.sol
和forge库中的Test.sol
。后者提供了一系列的测试用函数和cheatcode
。所谓cheatcode
直译为作弊码由于测试需要覆盖一些不易发生的特殊情况。我们需要提供作弊的方式直接生成这类特殊情况。在后文中大家会看到一个示例。
第8行声明token
属于SDUFECoin
(SDUFECoin由src/token.sol
定义);
第9行使用了Using For
语法具体可以参考这篇文章;
第10行声明receiver
变量该变量为地址类型(address)、常量(constant)、尽可在合约内及继承合约调用(internal)。address(1)
指地址0x0000000000000000000000000000000000000001
注意在以太坊中
address(0)
即0地址具有特殊含义具体可参考这篇文章。简单来说该地址是黑洞地址代币一旦转入无法转出且该地址可以通过合约调用给出ERC20代币。
第12-14行使用了setUp()
函数对SDUFECoin
进行初始化在此给出在src/token.sol
中的代码:
constructor (
string memory _name,
string memory _symbol,
uint8 _decimals
) ERC20 (_name, _symbol, _decimals) {}
通过对比不难看出我们依次对代币的名称(设置为SDUFECoinTest)、缩写(设置为SDCT)和基数(设置为8)进行了设置。
在下文中我们会编写一系列的测试用函数注意测试函数应与setUp()
函数处以同一层级保持相同缩进。如果您看不懂这句话可以参考上文给出的完整代码
首先编写一个简单的测试函数目的是测试不给交易费用则交易失败的逻辑。代码如下:
function testFailNoMintPricePaid() public {
token.mintTo(address(1));
}
注意测试函数名都应以test
开头如果判断失败逻辑应包含Fail
字段。如上文所述本次测试判断不输入ETH则失败的逻辑故而函数名为testFailNoMintPricePaid
测试函数都应使用public
字段声明。
编写测试输入大于最小交易金额E则交易成功的测试函数:
function testSwapPaid() public {
token.mintTo{value: 0.01 ether}(address(1));
}
此处在mintTo
函数后使用{value: 0.01 ether}
说明输入0.01 ether显然该函数应测试的是成功逻辑所以此处没有Fail
字段。
测试输入小于最小交易金额E则交易失败的测试函数:
function testFailMinPrice() public {
token.mintTo{value: 0.000000001 ether}(address(1));
}
此处转入的金额小于我们所设定的MINT_PRICE
应该触发NoPayMintPrice
错误。
通过输入高额交易量测试超过最大供应量失败的测试函数:
function testFailMaxsupply() public {
token.mintTo{value: 0.015 ether}(address(1));
}
此处向mintTo
函数输入了0.015 ether
经过简单计算尽可以得出输入的金额可以铸造1_500_000
单位代币显然大于最大供应量故而此处应抛出异常使用Fail
测试。
与上文内容相同但此处我们使用cheatcode
进行测试即通过直接篡改运行时数据测试:
function testFailMaxsupplyUseCheat() public {
uint256 slot = stdstore
.target(address(token))
.sig("totalSupply()")
.find();
bytes32 loc = bytes32(slot);
bytes32 mockedTotalSupply = bytes32(abi.encode(1_000_000));
vm.store(address(token), loc, mockedTotalSupply);
token.mintTo{value: 0.00000001 ether}(address(1));
}
在slot
变量中使用target
获得合约地址使用sig
对所需获得totalSupply
变量进行编码使用find
取出totalSupply
当前数值。loc
变量将slot
进行bytes32
编码mockedTotalSupply
先对1_000_000
(该值为最大代币供应量)进行abi
进行编码而后转译为bytes32
。定义完成以上内容后使用vm.store
将数据直接写入合约代码、对于该函数具体的内容可以参考文档。上述内容写入后totalSupply
值就会变成1_000_000
即最大发行量。后文在调用mintTo
函数铸造代币如果符合我们的代码逻辑则此处应该抛出异常。出现错误则代表testFailMaxsupplyUseCheat
通过检查。
上述内容涉及到了智能合约底层简单来说智能合约需要编译成字节码(bytes32)格式存储在区块链上EVM虚拟机获取链上编译后的合约然后运行。上文中我们所操作的就是编译后的智能合约所以大量使用了
bytes32
和abi
。更多关于abi
的信息可以参考这里
在后文给出的两个函数都较为复杂主要作用判断withdrawPayments
函数的作用。
首先我们检测在符合代码逻辑的前提下合约是否能正确运行。代码如下:
function testWithdrawalWorksAsOwner() public {
address payable payee = payable(address(0x1337));
uint256 priorPayeeBalance = payee.balance;
token.mintTo{value: 0.0001 ether}(address(receiver));
assertEq(address(token).balance, 0.0001 ether);
uint256 tokenBalance = address(token).balance;
token.withdrawPayments(payee);
assertEq(payee.balance, priorPayeeBalance + tokenBalance);
}
首先我们声明一个提取合约ETH的接收者即payee
;
然后定义存储当前接受者在接受ETH前的账户余额方便后文进行判断;
然后调用mintTo
函数进行铸造保证合约内存储一定量的ETH方便后文进行提取操作;
assertEq(address(token).balance, 0.0001 ether);
此行代码判断合约内的ETH余额是否等于0.0001 ether
即上文的铸造费用。在测试环境内没有gas费用所以一定相等。更多关于assertEq
的描述可以参考文档
uint256 tokenBalance = address(token).balance;
将合约内的ETH余额数量赋值给tokenBalance
token.withdrawPayments(payee);
调用withdrawPayments
函数将ETH提取给payee
此处其实是由合约创立者调用的函数如果不使用
startPrank
等函数则msg.sender
属性不会改变即所有函数都隐含有合约创立者调用。
assertEq(payee.balance, priorPayeeBalance + tokenBalance);
此行代码较为简单即判断完成ETH提取后账户余额是否等于提取ETH前的余额和合约内的余额。如果两者相等则证明函数逻辑没有问题。
最后测试在不是合约创立者的情况下提取合约内ETH失败的逻辑由于此处为测试失败所以测试函数名内含有Fail
字段。
function testWithdrawalFailsAsNotOwner() public {
token.mintTo{value: token.MINT_PRICE()}(address(receiver));
assertEq(address(token).balance, token.MINT_PRICE());
vm.expectRevert("Ownable: caller is not the owner");
vm.startPrank(address(0xd3ad));
token.withdrawPayments(payable(address(0xd3ad)));
vm.stopPrank();
}
此处省略前两行代码的具体含义。直接讨论较为重要且前文未提及的代码。
vm.expectRevert("Ownable: caller is not the owner");
该行代码的作用为清空前文所有cheatcode
的状态。一般来说调用此函数后一般立即Prank
。如果想知道更多关于expectRevert
的信息可以参考文档
vm.startPrank(address(0xd3ad));
该行代码改变了默认的调用者此处将调用者改为0xd3ad
通过此行代码的调用意味着下文的代码调用者不再是合约创建者理论上合约应该会调用失败。在下文我们直接调用了token.withdrawPayments(payable(address(0xd3ad)));
尝试将合约内的资金提取给非合约创建者此行代码应抛出异常。
vm.stopPrank();
停止调用者切换。
运行测试脚本
对于测试脚本的运行较为简单直接在终端内输入下述命令即可:
forge test
如果一切顺利读者将会看到类似下图的输出:
此图证明所有的测试都已经通过我们接下来可以进行一系列其他操作。
下文我们将给出最为简单但极其有用的gas报告。
在终端输入下述命令:
forge test --gas-report
读者可获得类似下图的输出:
在此处gas费的单位应该是gwei
其值相当于0.000000001Ether
智能合约的部署
智能合约部署脚本
foege
提供了forge create
命令用于合约部署该命令具有较多参数对于复杂的合约部署需要书写较为复杂的命令。在此处我们使用另一种更加方便且易读的solidity script
的方式部署合约。如果读者对forge create
感兴趣可以自行参考文档
solidity script
方式部署合约正如其名我们需要使用solidity
编写部署脚本一般来说都较为简单我们需要在script/token.s.sol
中写入以下内容:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
import "../src/token.sol";
contract TokenScript is Script {
function run() external {
vm.startBroadcast();
SDUFECoin token = new SDUFECoin("SDUFECoinTest", "SDCT", 8);
vm.stopBroadcast();
}
}
如果读者已经自行阅读过上文给出的代码说明此脚本内容应该可以都读懂此处将主要解释vm.startBroadcast()
和vm.stopBroadcast()
vm.startBroadcast()
代表广播开始读者如果了解区块链技术底层应知道每一笔交易都需要进行全网广播以保证每一个节点获得交易信息并进行存储在一定时间内将其打包进入区块。智能合约的部署与之类似也需要广播合约的字节码保证节点将其打包进入以太坊区块。startBroadcast
的功能正是开启广播以为这在此后的代码将被广播进入区块。
SDUFECoin token = new SDUFECoin("SDUFECoinTest", "SDCT", 8);
此行代码说明了构造了一个完整的代币此行代码所对照的字节码将被广播这一意味着合约上链。
vm.stopBroadcast
代币广播关闭我们已将所需要的代码进行了广播所以此处关闭了广播功能。
此部分内容也可以参考官方文档Solidity Scripting内容基本与上述内容一致。
智能合约的本地部署
合约的本地部署可以认为是系统性测试的一种。把智能合约部署到本地网络中我们可以通过cast
等工具调用合约函数得到在区块链真实环境的测试结果与上文提出的测试函数相比更加直观。
在本节内容中我们主要使用以下两种工具:
-
anvil
该工具主要用于搭建一个完全本地化的以太坊环境并提供10个含有1000 ETH的账户。在此节内容中我们主要使用一些较为简单的功能anvil
有fork
特定区块等高级功能但在此处我们并不会使用若想了解具体内容可以参考Overview of Anvil和anvil reference此节内容主要使用前者所介绍的功能如果向更加全面的了解此模块建议阅读后者。 -
cast
工具主要用于与区块链RPC进行交互比如进行合约内函数调用、发起交易、查询链上数据等可以认为是一个命令行式的etherscan
。可以参考Overview of Cast和cast reference
在终端运行anvil
命令你将看到如下输出:
注意该终端窗口不可关闭。
在项目根目录下创建.env
文件输入以下内容:
LOCAL_ACCOUNT=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
注意ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
应自行替换成你输出的结果中的任一一个Private Keys
值。
在终端内输入以下命令进行合约部署:
forge script script/token.s.sol:TokenScript --fork-url http://localhost:8545 --private-key $LOCAL_ACCOUNT --broadcast
如果正确部署则应得到如下结果:
在运行anvil
的终端窗口内应看到如下输出:
为方便终端命令编写使用下述命令将合约地址保存为系统变量:
export TOKEN_ADDRESS="0xe7f1725e7734ce288f8367e1bb143e90bb3f0512"
首先我们查询以下部署的智能合约的各项属性是否正确:
此处使用的命令为cast call $TOKEN_ADDRESS "name()(string)
类型(string)
说明对以太坊测试网络返回的结果应该如何解析成何种数据类型默认返回16进制数字在不指定数据类型的情况下几乎无法阅读。
cast call
的作用是在不发起交易的情况下获得智能合约的属性对应etherscan
中的readContract
界面前往此网页可以查看。
经过简单的核对我们发现这与我们部署时的属性完全相同。
由于
foundry
仍在更新如果上述命令无法正常运行请自行参考文档修改
我们主要验证一个核心函数即mintTo
函数在终端内输入以下命令:
cast send --value 0.0001ether --private-key $LOCAL_ACCOUNT $TOKEN_ADDRESS "mintTo(address)" 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 -j
cast send
主要用于签名发布交易此处中的0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
可以替换成任一一个公钥地址。--value
代表发送的代币数量--private-key
代表私钥-j
代表以json
的形式进行输出。
此处最难理解的应该是"mintTo(address)"
该行代表调用mintTo
函数括号内表明编码的数据类型即将0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
编码为address
类型。如果函数需要多个参数在函数名后面可以逐个列出并使用空格分割比如cast send --ledger 0x... "deposit(address,uint256)" 0x... 1
对于cast send
的命令更多信息请参考文档
完成上述操作后我们使用cast call
命令获取以下合约内的发行量(totalSupply)属性命令如下:
cast call $TOKEN_ADDRESS "totalSupply()(uint256)"
完整输出结果截图如下:
由于withdrawPayments
具有较高风险所以在此处我们也对其进行测试使用以下命令:
cast send --private-key 59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d $TOKEN_ADDRESS "withdrawPayments(address)" 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 -j
此处我们将private-key
的值进行了替换即合约调用者已不再是合约的拥有者该命令运行后会得到报错。如下:
ProviderError(JsonRpcError(JsonRpcError { code: 3, message: "execution reverted: Ownable: caller is not the owner", data: Some(String("0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000204f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572")) }))
然后我们使用正确的合约拥有者调用此函数命令如下:
cast send --private-key $LOCAL_ACCOUNT $TOKEN_ADDRESS "withdrawPayments(address)" 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 -j
输出结果如下:
{
"blockHash":"0xad1145a52589d4305b7504b18054b8457645be28cb7cc6e88fe7e13c808862e0",
"blockNumber":"0x5",
"contractAddress":null,
"cumulativeGasUsed":"0x78a5",
"effectiveGasPrice":"0xd20b3bd6",
"from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"gasUsed":"0x78a5",
"logs":[
],
"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"status":"0x1",
"to":"0xe7f1725e7734ce288f8367e1bb143e90bb3f0512",
"transactionHash":"0x9ed612dba2182a734121ad150220864e051ab92e66f82de1ca4354d3401975c7",
"transactionIndex":"0x0"
}
经过上述测试说明该函数的运行是正常的。
如果读者想继续进行测试可以自行测试transferOwnership
等函数这些函数由openzeppelin/Ownable.sol
定义。
当然读者也可自行尝试其他cast
命令如cast balance
(此命令用于查询指定地址的ETH余额参见cast-balance文档)
如果进行完成所有测试可以前往运行anvil
的终端使用Crtl + C
快捷键结束运行。
将合约部署到本地网络其实也是一种测试手段在下文我们将真正的将合约部署到以太坊上。
智能合约的网络部署
通过上述大量的测试我们可以确定合约可以部署到以太坊环境中故而在本篇博客的最后我们将介绍如何将合约上链。
合约上链需要以下准备:
-
以太坊钱包;
-
以太坊RPC服务商;
-
etherscan API密钥
考虑到许多初级开发者可能没有准备足够的ETH所以我们选择将合约部署到测试网络。在前文(初始化开发环境)中读者应该已经准备好了钱包和测试用ETH此处不再赘述如何获得。
我们首先介绍如何获得Ethersan的API密钥。该过程较为简单步骤如下:
-
前往Etherscan的官网注册页面
-
依次填入各类信息
-
等待Etherscan的激活邮件点击邮件中的激活链接认证账户
-
前往MyAPIkey页面点击
Add
输入API名称即可。
最终结果如下图:
打开.env
文件输入以下内容:
ETHERSCAN_KEY=你的API密钥
其次我们需要一个RPC接口与以太坊网络交互。由于我们没有在本地运行完整的以太坊本地节点所以我们没有办法直接与以太坊网络通信。一种比较简单的与以太坊网络通信的方法就是借助Relay Network
或者简单的认为是一个API接口。市面上有非常多的服务商提供此类服务较为著名的有infura
和alchemy
。前者是全球最大的Relay Network
服务商也是我所使用的服务商。该服务商提供了每日10万次的免费调用额度而且仅使用以太坊网络不需要绑定VISA等信用卡。
获得RPC URL的步骤如下:
-
前往注册页面注册账户并通过邮件激活账户
-
在Welcome页面随便选择点击
sumbit
提交 -
在
Create your first project
界面选择Ethereum
项目名字随便取一个 -
在
KEYS
选项卡内更改ENDPOINTS
至你想要的测试网络在此处我选择了Ropsten
读者可根据手中持有的测试ETH选择对应的网络 -
复制下图红框内的链接
打开.env
文件输入以下内容:
ROPSTEN_RPC_URL=替换为自己的网址
最后我们需要导出一个极其敏感的数据即以太坊账户私钥这里非常建议您使用新建的账户。此处以MetaMask浏览器扩展为例如果你选择了其他钱包请自行查找有关教程。
首先按照下图操作:
操作结束后应获得下图结果:
点击导出私钥
的按钮将显示的私钥复制下来写入.env
PRIVATE_KEY=自行替换
经过上述操作我们已基本完成了部署到以太坊网络中的大部分操作。
在终端内依次输入下述命令:
#读取.env文件内的内容并将其保存为环境变量
source .env
#与部署到本地网络类似使用命令部署到以太坊中
forge script script/token.s.sol:TokenScript --rpc-url $ROPSTEN_RPC_URL --private-key $PRIVATE_KEY --broadcast --verify --etherscan-api-key $ETHERSCAN_KEY -vvvv
此命令中的
-vvvv
的含义是显示4级测试输出具体可参考文档
等待以太坊网络确认并认证最终输出如下图:
此图仅为示例具体内容由于合约地址会出现不同
点击输出结果中的URL
即可再EtherScan中访问智能合约此处给出本次教程部署的智能合约网址。正如上文所述通过Etherscan提供的readContract
和writeContract
也可以可视化的进行一系列测试。
总结
本文完整介绍了智能合约的开发工作流不同于很多其他语言智能合约由于其新生性很多文档并不全面尤其设solmate
等库完全没有文档。这要求读者需要自行阅读合约源码或者选择openzeppelin
这种文档较为完整的库。读者读完此文章后如感觉意犹未尽可以参考Foundry给出的NFT tutorial
部署一个NFT智能合约官网中解释并不十分详细但也给出了完整的代码与开发流程可以前往此网页查看。本教程未来一定会更新你可以订阅本博客的RSS获取最新的文章。