Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NFT metadata: allow token0 numerator for certain pair ratios #60

Merged
merged 15 commits into from
Apr 2, 2021
Merged
38 changes: 37 additions & 1 deletion contracts/NonfungibleTokenPositionDescriptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity =0.7.6;

import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
import '@uniswap/lib/contracts/libraries/SafeERC20Namer.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import './interfaces/INonfungiblePositionManager.sol';
import './interfaces/INonfungibleTokenPositionDescriptor.sol';
import './interfaces/IERC20Metadata.sol';
Expand All @@ -11,7 +12,21 @@ import './libraries/NFTDescriptor.sol';

/// @title Describes NFT token positions
/// @notice Produces a string containing the data URI for a JSON metadata string
contract NonfungibleTokenPositionDescriptor is INonfungibleTokenPositionDescriptor {
contract NonfungibleTokenPositionDescriptor is INonfungibleTokenPositionDescriptor, Ownable {
ewilz marked this conversation as resolved.
Show resolved Hide resolved
// tokens that take priority as ratio numerator (such as stablecoins)
mapping(address => bool) public ratioNumeratorTokens;
// tokens that take priorty as ratio denominator (such as WETH)
mapping(address => bool) public ratioDenominatorTokens;

constructor(address[] memory numeratorTokens, address[] memory denominatorTokens) Ownable() {
ewilz marked this conversation as resolved.
Show resolved Hide resolved
for (uint256 i = 0; i < numeratorTokens.length; i++) {
ratioNumeratorTokens[numeratorTokens[i]] = true;
}
for (uint256 i = 0; i < denominatorTokens.length; i++) {
ratioDenominatorTokens[denominatorTokens[i]] = true;
}
}

/// @inheritdoc INonfungibleTokenPositionDescriptor
function tokenURI(INonfungiblePositionManager positionManager, uint256 tokenId)
external
Expand Down Expand Up @@ -39,6 +54,7 @@ contract NonfungibleTokenPositionDescriptor is INonfungibleTokenPositionDescript
token1Symbol: SafeERC20Namer.tokenSymbol(token1),
token0Decimals: IERC20Metadata(token0).decimals(),
token1Decimals: IERC20Metadata(token1).decimals(),
hasToken0RatioNumerator: hasToken0RatioNumerator(token0, token1),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make more sense to name this something like flipRatio?

tickLower: tickLower,
tickUpper: tickUpper,
tickSpacing: pool.tickSpacing(),
Expand All @@ -48,4 +64,24 @@ contract NonfungibleTokenPositionDescriptor is INonfungibleTokenPositionDescript
})
);
}

function hasToken0RatioNumerator(address token0, address token1) public view returns (bool) {
if (ratioNumeratorTokens[token1]) {
return false;
} else if (ratioNumeratorTokens[token0]) {
return true;
} else if (ratioDenominatorTokens[token1]) {
return true;
} else {
return false;
}
}

function addRatioNumeratorToken(address token) external onlyOwner() {
ewilz marked this conversation as resolved.
Show resolved Hide resolved
ratioNumeratorTokens[token] = true;
}

function addRatioDenominatorToken(address token) external onlyOwner() {
ratioDenominatorTokens[token] = true;
}
}
21 changes: 15 additions & 6 deletions contracts/libraries/NFTDescriptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ library NFTDescriptor {
string token1Symbol;
uint8 token0Decimals;
uint8 token1Decimals;
bool hasToken0RatioNumerator;
int24 tickLower;
int24 tickUpper;
int24 tickSpacing;
Expand All @@ -44,22 +45,24 @@ library NFTDescriptor {
'Uniswap V3 - ',
feeToPercentString(params.fee),
' - ',
params.token0Symbol,
params.hasToken0RatioNumerator ? params.token0Symbol : params.token1Symbol,
'/',
params.token1Symbol,
params.hasToken0RatioNumerator ? params.token1Symbol : params.token0Symbol,
' - ',
tickToDecimalString(
params.tickLower,
params.tickSpacing,
params.token0Decimals,
params.token1Decimals
params.token1Decimals,
params.hasToken0RatioNumerator
),
'<>',
tickToDecimalString(
params.tickUpper,
params.tickSpacing,
params.token0Decimals,
params.token1Decimals
params.token1Decimals,
params.hasToken0RatioNumerator
)
)
);
Expand Down Expand Up @@ -130,14 +133,20 @@ library NFTDescriptor {
int24 tick,
int24 tickSpacing,
uint8 token0Decimals,
uint8 token1Decimals
uint8 token1Decimals,
bool hasToken0RatioNumerator
) internal pure returns (string memory) {
if (tick == (TickMath.MIN_TICK / tickSpacing) * tickSpacing) {
return 'MIN';
} else if (tick == (TickMath.MAX_TICK / tickSpacing) * tickSpacing) {
return 'MAX';
} else {
return fixedPointToDecimalString(TickMath.getSqrtRatioAtTick(tick), token0Decimals, token1Decimals);
uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick);
if (hasToken0RatioNumerator) {
sqrtRatioX96 = uint160(uint256(1 << 192).div(sqrtRatioX96));
return fixedPointToDecimalString(sqrtRatioX96, token1Decimals, token0Decimals);
}
return fixedPointToDecimalString(sqrtRatioX96, token0Decimals, token1Decimals);
}
}

