hardhat框架实战-超强fork主网数据合约测试

  • cheng139
  • 更新于 2022-10-31 14:32
  • 阅读 6014

hardhat框架实战-超强fork主网数据合约测试

前言

接触智能合约也有一段时间了,平时写完合约后测试是一件非常麻烦的事,无奈当时啥也不知道只有点点点; 经历: 从开始在remix中部署上测试网,自己在remix界面点击交互测试;然后到使用hardhat部署到测试网,验证开源合约,然后在区块浏览器上继续点点点进行交互测试;再到今天最强的fork主网数据写测试脚本进行合约测试;这里面效率不知道高了多少倍,所以好东西不要藏着掖着,拿来分享呗,害,其中踩过的坑只有自己知道; 吹个牛,这个教程如果自己能够讲清楚对于合约开发人员来说起码价值几万块。 前提会使用hardhat框架进行基本的编译、部署、测试操作,前面的文章:hardhat框架使用

fork主网

为什么要fork主网,想象一下,你写的合约需要与主网已部署的合约进行交互,比如说pancake;那如何来模拟测试呢?难道都一一部署上主网进行测试,这样实在是太浪费gas和人力了,那今天介绍的fork主网测试就解决了这个问题。 简单介绍:fork主网就是将主网上的区块fork到本地网络,然后本地运行的节点网络就是刚才fork下来的,这里面包括整个区块链网络的所有数据,包括账户数据、合约数据等所有数据。 这样我们可以在这个网络中任意的模拟任意账户调用合约(合约数据就是主网的数据)与真实的主网数据进行交互

第一种方式

第二种方式

在这里插入图片描述


fork主网hardhat.config.js配置:

hardhat: {
      forking: {
        url: "https://rpc.ankr.com/bsc", // 区块链主网络节点,自己可以找个免费试用
        httpHeaders: {
            // Bearer后面是Api key,这个需要自己去区块浏览器自己注册申请,免费的完全够用
          "Authorization": "Bearer BGT4I8UIMD3VMRQQKGX24E8NST8H"
        }
      }
    },

在这里插入图片描述


链接地址: 主网测试网节点提供:主网节点提供商1 在这里插入图片描述


主网测试网节点提供:主网节点提供商2 在这里插入图片描述


API KEY申请:BCS API KEY申请 在这里插入图片描述

fork后启动本地网络

bash界面命令:

npx hardhat node

启动后效果如图(本地网络起的是8545端口,这个也可以用小狐狸钱包和remix进行连接): 在这里插入图片描述 在这里插入图片描述

小狐狸添加使用fork节点:

image.png

remix添加使用fork节点:

image.png

image.png

模拟任意账户测试

这样我们就可以使用fork主网数据的本地节点测试 现在想要模拟主网上的任意账户,发起转账等等操作,编写测试脚本试试:

模拟账户转账测试脚本

const {
    time,
    loadFixture,
  } = require("@nomicfoundation/hardhat-network-helpers"); // 时间、快照插件
  const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs"); // 断言插件
  const { expect } = require("chai"); // 断言插件
  const { ethers } = require("hardhat"); // ethers库,与区块链的交互库,轻量级且强大
  const hre = require("hardhat"); // 暂时不知道有什么用
  const BigNumber = require('bignumber'); // 引入bignumber

// 需要将 usdt 的 abi 保存在本地,去区块浏览器里面复制一份下来
const USDT_ABI = require("./ABI/usdt_abi.json");
// usdt 合约的主网地址,bsc链
const USDT_ADDRESS = "0x55d398326f99059fF775485246999027B3197955";

// 这是模拟的账户地址,可以去bsc上查查余额多少哈
const mockAddress = "0x589f4CEE8C6552AD6308CE2aCa807b8302e2375F";

let USDT; // USDT全局contract对象
let signer; // 签名账户对象,使用对象做任何交互操作,如转账、授权、调用合约
let owners; // 本地账户对象

