478 lines
18 KiB
Solidity
Raw Normal View History

2023-04-08 18:46:18 +00:00
// 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));
}
}