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