ethereumsolidityhardhat

Error while trying to mint new Uniswap V3 position


I've been trying to mint new position with Hardhat and a fork of Arbitrum Mainnet but when NonfungiblePositionManager.mint() is called, the transaction is reverted with:

Error: Transaction reverted without a reason string

I'm probably doing something wrong as it is the first time I use the fork features and I work with Uniswap V3 environments.

I thought the error was maybe with WETH, so I tried with a USDC/DAI pool but I still got the same error.

I've taken the example contrat from the Uniswap V3 official docs

Here's my smart contract

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);
        }
    }
}

Here's my Hardhat test file

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);
  });
});

Here's my hardhat test output:

  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)

Thank you for your help and your time, if you need anything else let me know.

Kind regards, Julian


Solution

  • For anyone using the same documentation code example here

    You have to change TickMath.MIN_TICK and TickMath.MAX_TICK to ticks with the good tickSpacing

    For example here the pool I used has 500 for fee value, which mean 10 tick spacing but MAX_TICK is equal to 887272 you could do

    tickUpper: MAX_TICK - MAX_TICK % tickSpacing

    You can get tick spacing by calling feeAmountTickSpacing in UniswapV3Factory contract or by calling tickSpacing of any pool contract

            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
            });