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
28 changes: 27 additions & 1 deletion contracts/NonfungibleTokenPositionDescriptor.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity =0.7.6;
pragma abicoder v2;

import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
import '@uniswap/lib/contracts/libraries/SafeERC20Namer.sol';
import '@openzeppelin/contracts/proxy/Initializable.sol';
import './interfaces/INonfungiblePositionManager.sol';
import './interfaces/INonfungibleTokenPositionDescriptor.sol';
import './interfaces/IERC20Metadata.sol';
Expand All @@ -11,7 +13,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, Initializable {
struct TokenRatioOrderPriority {
address token;
int256 priority;
}

// tokens that take priority order in price ratio - higher integers get numerator priority
mapping(address => int256) public tokenRatioPriority;

function initialize(TokenRatioOrderPriority[] calldata tokens) public initializer() {
for (uint256 i = 0; i < tokens.length; i++) {
updateTokenRatioPriority(tokens[i]);
}
}

/// @inheritdoc INonfungibleTokenPositionDescriptor
function tokenURI(INonfungiblePositionManager positionManager, uint256 tokenId)
external
Expand Down Expand Up @@ -39,6 +55,7 @@ contract NonfungibleTokenPositionDescriptor is INonfungibleTokenPositionDescript
token1Symbol: SafeERC20Namer.tokenSymbol(token1),
token0Decimals: IERC20Metadata(token0).decimals(),
token1Decimals: IERC20Metadata(token1).decimals(),
flipRatio: flipRatio(token0, token1),
tickLower: tickLower,
tickUpper: tickUpper,
tickSpacing: pool.tickSpacing(),
Expand All @@ -48,4 +65,13 @@ contract NonfungibleTokenPositionDescriptor is INonfungibleTokenPositionDescript
})
);
}

function flipRatio(address token0, address token1) public view returns (bool) {
return tokenRatioPriority[token0] > tokenRatioPriority[token1];
}

function updateTokenRatioPriority(TokenRatioOrderPriority calldata token) private {
tokenRatioPriority[token.token] = token.priority;
emit UpdateTokenRatioPriority(token.token, token.priority);
}
}
5 changes: 5 additions & 0 deletions contracts/interfaces/INonfungibleTokenPositionDescriptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import './INonfungiblePositionManager.sol';

