You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Hardhat主网分叉环境下UniswapV2代币交换合约测试报错求助

主网分叉环境下UniswapV2交换合约测试报错排查

问题概述

在Hardhat主网分叉环境测试基于UniswapV2的代币交换智能合约时,两个测试用例均抛出相同错误:TypeError: unsupported addressable value (argument="target", value=null),尝试过重建项目、重新分叉主网、使用Mock代币仍未解决。

测试代码(test.js)

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("UniswapExchange Contract", function () {
  let uniswapExchange;
  let uniswapExchangeAddress;
  let owner;
  let addr1;

  beforeEach(async function () {
    const UniswapExchange = await ethers.getContractFactory("UniswapExchange");
    uniswapExchange = await UniswapExchange.deploy();
    await uniswapExchange.waitForDeployment();
    uniswapExchangeAddress = await uniswapExchange.getAddress();
    [owner, addr1] = await ethers.getSigners();
  });

  it("Should swap tokens", async function () {
    const tokenInAddress = "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0"; 
    const tokenOutAddress = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; 
    const amountIn = ethers.parseEther("1");

    const tokenIn = await ethers.getContractAt("IERC20", tokenInAddress);
    await tokenIn.transfer(uniswapExchangeAddress, amountIn);

    await uniswapExchange.swapTokens(tokenInAddress, tokenOutAddress, amountIn);

    const tokenOut = await ethers.getContractAt("IERC20", tokenOutAddress);
    const ownerBalance = await tokenOut.balanceOf(owner.address);

    expect(ownerBalance).to.not.equal(0);
  });

  it("Should withdraw tokens by the owner", async function () {
    const tokenAddress = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
    const amountToWithdraw = ethers.parseEther("1");

    const token = await ethers.getContractAt("IERC20", tokenAddress);
    await token.transfer(uniswapExchangeAddress, amountToWithdraw);

    await uniswapExchange.connect(owner).withdrawTokens(tokenAddress, amountToWithdraw);

    const ownerBalance = await token.balanceOf(owner.address);

    expect(ownerBalance).to.not.equal(0);
  });
});

合约代码(contract.sol)

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

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

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function approve(address spender, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract UniswapExchange {
    address private constant UNISWAP_ROUTER_ADDRESS = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    address private owner;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Only the owner can call this function");
        _;
    }   

    function swapTokens(
        address tokenIn,
        address tokenOut,
        uint256 amountIn
    ) external {
        address[] memory path = new address[](2);
        path[0] = tokenIn;
        path[1] = tokenOut;

        IERC20(tokenIn).transfer(address(this), amountIn);
        IERC20(tokenIn).approve(UNISWAP_ROUTER_ADDRESS, amountIn);

        assembly {
            let uniswapRouter := add(0, UNISWAP_ROUTER_ADDRESS)
            let pathPtr := mload(0x40)
            mstore(pathPtr, 0x20)
            mstore(add(pathPtr, 0x20), 2)
            mstore(add(pathPtr, 0x40), tokenIn)
            mstore(add(pathPtr, 0x60), tokenOut)

            let swapCallSuccess := call(
                gas(),
                uniswapRouter,
                0,
                pathPtr,
                0x80,
                0,
                0
            )
            if eq(swapCallSuccess, 0) {
                revert(0, 0)
            }
        }

        uint256 swappedAmount = IERC20(tokenOut).balanceOf(address(this));
        IERC20(tokenOut).transfer(msg.sender, swappedAmount);
    }

    function withdrawTokens(address token, uint256 amount) external onlyOwner{
        IERC20(token).transfer(owner, amount);
    }
}

报错信息

两个测试用例均抛出以下错误栈:

TypeError: unsupported addressable value (argument="target", value=null, code=INVALID_ARGUMENT, version=6.7.0)
at makeError (node_modules/ethers/src.ts/utils/errors.ts:678:21)
at assert (node_modules/ethers/src.ts/utils/errors.ts:702:25)
at assertArgument (node_modules/ethers/src.ts/utils/errors.ts:714:5)
at resolveAddress (node_modules/ethers/src.ts/address/checks.ts:122:19)
at /Users/denys/Desktop/Solidity/Task/node_modules/ethers/src.ts/contract/contract.ts:166:60)
at ParamType.#walkAsync (node_modules/ethers/src.ts/abi/fragments.ts:777:24)
at ParamType.walkAsync (node_modules/ethers/src.ts/abi/fragments.ts:795:24)
at /Users/denys/Desktop/Solidity/Task/node_modules/ethers/src.ts/contract/contract.ts:164:22)
at Array.map ()
at resolveArgs (node_modules/ethers/src.ts/contract/contract.ts:163:37)

问题根源与修复方案

1. Ethers v6地址获取错误

问题:Ethers v6中,部署合约后直接访问uniswapExchange.address可能返回null,需通过await uniswapExchange.getAddress()获取有效地址。
修复:在beforeEach中添加地址获取逻辑(已在测试代码示例中修正),所有使用合约地址的地方替换为uniswapExchangeAddress。

2. 测试账户无ERC20代币余额

问题:主网分叉环境下,测试账户默认仅持有ETH,无目标ERC20代币,直接调用transfer会失败。
修复:模拟主网持有代币的账户(impersonate),将代币转入测试账户:

// 在测试用例开头添加
const whaleAddress = "0xF977814e90dA44bFA03b6295A0616a897441aceC"; // MATIC鲸鱼示例地址
await ethers.provider.send("hardhat_impersonateAccount", [whaleAddress]);
const whaleSigner = await ethers.getSigner(whaleAddress);

// 从鲸鱼账户转代币到测试账户
await tokenIn.connect(whaleSigner).transfer(owner.address, amountIn);

3. Uniswap Router调用参数错误

问题:合约中使用assembly调用swapExactTokensForTokens时,未完整构造函数所需的5个参数(仅传入path相关数据),导致调用失败。
修复:改用Solidity直接调用Router,避免assembly参数构造错误:

function swapTokens(
    address tokenIn,
    address tokenOut,
    uint256 amountIn
) external {
    address[] memory path = new address[](2);
    path[0] = tokenIn;
    path[1] = tokenOut;

    // 替换冗余transfer为transferFrom,通过授权扣取代币
    IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
    IERC20(tokenIn).approve(UNISWAP_ROUTER_ADDRESS, amountIn);

    // 正确调用Uniswap Router
    IUniswapV2Router02(UNISWAP_ROUTER_ADDRESS).swapExactTokensForTokens(
        amountIn,
        0, // 测试环境设为0,生产环境需计算最小输出
        path,
        msg.sender, // 直接转到调用者地址,无需中转合约
        block.timestamp + 3600 // 1小时过期时间
    );
}

4. 合约内冗余transfer调用

问题:swapTokens中重复调用IERC20(tokenIn).transfer(address(this), amountIn),此时msg.sender已将代币转入合约,该调用会因余额不足失败。
修复:将该调用替换为transferFrom,并要求用户提前授权合约操作代币。

内容的提问来源于stack exchange,提问作者dill

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 22:50:56