describe("Fork", function () {

  // 钩子函数,每次测试都会提前运行进行
  beforeEach(async function () {
    const provider = ethers.provider;
    // 构造 usdt 合约对象
    USDT = new ethers.Contract(USDT_ADDRESS, USDT_ABI, provider);

    // hardhat本地网络模拟账户设置
    await network.provider.request({
      method: "hardhat_impersonateAccount",
      params: [mockAddress],
    });

    // 获取签名的账户对象
    signer = await ethers.provider.getSigner(mockAddress);

    const [owner] = await ethers.getSigners();
    owners = owner;

  })

  it("Testing fork data", async function () {
    let totalSupply = await USDT.totalSupply();
    console.log("totalSupply is : ", totalSupply.toString());
    let thisAddressBalance = await USDT.balanceOf("0xE564ec151c89BdaD769adF6C1AA36F654dCCFc63");
    console.log("thisAddress is : ", thisAddressBalance);
  });

  it("模拟测试账户", async() =>{

    let BNBBalance = await signer.getBalance();
    console.log(`BNB balance is ${BNBBalance.toString() / 1e18}`);

    let ownerBNBBalance = await owners.getBalance();
    console.log(`ownerBNBBalance balance is ${ownerBNBBalance.toString() / 1e18}`);

    let USDTBalance = await USDT.balanceOf(signer.getAddress()) / 1e18;
    console.log(`USDT balance is ${USDTBalance.toString()}`);

  })

  it("转账操作测试", async() =>{

    // 打印转账前的账户余额
    let USDTBalanceA = await USDT.balanceOf(signer.getAddress()) / 1e18;
    console.log(`USDT balance before transfer is ${USDTBalanceA.toString()}`);

    const recipient = "0x49636C5e61bDab68FB9f33f79866e003c6e9D12d";

    let USDTBalanceB = await USDT.balanceOf(recipient) / 1e18;
    console.log(`USDT balance of recipient before transfer is ${USDTBalanceB.toString()}`);

    let USDTBalanceC = await USDT.balanceOf(owners.address) / 1e18;
    console.log(`USDT balance of recipient before transfer is ${USDTBalanceC.toString()}`);

    console.log("========Transfering========");

    // 转账操作
    await USDT.connect(signer).transfer(
      "0x0Ad4C111595e2F477dF897A22B0e1bDdb49e555f",
      ethers.utils.parseUnits("10000", 18)
    );

    await USDT.connect(signer).transfer(
      owners.address,
      ethers.utils.parseUnits("10000", 18)
    );

    // 打印转账后的账户余额
    USDTBalanceA = await USDT.balanceOf(signer.getAddress()) / 1e18;
    console.log(`USDT balance after transfer is ${USDTBalanceA.toString()}`);

    USDTBalanceB = await USDT.balanceOf(recipient) / 1e18;
    console.log(`USDT balance of recipient after transfer is ${USDTBalanceB.toString()}`);

    USDTBalanceC = await USDT.balanceOf(owners.address) / 1e18;
    console.log(`USDT balance of recipient after transfer is ${USDTBalanceC.toString()}`);

  })
});

之前已经把fork下来的本地网络运行起来了,现在进行运行上面测试脚本操作:

模拟账户转账测试结果

npx hardhat test test/Usdt.js --network localhost 在这里插入图片描述

测试结果,与主网的总供应量完全一致 在这里插入图片描述

直接调用线上合约测试

想象一个场景,编写合约发行一个token在pancake中操作需要进行收税操作,比如自动销毁2%打入黑洞地址;3%需要自动兑换成usdt打到一个营销地址;这时候就涉及到调用主网上的合约了,这时fork主网进行模拟测试的作用将发挥到极致

token的智能合约(截取核心底层_transfer部分):

待测试合约