Expand Down
12 changes: 10 additions & 2 deletions contracts/test/NFTDescriptorTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,17 @@ contract NFTDescriptorTest {
int24 tick,
int24 tickSpacing,
uint8 token0Decimals,
uint8 token1Decimals
uint8 token1Decimals,
bool hasToken0RatioNumerator
) public pure returns (string memory) {
return NFTDescriptor.tickToDecimalString(tick, tickSpacing, token0Decimals, token1Decimals);
return
NFTDescriptor.tickToDecimalString(
tick,
tickSpacing,
token0Decimals,
token1Decimals,
hasToken0RatioNumerator
);
}

function fixedPointToDecimalString(
Expand Down
63 changes: 51 additions & 12 deletions test/NFTDescriptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe('NFTDescriptor', () => {
const token1Symbol = await tokens[1].symbol()
const token0Decimals = await tokens[0].decimals()
const token1Decimals = await tokens[1].decimals()
const hasToken0RatioNumerator = false
const tickLower = getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM])
const tickUpper = getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM])
const tickSpacing = TICK_SPACINGS[FeeAmount.MEDIUM]
Expand All @@ -64,6 +65,7 @@ describe('NFTDescriptor', () => {
token1Symbol,
token0Decimals,
token1Decimals,
hasToken0RatioNumerator,
tickLower,
tickUpper,
tickSpacing,
Expand All @@ -86,6 +88,7 @@ describe('NFTDescriptor', () => {
const token1Symbol = await tokens[1].symbol()
const token0Decimals = await tokens[0].decimals()
const token1Decimals = await tokens[1].decimals()
const hasToken0RatioNumerator = false
const tickLower = -10
const tickUpper = 10
const tickSpacing = TICK_SPACINGS[FeeAmount.MEDIUM]
Expand All @@ -100,6 +103,7 @@ describe('NFTDescriptor', () => {
token1Symbol,
token0Decimals,
token1Decimals,
hasToken0RatioNumerator,
tickLower,
tickUpper,
tickSpacing,
Expand All @@ -122,6 +126,7 @@ describe('NFTDescriptor', () => {
const token1Symbol = await tokens[1].symbol()
const token0Decimals = await tokens[0].decimals()
const token1Decimals = await tokens[1].decimals()
const hasToken0RatioNumerator = false
const tickLower = -10
const tickUpper = 10
const tickSpacing = TICK_SPACINGS[FeeAmount.MEDIUM]
Expand All @@ -137,6 +142,7 @@ describe('NFTDescriptor', () => {
token1Symbol,
token0Decimals,
token1Decimals,
hasToken0RatioNumerator,
tickLower,
tickUpper,
tickSpacing,
Expand Down Expand Up @@ -170,20 +176,20 @@ describe('NFTDescriptor', () => {
})

it('returns MIN on lowest tick', async () => {
expect(await nftDescriptor.tickToDecimalString(minTick, tickSpacing, 18, 18)).to.equal('MIN')
expect(await nftDescriptor.tickToDecimalString(minTick, tickSpacing, 18, 18, false)).to.equal('MIN')
})

it('returns MAX on the highest tick', async () => {
expect(await nftDescriptor.tickToDecimalString(maxTick, tickSpacing, 18, 18)).to.equal('MAX')
expect(await nftDescriptor.tickToDecimalString(maxTick, tickSpacing, 18, 18, false)).to.equal('MAX')
})

it('returns the correct decimal string when the tick is in range', async () => {
expect(await nftDescriptor.tickToDecimalString(1, tickSpacing, 18, 18)).to.equal('1.0001')
expect(await nftDescriptor.tickToDecimalString(1, tickSpacing, 18, 18, false)).to.equal('1.0001')
})

it('returns the correct decimal string when tick is mintick for different tickspace', async () => {
const otherMinTick = getMinTick(TICK_SPACINGS[FeeAmount.HIGH])
expect(await nftDescriptor.tickToDecimalString(otherMinTick, tickSpacing, 18, 18)).to.equal(
expect(await nftDescriptor.tickToDecimalString(otherMinTick, tickSpacing, 18, 18, false)).to.equal(
'0.0000000000000000000000000000000000000029387'
)
})
Expand All @@ -197,20 +203,20 @@ describe('NFTDescriptor', () => {
})

it('returns MIN on lowest tick', async () => {
expect(await nftDescriptor.tickToDecimalString(minTick, tickSpacing, 18, 18)).to.equal('MIN')
expect(await nftDescriptor.tickToDecimalString(minTick, tickSpacing, 18, 18, false)).to.equal('MIN')
})

it('returns MAX on the highest tick', async () => {
expect(await nftDescriptor.tickToDecimalString(maxTick, tickSpacing, 18, 18)).to.equal('MAX')
expect(await nftDescriptor.tickToDecimalString(maxTick, tickSpacing, 18, 18, false)).to.equal('MAX')
})

it('returns the correct decimal string when the tick is in range', async () => {
expect(await nftDescriptor.tickToDecimalString(-1, tickSpacing, 18, 18)).to.equal('0.99990')
expect(await nftDescriptor.tickToDecimalString(-1, tickSpacing, 18, 18, false)).to.equal('0.99990')
})

it('returns the correct decimal string when tick is mintick for different tickspace', async () => {
const otherMinTick = getMinTick(TICK_SPACINGS[FeeAmount.HIGH])
expect(await nftDescriptor.tickToDecimalString(otherMinTick, tickSpacing, 18, 18)).to.equal(
expect(await nftDescriptor.tickToDecimalString(otherMinTick, tickSpacing, 18, 18, false)).to.equal(
'0.0000000000000000000000000000000000000029387'
)
})
Expand All @@ -224,24 +230,57 @@ describe('NFTDescriptor', () => {
})

it('returns MIN on lowest tick', async () => {
expect(await nftDescriptor.tickToDecimalString(minTick, tickSpacing, 18, 18)).to.equal('MIN')
expect(await nftDescriptor.tickToDecimalString(minTick, tickSpacing, 18, 18, false)).to.equal('MIN')
})

it('returns MAX on the highest tick', async () => {
expect(await nftDescriptor.tickToDecimalString(maxTick, tickSpacing, 18, 18)).to.equal('MAX')
expect(await nftDescriptor.tickToDecimalString(maxTick, tickSpacing, 18, 18, false)).to.equal('MAX')
})

it('returns the correct decimal string when the tick is in range', async () => {
expect(await nftDescriptor.tickToDecimalString(0, tickSpacing, 18, 18)).to.equal('1.0000')
expect(await nftDescriptor.tickToDecimalString(0, tickSpacing, 18, 18, false)).to.equal('1.0000')
})

it('returns the correct decimal string when tick is mintick for different tickspace', async () => {
const otherMinTick = getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM])
expect(await nftDescriptor.tickToDecimalString(otherMinTick, tickSpacing, 18, 18)).to.equal(
expect(await nftDescriptor.tickToDecimalString(otherMinTick, tickSpacing, 18, 18, false)).to.equal(
'0.0000000000000000000000000000000000000029387'
)
})
})

describe('when token0 should be the ratio numerator', () => {
it('returns the inverse of default ratio for medium sized numbers', async () => {
const tickSpacing = TICK_SPACINGS[FeeAmount.HIGH]
expect(await nftDescriptor.tickToDecimalString(10, tickSpacing, 18, 18, false)).to.eq('1.0010')
expect(await nftDescriptor.tickToDecimalString(10, tickSpacing, 18, 18, true)).to.eq('0.99900')
})

it('returns the inverse of default ratio for large numbers', async () => {
const tickSpacing = TICK_SPACINGS[FeeAmount.HIGH]
expect(await nftDescriptor.tickToDecimalString(487272, tickSpacing, 18, 18, false)).to.eq(
'1448400000000000000000'
)
expect(await nftDescriptor.tickToDecimalString(487272, tickSpacing, 18, 18, true)).to.eq(
'0.00000000000000000000069041'
)
})

it('returns the inverse of default ratio for small numbers', async () => {
const tickSpacing = TICK_SPACINGS[FeeAmount.HIGH]
expect(await nftDescriptor.tickToDecimalString(-387272, tickSpacing, 18, 18, false)).to.eq(
'0.000000000000000015200'
)
expect(await nftDescriptor.tickToDecimalString(-387272, tickSpacing, 18, 18, true)).to.eq('65791000000000000')
})

it('returns the correct string with differing token decimals', async () => {
const tickSpacing = TICK_SPACINGS[FeeAmount.HIGH]
expect(await nftDescriptor.tickToDecimalString(1000, tickSpacing, 18, 18, true)).to.eq('0.90484')
expect(await nftDescriptor.tickToDecimalString(1000, tickSpacing, 10, 18, true)).to.eq('90484000')
expect(await nftDescriptor.tickToDecimalString(1000, tickSpacing, 18, 10, true)).to.eq('0.0000000090484')
})
})
})

describe('#fixedPointToDecimalString', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/__snapshots__/NFTDescriptor.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`NFTDescriptor #constructTokenURI gas 1`] = `48645`;
exports[`NFTDescriptor #constructTokenURI gas 1`] = `48885`;
2 changes: 1 addition & 1 deletion test/shared/completeFixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const completeFixture: Fixture<{
])) as [TestERC20, TestERC20, TestERC20]

const positionDescriptorFactory = await ethers.getContractFactory('NonfungibleTokenPositionDescriptor')
const positionDescriptor = await positionDescriptorFactory.deploy()
const positionDescriptor = await positionDescriptorFactory.deploy([], [])

const positionManagerFactory = await ethers.getContractFactory('MockTimeNonfungiblePositionManager')
const nft = (await positionManagerFactory.deploy(
Expand Down