首页 > 其他分享 >使用Hardhat的forking功能在本地模拟EVM链真实环境

使用Hardhat的forking功能在本地模拟EVM链真实环境

时间:2024-08-28 15:56:01浏览次数:11  
标签:function forking EVM uint256 address DAI amounts path Hardhat

Hardhat Network可以复制主网区块链状态数据到本地环境,包括所有余额和部署的合约。称为forking mainnet,可以使得本地测试模拟主网环境,但不用gas,所做的交易也不会真的发生在主网。不止以太坊主网,其他兼容EVM的区块链都可以fork。我们来看一下如何使用这个重要功能。
如下例子,是如何使用Uniswap V2将WETH兑换为DAI,是典型的DeFi应用。这里涉及多个ERC20合约,以及Uniswap合约,如果都把他们通过源代码部署在本地进行测试显然不现实,而且状态数据也很难去模拟。
我们使用Hardhat forking功能,从指定区块号分叉到本地hardhat network节点,来模拟主网状态,这个过程中主网的状态数据我们需要从归档节点去获取,所以可以通过Alchemy这样的Relay去拉。let's Go!

配置Hardhat环境
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.24",
  networks: {
    hardhat: {
      forking: {
        url: "https://eth-mainnet.g.alchemy.com/v2/" + process.env.ALCHEMY_KEY,
        blockNumber: 20623798
      }
    }
  }
};

其中ALCHEMY_KEY是Alchemy的调用Key,配置到.env文件里。

Solidity合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract UniswapV2SwapExamples {
    address private constant UNISWAP_V2_ROUTER =
        0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;

    address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

    IUniswapV2Router private router = IUniswapV2Router(UNISWAP_V2_ROUTER);
    IERC20 private weth = IERC20(WETH);
    IERC20 private dai = IERC20(DAI);

    // Swap WETH to DAI
    function swapSingleHopExactAmountIn(uint256 amountIn, uint256 amountOutMin)
        external
        returns (uint256 amountOut)
    {
        weth.transferFrom(msg.sender, address(this), amountIn);
        weth.approve(address(router), amountIn);

        address[] memory path;
        path = new address[](2);
        path[0] = WETH;
        path[1] = DAI;

        uint256[] memory amounts = router.swapExactTokensForTokens(
            amountIn, amountOutMin, path, msg.sender, block.timestamp
        );

        // amounts[0] = WETH amount, amounts[1] = DAI amount
        return amounts[1];
    }

    // Swap DAI -> WETH -> USDC
    function swapMultiHopExactAmountIn(uint256 amountIn, uint256 amountOutMin)
        external
        returns (uint256 amountOut)
    {
        dai.transferFrom(msg.sender, address(this), amountIn);
        dai.approve(address(router), amountIn);

        address[] memory path;
        path = new address[](3);
        path[0] = DAI;
        path[1] = WETH;
        path[2] = USDC;

        uint256[] memory amounts = router.swapExactTokensForTokens(
            amountIn, amountOutMin, path, msg.sender, block.timestamp
        );

        // amounts[0] = DAI amount
        // amounts[1] = WETH amount
        // amounts[2] = USDC amount
        return amounts[2];
    }

    // Swap WETH to DAI
    function swapSingleHopExactAmountOut(
        uint256 amountOutDesired,
        uint256 amountInMax
    ) external returns (uint256 amountOut) {
        weth.transferFrom(msg.sender, address(this), amountInMax);
        weth.approve(address(router), amountInMax);

        address[] memory path;
        path = new address[](2);
        path[0] = WETH;
        path[1] = DAI;

        uint256[] memory amounts = router.swapTokensForExactTokens(
            amountOutDesired, amountInMax, path, msg.sender, block.timestamp
        );

        // Refund WETH to msg.sender
        if (amounts[0] < amountInMax) {
            weth.transfer(msg.sender, amountInMax - amounts[0]);
        }

        return amounts[1];
    }

    // Swap DAI -> WETH -> USDC
    function swapMultiHopExactAmountOut(
        uint256 amountOutDesired,
        uint256 amountInMax
    ) external returns (uint256 amountOut) {
        dai.transferFrom(msg.sender, address(this), amountInMax);
        dai.approve(address(router), amountInMax);

        address[] memory path;
        path = new address[](3);
        path[0] = DAI;
        path[1] = WETH;
        path[2] = USDC;

        uint256[] memory amounts = router.swapTokensForExactTokens(
            amountOutDesired, amountInMax, path, msg.sender, block.timestamp
        );

        // Refund DAI to msg.sender
        if (amounts[0] < amountInMax) {
            dai.transfer(msg.sender, amountInMax - amounts[0]);
        }

        return amounts[2];
    }
}

interface IUniswapV2Router {
    function swapExactTokensForTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external returns (uint256[] memory amounts);

