Uniswap V3 LP NFT Wrapper

Structure of NaftaWrapper

First of all, NaftaWrapper is an ERC721 NFT itself, and also it should comply with IFlashNFTReceiver interface, so that Nafta contract can use it for Flashloans.

A Wrapper should be able to wrap and unwrap the NFTs that are fed to it, and give back WrapperNFTs:

/// @notice Wraps Uniswap V3 NFT
/// @param tokenId The ID of the uniswap nft (minted wrappedNFT will have the same ID)
function wrap(uint256 tokenId) external {
  nftOwners[tokenId] = msg.sender;
  _safeMint(msg.sender, tokenId);
  IERC721(uniV3Address).safeTransferFrom(msg.sender, address(this), tokenId);

/// @notice Unwraps Uniswap V3 NFT
/// @param tokenId The ID of the uniswap nft (minted wrappedNFT has the same ID)
function unwrap(uint256 tokenId) external {
  require(nftOwners[tokenId] == msg.sender, "Only owner can unwrap NFT");
  require(ownerOf(tokenId) == msg.sender, "You must hold wrapped NFT to unwrap");
  IERC721(uniV3Address).safeTransferFrom(address(this), msg.sender, tokenId);

Notice we keep track of who wrapped the NFT with nftOwners - cause otherwise we wouldn’t know who can unwrap it (the owner() wouldn’t work here - cause when you flashLoan - you become an owner).

Then we have our payload function, that allows some useful action on the wrapped NFT, in our case - the extraction of fees:

function extractUniswapFees(uint256 tokenId, address recipient) external {
  require(ownerOf(tokenId) == msg.sender, "Only holder of wrapper can extract fees");
  INonfungiblePositionManager nonfungiblePositionManager = INonfungiblePositionManager(uniV3Address);

  // get required information about the UNI-V3 NFT position
  (, , address token0, address token1, , , , , , , , ) = nonfungiblePositionManager.positions(tokenId);

  INonfungiblePositionManager.CollectParams memory params = INonfungiblePositionManager.CollectParams({
    tokenId: tokenId,
    recipient: recipient,
    amount0Max: type(uint128).max,
    amount1Max: type(uint128).max

  // collect the fee's from the NFT
  (uint256 amount0, uint256 amount1) = nonfungiblePositionManager.collect(params);
  emit FeesCollected(token0, amount0, token1, amount1);

And finally, we have a standard IFlashNFTReceiver.executeOperation() function, that will be called by Nafta on any FlashLoan act:

/// @notice Handles Nafta flashloan to Extract UniswapV3 fees
/// @dev This function is called by Nafta contract.
/// @dev Nafta gives you the NFT and expects it back, so we need to approve it.
/// @dev Also it expects feeInWeth fee paid - so should also be approved.
/// @param nftAddress  The address of NFT contract
/// @param nftId  The address of NFT contract
/// @param msgSender address of the account calling the contract
/// @param data optional calldata passed into the function optional
/// @return returns a boolean true on success
function executeOperation(
  address nftAddress,
  uint256 nftId,
  uint256 feeInWeth,
  address msgSender,
  bytes calldata data
) external override returns (bool) {
  emit ExecuteCalled(nftAddress, nftId, feeInWeth, msgSender, data);

  require(nftAddress == address(this), "Only Wrapped UNIV3 NFTs are supported");

  // do the uniswap fee extraction thing
  this.extractUniswapFees(nftId, msgSender);

  // Approve NFT back to Nafta to return it
  this.approve(msg.sender, nftId);

  return true;

And that’s it!

As a bonus and as a UX convenience feature, we also have combined wrapAndAddToNafta() and unwrapAndRemoveFromNafta() functions that save our users the count of transactions they have to make by automatically adding the wrapped NFT to Nafta Pool (or removing it and unwrapping). This is not a requirement, but for sure a nice addition.

Last updated