mirror of
https://github.com/lux-amm/v4-periphery.git
synced 2026-10-30 20:54:39 +00:00
412 lines
18 KiB
Solidity
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);
|
|
}
|
|
}
|