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

使用Hardhat在Arbitrum分叉链铸造Uniswap V3头寸时交易回滚问题

问题:Hardhat分叉Arbitrum主网调用Uniswap V3 mint头寸时交易回滚

我尝试用Hardhat分叉Arbitrum主网铸造Uniswap V3新头寸,但调用NonfungiblePositionManager.mint()时交易回滚,报错:

Error: Transaction reverted without a reason string

这是我第一次使用分叉功能和Uniswap V3环境,试过切换到USDC/DAI池后问题依然存在。参考Uniswap V3官方文档编写的合约代码如下:

contract Ranger is IERC721Receiver {
    address public constant WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1;
    address public constant USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831;

    uint24 public constant poolFee = 500;

    INonfungiblePositionManager public constant nonfungiblePositionManager =
        INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);

    struct Deposit {
        address owner;
        uint128 liquidity;
        address token0;
        address token1;
    }

    mapping(uint256 => Deposit) public deposits;

    function onERC721Received(
        address operator,
        address,
        uint256 tokenId,
        bytes calldata
    ) external override returns (bytes4) {
        _createDeposit(operator, tokenId);
        return this.onERC721Received.selector;
    }

    function _createDeposit(address owner, uint256 tokenId) internal {
        (
            ,
            ,
            address token0,
            address token1,
            ,
            ,
            ,
            uint128 liquidity,
            ,
            ,
            ,

        ) = nonfungiblePositionManager.positions(tokenId);
        deposits[tokenId] = Deposit({
            owner: owner,
            liquidity: liquidity,
            token0: token0,
            token1: token1
        });

        console.log("Token ID: ", tokenId);
        console.log("Liquidity: ", liquidity);
    }

    function mintNewPosition(
        uint256 amount0ToMint,
        uint256 amount1ToMint
    )
        external
        returns (
            uint256 tokenId,
            uint128 liquidity,
            uint256 amount0,
            uint256 amount1
        )
    {
        TransferHelper.safeApprove(
            USDC,
            address(nonfungiblePositionManager),
            amount0ToMint
        );
        TransferHelper.safeApprove(
            WETH,
            address(nonfungiblePositionManager),
            amount1ToMint
        );
        INonfungiblePositionManager.MintParams
            memory params = INonfungiblePositionManager.MintParams({
                token0: USDC,
                token1: WETH,
                fee: poolFee,
                tickLower: TickMath.MIN_TICK,
                tickUpper: TickMath.MAX_TICK,
                amount0Desired: amount0ToMint,
                amount1Desired: amount1ToMint,
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(this),
                deadline: block.timestamp
            });

        (tokenId, liquidity, amount0, amount1) = nonfungiblePositionManager
            .mint(params);

        _createDeposit(msg.sender, tokenId);

        if (amount1 < amount1ToMint) {
            TransferHelper.safeApprove(
                USDC,
                address(nonfungiblePositionManager),
                0
            );
            uint256 refund1 = amount1ToMint - amount1;
            TransferHelper.safeTransfer(USDC, msg.sender, refund1);
        }
        if (amount0 < amount0ToMint) {
            TransferHelper.safeApprove(
                WETH,
                address(nonfungiblePositionManager),
                0
            );
            uint256 refund0 = amount0ToMint - amount0;
            TransferHelper.safeTransfer(WETH, msg.sender, refund0);
        }
    }
}

对应的Hardhat测试文件:

const WETH = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1";
const USDC = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";

const FEE = 500;

const WHALE = "0x090ee598777CaDDAd04Df4271B167F38E73a9Bf0";

const AMOUNT0 = 10000n;
const AMOUNT1 = 10n;