function _transfer(
        address from,
        address to,
        uint256 amount
    ) private {
        // 这里的hardhat console.log打印工具测试也比较挺好用,不过上线主网时需要清除,也是会耗gas的
        // 同时注意一个console.log中打印参数不超过3个,3个后将会报错
        console.log("the 1e18 is : %s", 1e18);
        require(!_blackList[from], "blackList"); // 黑名单禁止交易
        console.log("pass the blackList");

        uint256 balance = balanceOf(from);
        require(balance >= amount, "balanceNotEnough"); // 账户余额必须大于转账金额,否则交易revert
        // 太坑了
        require(startTradeBlock != 0, "No start trade");  // 需要手动开启交易
        console.log("this here in this");
        if (!_feeWhiteList[from]) { // 收税白名单设置
            console.log("need to get fee in swap");
            // 与pancake交易中,收税
            if (
                (_swapPairList[from] && to != address(this)) ||
                (_swapPairList[to] && from != address(this))
            ) {
                // 2%自动销毁,同时开启通缩,供应总量也减少
                uint256 _burnFee = amount.mul(burnFee).div(10000);
                if ((_tTotal - _burnFee) >= minTokenAmount) {
                    console.log(
                        "burn ---from is : %s, to is : %s, the amount is : %s",
                        from,
                        address(0),
                        _burnFee / 1e18
                    );
                    _tTotal -= _burnFee;
                    _takeTransfer(from, address(0), _burnFee);
                } else {
                    _takeTransfer(from, address(0), _burnFee);
                }

                // 市场收税
                uint256 _marketFee = amount.mul(marketFee).div(10000);
                _takeTransfer(from, marketAddress, _marketFee);

                uint256 _fundFee = amount.mul(fundFee).div(10000);
                _takeTransfer(from, address(this), _fundFee);
                address[] memory path = new address[](2);
                path[0] = address(this);
                path[1] = _fist; // 这个_fist就是USDT地址
                // 调用路由合约的支持收税swap方法将token兑换为usdt发送到feeFundAddress中
                // 注意需要先添加流动性才能够进行操作哦
                _swapRouter
                    .swapExactTokensForTokensSupportingFeeOnTransferTokens(
                        _fundFee,
                        0,
                        path,
                        address(feeFundAddress),
                        block.timestamp
                    );
                _takeTransfer(
                    from,
                    to,
                    amount - _burnFee - _marketFee - _fundFee
                );
            } else {
                // 普通转账
                _takeTransfer(from, to, amount);
            }
        } else {
            _takeTransfer(from, to, amount);
        }
    }

测试合约脚本

const {
    time,
    loadFixture,
  } = require("@nomicfoundation/hardhat-network-helpers");
  const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
  const { expect } = require("chai");
  const { ethers } = require("hardhat");
  const hre = require("hardhat");
  const BigNumber = require('bignumber'); // 引入bignumber

// 需要将 usdt 的 abi 保存在本地
const USDT_ABI = require("./ABI/usdt_abi.json");
const PAIR_ABI = require("./ABI/pair_abi.json");
const ROUTER_ABI = require("./ABI/router_abi.json");
const ECC_ABI = require("./ABI/ecc_abi.json");

// usdt 合约的主网地址
const USDT_ADDRESS = "0x55d398326f99059fF775485246999027B3197955";
// pancake路由合约主网地址
const ROUTER_ADDRESS = "0x10ED43C718714eb63d5aA57B78B54704E256024E";
// 模拟主网账户
const mockAddress = "0x589f4CEE8C6552AD6308CE2aCa807b8302e2375F";

