使用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
相关产品推荐
相关产品推荐

