Published on

Web3续(二)

Authors
  • avatar
    Name
    MissTree
    Twitter

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

测试合约

ethers测试参考 hardhat参考

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费。
  • 扩展性差:全节点需存储全部数据,导致网络拥堵(如比特币区块大小限制)。
  • 隐私问题:链上数据公开透明,敏感信息不适合直接存储。
  • 效率低:高频或大容量数据(如视频、日志)不适合上链。

NFT跨链 CCIP

详细信息请查看gitee 仓库