// 全局账户、合约对象
let USDT;
let signer;
let Token;
let ECC;
let PAIR;
let ROUTER;

  describe("ECC", function() {
    let ECCtoken;
    let owners;
    let address1;
    let address2;
    let address3;
    const marketAddress = "0x49636C5e61bDab68FB9f33f79866e003c6e9D12d";
    const ONE_DAY_IN_SECOND = 24 * 60 * 60;

  beforeEach(async function () {
    const [owner, addr1, addr2, addr3] = await ethers.getSigners();
    owners = owner;
    address1 = addr1;
    address2 = addr2;
    address3 = addr3;

    const provider = ethers.provider;
    // 构造 usdt 合约对象
    USDT = new ethers.Contract(USDT_ADDRESS, USDT_ABI, provider);
    TTC = new ethers.Contract(TTC_ADDRESS, TTC_ABI, provider);

    // 模拟用户操作
    await network.provider.request({
      method: "hardhat_impersonateAccount",
      params: [mockAddress],
    });

    await network.provider.request({
      method: "hardhat_impersonateAccount",
      params: [Customer],
    });

    signer = await ethers.provider.getSigner(mockAddress);

    Token = await ethers.getContractFactory("contracts/Ecc_Token.sol:ECC");
    ECCtoken = await Token.deploy();

    // 初始化配对合约对象
    PAIR = await new ethers.Contract(ECCtoken._mainPair(), PAIR_ABI, provider);
    // 初始化路由合约对象
    ROUTER = await new ethers.Contract(ROUTER_ADDRESS, ROUTER_ABI, provider);

    await ECCtoken.deployed();
    await ECCtoken.startTrade();
    console.log("ECC token address is : ", ECCtoken.address);
    await USDT.connect(signer).transfer(
      ECCtoken.address,
      ethers.utils.parseUnits("10000", 18)
    );

    // eccToken要初始化完后才能拿到合约对象
    ECC = await new ethers.Contract(ECCtoken.address, ECC_ABI, provider);

    // 测试设置接收地址
    await ECCtoken.setFeeFundAddress(ECCtoken.address);

    console.log("first ECC token amount is %s", await ECCtoken.balanceOf(ECCtoken.address) / 1e18);

    let USDTBalanceC = await USDT.balanceOf(ECCtoken.address) / 1e18;
    console.log(`USDT balance of recipient before transfer is ${USDTBalanceC.toString()}`);

    console.log("ECC与USDT Pair 合约 is : %s", await ECCtoken._mainPair());
    // 添加流动性,实际是合约本身添加流动性
    await ECCtoken._addLiquitity();
    USDTBalanceC = await USDT.balanceOf(ECCtoken.address) / 1e18;
    console.log(`USDT balance of recipient after transfer is ${USDTBalanceC.toString()}`);

    console.log("last ECC token amount is %s", await ECCtoken.balanceOf(ECCtoken.address) / 1e18);

    console.log("Ecc token of pair balance is : %s", await ECCtoken.balanceOf(ECCtoken._mainPair())/1e18);

    console.log(`USDT balance of pair address is ${(await USDT.balanceOf(ECCtoken._mainPair())/1e18).toString()}`);

    console.log("total is thisAddress is : ", thisAddressBalance);
    // 添加流动性后查看lptoken数量
    console.log("1the fundAddress of lpToken is : ", await PAIR.balanceOf(owner.address) / 1e18);
    console.log("2the fundAddress of lpToken is : ", await PAIR.balanceOf(ECCtoken.address) / 1e18);

    console.log("------------------------------------------初始化完毕------------------------------------------");

  });

  it("用户在pancake swap测试,用ecc兑换usdt", async() => {
    console.log("start---------------------------------------用户在pancake swap测试,用ecc兑换usdt---------------------------------------");
    let beforeSwap = await USDT.balanceOf(mockAddress);
    await ECCtoken.setHoldWhite(mockAddress, true);
    await ECCtoken.transfer(mockAddress, "1000000000000000000000");
    console.log("the signer address is %s", await ECCtoken.balanceOf(mockAddress) / 1e18);
    await time.increase(ONE_DAY_IN_SECOND);
    // 先进行授权给路由合约处理,ECC为合约对象,connect(signer)表示是signer在调用ECC合约对象,后面的1开头的一串字符串为uint256的最大值,即授权最大ecc金额给路由合约
    await ECC.connect(signer).approve(ROUTER_ADDRESS, "115792089237316195423570985008687907853269984665640564039457584007913129639935");
    // 兑换操作,同样是signer调用router合约的swapExactTokensForTokensSupportingFeeOnTransferTokens方法
    await ROUTER.connect(signer).swapExactTokensForTokensSupportingFeeOnTransferTokens("100000000000000000000", 0, [ECCtoken.address, USDT_ADDRESS], mockAddress, (Date.now()) + 10000);
    console.log("the market address is %s", await ECCtoken.balanceOf("0x49636C5e61bDab68FB9f33f79866e003c6e9D12d") / 1e18);
    console.log("the signer address is %s", await ECCtoken.balanceOf(mockAddress) / 1e18);
    console.log("the mockAddress balance of usdt is : %s", (await USDT.balanceOf(mockAddress) - beforeSwap)/1e18);
    console.log("end---------------------------------------用户在pancake swap测试,用ecc兑换usdt---------------------------------------");
  })

  it("普通用户测试添加流动性、移除流动性", async() => {
    console.log("start---------------------普通用户测试添加流动性、移除流动性---------------------");
    let ttcBalanceOfAddress = await TTC.balanceOf(ECCtoken.address);
    console.log("first :the ttc balance of this address is : ", ttcBalanceOfAddress/1e18);

    console.log("first transfer totalSupply is : ", await ECCtoken.totalSupply()/(10 ** 18));

    await ECCtoken.setHoldWhite(mockAddress, true);
    await ECCtoken.transfer(mockAddress, "100000000000000000000"); // 使用字符串也行 
    console.log("first address1 balance ecc is : ", await ECCtoken.balanceOf(mockAddress)/1e18);
    console.log("first transfer address usdt is : ", await USDT.balanceOf(mockAddress)/1e18);
    // await expect(ECCtoken.connect(address1).transfer(address2.address, "100000000000000000000")).to.be.revertedWith("Not have enough flow ECC");
    await time.increase(ONE_DAY_IN_SECOND);
    console.log("approve start-------------------");
    // signer账户ecc授权给路由合约
    await ECC.connect(signer).approve(ROUTER_ADDRESS, "115792089237316195423570985008687907853269984665640564039457584007913129639935");
    // signer账usdt授权给路由合约
    await USDT.connect(signer).approve(ROUTER_ADDRESS, "115792089237316195423570985008687907853269984665640564039457584007913129639935");
    // 添加流动性
    console.log("approve end-------------------");
    // signer账户调用路由合约的addLiquidity方法添加流动性
    await ROUTER.connect(signer).addLiquidity(ECCtoken.address, USDT_ADDRESS, "100000000000000000000", "1000000000000000000000", 0, 0, mockAddress, (Date.now()) + 10000);

    // lp Token的数量
    console.log("the lp token is : ", await PAIR.balanceOf(mockAddress)/1e18);
    console.log("after transfer address usdt is : ", await USDT.balanceOf(mockAddress)/1e18);

    // await expect(hardhatToken.connect(owner).transfer(addr2.address, 1000000000000)).to.be.revertedWith("Not enough tokens");
    console.log("afer transfer address1 balance is : ", await ECCtoken.balanceOf(mockAddress)/ 1e18);

    console.log("marketAddress balance is : ", await ECCtoken.balanceOf(marketAddress)/ 1e18);

    console.log("afer transfer totalSupply is : ", await ECCtoken.totalSupply()/(10 ** 18));

    console.log("---------------------移除流动性测试---------------------");

    // 先进行授权,在pair合约中对路由地址进行授权流动性
    PAIR.connect(signer).approve(ROUTER_ADDRESS, "115792089237316195423570985008687907853269984665640564039457584007913129639935");

    // 移除流动性操作
    console.log("first the ecc balance of Customer is : %s", await ECCtoken.balanceOf(mockAddress)/1e18);
    console.log("first the balance of usdt in Customer is : %s", await USDT.balanceOf(mockAddress)/1e18);
    // signer账户调用路由合约的removeLiquidity方法移除流动性
    await ROUTER.connect(signer).removeLiquidity(ECCtoken.address, USDT_ADDRESS, "290000000000000000000", 0, 0, mockAddress, (Date.now()) + 10000);

    console.log("second the ecc balance of Customer is : %s", await ECCtoken.balanceOf(mockAddress)/1e18);
    console.log("second the balance of usdt in Customer is : %s", await USDT.balanceOf(mockAddress)/1e18);
    console.log("after remove!the lp token is : ", await PAIR.balanceOf(mockAddress)/1e18);
    console.log("finally transfer totalSupply is : ", await ECCtoken.totalSupply()/(10 ** 18));
    console.log("end---------------------普通用户测试添加流动性、移除流动性---------------------");
  })
  })

