Files
v4-periphery/test/PositionDescriptor.t.sol

412 lines
18 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import {IPositionDescriptor} from "../src/interfaces/IPositionDescriptor.sol";
import {CurrencyRatioSortOrder} from "../src/libraries/CurrencyRatioSortOrder.sol";
import {IHooks} from "@luxamm/v4-core/src/interfaces/IHooks.sol";
import {LiquidityAmounts} from "@luxamm/v4-core/test/utils/LiquidityAmounts.sol";
import {TickMath} from "@luxamm/v4-core/src/libraries/TickMath.sol";
import {PositionConfig} from "./shared/PositionConfig.sol";
import {PosmTestSetup} from "./shared/PosmTestSetup.sol";
import {ActionConstants} from "../src/libraries/ActionConstants.sol";
import {Base64} from "./base64.sol";
import {Currency, CurrencyLibrary} from "@luxamm/v4-core/src/types/Currency.sol";
import {SafeCurrencyMetadata} from "../src/libraries/SafeCurrencyMetadata.sol";
import {AddressStringUtil} from "../src/libraries/AddressStringUtil.sol";
import {Descriptor} from "../src/libraries/Descriptor.sol";
contract PositionDescriptorTest is Test, PosmTestSetup {
using Base64 for string;
address public WETH9 = makeAddr("WETH");
address public DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address public USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address public USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
address public TBTC = 0x8dAEBADE922dF735c38C80C7eBD708Af50815fAa;
address public WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
string public nativeCurrencyLabel = "ETH";
bytes32 public nativeCurrencyLabelBytes = "ETH";
struct Token {
string description;
string image;
string name;
}
function setUp() public {
deployFreshManager();
(currency0, currency1) = deployAndMint2Currencies();
(key,) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1);
deployAndApprovePosm(manager);
}
function test_position_descriptor_initcodeHash() public {
vm.snapshotValue(
"position descriptor initcode hash (without constructor params, as uint256)",
uint256(keccak256(abi.encodePacked(vm.getCode("PositionDescriptor.sol:PositionDescriptor"))))
);
}
function test_bytecodeSize_positionDescriptor() public {
vm.snapshotValue("positionDescriptor bytecode size", address(positionDescriptor).code.length);
}
function test_bytecodeSize_proxy() public {
vm.snapshotValue("proxy bytecode size", address(proxyAsImplementation).code.length);
}
function test_setup_succeeds() public view {
assertEq(address(proxyAsImplementation.poolManager()), address(manager));
assertEq(proxyAsImplementation.wrappedNative(), WETH9);
}
function test_nativeCurrencyLabel_succeeds() public {
assertEq(proxyAsImplementation.nativeCurrencyLabel(), nativeCurrencyLabel);
IPositionDescriptor polDescriptor = deployDescriptor(manager, "POL");
assertEq(polDescriptor.nativeCurrencyLabel(), "POL");
IPositionDescriptor bnbDescriptor = deployDescriptor(manager, "BNB");
assertEq(bnbDescriptor.nativeCurrencyLabel(), "BNB");
IPositionDescriptor avaxDescriptor = deployDescriptor(manager, "AVAX");
assertEq(avaxDescriptor.nativeCurrencyLabel(), "AVAX");
}
function test_currencyRatioPriority_mainnet_succeeds() public {
vm.chainId(1);
assertEq(proxyAsImplementation.currencyRatioPriority(WETH9), CurrencyRatioSortOrder.DENOMINATOR);
assertEq(proxyAsImplementation.currencyRatioPriority(address(0)), CurrencyRatioSortOrder.DENOMINATOR);
assertEq(proxyAsImplementation.currencyRatioPriority(USDC), CurrencyRatioSortOrder.NUMERATOR_MOST);
assertEq(proxyAsImplementation.currencyRatioPriority(USDT), CurrencyRatioSortOrder.NUMERATOR_MORE);
assertEq(proxyAsImplementation.currencyRatioPriority(DAI), CurrencyRatioSortOrder.NUMERATOR);
assertEq(proxyAsImplementation.currencyRatioPriority(TBTC), CurrencyRatioSortOrder.DENOMINATOR_MORE);
assertEq(proxyAsImplementation.currencyRatioPriority(WBTC), CurrencyRatioSortOrder.DENOMINATOR_MOST);
assertEq(proxyAsImplementation.currencyRatioPriority(makeAddr("ALICE")), 0);
}
function test_currencyRatioPriority_notMainnet_succeeds() public {
assertEq(proxyAsImplementation.currencyRatioPriority(WETH9), CurrencyRatioSortOrder.DENOMINATOR);
assertEq(proxyAsImplementation.currencyRatioPriority(address(0)), CurrencyRatioSortOrder.DENOMINATOR);
assertEq(proxyAsImplementation.currencyRatioPriority(USDC), 0);
assertEq(proxyAsImplementation.currencyRatioPriority(USDT), 0);
assertEq(proxyAsImplementation.currencyRatioPriority(DAI), 0);
assertEq(proxyAsImplementation.currencyRatioPriority(TBTC), 0);
assertEq(proxyAsImplementation.currencyRatioPriority(WBTC), 0);
assertEq(proxyAsImplementation.currencyRatioPriority(makeAddr("ALICE")), 0);
}
function test_flipRatio_succeeds() public {
vm.chainId(1);
// bc price = token1/token0
assertTrue(proxyAsImplementation.flipRatio(USDC, WETH9));
assertFalse(proxyAsImplementation.flipRatio(DAI, USDC));
assertFalse(proxyAsImplementation.flipRatio(WBTC, WETH9));
assertFalse(proxyAsImplementation.flipRatio(WBTC, USDC));
assertFalse(proxyAsImplementation.flipRatio(WBTC, DAI));
}
function test_tokenURI_succeeds() public {
int24 tickLower = int24(key.tickSpacing);
int24 tickUpper = int24(key.tickSpacing * 2);
uint256 tokenId = lpm.nextTokenId();
Token memory token;
{
uint256 amount0Desired = 100e18;
uint256 amount1Desired = 100e18;
uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(tickLower),
TickMath.getSqrtPriceAtTick(tickUpper),
amount0Desired,
amount1Desired
);
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper});
mint(config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES);
// The prefix length is calculated by converting the string to bytes and finding its length
uint256 prefixLength = bytes("data:application/json;base64,").length;
string memory uri = proxyAsImplementation.tokenURI(lpm, tokenId);
// Convert the uri to bytes
bytes memory uriBytes = bytes(uri);
// Slice the uri to get only the base64-encoded part
bytes memory base64Part = new bytes(uriBytes.length - prefixLength);
for (uint256 i = 0; i < base64Part.length; i++) {
base64Part[i] = uriBytes[i + prefixLength];
}
// Decode the base64-encoded part
bytes memory decoded = Base64.decode(string(base64Part));
string memory json = string(decoded);
// decode json
bytes memory data = vm.parseJson(json);
token = abi.decode(data, (Token));
}
// quote is currency1, base is currency0
assertFalse(proxyAsImplementation.flipRatio(Currency.unwrap(key.currency0), Currency.unwrap(key.currency1)));
string memory symbol0 = SafeCurrencyMetadata.currencySymbol(Currency.unwrap(currency0), nativeCurrencyLabel);
string memory symbol1 = SafeCurrencyMetadata.currencySymbol(Currency.unwrap(currency1), nativeCurrencyLabel);
string memory fee = Descriptor.feeToPercentString(key.fee);
{
string memory tickToDecimal0 = Descriptor.tickToDecimalString(
tickLower,
key.tickSpacing,
SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency0)),
SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency1)),
false
);
string memory tickToDecimal1 = Descriptor.tickToDecimalString(
tickUpper,
key.tickSpacing,
SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency0)),
SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency1)),
false
);
assertEq(
token.name,
string(
abi.encodePacked(
"Uniswap - ", fee, " - ", symbol1, "/", symbol0, " - ", tickToDecimal0, "<>", tickToDecimal1
)
)
);
}
{
string memory managerAddress = toHexString(address(manager));
string memory currency0Address = toHexString(Currency.unwrap(currency0));
string memory currency1Address = toHexString(Currency.unwrap(currency1));
string memory id = uintToString(tokenId);
string memory hookAddress = address(key.hooks) == address(0)
? "No Hook"
: string(abi.encodePacked("0x", toHexString(address(key.hooks))));
assertEq(
token.description,
string(
abi.encodePacked(
abi.encodePacked(
unicode"This NFT represents a liquidity position in a Uniswap v4 ",
symbol1,
"-",
symbol0,
" pool. The owner of this NFT can modify or redeem the position.\n\nPool Manager Address: ",
managerAddress,
"\n",
symbol1,
" Address: ",
currency1Address
),
abi.encodePacked(
"\n",
symbol0,
" Address: ",
currency0Address,
"\nHook Address: ",
hookAddress,
"\nFee Tier: ",
fee,
"\nToken ID: ",
id,
"\n\n",
unicode"⚠️ DISCLAIMER: Due diligence is imperative when assessing this NFT. Make sure currency addresses match the expected currencies, as currency symbols may be imitated."
)
)
)
);
}
}
function test_native_tokenURI_succeeds() public {
(nativeKey,) = initPool(CurrencyLibrary.ADDRESS_ZERO, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1);
int24 tickLower = int24(nativeKey.tickSpacing);
int24 tickUpper = int24(nativeKey.tickSpacing * 2);
Token memory token;
uint256 tokenId = lpm.nextTokenId();
{
uint256 amount0Desired = 100e18;
uint256 amount1Desired = 100e18;
uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(tickLower),
TickMath.getSqrtPriceAtTick(tickUpper),
amount0Desired,
amount1Desired
);
PositionConfig memory config =
PositionConfig({poolKey: nativeKey, tickLower: tickLower, tickUpper: tickUpper});
mintWithNative(SQRT_PRICE_1_1, config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES);
// The prefix length is calculated by converting the string to bytes and finding its length
uint256 prefixLength = bytes("data:application/json;base64,").length;
string memory uri = proxyAsImplementation.tokenURI(lpm, tokenId);
// Convert the uri to bytes
bytes memory uriBytes = bytes(uri);
// Slice the uri to get only the base64-encoded part
bytes memory base64Part = new bytes(uriBytes.length - prefixLength);
for (uint256 i = 0; i < base64Part.length; i++) {
base64Part[i] = uriBytes[i + prefixLength];
}
// Decode the base64-encoded part
bytes memory decoded = Base64.decode(string(base64Part));
string memory json = string(decoded);
// decode json
bytes memory data = vm.parseJson(json);
token = abi.decode(data, (Token));
}
// quote is currency1, base is currency0
assertFalse(
proxyAsImplementation.flipRatio(Currency.unwrap(nativeKey.currency0), Currency.unwrap(nativeKey.currency1))
);
string memory symbol0 =
SafeCurrencyMetadata.currencySymbol(Currency.unwrap(nativeKey.currency0), nativeCurrencyLabel);
string memory symbol1 =
SafeCurrencyMetadata.currencySymbol(Currency.unwrap(nativeKey.currency1), nativeCurrencyLabel);
string memory fee = Descriptor.feeToPercentString(nativeKey.fee);
{
string memory tickToDecimal0 = Descriptor.tickToDecimalString(
tickLower,
nativeKey.tickSpacing,
SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency0)),
SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency1)),
false
);
string memory tickToDecimal1 = Descriptor.tickToDecimalString(
tickUpper,
nativeKey.tickSpacing,
SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency0)),
SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency1)),
false
);
assertEq(
token.name,
string(
abi.encodePacked(
"Uniswap - ", fee, " - ", symbol1, "/", symbol0, " - ", tickToDecimal0, "<>", tickToDecimal1
)
)
);
}
{
string memory managerAddress = toHexString(address(manager));
string memory currency0Address = Currency.unwrap(nativeKey.currency0) == address(0)
? "Native"
: toHexString(Currency.unwrap(nativeKey.currency0));
string memory currency1Address = Currency.unwrap(nativeKey.currency1) == address(0)
? "Native"
: toHexString(Currency.unwrap(nativeKey.currency1));
string memory id = uintToString(tokenId);
string memory hookAddress = address(nativeKey.hooks) == address(0)
? "No Hook"
: string(abi.encodePacked("0x", toHexString(address(nativeKey.hooks))));
assertEq(
token.description,
string(
abi.encodePacked(
abi.encodePacked(
unicode"This NFT represents a liquidity position in a Uniswap v4 ",
symbol1,
"-",
symbol0,
" pool. The owner of this NFT can modify or redeem the position.\n\nPool Manager Address: ",
managerAddress,
"\n",
symbol1,
" Address: ",
currency1Address,
"\n",
symbol0
),
abi.encodePacked(
" Address: ",
currency0Address,
"\nHook Address: ",
hookAddress,
"\nFee Tier: ",
fee,
"\nToken ID: ",
id,
"\n\n",
unicode"⚠️ DISCLAIMER: Due diligence is imperative when assessing this NFT. Make sure currency addresses match the expected currencies, as currency symbols may be imitated."
)
)
)
);
}
}
function test_tokenURI_revertsWithInvalidTokenId() public {
int24 tickLower = int24(key.tickSpacing);
int24 tickUpper = int24(key.tickSpacing * 2);
uint256 amount0Desired = 100e18;
uint256 amount1Desired = 100e18;
uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(tickLower),
TickMath.getSqrtPriceAtTick(tickUpper),
amount0Desired,
amount1Desired
);
PositionConfig memory config = PositionConfig({poolKey: key, tickLower: tickLower, tickUpper: tickUpper});
uint256 tokenId = lpm.nextTokenId();
mint(config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES);
vm.expectRevert(abi.encodeWithSelector(IPositionDescriptor.InvalidTokenId.selector, tokenId + 1));
proxyAsImplementation.tokenURI(lpm, tokenId + 1);
}
// Helper functions for testing purposes
function toHexString(address account) internal pure returns (string memory) {
return toHexString(uint256(uint160(account)), 20);
}
// different from AddressStringUtil.toHexString. this one is all lowercase hex and includes the 0x prefix
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
uint8 digit = uint8(value & 0xf);
buffer[i] = digit < 10 ? bytes1(digit + 48) : bytes1(digit + 87); // Lowercase hex (0x61 is 'a' in ASCII)
value >>= 4;
}
require(value == 0, "Hex length insufficient");
return string(buffer);
}
function uintToString(uint256 value) internal pure returns (string memory) {
if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
}