/// @title Describes position NFT tokens via URI
interface INonfungibleTokenPositionDescriptor {
/// @notice Emitted when a token is given a new priority order in the displayed price ratio
/// @param token The token being given priority order
/// @param priority Represents priority in ratio - higher integers get numerator priority
event UpdateTokenRatioPriority(address token, int256 priority);

/// @notice Produces the URI describing a particular token ID for a position manager
/// @param positionManager The position manager for which to describe the token
/// @param tokenId The ID of the token for which to produce a description, which may not be valid
Expand Down
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 flipRatio;
int24 tickLower;
int24 tickUpper;
int24 tickSpacing;
Expand All @@ -44,22 +45,24 @@ library NFTDescriptor {
'Uniswap V3 - ',
feeToPercentString(params.fee),
' - ',
params.token0Symbol,
params.flipRatio ? params.token0Symbol : params.token1Symbol,
'/',
params.token1Symbol,
params.flipRatio ? params.token1Symbol : params.token0Symbol,
' - ',
tickToDecimalString(
params.tickLower,
params.tickSpacing,
params.token0Decimals,
params.token1Decimals
params.token1Decimals,
params.flipRatio
),
'<>',
tickToDecimalString(
params.tickUpper,
params.tickSpacing,
params.token0Decimals,
params.token1Decimals
params.token1Decimals,
params.flipRatio
)
)
);
Expand Down Expand Up @@ -130,14 +133,20 @@ library NFTDescriptor {
int24 tick,
int24 tickSpacing,
uint8 token0Decimals,
uint8 token1Decimals
uint8 token1Decimals,
bool flipRatio
) 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 (flipRatio) {
sqrtRatioX96 = uint160(uint256(1 << 192).div(sqrtRatioX96));
return fixedPointToDecimalString(sqrtRatioX96, token1Decimals, token0Decimals);
}
return fixedPointToDecimalString(sqrtRatioX96, token0Decimals, token1Decimals);
}
}

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

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 flipRatio = 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,
flipRatio,
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 flipRatio = false
const tickLower = -10
const tickUpper = 10
const tickSpacing = TICK_SPACINGS[FeeAmount.MEDIUM]
Expand All @@ -100,6 +103,7 @@ describe('NFTDescriptor', () => {
token1Symbol,
token0Decimals,
token1Decimals,
flipRatio,
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 flipRatio = false
const tickLower = -10
const tickUpper = 10
const tickSpacing = TICK_SPACINGS[FeeAmount.MEDIUM]
Expand All @@ -137,6 +142,7 @@ describe('NFTDescriptor', () => {
token1Symbol,
token0Decimals,
token1Decimals,
flipRatio,
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
81 changes: 81 additions & 0 deletions test/NonfungibleTokenPositionDescriptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { BigNumberish, constants } from 'ethers'
import { waffle, ethers } from 'hardhat'

import { expect } from './shared/expect'
import { Fixture } from 'ethereum-waffle'
import {
MockTimeNonfungiblePositionManager,
NonfungibleTokenPositionDescriptor,
TestERC20,
IWETH9,
IUniswapV3Factory,
SwapRouter,
} from '../typechain'

describe('NonfungibleTokenPositionDescriptor', () => {
const wallets = waffle.provider.getWallets()

const nftPositionDescriptorFixture: Fixture<{
tokens: [TestERC20, TestERC20, TestERC20, TestERC20, TestERC20]
nftPositionDescriptor: NonfungibleTokenPositionDescriptor
}> = async (wallets, provider) => {
const tokenFactory = await ethers.getContractFactory('TestERC20')
const NonfungibleTokenPositionDescriptorFactory = await ethers.getContractFactory(
'NonfungibleTokenPositionDescriptor'
)
const tokens = (await Promise.all([
tokenFactory.deploy(constants.MaxUint256.div(2)), // do not use maxu25e6 to avoid overflowing
tokenFactory.deploy(constants.MaxUint256.div(2)),
tokenFactory.deploy(constants.MaxUint256.div(2)),
tokenFactory.deploy(constants.MaxUint256.div(2)),
tokenFactory.deploy(constants.MaxUint256.div(2)),
])) as [TestERC20, TestERC20, TestERC20, TestERC20, TestERC20]
tokens.sort((a, b) => (a.address.toLowerCase() < b.address.toLowerCase() ? -1 : 1))
const nftPositionDescriptor = (await NonfungibleTokenPositionDescriptorFactory.deploy()) as NonfungibleTokenPositionDescriptor
return {
nftPositionDescriptor,
tokens,
}
}

let nftPositionDescriptor: NonfungibleTokenPositionDescriptor
let tokens: [TestERC20, TestERC20, TestERC20, TestERC20, TestERC20]

let loadFixture: ReturnType<typeof waffle.createFixtureLoader>

before('create fixture loader', async () => {
loadFixture = waffle.createFixtureLoader(wallets)
})

beforeEach('load fixture', async () => {
;({ nftPositionDescriptor, tokens } = await loadFixture(nftPositionDescriptorFixture))
await nftPositionDescriptor.initialize([
{ token: tokens[0].address, priority: -2 },
{ token: tokens[1].address, priority: -1 },
{ token: tokens[3].address, priority: 1 },
{ token: tokens[4].address, priority: 2 },
])
})

describe('#flipRatio', () => {
it('returns false if neither token has priority ordering', async () => {
expect(await nftPositionDescriptor.flipRatio(tokens[2].address, tokens[2].address)).to.eq(false)
})

it('returns true if both tokens are numerators but token0 has a higher priority ordering', async () => {
expect(await nftPositionDescriptor.flipRatio(tokens[4].address, tokens[3].address)).to.eq(true)
})

it('returns true if both tokens are denominators but token1 has lower priority ordering', async () => {
expect(await nftPositionDescriptor.flipRatio(tokens[1].address, tokens[0].address)).to.eq(true)
})

it('returns true if token0 is a numerator and token1 is a denominator', async () => {
expect(await nftPositionDescriptor.flipRatio(tokens[3].address, tokens[1].address)).to.eq(true)
})

it('returns false if token1 is a numerator and token0 is a denominator', async () => {
expect(await nftPositionDescriptor.flipRatio(tokens[1].address, tokens[3].address)).to.eq(false)
})
})
})
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`;