describe("Ranger", async () => {
  let contract: Contract;
  let accounts: HardhatEthersSigner[];
  let weth: IERC20;
  let usdc: IERC20;
  //   let dai: IERC20;

  before(async () => {
    await deployments.fixture(["all"]);

    accounts = await ethers.getSigners();

    const tmp = await deployments.get("Ranger");
    contract = await ethers.getContractAt(tmp.abi, tmp.address);

    usdc = await ethers.getContractAt("IERC20", USDC);
    weth = await ethers.getContractAt("IERC20", WETH);

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

    const whale = await ethers.getSigner(WHALE);

    const token0amount = AMOUNT0 * 10n ** 6n;
    const token1amount = AMOUNT1 * 10n ** 18n;

    expect(await usdc.balanceOf(whale.address)).to.gte(token0amount);
    expect(await weth.balanceOf(whale.address)).to.gte(token1amount);

    await usdc.connect(whale).transfer(accounts[0].address, token0amount);
    await weth.connect(whale).transfer(accounts[0].address, token1amount);
  });

  it("Pool with given parameters exist", async () => {
    const factory = await ethers.getContractAt(
      "IUniswapV3Factory",
      "0x1F98431c8aD98523631AE4a59f267346ea31F984"
    );

    const pool = await factory.getPool(USDC, WETH, FEE);
    expect(pool).to.not.equal("0x0000000000000000000000000000000000000000");
  });

  it("Mint new position", async () => {
    const token0amount = AMOUNT0 * 10n ** 6n;
    const token1amount = AMOUNT1 * 10n ** 18n;

    const contractAddress = await contract.getAddress();

    await usdc.connect(accounts[0]).transfer(contractAddress, token0amount);
    await weth.connect(accounts[0]).transfer(contractAddress, token1amount);

    expect(await usdc.balanceOf(contractAddress)).to.gte(token0amount);
    expect(await weth.balanceOf(contractAddress)).to.gte(token1amount);

    await contract.mintNewPosition(token0amount, token1amount);
  });
});

测试输出结果:

Ranger
    ✔ Pool with given parameters exist (578ms)
    1) Mint new position


  1 passing (7s)
  1 failing

  1) Ranger
       Mint new position:
     Error: Transaction reverted without a reason string
    at <UnrecognizedContract>.<unknown> (0xc36442b4a4522e871399cd717abdd847ab11fe88)
    at Ranger.mintNewPosition (contracts/Ranger.sol:132)
    at EdrProviderWrapper.request (node_modules/hardhat/src/internal/hardhat-network/provider/provider.ts:429:41)
    at async HardhatEthersSigner.sendTransaction (node_modules/@nomicfoundation/hardhat-ethers/src/signers.ts:125:18)
    at async send (node_modules/ethers/src.ts/contract/contract.ts:313:20)
    at async Proxy.mintNewPosition (node_modules/ethers/src.ts/contract/contract.ts:352:16)
    at async Context.<anonymous> (test/ranger.test.ts:84:5)
可能的原因与修复方案

1. 代币顺序错误(核心问题)

Uniswap V3要求token0必须是按地址字典序排序后的更小地址,对比你使用的两个代币地址:

  • WETH: 0x82aF4944...
  • USDC: 0xaf88d065...

字典序中0x82小于0xaf,所以token0应该是WETH,token1是USDC,你代码里完全写反了,这会导致mint调用找不到对应池的正确状态,直接回滚。

修复代码:修改mintNewPosition中的MintParams参数:

INonfungiblePositionManager.MintParams
    memory params = INonfungiblePositionManager.MintParams({
        token0: WETH,
        token1: USDC,
        fee: poolFee,
        tickLower: TickMath.MIN_TICK,
        tickUpper: TickMath.MAX_TICK,
        amount0Desired: amount1ToMint, // 对应WETH的期望数量
        amount1Desired: amount0ToMint, // 对应USDC的期望数量
        amount0Min: 0,
        amount1Min: 0,
        recipient: address(this),
        deadline: block.timestamp
    });

同时同步调整approve的代币顺序:

// 先授权WETH,再授权USDC
TransferHelper.safeApprove(
    WETH,
    address(nonfungiblePositionManager),
    amount1ToMint
);
TransferHelper.safeApprove(
    USDC,
    address(nonfungiblePositionManager),
    amount0ToMint
);

2. 退款逻辑代币混淆

原代码中的退款逻辑把代币对应关系搞反了,修复后对应正确的代币:

if (amount0 < amount1ToMint) { 
    TransferHelper.safeApprove(WETH, address(nonfungiblePositionManager), 0);
    uint256 refund = amount1ToMint - amount0;
    TransferHelper.safeTransfer(WETH, msg.sender, refund);
}
if (amount1 < amount0ToMint) { 
    TransferHelper.safeApprove(USDC, address(nonfungiblePositionManager), 0);
    uint256 refund = amount0ToMint - amount1;
    TransferHelper.safeTransfer(USDC, msg.sender, refund);
}

3. 检查分叉区块高度

如果分叉的区块过于陈旧,可能池的状态发生了变更,建议在hardhat.config.ts中指定一个较新的区块号:

networks: {
  hardhat: {
    forking: {
      url: "你的Arbitrum RPC节点URL",
      blockNumber: 200000000 // 替换为当前最新的区块号
    }
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 02:15:56