- Published on
Web3续(二)
- Authors

- Name
- MissTree
Hardhat
Hardhat 是一个用于 Ethereum 开发的开发环境,它提供了一套完整的工具和功能,帮助开发者更轻松地构建、测试和部署智能合约。
安装
安装文档 依赖:
- hardhat:本地开发、测试网络部署、主网交互等。
- @chainlink/contracts:在智能合约中调用 Chainlink 的去中心化预言机服务。
- @chainlink/env-enc:加密 .env 文件中的敏感数据,避免明文存储。
- dotenv:加载 .env 文件中的环境变量。
- @nomicfoundation/hardhat-toolbox:包含常用的 Hardhat 插件和工具。
- hardhat-ethers(与以太坊交互)
- hardhat-network-helpers(测试网络工具)
- hardhat-etherscan(合约验证)
- 其他测试和开发依赖(如 chai、mocha)。
- @nomicfoundation/hardhat-ethers:在 Hardhat 测试和脚本中使用 ethers.js 的功能。
- ethers:在脚本或测试中直接使用 ethers.js 功能。
- hardhat-deploy:更灵活地管理合约部署流程(如分离开发/生产环境配置)。
- hardhat-deploy-ethers:在部署脚本中使用 ethers.js 的功能(如获取合约实例、发送交易)。
npm install --save-dev hardhat
# 创建hardhat项目
npx hardhat
# 安装插件
npm install --save-dev @chainlink/contracts @chainlink/env-enc dotenv @nomicfoundation/hardhat-toolbox @openzeppelin/contracts
# 初始化编译 会和 remix 一样在根目录生成一个 artifacts 文件夹
npx hardhat compile
项目结构
├── contracts //编写合约的文件
│ └── Greeter.sol
├── scripts // 用于将当前的合约部署到线上或者测试环境
│ └── deploy.js
├── test // 测试合约的文件
│ └── Greeter.test.js
├── task // 分布合约部署 可以自定义指令,在执行 npx hardhat 命令看到
│ └── index.js
├── hardhat.config.js //配置文件
└── package.json
编写合约
- 创建一个收款函数
- 记录投资人并且查看
- 在锁定期内,达到目标值,生产商可以提款
- 在锁定期内,没有达到目标值,投资人在锁定期以后退款
// contract/FundMe.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
contract FundMe {
mapping(address => uint256) public fundersToAmount;
uint256 constant MINIMUM_VALUE = 100 * 10 ** 18; //USD
AggregatorV3Interface public dataFeed;
uint256 constant TARGET = 1000 * 10 ** 18;
address public owner;
uint256 deploymentTimestamp;
uint256 lockTime;
address erc20Addr;
// 标记一个可以查看到成功的变量
bool public getFundSuccess = false;
event FundWithdrawByOwner(uint256);
event RefundByFunder(address, uint256);
constructor(uint256 _lockTime, address dataFeedAddr) {
// sepolia 测试
dataFeed = AggregatorV3Interface(dataFeedAddr);
owner = msg.sender;
deploymentTimestamp = block.timestamp;
lockTime = _lockTime;
}
// 获取当前账户合约余额
function fund() external payable {
require(convertEthToUsd(msg.value) >= MINIMUM_VALUE, "Send more ETH");
require(block.timestamp < deploymentTimestamp + lockTime, "window is closed");
fundersToAmount[msg.sender] = msg.value;
}
// 获取汇率
function getChainlinkDataFeedLatestAnswer() public view returns (int) {
(
/* uint80 roundID */,
int answer,
/*uint startedAt*/,
/*uint timeStamp*/,
/*uint80 answeredInRound*/
) = dataFeed.latestRoundData();
return answer;
}
// ETH 转换为 USD
function convertEthToUsd(uint256 ethAmount) internal view returns(uint256){
uint256 ethPrice = uint256(getChainlinkDataFeedLatestAnswer());
return ethAmount * ethPrice / (10 ** 8);
}
// 获取指定账户合约余额
function transferOwnership(address newOwner) public onlyOwner{
owner = newOwner;
}
// 获取合约余额
function getFund() external windowClosed onlyOwner{
require(convertEthToUsd(address(this).balance) >= TARGET, "Target is not reached");
bool success;
uint256 balance = address(this).balance;
(success, ) = payable(msg.sender).call{value: balance}("");
require(success, "transfer tx failed");
fundersToAmount[msg.sender] = 0;
getFundSuccess = true; // flag
// emit 控制台输出 balance
emit FundWithdrawByOwner(balance);
}
// 退款
function refund() external windowClosed {
require(convertEthToUsd(address(this).balance) < TARGET, "Target is reached");
require(fundersToAmount[msg.sender] != 0, "there is no fund for you");
bool success;
uint256 balance = fundersToAmount[msg.sender];
(success, ) = payable(msg.sender).call{value: balance}("");
require(success, "transfer tx failed");
fundersToAmount[msg.sender] = 0;
emit RefundByFunder(msg.sender, balance);
}
// 转账
function setFunderToAmount(address funder, uint256 amountToUpdate) external {
require(msg.sender == erc20Addr, "you do not have permission to call this funtion");
fundersToAmount[funder] = amountToUpdate;
}
// 设置erc20地址
function setErc20Addr(address _erc20Addr) public onlyOwner {
erc20Addr = _erc20Addr;
}
modifier windowClosed() {
require(block.timestamp >= deploymentTimestamp + lockTime, "window is not closed");
_;
}
modifier onlyOwner() {
require(msg.sender == owner, "this function can only be called by owner");
_;
}
}
// contracts/MockV3Aggregator.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@chainlink/contracts/src/v0.8/tests/MockV3Aggregator.sol";
// contracts/src/v0.8/shared/mocks/MockV3Aggregator.sol
部署合约
// scripts/deploy.js
const { ethers } = require("hardhat")
async function main() {
const fundMeFactory = await ethers.getContractFactory("FundMe")
console.log("contract deploying")
// deploy contract from factory
const fundMe = await fundMeFactory.deploy(300)
await fundMe.waitForDeployment()
console.log(`contract has been deployed successfully, contract address is ${fundMe.target}`);
// verify fundme etherscan的sepolia测试环境id为11155111
if(hre.network.config.chainId == 11155111 && process.env.ETHERSCAN_API_KEY) {
console.log("Waiting for 5 confirmations")
await fundMe.deploymentTransaction().wait(5)
await verifyFundMe(fundMe.target, [300])
} else {
console.log("verification skipped..")
}
// init 2 accounts
const [firstAccount, secondAccount] = await ethers.getSigners()
// fund contract with first account
const fundTx = await fundMe.fund({value: ethers.parseEther("0.5")})
await fundTx.wait()
console.log(`2 accounts are ${firstAccount.address} and ${secondAccount.address}`)
// check balance of contract
const balanceOfContract = await ethers.provider.getBalance(fundMe.target)
console.log(`Balance of the contract is ${balanceOfContract}`)
// fund contract with second account
const fundTxWithSecondAccount = await fundMe.connect(secondAccount).fund({value: ethers.parseEther("0.5")})
await fundTxWithSecondAccount.wait()
// check balance of contract
const balanceOfContractAfterSecondFund = await ethers.provider.getBalance(fundMe.target)
console.log(`Balance of the contract is ${balanceOfContractAfterSecondFund}`)
// check mapping
const firstAccountbalanceInFundMe = await fundMe.fundersToAmount(firstAccount.address)
const secondAccountbalanceInFundMe = await fundMe.fundersToAmount(secondAccount.address)
}
async function verifyFundMe(fundMeAddr, args) {
await hre.run("verify:verify", {
address: fundMeAddr,
constructorArguments: args,
});
}
main().then().catch((error) => {
console.error(error)
process.exit(0)
})
执行部署代码
# 部署到本地网络
npx hardhat run scripts/deploy.js
# 部署到指定网络
npx hardhat run scripts/deploy.js --network sepolia
# 交互部署合约 hardhat task部署`FundMe`合约
npx hardhat deploy-fundme --network sepolia
# 通过hardhat task与`FundMe`合约交互
npx hardhat fund-fundme --network sepolia
测试合约
hardhat 配置
require("@nomicfoundation/hardhat-toolbox");
require("@chainlink/env-enc").config()
require("./tasks")
require("hardhat-deploy")
require("@nomicfoundation/hardhat-ethers");
require("hardhat-deploy");
require("hardhat-deploy-ethers");
const SEPOLIA_URL = process.env.SEPOLIA_URL
const PRIVATE_KEY = process.env.PRIVATE_KEY
const PRIVATE_KEY_1 = process.env.PRIVATE_KEY_1
const EHTERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY
module.exports = {
solidity: "0.8.20",
defaultNetwork: "hardhat",
mocha: {
// hardhat的默认timeout是40秒,这里设置成5分钟
timeout: 300000
},
networks: {
sepolia: {
url: SEPOLIA_URL,
accounts: [PRIVATE_KEY, PRIVATE_KEY_1],
chainId: 11155111
}
},
etherscan: {
apiKey: {
sepolia: EHTERSCAN_API_KEY
}
},
namedAccounts: {
firstAccount: {
default: 0
},
secondAccount: {
default: 1
},
},
gasReporter: {
enabled: true // 是否支持gasReporter的查看
}
};
// helper-hardhat-config.js
const DECIMAL = 8
const INITIAL_ANSWER = 300000000000
const devlopmentChains = ["hardhat", "local"]
const LOCK_TIME = 180
const CONFIRMATIONS = 5
const networkConfig = {
11155111: {
ethUsdDataFeed: "0x694AA1769357215DE4FAC081bf1f309aDC325306"
},
97: {
ethUsdDataFeed: "0x143db3CEEfbdfe5631aDD3E50f7614B6ba708BA7"
}
}
module.exports = {
DECIMAL,
INITIAL_ANSWER,
devlopmentChains,
networkConfig,
LOCK_TIME,
CONFIRMATIONS
}
test/.test.js
测试合约代码流程基本还是和前端的vitest和jest类似,都基本是断言处理
const { ethers, deployments, getNamedAccounts, network } = require("hardhat")
const { assert, expect } = require("chai")
const helpers = require("@nomicfoundation/hardhat-network-helpers")
const {devlopmentChains} = require("../../helper-hardhat-config")
!devlopmentChains.includes(network.name)
? describe.skip
: describe("test fundme contract", async function() {
let fundMe
let fundMeSecondAccount
let firstAccount
let secondAccount
let mockV3Aggregator
beforeEach(async function() {
await deployments.fixture(["all"])
firstAccount = (await getNamedAccounts()).firstAccount
secondAccount = (await getNamedAccounts()).secondAccount
const fundMeDeployment = await deployments.get("FundMe")
mockV3Aggregator = await deployments.get("MockV3Aggregator")
fundMe = await ethers.getContractAt("FundMe", fundMeDeployment.address)
fundMeSecondAccount = await ethers.getContract("FundMe", secondAccount)
})
it("test if the owner is msg.sender", async function() {
await fundMe.waitForDeployment()
assert.equal((await fundMe.owner()), firstAccount)
})
it("test if the datafeed is assigned correctly", async function() {
await fundMe.waitForDeployment()
assert.equal((await fundMe.dataFeed()), mockV3Aggregator.address)
})
// fund, getFund, refund
// unit test for fund
// window open, value greater then minimum value, funder balance
it("window closed, value grater than minimum, fund failed",
async function() {
// make sure the window is closed
await helpers.time.increase(200)
await helpers.mine()
//value is greater minimum value
await expect(fundMe.fund({value: ethers.parseEther("0.1")}))
.to.be.revertedWith("window is closed")
}
)
it("window open, value is less than minimum, fund failed",
async function() {
await expect(fundMe.fund({value: ethers.parseEther("0.01")}))
.to.be.revertedWith("Send more ETH")
}
)
it("Window open, value is greater minimum, fund success",
async function() {
// greater than minimum
await fundMe.fund({value: ethers.parseEther("0.1")})
const balance = await fundMe.fundersToAmount(firstAccount)
await expect(balance).to.equal(ethers.parseEther("0.1"))
}
)
// unit test for getFund
// onlyOwner, windowClose, target reached
it("not onwer, window closed, target reached, getFund failed",
async function() {
// make sure the target is reached
await fundMe.fund({value: ethers.parseEther("1")})
// make sure the window is closed
await helpers.time.increase(200)
await helpers.mine()
await expect(fundMeSecondAccount.getFund())
.to.be.revertedWith("this function can only be called by owner")
}
)
it("window open, target reached, getFund failed",
async function() {
await fundMe.fund({value: ethers.parseEther("1")})
await expect(fundMe.getFund())
.to.be.revertedWith("window is not closed")
}
)
it("window closed, target not reached, getFund failed",
async function() {
await fundMe.fund({value: ethers.parseEther("0.1")})
// make sure the window is closed
await helpers.time.increase(200)
await helpers.mine()
await expect(fundMe.getFund())
.to.be.revertedWith("Target is not reached")
}
)
it("window closed, target reached, getFund success",
async function() {
await fundMe.fund({value: ethers.parseEther("1")})
// make sure the window is closed
await helpers.time.increase(200)
await helpers.mine()
await expect(fundMe.getFund())
.to.emit(fundMe, "FundWithdrawByOwner")
.withArgs(ethers.parseEther("1"))
}
)
// refund
// windowClosed, target not reached, funder has balance
it("window open, target not reached, funder has balance",
async function() {
await fundMe.fund({value: ethers.parseEther("0.1")})
await expect(fundMe.refund())
.to.be.revertedWith("window is not closed");
}
)
it("window closed, target reach, funder has balance",
async function() {
await fundMe.fund({value: ethers.parseEther("1")})
// make sure the window is closed
await helpers.time.increase(200)
await helpers.mine()
await expect(fundMe.refund())
.to.be.revertedWith("Target is reached");
}
)
it("window closed, target not reach, funder does not has balance",
async function() {
await fundMe.fund({value: ethers.parseEther("0.1")})
// make sure the window is closed
await helpers.time.increase(200)
await helpers.mine()
await expect(fundMeSecondAccount.refund())
.to.be.revertedWith("there is no fund for you");
}
)
it("window closed, target not reached, funder has balance",
async function() {
await fundMe.fund({value: ethers.parseEther("0.1")})
// make sure the window is closed
await helpers.time.increase(200)
await helpers.mine()
await expect(fundMe.refund())
.to.emit(fundMe, "RefundByFunder")
.withArgs(firstAccount, ethers.parseEther("0.1"))
}
)
})
// test/fundme.staging.test.js
const { ethers, deployments, getNamedAccounts } = require("hardhat")
const { assert, expect } = require("chai")
const helpers = require("@nomicfoundation/hardhat-network-helpers")
const {devlopmentChains} = require("../../helper-hardhat-config")
devlopmentChains.includes(network.name)
? describe.skip
: describe("test fundme contract", async function() {
let fundMe
let firstAccount
beforeEach(async function() {
await deployments.fixture(["all"])
firstAccount = (await getNamedAccounts()).firstAccount
const fundMeDeployment = await deployments.get("FundMe")
fundMe = await ethers.getContractAt("FundMe", fundMeDeployment.address)
})
// test fund and getFund successfully
it("fund and getFund successfully",
async function() {
// make sure target reached
await fundMe.fund({value: ethers.parseEther("0.5")}) // 3000 * 0.5 = 1500
// make sure window closed
await new Promise(resolve => setTimeout(resolve, 181 * 1000))
// make sure we can get receipt
const getFundTx = await fundMe.getFund()
const getFundReceipt = await getFundTx.wait()
expect(getFundReceipt)
.to.be.emit(fundMe, "FundWithdrawByOwner")
.withArgs(ethers.parseEther("0.5"))
}
)
// test fund and refund successfully
it("fund and refund successfully",
async function() {
// make sure target not reached
await fundMe.fund({value: ethers.parseEther("0.1")}) // 3000 * 0.1 = 300
// make sure window closed
await new Promise(resolve => setTimeout(resolve, 181 * 1000))
// make sure we can get receipt
const refundTx = await fundMe.refund()
const refundReceipt = await refundTx.wait()
expect(refundReceipt)
.to.be.emit(fundMe, "RefundByFunder")
.withArgs(firstAccount, ethers.parseEther("0.1"))
}
)
})
task
// tasks/index.js
exports.deployConract = require("./deploy-fundme")
exports.interactContract = require("./interact-fundme")
// tasks/deploy-fundme.js
const { task } = require("hardhat/config")
task("deploy-fundme", "deploy and verify fundme conract").setAction(async(taskArgs, hre) => {
// create factory
const fundMeFactory = await ethers.getContractFactory("FundMe")
console.log("contract deploying")
// deploy contract from factory
const fundMe = await fundMeFactory.deploy(300)
await fundMe.waitForDeployment()
console.log(`contract has been deployed successfully, contract address is ${fundMe.target}`);
// verify fundme
if(hre.network.config.chainId == 11155111 && process.env.ETHERSCAN_API_KEY) {
console.log("Waiting for 5 confirmations")
await fundMe.deploymentTransaction().wait(5)
await verifyFundMe(fundMe.target, [300])
} else {
console.log("verification skipped..")
}
} )
async function verifyFundMe(fundMeAddr, args) {
await hre.run("verify:verify", {
address: fundMeAddr,
constructorArguments: args,
});
}
module.exports = {}
// tasks/interact-fundme.js
const { task } = require("hardhat/config")
task("interact-fundme", "interact with fundme contract")
.addParam("addr", "fundme contract address")
.setAction(async(taskArgs, hre) => {
const fundMeFactory = await ethers.getContractFactory("FundMe")
const fundMe = fundMeFactory.attach(taskArgs.addr)
// init 2 accounts
const [firstAccount, secondAccount] = await ethers.getSigners()
// fund contract with first account
const fundTx = await fundMe.fund({value: ethers.parseEther("0.5")})
await fundTx.wait()
// check balance of contract
const balanceOfContract = await ethers.provider.getBalance(fundMe.target)
console.log(`Balance of the contract is ${balanceOfContract}`)
// fund contract with second account
const fundTxWithSecondAccount = await fundMe.connect(secondAccount).fund({value: ethers.parseEther("0.5")})
await fundTxWithSecondAccount.wait()
// check balance of contract
const balanceOfContractAfterSecondFund = await ethers.provider.getBalance(fundMe.target)
console.log(`Balance of the contract is ${balanceOfContractAfterSecondFund}`)
// check mapping
const firstAccountbalanceInFundMe = await fundMe.fundersToAmount(firstAccount.address)
const secondAccountbalanceInFundMe = await fundMe.fundersToAmount(secondAccount.address)
console.log(`Balance of first account ${firstAccount.address} is ${firstAccountbalanceInFundMe}`)
console.log(`Balance of second account ${secondAccount.address} is ${secondAccountbalanceInFundMe}`)
})
module.exports = {}
deploy
const { network } = require("hardhat")
const {devlopmentChains, networkConfig, LOCK_TIME, CONFIRMATIONS} = require("../helper-hardhat-config")
// getNamedAccounts:获取hardhat-config.js中定义的账户 deployments:获取部署的合约
module.exports= async({getNamedAccounts, deployments}) => {
const {firstAccount} = await getNamedAccounts()
const {deploy} = deployments
let dataFeedAddr
let confirmations
if(devlopmentChains.includes(network.name)) {
const mockV3Aggregator = await deployments.get("MockV3Aggregator")
dataFeedAddr = mockV3Aggregator.address
confirmations = 0
} else {
dataFeedAddr = networkConfig[network.config.chainId].ethUsdDataFeed
confirmations = CONFIRMATIONS
}
const fundMe = await deploy("FundMe", {
from: firstAccount,
args: [LOCK_TIME, dataFeedAddr],
log: true,
waitConfirmations: confirmations
})
if(hre.network.config.chainId == 11155111 && process.env.ETHERSCAN_API_KEY) {
await hre.run("verify:verify", {
address: fundMe.address,
constructorArguments: [LOCK_TIME, dataFeedAddr],
});
} else {
console.log("Network is not sepolia, verification skipped...")
}
}
module.exports.tags = ["all", "fundme"]
# 编写测试代码
npx hardhat test test/Greeter.test.js
# 运行测试代码
npx hardhat test
Mock合约
# 可以执行hardhat task 生成mock合约
npx hardhat deploy --tags test
Dapp
现在的Dapp应用基本可以在opensea上找到,使用的主要是ERC-721和ERC-1155协议。应用的存储基本采用链下存储(链上存储主要是合约地址和用户地址,链上存储的合约地址和用户地址可以用来查询链下存储的数据),避免链上存储(On-Chain)的局限性,同时通过技术手段确保链下数据的安全性与可验证性。解决了链上以下问题:
- 成本高:区块链(如以太坊)的存储费用昂贵,每KB数据需消耗Gas费。
- 扩展性差:全节点需存储全部数据,导致网络拥堵(如比特币区块大小限制)。
- 隐私问题:链上数据公开透明,敏感信息不适合直接存储。
- 效率低:高频或大容量数据(如视频、日志)不适合上链。
详细信息请查看gitee 仓库