Hardhat主网分叉环境下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