    function swapTokensForExactTokens(
        uint256 amountOut,
        uint256 amountInMax,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external returns (uint256[] memory amounts);
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

interface IWETH is IERC20 {
    function deposit() external payable;
    function withdraw(uint256 amount) external;
}

选自Solidity by Example - Uniswap V2 Swap

Hardhat部署合约并进行交互
const { ethers } = require("hardhat");

async function main() {
    const [deployer] = await ethers.getSigners();
    const accountBalance = await ethers.provider.getBalance(deployer.address);
    console.log("使用如下账户部署合约:", await deployer.getAddress());
    console.log("deployer账户余额:", accountBalance);

    const factory = await ethers.getContractFactory("UniswapV2SwapExamples");
    const contract = await factory.deploy();
    const uniExamAddress = await contract.getAddress();
    console.log("UniswapV2SwapExamples合约地址: ", uniExamAddress);
    

    const wethAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
    const daiAddress = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
    const usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

    const wethABI = ["function deposit() external payable",
                     "function withdraw(uint256 amount) external",
                     "function approve(address spender, uint256 amount) external returns (bool)"   //从ERC20继承来的
                    ];
    const erc20ABI = [
                        "function balanceOf(address account) external view returns (uint256)"
                    ];
    
    //根据wETH合约地址构造其合约对象,还有两个入参分别是合约接口定义ABI、Provider/Signer
    const wethContract = await new ethers.Contract(wethAddress, wethABI, ethers.provider);
    console.log("WETH合约地址:", await wethContract.getAddress());

    const wethContractSigner = await wethContract.connect(deployer); //后面都是交易,不是只读操作了,所以需要签名
    let wethAmount = ethers.parseEther("1.0");
    await wethContractSigner.deposit({value: wethAmount}); //deployer用户往wETH合约存入1以太,获得1WETH
    await wethContractSigner.approve(uniExamAddress, wethAmount); //deployer用户授权uniExamples合约可以划转1WETH
    
    const contractSigned = contract.connect(deployer);
    //uniExamples合约从用户的wETH账户里划转1WETH到自己账户,再授权uniswap的router合约从自己账户划转1WETH、由router合约完成1 WETH -> 1 DAI的兑换,如果兑换成功、router合约会把1 DAI转到本例中的deployer地址
    let daiAmountMin = 1;
    let amountOut = await contractSigned.swapSingleHopExactAmountIn(wethAmount, daiAmountMin);

    console.log("兑换DAI:", amountOut);

    console.log("deployer账户余额:", ethers.formatEther(await ethers.provider.getBalance(deployer.address)));



    //查询DAI余额
    const daiContract = await new ethers.Contract(daiAddress, erc20ABI, ethers.provider);
    let daiBalanceOfDeployer = await daiContract.balanceOf(deployer.address);
    console.log("%s地址DAI余额为%s", deployer.address, daiBalanceOfDeployer);
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});

运行 npx hardhat run .\ignition\modules\defi.js --network hardhat

使用如下账户部署合约: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
deployer账户余额: 10000000000000000000000n
UniswapV2SwapExamples合约地址:  0xe8c3F27D20472e4f3C546A3f73C04B54DD72871d
WETH合约地址: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
兑换DAI: ContractTransactionResponse {
  provider: HardhatEthersProvider {
    _hardhatProvider: LazyInitializationProviderAdapter {
      _providerFactory: [AsyncFunction (anonymous)],
      _emitter: [EventEmitter],
      _initializingPromise: [Promise],
      provider: [BackwardsCompatibilityProviderAdapter]
    },
    _networkName: 'hardhat',
    _blockListeners: [],
    _transactionHashListeners: Map(0) {},
    _eventListeners: []
  },
  blockNumber: 20623802,
  blockHash: '0x95e8505aabded45bd4f4d40fb071e5d0186ffe409a9074b24c6e59c70d1150f3',
  index: undefined,
  hash: '0x58ea9944dfde011c959968129799c98db89792529c4cd3378c9dbd55326d1c1b',
  type: 2,
  to: '0xe8c3F27D20472e4f3C546A3f73C04B54DD72871d',
  from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
  nonce: 752,
  gasLimit: 30000000n,
  gasPrice: 1535820145n,
  maxPriorityFeePerGas: 1000000000n,
  maxFeePerGas: 2071640290n,
  maxFeePerBlobGas: null,
  data: '0xe47677770000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001',
  value: 0n,
  chainId: 31337n,
  signature: Signature { r: "0xc63f4c5d024d05b9c4379373e815ec06f5228b3993c7aa68c89d39138f232457", s: "0x7315acd37f7962f582b5b33437b0b5eb6011cdcf4958e1117cc2355ad49ba7d2", yParity: 0, networkV: null },
  accessList: [],
  blobVersionedHashes: null
}
deployer账户余额: 9998.997207149629193106
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266地址DAI余额为2435012356407782979818n
测试结果

运行结果符合预期,deployer账户结余9999以太,1个以太通过wETH -> DAI兑换为了2435个DAI, 这与今天上午(2024-8-28)的以太坊价格匹配,因为我们forking的区块号是上午出块的一个区块。

可以看到,我们编写了UniswapV2SwapExamples合约,用的是Hardhat本地模拟账户,但是交互中使用的WETH、DAI以及Uniswap V2的router合约都是跟真实主网上的业务逻辑一样的,且在指定区块号的状态数据也是一样的。这会大大提高测试DeFi应用的效率。

参考

https://hardhat.org/hardhat-network/docs/guides/forking-other-networks

标签:function,forking,EVM,uint256,address,DAI,amounts,path,Hardhat
From: https://www.cnblogs.com/lyhero11/p/18384942

相关文章