这里的测试脚本有待改进的地方很多,比如把打印的日志换成断言,这样在大量测试时候就不需要进行人为检查,只需要看测试结果通过与否,省时省力

测试结果:

在这里插入图片描述在这里插入图片描述在这里插入图片描述

总结

fork主网这里面应用场景很多,如模拟交易进行套利,攻击等;最常见的还是合约测试,这里将测试功能发挥到极致,无需耗费gas直接编写脚本测试全覆盖;

使用感受:对比之前的点点点操作,这里的省的时间真的不是一点半点,而且效率直线提升;同时你现在不需再去水龙头领什么测试币,直接本地,编写脚本直接测试完毕;只要测试脚本方法全覆盖,合约基本没有什么问题;相反点点点属于黑盒测试,避免不了一些逻辑上漏洞;所以单元测试是非常重要的一环节,对于合约开发来说更是必不可少的!

参考

文章:https://mirror.xyz/xyyme.eth/Z2qjTJJtaQHcwLc-9yHOGpXbeE7RHGdm5EafGjq7qhw hardhat官网:https://hardhat.org/hardhat-network/docs/guides/forking-other-networks hardhat官网fork后用法:https://hardhat.org/hardhat-network/docs/reference ethers文档:https://docs.ethers.io/v5/

点赞 7
收藏 12
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

2 条评论

请先 登录 后评论
cheng139
cheng139
0x5D19...3f0F
爱好去中心化