From ed591f75df3e0827490e5eab54fbc94eef933579 Mon Sep 17 00:00:00 2001 From: clemlak Date: Fri, 18 Aug 2023 12:13:55 +0400 Subject: [PATCH 01/20] feat: update NFT visual --- contracts/PositionRenderer.sol | 610 +++++++++++++++++++++++++++++---- 1 file changed, 543 insertions(+), 67 deletions(-) diff --git a/contracts/PositionRenderer.sol b/contracts/PositionRenderer.sol index 9a9782b5..9157bd9e 100644 --- a/contracts/PositionRenderer.sol +++ b/contracts/PositionRenderer.sol @@ -14,24 +14,66 @@ contract PositionRenderer { using Strings for *; using SafeCastLib for *; + struct Pair { + address asset; + string assetSymbol; + string assetName; + uint8 assetDecimals; + address quote; + string quoteSymbol; + string quoteName; + uint8 quoteDecimals; + } + + struct Pool { + uint128 virtualX; + uint128 virtualY; + uint16 feeBasisPoints; + uint16 priorityFeeBasisPoints; + address controller; + address strategy; + uint256 spotPriceWad; + } + + struct Config { + uint128 strikePriceWad; + uint32 volatilityBasisPoints; + uint32 durationSeconds; + uint32 creationTimestamp; + bool isPerpetual; + } + + struct Properties { + Pair pair; + Pool pool; + Config config; + } + + string private constant PRIMITIVE_LOGO = + ''; + function uri(uint256 id) external view returns (string memory) { + Properties memory properties = _getProperties(id); + return string.concat( "data:application/json;base64,", Base64.encode( bytes( abi.encodePacked( '{"name":"', - _generateName(id), + _generateName(properties), '","image":"', - _generateImage(), + _generateImage(properties), '","license":"MIT","creator":"primitive.eth",', - '"description":"Concentrated liquidity tokens of a two-token AMM",', + '"description":"This NFT represents a liquidity position in a Portfolio pool. The owner of this NFT can modify or redeem this position.\\n\\n', + unicode"⚠️ WARNING: Transferring this NFT makes the new recipient the owner of the position.", + '",', '"properties":{', - _generatePair(id), + _generatePair(properties), ",", - _generatePool(id), + _generatePool(properties), ",", - _generateConfig(id), + _generateConfig(properties), "}}" ) ) @@ -39,113 +81,547 @@ contract PositionRenderer { ); } - function _generateName(uint256 id) private view returns (string memory) { - (address tokenAsset,, address tokenQuote,) = IPortfolio(msg.sender) - .pairs(PoolId.wrap(id.safeCastTo64()).pairId()); - + function _generateImage(Properties memory properties) + private + view + returns (string memory) + { return string.concat( - "Primitive Portfolio LP ", - ERC20(tokenAsset).symbol(), - "-", - ERC20(tokenQuote).symbol() + "data:image/svg+xml;base64,", + Base64.encode(bytes(_generateSVG(properties))) ); } - function _generateImage() private pure returns (string memory) { + function _getPair(uint256 id) internal view returns (Pair memory) { + ( + address tokenAsset, + uint8 decimalsAsset, + address tokenQuote, + uint8 decimalsQuote + ) = IPortfolio(msg.sender).pairs(uint24(id)); + + return Pair({ + asset: tokenAsset, + assetSymbol: ERC20(tokenAsset).symbol(), + assetName: ERC20(tokenAsset).name(), + assetDecimals: decimalsAsset, + quote: tokenQuote, + quoteSymbol: ERC20(tokenQuote).symbol(), + quoteName: ERC20(tokenQuote).name(), + quoteDecimals: decimalsQuote + }); + } + + function _getPool(uint256 id) internal view returns (Pool memory) { + ( + uint128 virtualX, + uint128 virtualY, + , + , + uint16 feeBasisPoints, + uint16 priorityFeeBasisPoints, + address controller, + address strategy + ) = IPortfolio(msg.sender).pools(uint64(id)); + + uint256 spotPriceWad = IPortfolio(msg.sender).getSpotPrice(uint64(id)); + + return Pool({ + virtualX: virtualX, + virtualY: virtualY, + feeBasisPoints: feeBasisPoints, + priorityFeeBasisPoints: priorityFeeBasisPoints, + controller: controller, + strategy: strategy, + spotPriceWad: spotPriceWad + }); + } + + function _getConfig( + uint256 id, + address strategy + ) internal view returns (Config memory) { + ( + uint128 strikePriceWad, + uint32 volatilityBasisPoints, + uint32 durationSeconds, + uint32 creationTimestamp, + bool isPerpetual + ) = NormalStrategy(strategy).configs(uint64(id)); + + return Config({ + strikePriceWad: strikePriceWad, + volatilityBasisPoints: volatilityBasisPoints, + durationSeconds: durationSeconds, + creationTimestamp: creationTimestamp, + isPerpetual: isPerpetual + }); + } + + function _getProperties(uint256 id) + private + view + returns (Properties memory) + { + Pair memory pair = _getPair(id); + Pool memory pool = _getPool(id); + Config memory config = _getConfig(id, pool.strategy); + + return Properties({ pair: pair, pool: pool, config: config }); + } + + function _generateName(Properties memory properties) + private + pure + returns (string memory) + { return string.concat( - "data:image/svg+xml;base64,", - Base64.encode( - bytes( - '' - ) - ) + "Primitive Portfolio LP ", + properties.pair.assetSymbol, + "-", + properties.pair.quoteSymbol ); } - function _generatePair(uint256 id) private view returns (string memory) { - (address tokenAsset,, address tokenQuote,) = IPortfolio(msg.sender) - .pairs(PoolId.wrap(id.safeCastTo64()).pairId()); - + function _generatePair(Properties memory properties) + private + pure + returns (string memory) + { return string.concat( '"asset_name":"', - ERC20(tokenAsset).name(), + properties.pair.assetName, '",', '"asset_symbol":"', - ERC20(tokenAsset).symbol(), + properties.pair.assetSymbol, '",', '"asset_address":"', - Strings.toHexString(tokenAsset), + properties.pair.asset.toHexString(), '",', '"quote_name":"', - ERC20(tokenQuote).name(), + properties.pair.quoteName, '",', '"quote_symbol":"', - ERC20(tokenQuote).symbol(), + properties.pair.quoteSymbol, '",', '"quote_address":"', - Strings.toHexString(tokenQuote), + properties.pair.quote.toHexString(), '"' ); } - function _generatePool(uint256 id) private view returns (string memory) { - ( - , - , - , - , - uint16 feeBasisPoints, - uint16 priorityFeeBasisPoints, - address controller, - address strategy - ) = IPortfolio(msg.sender).pools(uint64(id)); - + function _generatePool(Properties memory properties) + private + pure + returns (string memory) + { return string.concat( '"fee_basis_points":"', - feeBasisPoints.toString(), + properties.pool.feeBasisPoints.toString(), '",', '"priority_fee_basis_points":"', - priorityFeeBasisPoints.toString(), + properties.pool.priorityFeeBasisPoints.toString(), '",', '"controller":"', - Strings.toHexString(controller), + Strings.toHexString(properties.pool.controller), '",', '"strategy":"', - Strings.toHexString(strategy), + Strings.toHexString(properties.pool.strategy), '"' ); } - function _generateConfig(uint256 id) private view returns (string memory) { - (,,,,,,, address strategy) = IPortfolio(msg.sender).pools(uint64(id)); - - if (strategy == address(0)) { - strategy = IPortfolio(msg.sender).DEFAULT_STRATEGY(); - } - - ( - uint128 strikePriceWad, - uint32 volatilityBasisPoints, - uint32 durationSeconds, - uint32 creationTimestamp, - bool isPerpetual - ) = NormalStrategy(strategy).configs(uint64(id)); - + function _generateConfig(Properties memory properties) + private + pure + returns (string memory) + { return string.concat( '"strike_price_wad":"', - strikePriceWad.toString(), + properties.config.strikePriceWad.toString(), '",', '"volatility_basis_points":"', - volatilityBasisPoints.toString(), + properties.config.volatilityBasisPoints.toString(), '",', '"duration_seconds":"', - durationSeconds.toString(), + properties.config.durationSeconds.toString(), '",', '"creation_timestamp":"', - creationTimestamp.toString(), + properties.config.creationTimestamp.toString(), '",', '"is_perpetual":', - isPerpetual ? "true" : "false" + properties.config.isPerpetual ? "true" : "false" ); } + + function _generateSVG(Properties memory properties) + private + view + returns (string memory) + { + return string.concat( + '', + _generateSVGNoise(), + _generateSVGGradient(), + '' + '', + PRIMITIVE_LOGO, + _generateStats(properties), + "" + ); + } + + function _generateStats(Properties memory properties) + private + view + returns (string memory) + { + return string.concat( + _generateSVGTitle(properties), + _generateSVGSpotPrice(properties), + _generateSVGStrikePrice(properties), + _generateSVGReserves(properties), + _generateSVGPoolValuation(properties), + _generateSVGSwapFee(properties) + ); + } + + function _generateSVGNoise() internal pure returns (string memory) { + return + ' '; + } + + function _generateSVGGradient() internal pure returns (string memory) { + return string.concat( + '' + ); + } + + function _generateSVGTitle(Properties memory properties) + internal + view + returns (string memory) + { + return string.concat( + _drawText( + 550, + 75, + "#fff", + "3.25em", + "monospace", + "end", + string.concat( + properties.pair.assetSymbol, + " - ", + properties.pair.quoteSymbol + ) + ), + _drawText( + 550, + 100, + "#ffffff80", + "1.75em", + "monospace", + "end", + properties.config.isPerpetual + ? "Never expires" + : _calculateCountdown( + properties.config.creationTimestamp + + properties.config.durationSeconds + ) + ) + ); + } + + function _generateSVGSpotPrice(Properties memory properties) + internal + pure + returns (string memory) + { + return string.concat( + _drawText( + 50, + 200, + "#ffffff80", + "1.75em", + "monospace", + "start", + "Spot Price" + ), + _drawText( + 50, + 240, + "#fff", + "2.5em", + "monospace", + "start", + string.concat( + properties.pool.spotPriceWad.toString(), + " ", + properties.pair.quoteSymbol + ) + ) + ); + } + + function _generateSVGStrikePrice(Properties memory properties) + internal + pure + returns (string memory) + { + return string.concat( + _drawText( + 325, + 200, + "#ffffff80", + "1.75em", + "monospace", + "start", + "Strike Price" + ), + _drawText( + 325, + 240, + "#fff", + "2.5em", + "monospace", + "start", + string.concat( + properties.config.strikePriceWad.toString(), + " ", + properties.pair.quoteSymbol + ) + ) + ); + } + + function _generateSVGReserves(Properties memory properties) + internal + pure + returns (string memory) + { + return ( + string.concat( + _drawText( + 50, + 320, + "#ffffff80", + "1.75em", + "monospace", + "start", + "Asset Reserve" + ), + _drawText( + 50, + 360, + "#fff", + "2.5em", + "monospace", + "start", + string.concat( + properties.pool.virtualX.toString(), + " ", + properties.pair.assetSymbol + ) + ), + _drawText( + 325, + 320, + "#ffffff80", + "1.75em", + "monospace", + "start", + "Quote Reserve" + ), + _drawText( + 325, + 360, + "#fff", + "2.5em", + "monospace", + "start", + string.concat( + properties.pool.virtualY.toString(), + " ", + properties.pair.quoteSymbol + ) + ) + ) + ); + } + + function _generateSVGPoolValuation(Properties memory properties) + internal + pure + returns (string memory) + { + return ( + string.concat( + _drawText( + 50, + 440, + "#ffffff80", + "1.75em", + "monospace", + "start", + "Pool Valuation" + ), + _drawText( + 50, + 480, + "#fff", + "2.5em", + "monospace", + "start", + string.concat( + properties.config.strikePriceWad.toString(), + " ", + properties.pair.quoteSymbol + ) + ) + ) + ); + } + + function _generateSVGSwapFee(Properties memory properties) + internal + pure + returns (string memory) + { + return ( + string.concat( + _drawText( + 325, + 440, + "#ffffff80", + "1.75em", + "monospace", + "start", + "Swap Fee" + ), + _drawText( + 325, + 480, + "#fff", + "2.5em", + "monospace", + "start", + string.concat( + properties.pool.feeBasisPoints.toString(), " %" + ) + ) + ) + ); + } + + function _drawText( + uint256 x, + uint256 y, + string memory fill, + string memory fontSize, + string memory fontFamily, + string memory textAnchor, + string memory text + ) internal pure returns (string memory) { + return string.concat( + '', + text, + "" + ); + } + + function _calculateCountdown(uint256 deadline) + internal + view + returns (string memory) + { + uint256 timeLeft = deadline - block.timestamp; + uint256 daysLeft = timeLeft / 86400; + uint256 hoursLeft = (timeLeft % 86400) / 3600; + uint256 minutesLeft = (timeLeft % 3600) / 60; + uint256 secondsLeft = timeLeft % 60; + + // TODO: Fix the plurals + if (daysLeft >= 1) { + return (string.concat("Expires in ", daysLeft.toString(), " days")); + } + + if (hoursLeft >= 1) { + return + (string.concat("Expires in ", hoursLeft.toString(), " hours")); + } + + if (minutesLeft >= 1) { + return ( + string.concat("Expires in ", minutesLeft.toString(), " minutes") + ); + } + + return + (string.concat("Expires in ", secondsLeft.toString(), " seconds")); + } } + +/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + USDT - USDC + Expires in 66 days + + Spot Price + 0.99 USDC + + Strike Price + 1.00 USDC + + Asset Reserve + 435,235 USDT + + Quote Reserve + 452,673 USDC + + Pool Valuation + 883,555.65 USDC + +*/ From 91102a611d8d50baf2a423324aabc35180746c57 Mon Sep 17 00:00:00 2001 From: clemlak Date: Mon, 21 Aug 2023 19:00:19 +0400 Subject: [PATCH 02/20] feat: refactor NFT to use HTML (instead of SVG) --- contracts/PositionRenderer.sol | 532 ++++++++++++++++++++------------- 1 file changed, 325 insertions(+), 207 deletions(-) diff --git a/contracts/PositionRenderer.sol b/contracts/PositionRenderer.sol index 9157bd9e..973668ab 100644 --- a/contracts/PositionRenderer.sol +++ b/contracts/PositionRenderer.sol @@ -26,6 +26,7 @@ contract PositionRenderer { } struct Pool { + uint256 poolId; uint128 virtualX; uint128 virtualY; uint16 feeBasisPoints; @@ -33,6 +34,7 @@ contract PositionRenderer { address controller; address strategy; uint256 spotPriceWad; + bool hasDefaultStrategy; } struct Config { @@ -50,7 +52,7 @@ contract PositionRenderer { } string private constant PRIMITIVE_LOGO = - ''; + ''; function uri(uint256 id) external view returns (string memory) { Properties memory properties = _getProperties(id); @@ -62,8 +64,8 @@ contract PositionRenderer { abi.encodePacked( '{"name":"', _generateName(properties), - '","image":"', - _generateImage(properties), + '","animation_url":"', + _generateHTML(properties), '","license":"MIT","creator":"primitive.eth",', '"description":"This NFT represents a liquidity position in a Portfolio pool. The owner of this NFT can modify or redeem this position.\\n\\n', unicode"⚠️ WARNING: Transferring this NFT makes the new recipient the owner of the position.", @@ -81,6 +83,7 @@ contract PositionRenderer { ); } + /* function _generateImage(Properties memory properties) private view @@ -91,6 +94,7 @@ contract PositionRenderer { Base64.encode(bytes(_generateSVG(properties))) ); } + */ function _getPair(uint256 id) internal view returns (Pair memory) { ( @@ -127,13 +131,16 @@ contract PositionRenderer { uint256 spotPriceWad = IPortfolio(msg.sender).getSpotPrice(uint64(id)); return Pool({ + poolId: id, virtualX: virtualX, virtualY: virtualY, feeBasisPoints: feeBasisPoints, priorityFeeBasisPoints: priorityFeeBasisPoints, controller: controller, strategy: strategy, - spotPriceWad: spotPriceWad + spotPriceWad: spotPriceWad, + hasDefaultStrategy: strategy + == IPortfolio(msg.sender).DEFAULT_STRATEGY() }); } @@ -254,20 +261,42 @@ contract PositionRenderer { ); } - function _generateSVG(Properties memory properties) + function _generateHTML(Properties memory properties) private view returns (string memory) { - return string.concat( - '', - _generateSVGNoise(), - _generateSVGGradient(), - '' - '', - PRIMITIVE_LOGO, + string memory color0 = _generateColor(properties.pool.poolId / 10); + string memory color1 = _generateColor(properties.pool.poolId * 10); + + string memory data = string.concat( + " ', _generateStats(properties), - "" + _generateHTMLFooter(properties), + "" + ); + + return + string.concat("data:text/html;base64,", Base64.encode(bytes(data))); + } + + function _generateStat( + string memory label, + string memory amount, + bool alignRight + ) private pure returns (string memory) { + return string.concat( + "', + label, + "
", + amount, + "" ); } @@ -277,247 +306,332 @@ contract PositionRenderer { returns (string memory) { return string.concat( - _generateSVGTitle(properties), - _generateSVGSpotPrice(properties), - _generateSVGStrikePrice(properties), - _generateSVGReserves(properties), - _generateSVGPoolValuation(properties), - _generateSVGSwapFee(properties) - ); - } - - function _generateSVGNoise() internal pure returns (string memory) { - return - ' '; - } - - function _generateSVGGradient() internal pure returns (string memory) { - return string.concat( - '' + '', + "", + _generateHTMLTitle(properties), + "", + _generateHTMLSpotPrice(properties), + _generateHTMLStrikePrice(properties), + "", + _generateHTMLAssetReserves(properties), + _generateHTMLQuoteReserves(properties), + "", + _generateHTMLPoolValuation(properties), + _generateHTMLSwapFee(properties), + "
", + PRIMITIVE_LOGO, + "
" ); } - function _generateSVGTitle(Properties memory properties) + function _generateHTMLTitle(Properties memory properties) internal view returns (string memory) { return string.concat( - _drawText( - 550, - 75, - "#fff", - "3.25em", - "monospace", - "end", + _generateStat( string.concat( properties.pair.assetSymbol, - " - ", + "-", properties.pair.quoteSymbol - ) - ), - _drawText( - 550, - 100, - "#ffffff80", - "1.75em", - "monospace", - "end", + ), properties.config.isPerpetual - ? "Never expires" + ? "Perpetual pool" : _calculateCountdown( properties.config.creationTimestamp + properties.config.durationSeconds - ) + ), + true ) ); } - function _generateSVGSpotPrice(Properties memory properties) + function _generateHTMLSpotPrice(Properties memory properties) internal pure returns (string memory) { return string.concat( - _drawText( - 50, - 200, - "#ffffff80", - "1.75em", - "monospace", - "start", - "Spot Price" - ), - _drawText( - 50, - 240, - "#fff", - "2.5em", - "monospace", - "start", + _generateStat( + "Spot Price", string.concat( - properties.pool.spotPriceWad.toString(), + abbreviateAmount( + properties.pool.spotPriceWad, + properties.pair.quoteDecimals + ), " ", properties.pair.quoteSymbol - ) + ), + false ) ); } - function _generateSVGStrikePrice(Properties memory properties) + function _generateHTMLStrikePrice(Properties memory properties) internal pure returns (string memory) { return string.concat( - _drawText( - 325, - 200, - "#ffffff80", - "1.75em", - "monospace", - "start", - "Strike Price" - ), - _drawText( - 325, - 240, - "#fff", - "2.5em", - "monospace", - "start", + _generateStat( + "Strike Price", string.concat( - properties.config.strikePriceWad.toString(), + abbreviateAmount( + properties.config.strikePriceWad, + properties.pair.quoteDecimals + ), " ", properties.pair.quoteSymbol - ) + ), + false ) ); } - function _generateSVGReserves(Properties memory properties) + function _generateHTMLAssetReserves(Properties memory properties) internal pure returns (string memory) { - return ( - string.concat( - _drawText( - 50, - 320, - "#ffffff80", - "1.75em", - "monospace", - "start", - "Asset Reserve" + return string.concat( + _generateStat( + "Asset Reserves", + string.concat( + abbreviateAmount( + properties.pool.virtualX, properties.pair.assetDecimals + ), + " ", + properties.pair.assetSymbol ), - _drawText( - 50, - 360, - "#fff", - "2.5em", - "monospace", - "start", - string.concat( - properties.pool.virtualX.toString(), - " ", - properties.pair.assetSymbol - ) + false + ) + ); + } + + function _generateHTMLQuoteReserves(Properties memory properties) + internal + pure + returns (string memory) + { + return string.concat( + _generateStat( + "Asset Reserves", + string.concat( + abbreviateAmount( + properties.pool.virtualY, properties.pair.quoteDecimals + ), + " ", + properties.pair.quoteSymbol ), - _drawText( - 325, - 320, - "#ffffff80", - "1.75em", - "monospace", - "start", - "Quote Reserve" + false + ) + ); + } + + function _generateHTMLPoolValuation(Properties memory properties) + internal + pure + returns (string memory) + { + uint256 poolValuation = ( + properties.pool.virtualX * properties.pool.spotPriceWad + ) / properties.pair.quoteDecimals + properties.pool.virtualY; + + return string.concat( + _generateStat( + "Pool Valuation", + string.concat( + abbreviateAmount( + poolValuation, properties.pair.quoteDecimals + ), + " ", + properties.pair.quoteSymbol ), - _drawText( - 325, - 360, - "#fff", - "2.5em", - "monospace", - "start", - string.concat( - properties.pool.virtualY.toString(), - " ", - properties.pair.quoteSymbol - ) - ) + false ) ); } - function _generateSVGPoolValuation(Properties memory properties) + function _generateHTMLSwapFee(Properties memory properties) internal pure returns (string memory) { - return ( - string.concat( - _drawText( - 50, - 440, - "#ffffff80", - "1.75em", - "monospace", - "start", - "Pool Valuation" + return string.concat( + _generateStat( + "Swap Fee", + string.concat( + abbreviateAmount(properties.pool.feeBasisPoints, 4), " %" ), - _drawText( - 50, - 480, - "#fff", - "2.5em", - "monospace", - "start", - string.concat( - properties.config.strikePriceWad.toString(), - " ", - properties.pair.quoteSymbol - ) - ) + false ) ); } - function _generateSVGSwapFee(Properties memory properties) + function _generateHTMLFooter(Properties memory properties) internal pure returns (string memory) { + string memory controlledLabel = properties.pool.controller == address(0) + ? "This pool is not controlled" + : string.concat( + "This pool is controlled by ", + properties.pool.controller.toHexString() + ); + return ( string.concat( - _drawText( - 325, - 440, - "#ffffff80", - "1.75em", - "monospace", - "start", - "Swap Fee" - ), - _drawText( - 325, - 480, - "#fff", - "2.5em", - "monospace", - "start", - string.concat( - properties.pool.feeBasisPoints.toString(), " %" - ) - ) + '" ) ); } + function _calculateCountdown(uint256 deadline) + internal + view + returns (string memory) + { + uint256 timeLeft = deadline - block.timestamp; + uint256 daysLeft = timeLeft / 86400; + uint256 hoursLeft = (timeLeft % 86400) / 3600; + uint256 minutesLeft = (timeLeft % 3600) / 60; + uint256 secondsLeft = timeLeft % 60; + + // TODO: Fix the plurals + if (daysLeft >= 1) { + return (string.concat("Expires in ", daysLeft.toString(), " days")); + } + + if (hoursLeft >= 1) { + return + (string.concat("Expires in ", hoursLeft.toString(), " hours")); + } + + if (minutesLeft >= 1) { + return ( + string.concat("Expires in ", minutesLeft.toString(), " minutes") + ); + } + + return + (string.concat("Expires in ", secondsLeft.toString(), " seconds")); + } + + /// @dev Escape character for "≥". + string internal constant SIGN_GE = "≥"; + + /// @dev Escape character for ">". + string internal constant SIGN_GT = ">"; + + /// @dev Escape character for "<". + string internal constant SIGN_LT = "<"; + + /// @notice Creates an abbreviated representation of the provided amount, rounded down and prefixed with ">= ". + /// @dev The abbreviation uses these suffixes: + /// - "K" for thousands + /// - "M" for millions + /// - "B" for billions + /// - "T" for trillions + /// For example, if the input is 1,234,567, the output is ">= 1.23M". + /// @param amount The amount to abbreviate, denoted in units of `decimals`. + /// @param decimals The number of decimals to assume when abbreviating the amount. + /// @return abbreviation The abbreviated representation of the provided amount, as a string. + function abbreviateAmount( + uint256 amount, + uint256 decimals + ) internal pure returns (string memory) { + if (amount == 0) { + return "0"; + } + + uint256 truncatedAmount; + unchecked { + truncatedAmount = decimals == 0 ? amount : amount / 10 ** decimals; + } + + // Return dummy values when the truncated amount is either very small or very big. + if (truncatedAmount < 1) { + return string.concat(SIGN_LT, " 1"); + } else if (truncatedAmount >= 1e15) { + return string.concat(SIGN_GT, " 999.99T"); + } + + string[5] memory suffixes = ["", "K", "M", "B", "T"]; + uint256 fractionalAmount; + uint256 suffixIndex = 0; + + // Truncate repeatedly until the amount is less than 1000. + unchecked { + while (truncatedAmount >= 1000) { + fractionalAmount = (truncatedAmount / 10) % 100; // keep the first two digits after the decimal point + truncatedAmount /= 1000; + suffixIndex += 1; + } + } + + // Concatenate the calculated parts to form the final string. + string memory prefix = string.concat(SIGN_GE, " "); + string memory wholePart = truncatedAmount.toString(); + string memory fractionalPart = + stringifyFractionalAmount(fractionalAmount); + return string.concat( + prefix, wholePart, fractionalPart, suffixes[suffixIndex] + ); + } + + /// @notice Converts the provided fractional amount to a string prefixed by a dot. + /// @param fractionalAmount A numerical value with 2 implied decimals. + function stringifyFractionalAmount(uint256 fractionalAmount) + internal + pure + returns (string memory) + { + // Return the empty string if the fractional amount is zero. + if (fractionalAmount == 0) { + return ""; + } + // Add a leading zero if the fractional part is less than 10, e.g. for "1", this function returns ".01%". + else if (fractionalAmount < 10) { + return string.concat(".0", fractionalAmount.toString()); + } + // Otherwise, stringify the fractional amount simply. + else { + return string.concat(".", fractionalAmount.toString()); + } + } + + function _generateColor(uint256 seed) + internal + pure + returns (string memory) + { + return string.concat( + "rgb(", + _generateNumber(seed, 255).toString(), + ",", + _generateNumber(seed + 1, 255).toString(), + ",", + _generateNumber(seed + 2, 255).toString(), + ")" + ); + } + + function _generateNumber( + uint256 seed, + uint256 max + ) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(seed))) % max; + } + + /* function _drawText( uint256 x, uint256 y, @@ -546,36 +660,40 @@ contract PositionRenderer { ); } - function _calculateCountdown(uint256 deadline) - internal + function _generateSVGNoise() internal pure returns (string memory) { + return + ' '; + } + + function _generateSVGGradient() internal pure returns (string memory) { + return string.concat( + '' + ); + } + + function _generateSVG(Properties memory properties) + private view returns (string memory) { - uint256 timeLeft = deadline - block.timestamp; - uint256 daysLeft = timeLeft / 86400; - uint256 hoursLeft = (timeLeft % 86400) / 3600; - uint256 minutesLeft = (timeLeft % 3600) / 60; - uint256 secondsLeft = timeLeft % 60; - - // TODO: Fix the plurals - if (daysLeft >= 1) { - return (string.concat("Expires in ", daysLeft.toString(), " days")); - } - - if (hoursLeft >= 1) { - return - (string.concat("Expires in ", hoursLeft.toString(), " hours")); - } - - if (minutesLeft >= 1) { - return ( - string.concat("Expires in ", minutesLeft.toString(), " minutes") - ); - } - - return - (string.concat("Expires in ", secondsLeft.toString(), " seconds")); + return string.concat( + '', + _generateSVGNoise(), + _generateSVGGradient(), + '' + '', + PRIMITIVE_LOGO, + _generateStats(properties), + _generateSVGFooter(properties), + "" + ); } + + */ } /* From 84a19b6e305ab1cedcad90386aaf6f2a4d50f7b6 Mon Sep 17 00:00:00 2001 From: clemlak Date: Wed, 30 Aug 2023 10:13:12 +0400 Subject: [PATCH 03/20] feat: add custom strings library --- contracts/libraries/StringsLib.sol | 92 ++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 contracts/libraries/StringsLib.sol diff --git a/contracts/libraries/StringsLib.sol b/contracts/libraries/StringsLib.sol new file mode 100644 index 00000000..94303569 --- /dev/null +++ b/contracts/libraries/StringsLib.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import { Math } from "openzeppelin/utils/math/Math.sol"; + +/** + * @dev Modified version of: + * OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol) + */ +library Strings { + bytes16 private constant _SYMBOLS = "0123456789abcdef"; + uint8 private constant _ADDRESS_LENGTH = 20; + + /** + * @dev The `value` string doesn't fit in the specified `length`. + */ + error StringsInsufficientHexLength(uint256 value, uint256 length); + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + unchecked { + uint256 length = Math.log10(value) + 1; + string memory buffer = new string(length); + uint256 ptr; + /// @solidity memory-safe-assembly + assembly { + ptr := add(buffer, add(32, length)) + } + while (true) { + ptr--; + /// @solidity memory-safe-assembly + assembly { + mstore8(ptr, byte(mod(value, 10), _SYMBOLS)) + } + value /= 10; + if (value == 0) break; + } + return buffer; + } + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + unchecked { + return toHexString(value, Math.log256(value) + 1); + } + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString( + uint256 value, + uint256 length + ) internal pure returns (string memory) { + uint256 localValue = value; + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = _SYMBOLS[localValue & 0xf]; + localValue >>= 4; + } + if (localValue != 0) { + revert StringsInsufficientHexLength(value, length); + } + return string(buffer); + } + + /** + * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation. + */ + function toHexString(address addr) internal pure returns (string memory) { + return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH); + } + + function toHexColor(bytes3 value) internal pure returns (string memory) { + bytes memory result = new bytes(6); + + for (uint256 i = 0; i < 3; i++) { + result[i * 2] = _SYMBOLS[uint8(value[i] >> 4)]; + result[i * 2 + 1] = _SYMBOLS[uint8(value[i] & 0x0F)]; + } + + return string.concat("#", string(result)); + } +} From 066707f966e0ae82a74829bd9dde8b751700724c Mon Sep 17 00:00:00 2001 From: clemlak Date: Wed, 30 Aug 2023 16:20:32 +0400 Subject: [PATCH 04/20] test: add controlled pool uri test --- test/TestPortfolioUri.t.sol | 131 ++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/test/TestPortfolioUri.t.sol b/test/TestPortfolioUri.t.sol index 9dca2951..80e8ebbd 100644 --- a/test/TestPortfolioUri.t.sol +++ b/test/TestPortfolioUri.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.4; import "openzeppelin/utils/Base64.sol"; import "solmate/tokens/ERC1155.sol"; +import "../contracts/libraries/AssemblyLib.sol"; import "./Setup.sol"; contract TestPortfolioUri is Setup { @@ -20,6 +21,136 @@ contract TestPortfolioUri is Setup { console.log(uri); } + function test_metadata() public { + address asset = address(new MockERC20("Ethereum", "ETH", 18)); + address quote = address(new MockERC20("USD Coin", "USDC", 6)); + uint24 pairId = subject().createPair(asset, quote); + + uint256 strikePriceWad = AssemblyLib.scaleToWad(2000 * 10 ** 6, 6); + uint256 volatilityBasisPoints = 1_000; + uint256 durationSeconds = 86_400 * 7; + bool isPerpetual = false; + uint256 priceWad = AssemblyLib.scaleToWad(2000 * 10 ** 6, 6); + + (bytes memory strategyData, uint256 initialX, uint256 initialY) = + INormalStrategy(subject().DEFAULT_STRATEGY()).getStrategyData( + strikePriceWad, + volatilityBasisPoints, + durationSeconds, + isPerpetual, + priceWad + ); + + uint64 poolId = subject().createPool( + pairId, + initialX, + initialY, + 100, + 0, + address(0), + subject().DEFAULT_STRATEGY(), + strategyData + ); + + uint256 amount0 = 10 ether; + uint256 amount1 = 20_000 * 10 ** 6; + + MockERC20(asset).mint(address(this), 100 ether); + MockERC20(asset).approve(address(subject()), 100 ether); + MockERC20(quote).mint(address(this), 200_000 * 10 ** 6); + MockERC20(quote).approve(address(subject()), 200_000 * 10 ** 6); + + uint128 deltaLiquidity = + subject().getMaxLiquidity(poolId, amount0, amount1); + + console.log(deltaLiquidity); + + (uint128 deltaAsset, uint128 deltaQuote) = + subject().getLiquidityDeltas(poolId, int128(deltaLiquidity)); + + (uint256 usedDeltaAsset, uint256 usedDeltaQuote) = subject().allocate( + false, + address(this), + poolId, + deltaLiquidity, + uint128(amount0), + uint128(amount1) + ); + + console.log(deltaAsset); + console.log(deltaQuote); + console.log("Used:", usedDeltaAsset); + console.log("Used:", usedDeltaQuote); + + string memory uri = ERC1155(address(subject())).uri(poolId); + console.log(uri); + } + + function test_metadata_controlled_pool() public { + address asset = address(new MockERC20("Ethereum", "ETH", 18)); + address quote = address(new MockERC20("USD Coin", "USDC", 6)); + uint24 pairId = subject().createPair(asset, quote); + + uint256 strikePriceWad = AssemblyLib.scaleToWad(2000 * 10 ** 6, 6); + uint256 volatilityBasisPoints = 1_000; + uint256 durationSeconds = 86_400 * 7; + bool isPerpetual = false; + uint256 priceWad = AssemblyLib.scaleToWad(2000 * 10 ** 6, 6); + + (bytes memory strategyData, uint256 initialX, uint256 initialY) = + INormalStrategy(subject().DEFAULT_STRATEGY()).getStrategyData( + strikePriceWad, + volatilityBasisPoints, + durationSeconds, + isPerpetual, + priceWad + ); + + uint64 poolId = subject().createPool( + pairId, + initialX, + initialY, + 200, + 100, + address(this), + subject().DEFAULT_STRATEGY(), + strategyData + ); + + uint256 amount0 = 10 ether; + uint256 amount1 = 20_000 * 10 ** 6; + + MockERC20(asset).mint(address(this), 100 ether); + MockERC20(asset).approve(address(subject()), 100 ether); + MockERC20(quote).mint(address(this), 200_000 * 10 ** 6); + MockERC20(quote).approve(address(subject()), 200_000 * 10 ** 6); + + uint128 deltaLiquidity = + subject().getMaxLiquidity(poolId, amount0, amount1); + + console.log(deltaLiquidity); + + (uint128 deltaAsset, uint128 deltaQuote) = + subject().getLiquidityDeltas(poolId, int128(deltaLiquidity)); + + (uint256 usedDeltaAsset, uint256 usedDeltaQuote) = subject().allocate( + false, + address(this), + poolId, + deltaLiquidity, + uint128(amount0), + uint128(amount1) + ); + + console.log(deltaAsset); + console.log(deltaQuote); + console.log("Used:", usedDeltaAsset); + console.log("Used:", usedDeltaQuote); + + string memory uri = ERC1155(address(subject())).uri(poolId); + console.log(uri); + } + function test_balanceOf_allocating_sets_balance() public defaultConfig From 827420df694ef286700b98ce2f1b886fd167c5a2 Mon Sep 17 00:00:00 2001 From: clemlak Date: Wed, 30 Aug 2023 16:20:44 +0400 Subject: [PATCH 05/20] feat: update HTML rendering --- contracts/PositionRenderer.sol | 259 ++++++++++----------------------- 1 file changed, 79 insertions(+), 180 deletions(-) diff --git a/contracts/PositionRenderer.sol b/contracts/PositionRenderer.sol index 973668ab..91c56a3e 100644 --- a/contracts/PositionRenderer.sol +++ b/contracts/PositionRenderer.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.4; import "openzeppelin/utils/Base64.sol"; -import "openzeppelin/utils/Strings.sol"; import "solmate/tokens/ERC20.sol"; import "solmate/utils/SafeCastLib.sol"; + +import "./libraries/StringsLib.sol"; import "./interfaces/IPortfolio.sol"; import "./interfaces/IStrategy.sol"; import "./strategies/NormalStrategy.sol"; @@ -52,7 +53,12 @@ contract PositionRenderer { } string private constant PRIMITIVE_LOGO = - ''; + ''; + + string private constant STYLE_0 = + ""; function uri(uint256 id) external view returns (string memory) { Properties memory properties = _getProperties(id); @@ -83,19 +89,6 @@ contract PositionRenderer { ); } - /* - function _generateImage(Properties memory properties) - private - view - returns (string memory) - { - return string.concat( - "data:image/svg+xml;base64,", - Base64.encode(bytes(_generateSVG(properties))) - ); - } - */ - function _getPair(uint256 id) internal view returns (Pair memory) { ( address tokenAsset, @@ -223,6 +216,15 @@ contract PositionRenderer { returns (string memory) { return string.concat( + '"asset_reserves":"', + (properties.pool.virtualX / 1).toString(), + '",', + '"quote_reserves":"', + (properties.pool.virtualY / 1).toString(), + '",', + '"spot_price_wad":"', + (properties.pool.spotPriceWad / 10 ** 18).toString(), + '",', '"fee_basis_points":"', properties.pool.feeBasisPoints.toString(), '",', @@ -245,7 +247,7 @@ contract PositionRenderer { { return string.concat( '"strike_price_wad":"', - properties.config.strikePriceWad.toString(), + (properties.config.strikePriceWad / 10 ** 18).toString(), '",', '"volatility_basis_points":"', properties.config.volatilityBasisPoints.toString(), @@ -266,15 +268,34 @@ contract PositionRenderer { view returns (string memory) { - string memory color0 = _generateColor(properties.pool.poolId / 10); - string memory color1 = _generateColor(properties.pool.poolId * 10); + string memory color0 = Strings.toHexColor( + bytes3( + keccak256(abi.encode((properties.pool.poolId >> 232) << 232)) + ) + ); + string memory color1 = Strings.toHexColor( + bytes3(keccak256(abi.encode(properties.pool.poolId << 232))) + ); + + string memory title = string.concat( + properties.pair.assetSymbol, + "-", + properties.pair.quoteSymbol, + " Portfolio LP" + ); string memory data = string.concat( - " ', + STYLE_1, + "", + '
', + '', _generateStats(properties), _generateHTMLFooter(properties), "" @@ -286,17 +307,23 @@ contract PositionRenderer { function _generateStat( string memory label, - string memory amount, - bool alignRight + string memory amount + ) private pure returns (string memory) { + return string.concat( + '', label, "
", amount, "" + ); + } + + function _generateTitle( + string memory label, + string memory amount ) private pure returns (string memory) { return string.concat( - "', + '', label, - "
", + '
', amount, - "" + "" ); } @@ -306,7 +333,7 @@ contract PositionRenderer { returns (string memory) { return string.concat( - '', + '
', "", @@ -330,7 +357,7 @@ contract PositionRenderer { returns (string memory) { return string.concat( - _generateStat( + _generateTitle( string.concat( properties.pair.assetSymbol, "-", @@ -341,8 +368,7 @@ contract PositionRenderer { : _calculateCountdown( properties.config.creationTimestamp + properties.config.durationSeconds - ), - true + ) ) ); } @@ -357,13 +383,12 @@ contract PositionRenderer { "Spot Price", string.concat( abbreviateAmount( - properties.pool.spotPriceWad, + properties.pool.spotPriceWad / 10 ** 18, properties.pair.quoteDecimals ), " ", properties.pair.quoteSymbol - ), - false + ) ) ); } @@ -378,13 +403,12 @@ contract PositionRenderer { "Strike Price", string.concat( abbreviateAmount( - properties.config.strikePriceWad, + properties.config.strikePriceWad / 10 ** 18, properties.pair.quoteDecimals ), " ", properties.pair.quoteSymbol - ), - false + ) ) ); } @@ -399,12 +423,12 @@ contract PositionRenderer { "Asset Reserves", string.concat( abbreviateAmount( - properties.pool.virtualX, properties.pair.assetDecimals + properties.pool.virtualX / 10 ** 18, + properties.pair.assetDecimals ), " ", properties.pair.assetSymbol - ), - false + ) ) ); } @@ -419,12 +443,12 @@ contract PositionRenderer { "Asset Reserves", string.concat( abbreviateAmount( - properties.pool.virtualY, properties.pair.quoteDecimals + properties.pool.virtualY / 10 ** 18, + properties.pair.quoteDecimals ), " ", properties.pair.quoteSymbol - ), - false + ) ) ); } @@ -443,12 +467,11 @@ contract PositionRenderer { "Pool Valuation", string.concat( abbreviateAmount( - poolValuation, properties.pair.quoteDecimals + poolValuation / 10 ** 18, properties.pair.quoteDecimals ), " ", properties.pair.quoteSymbol - ), - false + ) ) ); } @@ -463,8 +486,7 @@ contract PositionRenderer { "Swap Fee", string.concat( abbreviateAmount(properties.pool.feeBasisPoints, 4), " %" - ), - false + ) ) ); } @@ -483,7 +505,7 @@ contract PositionRenderer { return ( string.concat( - '" + '" + ); + } + + function _generatePercentStat( + string memory label, + uint256 amount + ) private pure returns (string memory) { + return string.concat( + '" ); } @@ -382,17 +407,11 @@ contract PositionRenderer { return string.concat( _generateStat( "Spot Price", - string.concat( - StringsLib.toFormatAmount( - AssemblyLib.scaleFromWadDown( - properties.pool.spotPriceWad, - properties.pair.quoteDecimals - ), - properties.pair.quoteDecimals - ), - " ", - properties.pair.quoteSymbol - ) + AssemblyLib.scaleFromWadDown( + properties.pool.spotPriceWad, properties.pair.quoteDecimals + ), + properties.pair.quoteDecimals, + properties.pair.quoteSymbol ) ); } @@ -405,17 +424,12 @@ contract PositionRenderer { return string.concat( _generateStat( "Strike Price", - string.concat( - StringsLib.toFormatAmount( - AssemblyLib.scaleFromWadDown( - properties.config.strikePriceWad, - properties.pair.quoteDecimals - ), - properties.pair.quoteDecimals - ), - " ", - properties.pair.quoteSymbol - ) + AssemblyLib.scaleFromWadDown( + properties.config.strikePriceWad, + properties.pair.quoteDecimals + ), + properties.pair.quoteDecimals, + properties.pair.quoteSymbol ) ); } @@ -428,17 +442,11 @@ contract PositionRenderer { return string.concat( _generateStat( "Asset Reserves", - string.concat( - StringsLib.toFormatAmount( - AssemblyLib.scaleFromWadDown( - properties.pool.virtualX, - properties.pair.assetDecimals - ), - properties.pair.assetDecimals - ), - " ", - properties.pair.assetSymbol - ) + AssemblyLib.scaleFromWadDown( + properties.pool.virtualX, properties.pair.assetDecimals + ), + properties.pair.assetDecimals, + properties.pair.assetSymbol ) ); } @@ -451,17 +459,11 @@ contract PositionRenderer { return string.concat( _generateStat( "Asset Reserves", - string.concat( - StringsLib.toFormatAmount( - AssemblyLib.scaleFromWadDown( - properties.pool.virtualY, - properties.pair.quoteDecimals - ), - properties.pair.quoteDecimals - ), - " ", - properties.pair.quoteSymbol - ) + AssemblyLib.scaleFromWadDown( + properties.pool.virtualY, properties.pair.quoteDecimals + ), + properties.pair.quoteDecimals, + properties.pair.quoteSymbol ) ); } @@ -484,13 +486,9 @@ contract PositionRenderer { return string.concat( _generateStat( "Pool Valuation", - string.concat( - StringsLib.toFormatAmount( - poolValuation, properties.pair.quoteDecimals - ), - " ", - properties.pair.quoteSymbol - ) + poolValuation, + properties.pair.quoteDecimals, + properties.pair.quoteSymbol ) ); } @@ -501,9 +499,7 @@ contract PositionRenderer { returns (string memory) { return string.concat( - _generateStat( - "Swap Fee", properties.pool.feeBasisPoints.toStringPercent() - ) + _generatePercentStat("Swap Fee", properties.pool.feeBasisPoints) ); } From 2ddf3c5453486dd787bc98fd3570a3f8adfe19a8 Mon Sep 17 00:00:00 2001 From: clemlak Date: Fri, 1 Sep 2023 18:03:25 +0400 Subject: [PATCH 13/20] feat: change HTML generation --- contracts/PositionRenderer.sol | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/contracts/PositionRenderer.sol b/contracts/PositionRenderer.sol index 2eef125b..6a9e382d 100644 --- a/contracts/PositionRenderer.sol +++ b/contracts/PositionRenderer.sol @@ -71,8 +71,8 @@ contract PositionRenderer { abi.encodePacked( '{"name":"', _generateName(properties), - '","animation_url":"', - _generateHTML(properties), + '","animation_url":"data:text/html;base64,', + Base64.encode(bytes(_generateHTML(properties))), '","license":"MIT","creator":"primitive.eth",', '"description":"This NFT represents a liquidity position in a Portfolio pool. The owner of this NFT can modify or redeem this position.\\n\\n', unicode"⚠️ WARNING: Transferring this NFT makes the new recipient the owner of the position.", @@ -285,7 +285,7 @@ contract PositionRenderer { " Portfolio LP" ); - string memory data = string.concat( + return string.concat( '', title, "", @@ -301,9 +301,6 @@ contract PositionRenderer { _generateHTMLFooter(properties), "" ); - - return - string.concat("data:text/html;base64,", Base64.encode(bytes(data))); } function _generateStat( From 11e9c94b86c59ca2224a72ada93c5777ea0695d5 Mon Sep 17 00:00:00 2001 From: clemlak Date: Fri, 1 Sep 2023 18:03:37 +0400 Subject: [PATCH 14/20] test: update uri and balanceOf tests --- test/TestPortfolioBalanceOf.t copy.sol | 68 +++++++ test/TestPortfolioUri.t.sol | 236 ------------------------- test/TestPositionRendererUri.t.sol | 166 +++++++++++++++++ 3 files changed, 234 insertions(+), 236 deletions(-) create mode 100644 test/TestPortfolioBalanceOf.t copy.sol delete mode 100644 test/TestPortfolioUri.t.sol create mode 100644 test/TestPositionRendererUri.t.sol diff --git a/test/TestPortfolioBalanceOf.t copy.sol b/test/TestPortfolioBalanceOf.t copy.sol new file mode 100644 index 00000000..396941fc --- /dev/null +++ b/test/TestPortfolioBalanceOf.t copy.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.4; + +import "solmate/tokens/ERC1155.sol"; +import "../contracts/libraries/AssemblyLib.sol"; +import "./Setup.sol"; +import "../contracts/strategies/NormalStrategy.sol"; + +contract TestPortfolioBalanceOf is Setup { + function test_balanceOf_allocating_increases_balance() + public + defaultConfig + useActor + usePairTokens(10 ether) + { + uint128 liquidity = 1 ether; + + subject().allocate( + false, + address(this), + ghost().poolId, + liquidity, + type(uint128).max, + type(uint128).max + ); + + subject().allocate( + false, + address(this), + ghost().poolId, + liquidity, + type(uint128).max, + type(uint128).max + ); + + assertEq( + ERC1155(address(subject())).balanceOf(address(this), ghost().poolId), + liquidity * 2 - BURNED_LIQUIDITY + ); + } + + function test_balanceOf_deallocating_reduces_balance() + public + defaultConfig + useActor + usePairTokens(10 ether) + { + uint128 liquidity = 1 ether; + + subject().allocate( + false, + address(this), + ghost().poolId, + liquidity, + type(uint128).max, + type(uint128).max + ); + + subject().deallocate( + false, ghost().poolId, uint128(liquidity - BURNED_LIQUIDITY), 0, 0 + ); + + assertEq( + ERC1155(address(subject())).balanceOf(address(this), ghost().poolId), + 0 + ); + } +} diff --git a/test/TestPortfolioUri.t.sol b/test/TestPortfolioUri.t.sol deleted file mode 100644 index 3fb74e1a..00000000 --- a/test/TestPortfolioUri.t.sol +++ /dev/null @@ -1,236 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.4; - -import "solmate/tokens/ERC1155.sol"; -import "../contracts/libraries/AssemblyLib.sol"; -import "./Setup.sol"; - -contract TestPortfolioUri is Setup { - function test_uri() public defaultConfig useActor usePairTokens(10 ether) { - subject().allocate( - false, - address(this), - ghost().poolId, - 1 ether, - type(uint128).max, - type(uint128).max - ); - - string memory uri = ERC1155(address(subject())).uri(ghost().poolId); - console.log(uri); - } - - function test_metadata() public { - address asset = address(new MockERC20("Ethereum", "ETH", 18)); - address quote = address(new MockERC20("USD Coin", "USDC", 6)); - uint24 pairId = subject().createPair(asset, quote); - - uint256 strikePriceWad = AssemblyLib.scaleToWad(2000 * 10 ** 6, 6); - uint256 volatilityBasisPoints = 1_000; - uint256 durationSeconds = 86_400 * 3; - bool isPerpetual = false; - uint256 priceWad = AssemblyLib.scaleToWad(2000 * 10 ** 6, 6); - - (bytes memory strategyData, uint256 initialX, uint256 initialY) = - INormalStrategy(subject().DEFAULT_STRATEGY()).getStrategyData( - strikePriceWad, - volatilityBasisPoints, - durationSeconds, - isPerpetual, - priceWad - ); - - uint64 poolId = subject().createPool( - pairId, - initialX, - initialY, - 100, - 0, - address(0), - subject().DEFAULT_STRATEGY(), - strategyData - ); - - uint256 amount0 = 10 ether; - uint256 amount1 = 20_000 * 10 ** 6; - - MockERC20(asset).mint(address(this), 100 ether); - MockERC20(asset).approve(address(subject()), 100 ether); - MockERC20(quote).mint(address(this), 200_000 * 10 ** 6); - MockERC20(quote).approve(address(subject()), 200_000 * 10 ** 6); - - uint128 deltaLiquidity = - subject().getMaxLiquidity(poolId, amount0, amount1); - - console.log(deltaLiquidity); - - (uint128 deltaAsset, uint128 deltaQuote) = - subject().getLiquidityDeltas(poolId, int128(deltaLiquidity)); - - (uint256 usedDeltaAsset, uint256 usedDeltaQuote) = subject().allocate( - false, - address(this), - poolId, - deltaLiquidity, - uint128(amount0), - uint128(amount1) - ); - - console.log(deltaAsset); - console.log(deltaQuote); - console.log("Used:", usedDeltaAsset); - console.log("Used:", usedDeltaQuote); - - string memory uri = ERC1155(address(subject())).uri(poolId); - console.log(uri); - } - - /* - function test_metadata_controlled_pool() public { - address asset = address(new MockERC20("Ethereum", "ETH", 18)); - address quote = address(new MockERC20("USD Coin", "USDC", 6)); - uint24 pairId = subject().createPair(asset, quote); - - uint256 strikePriceWad = AssemblyLib.scaleToWad(2000 * 10 ** 6, 6); - uint256 volatilityBasisPoints = 1_000; - uint256 durationSeconds = 86_400 * 7; - bool isPerpetual = false; - uint256 priceWad = AssemblyLib.scaleToWad(2000 * 10 ** 6, 6); - - (bytes memory strategyData, uint256 initialX, uint256 initialY) = - INormalStrategy(subject().DEFAULT_STRATEGY()).getStrategyData( - strikePriceWad, - volatilityBasisPoints, - durationSeconds, - isPerpetual, - priceWad - ); - - uint64 poolId = subject().createPool( - pairId, - initialX, - initialY, - 200, - 100, - address(this), - subject().DEFAULT_STRATEGY(), - strategyData - ); - - uint256 amount0 = 10 ether; - uint256 amount1 = 20_000 * 10 ** 6; - - MockERC20(asset).mint(address(this), 100 ether); - MockERC20(asset).approve(address(subject()), 100 ether); - MockERC20(quote).mint(address(this), 200_000 * 10 ** 6); - MockERC20(quote).approve(address(subject()), 200_000 * 10 ** 6); - - uint128 deltaLiquidity = - subject().getMaxLiquidity(poolId, amount0, amount1); - - console.log(deltaLiquidity); - - (uint128 deltaAsset, uint128 deltaQuote) = - subject().getLiquidityDeltas(poolId, int128(deltaLiquidity)); - - (uint256 usedDeltaAsset, uint256 usedDeltaQuote) = subject().allocate( - false, - address(this), - poolId, - deltaLiquidity, - uint128(amount0), - uint128(amount1) - ); - - console.log(deltaAsset); - console.log(deltaQuote); - console.log("Used:", usedDeltaAsset); - console.log("Used:", usedDeltaQuote); - - string memory uri = ERC1155(address(subject())).uri(poolId); - console.log(uri); - } - - function test_balanceOf_allocating_sets_balance() - public - defaultConfig - useActor - usePairTokens(10 ether) - { - uint128 liquidity = 1 ether; - - subject().allocate( - false, - address(this), - ghost().poolId, - liquidity, - type(uint128).max, - type(uint128).max - ); - - assertEq( - ERC1155(address(subject())).balanceOf(address(this), ghost().poolId), - liquidity - BURNED_LIQUIDITY - ); - } - */ - - function test_balanceOf_allocating_increases_balance() - public - defaultConfig - useActor - usePairTokens(10 ether) - { - uint128 liquidity = 1 ether; - - subject().allocate( - false, - address(this), - ghost().poolId, - liquidity, - type(uint128).max, - type(uint128).max - ); - - subject().allocate( - false, - address(this), - ghost().poolId, - liquidity, - type(uint128).max, - type(uint128).max - ); - - assertEq( - ERC1155(address(subject())).balanceOf(address(this), ghost().poolId), - liquidity * 2 - BURNED_LIQUIDITY - ); - } - - function test_balanceOf_deallocating_reduces_balance() - public - defaultConfig - useActor - usePairTokens(10 ether) - { - uint128 liquidity = 1 ether; - - subject().allocate( - false, - address(this), - ghost().poolId, - liquidity, - type(uint128).max, - type(uint128).max - ); - - subject().deallocate( - false, ghost().poolId, uint128(liquidity - BURNED_LIQUIDITY), 0, 0 - ); - - assertEq( - ERC1155(address(subject())).balanceOf(address(this), ghost().poolId), - 0 - ); - } -} diff --git a/test/TestPositionRendererUri.t.sol b/test/TestPositionRendererUri.t.sol new file mode 100644 index 00000000..293462a4 --- /dev/null +++ b/test/TestPositionRendererUri.t.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.4; + +import "solmate/tokens/ERC1155.sol"; +import "../contracts/libraries/AssemblyLib.sol"; +import "./Setup.sol"; +import "../contracts/strategies/NormalStrategy.sol"; + +contract TestPositionRendererUri is Setup { + struct MetaContext { + address asset; + address quote; + uint256 strikePriceWad; + uint256 durationSeconds; + uint16 swapFee; + bool isPerpetual; + uint256 priceWad; + address controller; + address strategy; + uint16 prioritySwapFee; + } + + function _createPool(MetaContext memory ctx) + public + returns (uint64 poolId) + { + uint24 pairId = subject().createPair(ctx.asset, ctx.quote); + + (bytes memory strategyData, uint256 initialX, uint256 initialY) = + INormalStrategy(subject().DEFAULT_STRATEGY()).getStrategyData( + ctx.strikePriceWad, + 1_00, // volatilityBasisPoints + ctx.durationSeconds, + ctx.isPerpetual, + ctx.priceWad + ); + + poolId = subject().createPool( + pairId, + initialX, + initialY, + ctx.swapFee, + ctx.prioritySwapFee, + ctx.controller, + ctx.strategy, + strategyData + ); + } + + function _allocate( + MetaContext memory ctx, + uint64 poolId, + uint256 amount0, + uint256 amount1 + ) public { + MockERC20(ctx.asset).mint(address(this), amount0); + MockERC20(ctx.asset).approve(address(subject()), amount0); + MockERC20(ctx.quote).mint(address(this), amount1); + MockERC20(ctx.quote).approve(address(subject()), amount1); + + uint128 deltaLiquidity = + subject().getMaxLiquidity(poolId, amount0, amount1); + + (uint128 deltaAsset, uint128 deltaQuote) = + subject().getLiquidityDeltas(poolId, int128(deltaLiquidity)); + + subject().allocate( + false, address(this), poolId, deltaLiquidity, deltaAsset, deltaQuote + ); + } + + function test_uri() public { + MetaContext memory ctx = MetaContext({ + asset: address(new MockERC20("Ethereum", "ETH", 18)), + quote: address(new MockERC20("USD Coin", "USDC", 6)), + strikePriceWad: AssemblyLib.scaleToWad(1666 * 10 ** 6, 6), + durationSeconds: 86_400 * 3, + swapFee: 400, + isPerpetual: false, + priceWad: AssemblyLib.scaleToWad(1666 * 10 ** 6, 6), + controller: address(0), + strategy: address(0), + prioritySwapFee: 0 + }); + + uint64 poolId = _createPool(ctx); + _allocate(ctx, poolId, 1 ether, 2_000 * 10 ** 6); + console.log(ERC1155(address(subject())).uri(poolId)); + } + + function test_uri_controlled_pool() public { + MetaContext memory ctx = MetaContext({ + asset: address(new MockERC20("Ethereum", "ETH", 18)), + quote: address(new MockERC20("USD Coin", "USDC", 6)), + strikePriceWad: AssemblyLib.scaleToWad(1666 * 10 ** 6, 6), + durationSeconds: 86_400 * 3, + swapFee: 400, + isPerpetual: false, + priceWad: AssemblyLib.scaleToWad(1666 * 10 ** 6, 6), + controller: address(this), + strategy: address(0), + prioritySwapFee: 200 + }); + + uint64 poolId = _createPool(ctx); + _allocate(ctx, poolId, 1 ether, 2_000 * 10 ** 6); + console.log(ERC1155(address(subject())).uri(poolId)); + } + + function test_uri_perpetual() public { + MetaContext memory ctx = MetaContext({ + asset: address(new MockERC20("Ethereum", "ETH", 18)), + quote: address(new MockERC20("USD Coin", "USDC", 6)), + strikePriceWad: AssemblyLib.scaleToWad(1666 * 10 ** 6, 6), + durationSeconds: 86_400 * 3, + swapFee: 400, + isPerpetual: true, + priceWad: AssemblyLib.scaleToWad(1666 * 10 ** 6, 6), + controller: address(0), + strategy: address(0), + prioritySwapFee: 0 + }); + + uint64 poolId = _createPool(ctx); + _allocate(ctx, poolId, 1 ether, 2_000 * 10 ** 6); + console.log(ERC1155(address(subject())).uri(poolId)); + } + + function test_uri_custom_strategy() public { + MetaContext memory ctx = MetaContext({ + asset: address(new MockERC20("Ethereum", "ETH", 18)), + quote: address(new MockERC20("USD Coin", "USDC", 6)), + strikePriceWad: AssemblyLib.scaleToWad(1666 * 10 ** 6, 6), + durationSeconds: 86_400 * 3, + swapFee: 400, + isPerpetual: false, + priceWad: AssemblyLib.scaleToWad(1666 * 10 ** 6, 6), + controller: address(0), + strategy: address(new NormalStrategy(address(subject()))), + prioritySwapFee: 0 + }); + + uint64 poolId = _createPool(ctx); + _allocate(ctx, poolId, 1 ether, 2_000 * 10 ** 6); + console.log(ERC1155(address(subject())).uri(poolId)); + } + + function test_uri_controlled_custom_strategy() public { + MetaContext memory ctx = MetaContext({ + asset: address(new MockERC20("Ethereum", "ETH", 18)), + quote: address(new MockERC20("USD Coin", "USDC", 6)), + strikePriceWad: AssemblyLib.scaleToWad(1666 * 10 ** 6, 6), + durationSeconds: 86_400 * 3, + swapFee: 400, + isPerpetual: false, + priceWad: AssemblyLib.scaleToWad(1666 * 10 ** 6, 6), + controller: address(this), + strategy: address(new NormalStrategy(address(subject()))), + prioritySwapFee: 100 + }); + + uint64 poolId = _createPool(ctx); + _allocate(ctx, poolId, 1 ether, 2_000 * 10 ** 6); + console.log(ERC1155(address(subject())).uri(poolId)); + } +} From a1e71a0569cc5a041249b080fd124ee2c427e799 Mon Sep 17 00:00:00 2001 From: clemlak Date: Fri, 1 Sep 2023 18:04:21 +0400 Subject: [PATCH 15/20] test: rename test file --- ...PortfolioBalanceOf.t copy.sol => TestPortfolioBalanceOf.t.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{TestPortfolioBalanceOf.t copy.sol => TestPortfolioBalanceOf.t.sol} (100%) diff --git a/test/TestPortfolioBalanceOf.t copy.sol b/test/TestPortfolioBalanceOf.t.sol similarity index 100% rename from test/TestPortfolioBalanceOf.t copy.sol rename to test/TestPortfolioBalanceOf.t.sol From 7c5047a5a888dead83e0a56b33e70e9b68353d54 Mon Sep 17 00:00:00 2001 From: clemlak Date: Fri, 1 Sep 2023 18:14:40 +0400 Subject: [PATCH 16/20] test: update _createPool func util in uri tests --- test/TestPositionRendererUri.t.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/TestPositionRendererUri.t.sol b/test/TestPositionRendererUri.t.sol index 293462a4..96bf90fb 100644 --- a/test/TestPositionRendererUri.t.sol +++ b/test/TestPositionRendererUri.t.sol @@ -24,7 +24,11 @@ contract TestPositionRendererUri is Setup { public returns (uint64 poolId) { - uint24 pairId = subject().createPair(ctx.asset, ctx.quote); + uint24 pairId = subject().getPairId(ctx.asset, ctx.quote); + + if (pairId == 0) { + pairId = subject().createPair(ctx.asset, ctx.quote); + } (bytes memory strategyData, uint256 initialX, uint256 initialY) = INormalStrategy(subject().DEFAULT_STRATEGY()).getStrategyData( From 68a7ac6f827768e25018a30d1e86dd4ce2e5e3f1 Mon Sep 17 00:00:00 2001 From: clemlak Date: Mon, 4 Sep 2023 16:24:38 +0400 Subject: [PATCH 17/20] chore: add NatSpec --- contracts/PositionRenderer.sol | 166 +++++++++++++++++++++++---------- 1 file changed, 119 insertions(+), 47 deletions(-) diff --git a/contracts/PositionRenderer.sol b/contracts/PositionRenderer.sol index 6a9e382d..b1c371d6 100644 --- a/contracts/PositionRenderer.sol +++ b/contracts/PositionRenderer.sol @@ -3,18 +3,26 @@ pragma solidity ^0.8.4; import "openzeppelin/utils/Base64.sol"; import "solmate/tokens/ERC20.sol"; -import "solmate/utils/SafeCastLib.sol"; import "./libraries/StringsLib.sol"; import "./libraries/AssemblyLib.sol"; import "./interfaces/IPortfolio.sol"; -import "./interfaces/IStrategy.sol"; import "./strategies/NormalStrategy.sol"; -/// @dev Contract to render a position. +/** + * @title + * PositionRenderer + * + * @author + * Primitive™ + * + * @dev + * Prepares the metadata and generates the visual representation of the + * liquidity pool tokens. + * This contract is not meant to be called directly. + */ contract PositionRenderer { using StringsLib for *; - using SafeCastLib for *; struct Pair { address asset; @@ -61,6 +69,12 @@ contract PositionRenderer { string private constant STYLE_1 = ");animation:r 10s linear infinite;background-size:200% 200%;will-change:background-position;width:100vw;height:100vh;position:absolute;top:0;left:0;z-index:-2}#n{height:100vh;width:100vw;position:absolute;top:0;right:0;z-index:-1}@keyframes r{0%,100%{background-position:left top}50%{background-position:right bottom}}#t{font-size:6vh}.s{border-spacing:0 1rem}.s td{font-size:5vh}#i{height:15vh}.l{font-size:3.25vh;opacity:.5}.f{background-color:#00000020;padding:1rem;border-radius:8px}.f p{font-size:3vh;margin:0}"; + /** + * @dev Returns the metadata of the required liquidity pool token, following + * the ERC-1155 standard. + * @param id Id of the required pool. + * @return Minified Base64-encoded JSON containing the metadata. + */ function uri(uint256 id) external view returns (string memory) { Properties memory properties = _getProperties(id); @@ -90,6 +104,10 @@ contract PositionRenderer { ); } + /** + * @dev Returns the data associated with the asset / quote pair. + * @param id Id of the required pool. + */ function _getPair(uint256 id) internal view returns (Pair memory) { ( address tokenAsset, @@ -110,6 +128,10 @@ contract PositionRenderer { }); } + /** + * @dev Returns the data associated with the current pool. + * @param id Id of the required pool. + */ function _getPool(uint256 id) internal view returns (Pool memory) { ( uint128 virtualX, @@ -138,6 +160,10 @@ contract PositionRenderer { }); } + /** + * @dev Returns the data associated with the current pool config. + * @param id Id of the required pool. + */ function _getConfig( uint256 id, address strategy @@ -159,6 +185,11 @@ contract PositionRenderer { }); } + /** + * @dev Returns all data associated with the current pool packed within a + * struct. + * @param id Id of the required pool. + */ function _getProperties(uint256 id) private view @@ -171,6 +202,9 @@ contract PositionRenderer { return Properties({ pair: pair, pool: pool, config: config }); } + /** + * @dev Generates the name of the NFT. + */ function _generateName(Properties memory properties) private pure @@ -184,6 +218,9 @@ contract PositionRenderer { ); } + /** + * @dev Outputs all the data associated with the current pair in JSON format. + */ function _generatePair(Properties memory properties) private pure @@ -211,6 +248,9 @@ contract PositionRenderer { ); } + /** + * @dev Outputs all the data associated with the current pool in JSON format. + */ function _generatePool(Properties memory properties) private pure @@ -241,6 +281,10 @@ contract PositionRenderer { ); } + /** + * @dev Outputs all the data associated with the current pool config in JSON + * format. + */ function _generateConfig(Properties memory properties) private pure @@ -264,6 +308,9 @@ contract PositionRenderer { ); } + /** + * @dev Generates the visual representation of the NFT in HTML. + */ function _generateHTML(Properties memory properties) private view @@ -298,11 +345,18 @@ contract PositionRenderer { '
', '', _generateStats(properties), - _generateHTMLFooter(properties), + _generateFooter(properties), "" ); } + /** + * @dev Generates a
" ); } + /** + * @dev Generates the stats
", PRIMITIVE_LOGO, "', label, "
", amount, "
', + label, + "
", + " ", + symbol, + "
', + label, + "
", + " %
element representing a stat. + * @param label Name of the stat. + * @param amount Full amount (including the decimals). + * @param decimals Decimals of the token. + * @param symbol Ticker of the token. + */ function _generateStat( string memory label, uint256 amount, @@ -323,6 +377,11 @@ contract PositionRenderer { ); } + /** + * @dev Generates a element representing a percentage stat. + * @param label Name of the stat. + * @param amount Full amount (using a 10,000 base). + */ function _generatePercentStat( string memory label, uint256 amount @@ -337,19 +396,33 @@ contract PositionRenderer { ); } - function _generateTitle( - string memory label, - string memory amount - ) private pure returns (string memory) { + /** + * @dev Generates a element containing the title. + */ + function _generateTitle(Properties memory properties) + private + view + returns (string memory) + { return string.concat( '', - label, + string.concat( + properties.pair.assetSymbol, "-", properties.pair.quoteSymbol + ), '
', - amount, + properties.config.isPerpetual + ? "Perpetual pool" + : ( + properties.config.creationTimestamp + + properties.config.durationSeconds + ).toCountdown(), "
element. + */ function _generateStats(Properties memory properties) private view @@ -360,43 +433,24 @@ contract PositionRenderer { "", - _generateHTMLTitle(properties), + _generateTitle(properties), "", - _generateHTMLSpotPrice(properties), - _generateHTMLStrikePrice(properties), + _generateSpotPrice(properties), + _generateStrikePrice(properties), "", - _generateHTMLAssetReserves(properties), - _generateHTMLQuoteReserves(properties), + _generateAssetReserves(properties), + _generateQuoteReserves(properties), "", - _generateHTMLPoolValuation(properties), - _generateHTMLSwapFee(properties), + _generatePoolValuation(properties), + _generateSwapFee(properties), "
", PRIMITIVE_LOGO, "
" ); } - function _generateHTMLTitle(Properties memory properties) - internal - view - returns (string memory) - { - return string.concat( - _generateTitle( - string.concat( - properties.pair.assetSymbol, - "-", - properties.pair.quoteSymbol - ), - properties.config.isPerpetual - ? "Perpetual pool" - : ( - properties.config.creationTimestamp - + properties.config.durationSeconds - ).toCountdown() - ) - ); - } - - function _generateHTMLSpotPrice(Properties memory properties) + /** + * @dev Generates the spot price element. + */ + function _generateSpotPrice(Properties memory properties) internal pure returns (string memory) @@ -413,7 +467,10 @@ contract PositionRenderer { ); } - function _generateHTMLStrikePrice(Properties memory properties) + /** + * @dev Generates the strike price element. + */ + function _generateStrikePrice(Properties memory properties) internal pure returns (string memory) @@ -431,7 +488,10 @@ contract PositionRenderer { ); } - function _generateHTMLAssetReserves(Properties memory properties) + /** + * @dev Calculates the asset reserves and generates the element. + */ + function _generateAssetReserves(Properties memory properties) internal pure returns (string memory) @@ -448,7 +508,10 @@ contract PositionRenderer { ); } - function _generateHTMLQuoteReserves(Properties memory properties) + /** + * @dev Calculates the quote reserves and generates the element. + */ + function _generateQuoteReserves(Properties memory properties) internal pure returns (string memory) @@ -465,7 +528,10 @@ contract PositionRenderer { ); } - function _generateHTMLPoolValuation(Properties memory properties) + /** + * @dev Calculates the pool valuation and generates the element. + */ + function _generatePoolValuation(Properties memory properties) internal pure returns (string memory) @@ -490,7 +556,10 @@ contract PositionRenderer { ); } - function _generateHTMLSwapFee(Properties memory properties) + /** + * @dev Generates the swap fee element. + */ + function _generateSwapFee(Properties memory properties) internal pure returns (string memory) @@ -500,7 +569,10 @@ contract PositionRenderer { ); } - function _generateHTMLFooter(Properties memory properties) + /** + * @dev Generates the footer
element. + */ + function _generateFooter(Properties memory properties) internal pure returns (string memory) From a9c574d8061b027864827b8548eccb99ec0bdc9b Mon Sep 17 00:00:00 2001 From: clemlak Date: Tue, 5 Sep 2023 09:49:13 +0400 Subject: [PATCH 18/20] fix: pair details, color generation --- contracts/PositionRenderer.sol | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/contracts/PositionRenderer.sol b/contracts/PositionRenderer.sol index b1c371d6..ec0e1674 100644 --- a/contracts/PositionRenderer.sol +++ b/contracts/PositionRenderer.sol @@ -6,6 +6,7 @@ import "solmate/tokens/ERC20.sol"; import "./libraries/StringsLib.sol"; import "./libraries/AssemblyLib.sol"; +import { PoolIdLib } from "./libraries/PoolLib.sol"; import "./interfaces/IPortfolio.sol"; import "./strategies/NormalStrategy.sol"; @@ -106,7 +107,7 @@ contract PositionRenderer { /** * @dev Returns the data associated with the asset / quote pair. - * @param id Id of the required pool. + * @param id Id of the pair associated with the required pool. */ function _getPair(uint256 id) internal view returns (Pair memory) { ( @@ -114,7 +115,9 @@ contract PositionRenderer { uint8 decimalsAsset, address tokenQuote, uint8 decimalsQuote - ) = IPortfolio(msg.sender).pairs(uint24(id)); + ) = IPortfolio(msg.sender).pairs( + uint24(PoolIdLib.pairId(PoolId.wrap(uint64(id)))) + ); return Pair({ asset: tokenAsset, @@ -318,11 +321,17 @@ contract PositionRenderer { { string memory color0 = StringsLib.toHexColor( bytes3( - keccak256(abi.encode((properties.pool.poolId >> 232) << 232)) + keccak256( + abi.encode(properties.pool.poolId, properties.pair.asset) + ) ) ); string memory color1 = StringsLib.toHexColor( - bytes3(keccak256(abi.encode(properties.pool.poolId << 232))) + bytes3( + keccak256( + abi.encode(properties.pool.poolId, properties.pair.quote) + ) + ) ); string memory title = string.concat( From 6ed81d1ee1d70457a2caf496875b4d06e29849ec Mon Sep 17 00:00:00 2001 From: clemlak Date: Tue, 5 Sep 2023 09:49:23 +0400 Subject: [PATCH 19/20] test: add test_uri_many_pools --- test/TestPositionRendererUri.t.sol | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/TestPositionRendererUri.t.sol b/test/TestPositionRendererUri.t.sol index 96bf90fb..cf3346d7 100644 --- a/test/TestPositionRendererUri.t.sol +++ b/test/TestPositionRendererUri.t.sol @@ -167,4 +167,25 @@ contract TestPositionRendererUri is Setup { _allocate(ctx, poolId, 1 ether, 2_000 * 10 ** 6); console.log(ERC1155(address(subject())).uri(poolId)); } + + function test_uri_many_pools() public { + MetaContext memory ctx = MetaContext({ + asset: address(new MockERC20("Ethereum", "ETH", 18)), + quote: address(new MockERC20("USD Coin", "USDC", 6)), + strikePriceWad: AssemblyLib.scaleToWad(1666 * 10 ** 6, 6), + durationSeconds: 86_400 * 3, + swapFee: 400, + isPerpetual: false, + priceWad: AssemblyLib.scaleToWad(1666 * 10 ** 6, 6), + controller: address(0), + strategy: address(0), + prioritySwapFee: 0 + }); + + _createPool(ctx); + _createPool(ctx); + uint64 poolId = _createPool(ctx); + _allocate(ctx, poolId, 1 ether, 2_000 * 10 ** 6); + console.log(ERC1155(address(subject())).uri(poolId)); + } } From d0569aaae9ee457e862b366790d07cd4c3644888 Mon Sep 17 00:00:00 2001 From: clemlak Date: Tue, 5 Sep 2023 14:43:27 +0400 Subject: [PATCH 20/20] chore: add test script to generate data --- scripts/TestDeploy.s.sol | 121 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 scripts/TestDeploy.s.sol diff --git a/scripts/TestDeploy.s.sol b/scripts/TestDeploy.s.sol new file mode 100644 index 00000000..67a946c6 --- /dev/null +++ b/scripts/TestDeploy.s.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import "solmate/test/utils/mocks/MockERC20.sol"; + +import "../contracts/test/SimpleRegistry.sol"; +import "../contracts/Portfolio.sol"; +import "../contracts/PositionRenderer.sol"; +import "../contracts/libraries/AssemblyLib.sol"; + +contract TestDeploy is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address sender = vm.addr(deployerPrivateKey); + console.log("Sender:", sender); + vm.startBroadcast(deployerPrivateKey); + + MockERC20 usdt = new MockERC20("USDT", "USDT", 18); + MockERC20 dai = new MockERC20("DAI", "DAI", 18); + MockERC20 weth = new MockERC20("Wrapped Ether", "WETH", 18); + MockERC20 usdc = new MockERC20("USD Coin", "USDC", 6); + + address registry = address(new SimpleRegistry()); + address positionRenderer = address(new PositionRenderer()); + Portfolio portfolio = + new Portfolio(address(weth), registry, positionRenderer); + + // USDT - DAI + + uint64 poolId = _createPool( + portfolio, + address(usdt), + address(dai), + AssemblyLib.scaleToWad(1 ether, 18) + ); + _allocate(sender, portfolio, poolId, usdt, dai, 1000 ether, 1000 ether); + + // USDT - USDC + + poolId = _createPool( + portfolio, + address(usdt), + address(usdc), + AssemblyLib.scaleToWad(1 * 10 ** 6, 6) + ); + _allocate( + sender, portfolio, poolId, usdt, usdc, 2000 ether, 2000 * 10 ** 6 + ); + + // WETH - USDC + poolId = _createPool( + portfolio, + address(weth), + address(usdc), + AssemblyLib.scaleToWad(2000 * 10 ** 6, 6) + ); + _allocate( + sender, portfolio, poolId, weth, usdc, 2 ether, 4000 * 10 ** 6 + ); + + console.log(unicode"🚀 Contracts deployed!"); + console.log("Portfolio:", address(portfolio)); + + vm.stopBroadcast(); + } + + function _createPool( + Portfolio portfolio, + address asset, + address quote, + uint256 price + ) public returns (uint64) { + uint24 pairId = portfolio.getPairId(asset, quote); + + if (pairId == 0) pairId = portfolio.createPair(asset, quote); + + (bytes memory strategyData, uint256 initialX, uint256 initialY) = + INormalStrategy(portfolio.DEFAULT_STRATEGY()).getStrategyData( + price, 1_00, 3 days, false, price + ); + + uint64 poolId = portfolio.createPool( + pairId, + initialX, + initialY, + 100, + 0, + address(0), + address(0), + strategyData + ); + + return poolId; + } + + function _allocate( + address sender, + Portfolio portfolio, + uint64 poolId, + MockERC20 asset, + MockERC20 quote, + uint128 amount0, + uint128 amount1 + ) public { + asset.mint(sender, amount0); + asset.approve(address(portfolio), amount0); + quote.mint(sender, amount1); + quote.approve(address(portfolio), amount1); + + uint128 deltaLiquidity = + portfolio.getMaxLiquidity(poolId, amount0, amount1); + + (uint128 deltaAsset, uint128 deltaQuote) = + portfolio.getLiquidityDeltas(poolId, int128(deltaLiquidity)); + + portfolio.allocate( + false, sender, poolId, deltaLiquidity, deltaAsset, deltaQuote + ); + } +}