// SPDX-License-Identifier: UNLICENSED pragma solidity >=0.7.0; pragma abicoder v2; import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; import '@uniswap/v3-core/contracts/libraries/TickMath.sol'; import '@uniswap/v3-core/contracts/libraries/BitMath.sol'; import '@uniswap/v3-core/contracts/libraries/FullMath.sol'; import '@openzeppelin/contracts/utils/Strings.sol'; import '@openzeppelin/contracts/math/SafeMath.sol'; import '@openzeppelin/contracts/math/SignedSafeMath.sol'; import 'base64-sol/base64.sol'; import './HexStrings.sol'; import './NFTSVG.sol'; library NFTDescriptor { using TickMath for int24; using Strings for uint256; using SafeMath for uint256; using SafeMath for uint160; using SafeMath for uint8; using SignedSafeMath for int256; using HexStrings for uint256; uint256 constant sqrt10X128 = 1076067327063303206878105757264492625226; struct ConstructTokenURIParams { uint256 tokenId; address quoteTokenAddress; address baseTokenAddress; string quoteTokenSymbol; string baseTokenSymbol; uint8 quoteTokenDecimals; uint8 baseTokenDecimals; bool flipRatio; int24 tickLower; int24 tickUpper; int24 tickCurrent; int24 tickSpacing; uint24 fee; address poolAddress; } function constructTokenURI(ConstructTokenURIParams memory params) public pure returns (string memory) { string memory name = generateName(params, feeToPercentString(params.fee)); string memory descriptionPartOne = generateDescriptionPartOne( escapeQuotes(params.quoteTokenSymbol), escapeQuotes(params.baseTokenSymbol), addressToString(params.poolAddress) ); string memory descriptionPartTwo = generateDescriptionPartTwo( params.tokenId.toString(), escapeQuotes(params.baseTokenSymbol), addressToString(params.quoteTokenAddress), addressToString(params.baseTokenAddress), feeToPercentString(params.fee) ); string memory image = Base64.encode(bytes(generateSVGImage(params))); return string( abi.encodePacked( 'data:application/json;base64,', Base64.encode( bytes( abi.encodePacked( '{"name":"', name, '", "description":"', descriptionPartOne, descriptionPartTwo, '", "image": "', 'data:image/svg+xml;base64,', image, '"}' ) ) ) ) ); } function escapeQuotes(string memory symbol) internal pure returns (string memory) { bytes memory symbolBytes = bytes(symbol); uint8 quotesCount = 0; for (uint8 i = 0; i < symbolBytes.length; i++) { if (symbolBytes[i] == '"') { quotesCount++; } } if (quotesCount > 0) { bytes memory escapedBytes = new bytes(symbolBytes.length + (quotesCount)); uint256 index; for (uint8 i = 0; i < symbolBytes.length; i++) { if (symbolBytes[i] == '"') { escapedBytes[index++] = '\\'; } escapedBytes[index++] = symbolBytes[i]; } return string(escapedBytes); } return symbol; } function generateDescriptionPartOne( string memory quoteTokenSymbol, string memory baseTokenSymbol, string memory poolAddress ) private pure returns (string memory) { return string( abi.encodePacked( 'This NFT represents a liquidity position in a Uniswap V3 ', quoteTokenSymbol, '-', baseTokenSymbol, ' pool. ', 'The owner of this NFT can modify or redeem the position.\\n', '\\nPool Address: ', poolAddress, '\\n', quoteTokenSymbol ) ); } function generateDescriptionPartTwo( string memory tokenId, string memory baseTokenSymbol, string memory quoteTokenAddress, string memory baseTokenAddress, string memory feeTier ) private pure returns (string memory) { return string( abi.encodePacked( ' Address: ', quoteTokenAddress, '\\n', baseTokenSymbol, ' Address: ', baseTokenAddress, '\\nFee Tier: ', feeTier, '\\nToken ID: ', tokenId, '\\n\\n', unicode'⚠️ DISCLAIMER: Due diligence is imperative when assessing this NFT. Make sure token addresses match the expected tokens, as token symbols may be imitated.' ) ); } function generateName(ConstructTokenURIParams memory params, string memory feeTier) private pure returns (string memory) { return string( abi.encodePacked( 'Uniswap - ', feeTier, ' - ', escapeQuotes(params.quoteTokenSymbol), '/', escapeQuotes(params.baseTokenSymbol), ' - ', tickToDecimalString( !params.flipRatio ? params.tickLower : params.tickUpper, params.tickSpacing, params.baseTokenDecimals, params.quoteTokenDecimals, params.flipRatio ), '<>', tickToDecimalString( !params.flipRatio ? params.tickUpper : params.tickLower, params.tickSpacing, params.baseTokenDecimals, params.quoteTokenDecimals, params.flipRatio ) ) ); } struct DecimalStringParams { // significant figures of decimal uint256 sigfigs; // length of decimal string uint8 bufferLength; // ending index for significant figures (funtion works backwards when copying sigfigs) uint8 sigfigIndex; // index of decimal place (0 if no decimal) uint8 decimalIndex; // start index for trailing/leading 0's for very small/large numbers uint8 zerosStartIndex; // end index for trailing/leading 0's for very small/large numbers uint8 zerosEndIndex; // true if decimal number is less than one bool isLessThanOne; // true if string should include "%" bool isPercent; } function generateDecimalString(DecimalStringParams memory params) private pure returns (string memory) { bytes memory buffer = new bytes(params.bufferLength); if (params.isPercent) { buffer[buffer.length - 1] = '%'; } if (params.isLessThanOne) { buffer[0] = '0'; buffer[1] = '.'; } // add leading/trailing 0's for (uint256 zerosCursor = params.zerosStartIndex; zerosCursor < params.zerosEndIndex.add(1); zerosCursor++) { buffer[zerosCursor] = bytes1(uint8(48)); } // add sigfigs while (params.sigfigs > 0) { if (params.decimalIndex > 0 && params.sigfigIndex == params.decimalIndex) { buffer[params.sigfigIndex--] = '.'; } buffer[params.sigfigIndex--] = bytes1(uint8(uint256(48).add(params.sigfigs % 10))); params.sigfigs /= 10; } return string(buffer); } function tickToDecimalString( int24 tick, int24 tickSpacing, uint8 baseTokenDecimals, uint8 quoteTokenDecimals, bool flipRatio ) internal pure returns (string memory) { if (tick == (TickMath.MIN_TICK / tickSpacing) * tickSpacing) { return !flipRatio ? 'MIN' : 'MAX'; } else if (tick == (TickMath.MAX_TICK / tickSpacing) * tickSpacing) { return !flipRatio ? 'MAX' : 'MIN'; } else { uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick); if (flipRatio) { sqrtRatioX96 = uint160(uint256(1 << 192).div(sqrtRatioX96)); } return fixedPointToDecimalString(sqrtRatioX96, baseTokenDecimals, quoteTokenDecimals); } } function sigfigsRounded(uint256 value, uint8 digits) private pure returns (uint256, bool) { bool extraDigit; if (digits > 5) { value = value.div((10**(digits - 5))); } bool roundUp = value % 10 > 4; value = value.div(10); if (roundUp) { value = value + 1; } // 99999 -> 100000 gives an extra sigfig if (value == 100000) { value /= 10; extraDigit = true; } return (value, extraDigit); } function adjustForDecimalPrecision( uint160 sqrtRatioX96, uint8 baseTokenDecimals, uint8 quoteTokenDecimals ) private pure returns (uint256 adjustedSqrtRatioX96) { uint256 difference = abs(int256(baseTokenDecimals).sub(int256(quoteTokenDecimals))); if (difference > 0 && difference <= 18) { if (baseTokenDecimals > quoteTokenDecimals) { adjustedSqrtRatioX96 = sqrtRatioX96.mul(10**(difference.div(2))); if (difference % 2 == 1) { adjustedSqrtRatioX96 = FullMath.mulDiv(adjustedSqrtRatioX96, sqrt10X128, 1 << 128); } } else { adjustedSqrtRatioX96 = sqrtRatioX96.div(10**(difference.div(2))); if (difference % 2 == 1) { adjustedSqrtRatioX96 = FullMath.mulDiv(adjustedSqrtRatioX96, 1 << 128, sqrt10X128); } } } else { adjustedSqrtRatioX96 = uint256(sqrtRatioX96); } } function abs(int256 x) private pure returns (uint256) { return uint256(x >= 0 ? x : -x); } // @notice Returns string that includes first 5 significant figures of a decimal number // @param sqrtRatioX96 a sqrt price function fixedPointToDecimalString( uint160 sqrtRatioX96, uint8 baseTokenDecimals, uint8 quoteTokenDecimals ) internal pure returns (string memory) { uint256 adjustedSqrtRatioX96 = adjustForDecimalPrecision(sqrtRatioX96, baseTokenDecimals, quoteTokenDecimals); uint256 value = FullMath.mulDiv(adjustedSqrtRatioX96, adjustedSqrtRatioX96, 1 << 64); bool priceBelow1 = adjustedSqrtRatioX96 < 2**96; if (priceBelow1) { // 10 ** 43 is precision needed to retreive 5 sigfigs of smallest possible price + 1 for rounding value = FullMath.mulDiv(value, 10**44, 1 << 128); } else { // leave precision for 4 decimal places + 1 place for rounding value = FullMath.mulDiv(value, 10**5, 1 << 128); } // get digit count uint256 temp = value; uint8 digits; while (temp != 0) { digits++; temp /= 10; } // don't count extra digit kept for rounding digits = digits - 1; // address rounding (uint256 sigfigs, bool extraDigit) = sigfigsRounded(value, digits); if (extraDigit) { digits++; } DecimalStringParams memory params; if (priceBelow1) { // 7 bytes ( "0." and 5 sigfigs) + leading 0's bytes params.bufferLength = uint8(uint8(7).add(uint8(43).sub(digits))); params.zerosStartIndex = 2; params.zerosEndIndex = uint8(uint256(43).sub(digits).add(1)); params.sigfigIndex = uint8(params.bufferLength.sub(1)); } else if (digits >= 9) { // no decimal in price string params.bufferLength = uint8(digits.sub(4)); params.zerosStartIndex = 5; params.zerosEndIndex = uint8(params.bufferLength.sub(1)); params.sigfigIndex = 4; } else { // 5 sigfigs surround decimal params.bufferLength = 6; params.sigfigIndex = 5; params.decimalIndex = uint8(digits.sub(5).add(1)); } params.sigfigs = sigfigs; params.isLessThanOne = priceBelow1; params.isPercent = false; return generateDecimalString(params); } // @notice Returns string as decimal percentage of fee amount. // @param fee fee amount function feeToPercentString(uint24 fee) internal pure returns (string memory) { if (fee == 0) { return '0%'; } uint24 temp = fee; uint256 digits; uint8 numSigfigs; while (temp != 0) { if (numSigfigs > 0) { // count all digits preceding least significant figure numSigfigs++; } else if (temp % 10 != 0) { numSigfigs++; } digits++; temp /= 10; } DecimalStringParams memory params; uint256 nZeros; if (digits >= 5) { // if decimal > 1 (5th digit is the ones place) uint256 decimalPlace = digits.sub(numSigfigs) >= 4 ? 0 : 1; nZeros = digits.sub(5) < (numSigfigs.sub(1)) ? 0 : digits.sub(5).sub(numSigfigs.sub(1)); params.zerosStartIndex = numSigfigs; params.zerosEndIndex = uint8(params.zerosStartIndex.add(nZeros).sub(1)); params.sigfigIndex = uint8(params.zerosStartIndex.sub(1).add(decimalPlace)); params.bufferLength = uint8(nZeros.add(numSigfigs.add(1)).add(decimalPlace)); } else { // else if decimal < 1 nZeros = uint256(5).sub(digits); params.zerosStartIndex = 2; params.zerosEndIndex = uint8(nZeros.add(params.zerosStartIndex).sub(1)); params.bufferLength = uint8(nZeros.add(numSigfigs.add(2))); params.sigfigIndex = uint8((params.bufferLength).sub(2)); params.isLessThanOne = true; } params.sigfigs = uint256(fee).div(10**(digits.sub(numSigfigs))); params.isPercent = true; params.decimalIndex = digits > 4 ? uint8(digits.sub(4)) : 0; return generateDecimalString(params); } function addressToString(address addr) internal pure returns (string memory) { return (uint256(addr)).toHexString(20); } function generateSVGImage(ConstructTokenURIParams memory params) internal pure returns (string memory svg) { NFTSVG.SVGParams memory svgParams = NFTSVG.SVGParams({ quoteToken: addressToString(params.quoteTokenAddress), baseToken: addressToString(params.baseTokenAddress), poolAddress: params.poolAddress, quoteTokenSymbol: params.quoteTokenSymbol, baseTokenSymbol: params.baseTokenSymbol, feeTier: feeToPercentString(params.fee), tickLower: params.tickLower, tickUpper: params.tickUpper, tickSpacing: params.tickSpacing, overRange: overRange(params.tickLower, params.tickUpper, params.tickCurrent), tokenId: params.tokenId, color0: tokenToColorHex(uint256(params.quoteTokenAddress), 136), color1: tokenToColorHex(uint256(params.baseTokenAddress), 136), color2: tokenToColorHex(uint256(params.quoteTokenAddress), 0), color3: tokenToColorHex(uint256(params.baseTokenAddress), 0), x1: scale(getCircleCoord(uint256(params.quoteTokenAddress), 16, params.tokenId), 0, 255, 16, 274), y1: scale(getCircleCoord(uint256(params.baseTokenAddress), 16, params.tokenId), 0, 255, 100, 484), x2: scale(getCircleCoord(uint256(params.quoteTokenAddress), 32, params.tokenId), 0, 255, 16, 274), y2: scale(getCircleCoord(uint256(params.baseTokenAddress), 32, params.tokenId), 0, 255, 100, 484), x3: scale(getCircleCoord(uint256(params.quoteTokenAddress), 48, params.tokenId), 0, 255, 16, 274), y3: scale(getCircleCoord(uint256(params.baseTokenAddress), 48, params.tokenId), 0, 255, 100, 484) }); return NFTSVG.generateSVG(svgParams); } function overRange( int24 tickLower, int24 tickUpper, int24 tickCurrent ) private pure returns (int8) { if (tickCurrent < tickLower) { return -1; } else if (tickCurrent > tickUpper) { return 1; } else { return 0; } } function scale( uint256 n, uint256 inMn, uint256 inMx, uint256 outMn, uint256 outMx ) private pure returns (string memory) { return (n.sub(inMn).mul(outMx.sub(outMn)).div(inMx.sub(inMn)).add(outMn)).toString(); } function tokenToColorHex(uint256 token, uint256 offset) internal pure returns (string memory str) { return string((token >> offset).toHexStringNoPrefix(3)); } function getCircleCoord( uint256 tokenAddress, uint256 offset, uint256 tokenId ) internal pure returns (uint256) { return (sliceTokenHex(tokenAddress, offset) * tokenId) % 255; } function sliceTokenHex(uint256 token, uint256 offset) internal pure returns (uint256) { return uint256(uint8(token >> offset)); } }