  • [Vue] useVModel
    Onewaydatabinding,theparentcomponentpasingdatathrough v-modeltochildcomponent,ifchildmodifythedata, v-modelwilltakecareemitthechangedbacktoparentcomponent.Thisparttenworksfine.Buttheremightbesomeproblem,forexample,wha......
  • 【Azure Fabric Service】Service Fabric部署失败问题 Provisioning of VM extension
    问题描述ServiceFabric部署失败,错误信息:ProvisioningofVMextensionConfigureVMhastimedout.  Extensionprovisioninghastakentoolongtocomplete.Theextensiondidnot reportamessage.Moreinformationontroubleshootingisavailableat https://aka......
  • Python-无ABI文件打包EVM合约方法名及参数方法
    #pipinstalleth-abiimporteth_abi#pipinstallsafe-pysha3fromsha3importkeccak_256defkeccak_256_hash(data:str)->bytes: k=keccak_256() k.update(data.encode()) returnk.digest()defpack_abi_data(method:str=None,params:list=No......
  • JEEVMS仓库管理系统任意文件读取漏洞
    漏洞描述该漏洞由于系统未能正确实施或执行对文件的访问控制权限控制,允许未经授权的用户访问或读取文件,并且应用程序未能对用户输入进行适当验证,攻击者可以构造特殊的输入,如路径遍历攻击读取系统文件内容,导致信息泄露Fofa:body="plug-in/lhgDialog/lhgdialog.min.js?skin=metro......
  • 内核config文件打开CONFIG_DEVMEM后出现For kernel requirements at matrix level 5,
    内核config文件打开CONFIG_DEVMEM后出现编译错误:checkvintfI04-1823:30:02409602409602check_vintf.cpp:84]List'out/target/product/sc126/system/product/etc/vintf/':NosuchfileordirectorycheckvintfI04-1823:30:02409602409602check_vintf.cpp:84]L......
  • 基于Ordinals在比特币L1网络实现EVM图灵完备智能合约支持——BxE协议
    1.BxE项目背景区块链技术自诞生以来,为金融、供应链、数字身份等领域带来了变革性的创新。然而,作为第一个成功应用区块链技术的比特币,存在着一些局限性,如较低的交易吞吐量、较高的能源消耗以及有限的脚本功能。这使得比特币在支持复杂应用和智能合约方面显得力不从心。为了解决......
  • Hardhat框架使用及生成交易trace
    Hardhat介绍hardhat-tutorial安装Hardhat框架安装nvmbrewinstallnvm~/.zshrc添加nvm配置#NVMCONFIGexportNVM_DIR="$HOME/.nvm" [-s"/usr/local/opt/nvm/nvm.sh"]&&\."/usr/local/opt/nvm/nvm.sh"#Thisloadsnvm [-s"/us......
  • Vulnhub-EVM-WP
    Vulnhub-EVM前言靶机地址:EVM:1~VulnHub攻击机:kalilinux靶机描述:这是为初学者设计的超级友好的盒子信息收集主机发现sudonmap-sn192.168.56.0/24端口扫描sudonmap-p-192.168.56.103详细信息扫描sudonmap-p22,53,80,110,139,143,445-A192.168.56.103......
  • android读寄存器的工具-devmem
    一、概述在Linux/android开发中着实用到的调试工具并不是很多。devmem的方式是提供给驱动开发人员,在应用层能够侦测内存地址中的数据变化,以此来检测驱动中对内存或者相关配置的正确性验证。基本原理通过设备文件/dev/mem实现对物理内存的读写。二、用法内核中配置CONFIG_......
  • "the tx doesn't have the correct nonce":使用hardhat调用ganache上部署的合约遇到的
    完整的报错==================>查询存证请求存证请求内容,datahash:0xaad2171441bd73b773e9a9e062753909360bdfcabbddbe93c6c58b13c5c0feaa,创建人:0xF7A1938Fecc594aaF126d46fd173cE74A659ad9A,附加信息:0x66656974757a6920616920646f756368757a69,已投票:0n,共需投票:2n==......