diff --git a/contracts/PositionRenderer.sol b/contracts/PositionRenderer.sol index 9a9782b5..ec0e1674 100644 --- a/contracts/PositionRenderer.sol +++ b/contracts/PositionRenderer.sol @@ -2,36 +2,102 @@ 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 "./libraries/AssemblyLib.sol"; +import { PoolIdLib } from "./libraries/PoolLib.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 Strings for *; - using SafeCastLib for *; + using StringsLib for *; + + struct Pair { + address asset; + string assetSymbol; + string assetName; + uint8 assetDecimals; + address quote; + string quoteSymbol; + string quoteName; + uint8 quoteDecimals; + } + + struct Pool { + uint256 poolId; + uint128 virtualX; + uint128 virtualY; + uint16 feeBasisPoints; + uint16 priorityFeeBasisPoints; + address controller; + address strategy; + uint256 spotPriceWad; + bool hasDefaultStrategy; + } + + 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 = + ''; + + string private constant STYLE_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); + return string.concat( "data:application/json;base64,", Base64.encode( bytes( abi.encodePacked( '{"name":"', - _generateName(id), - '","image":"', - _generateImage(), + _generateName(properties), + '","animation_url":"data:text/html;base64,', + Base64.encode(bytes(_generateHTML(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 +105,504 @@ contract PositionRenderer { ); } - function _generateName(uint256 id) private view returns (string memory) { - (address tokenAsset,, address tokenQuote,) = IPortfolio(msg.sender) - .pairs(PoolId.wrap(id.safeCastTo64()).pairId()); - - return string.concat( - "Primitive Portfolio LP ", - ERC20(tokenAsset).symbol(), - "-", - ERC20(tokenQuote).symbol() + /** + * @dev Returns the data associated with the asset / quote pair. + * @param id Id of the pair associated with the required pool. + */ + function _getPair(uint256 id) internal view returns (Pair memory) { + ( + address tokenAsset, + uint8 decimalsAsset, + address tokenQuote, + uint8 decimalsQuote + ) = IPortfolio(msg.sender).pairs( + uint24(PoolIdLib.pairId(PoolId.wrap(uint64(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 + }); + } + + /** + * @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, + 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({ + poolId: id, + virtualX: virtualX, + virtualY: virtualY, + feeBasisPoints: feeBasisPoints, + priorityFeeBasisPoints: priorityFeeBasisPoints, + controller: controller, + strategy: strategy, + spotPriceWad: spotPriceWad, + hasDefaultStrategy: strategy + == IPortfolio(msg.sender).DEFAULT_STRATEGY() + }); + } + + /** + * @dev Returns the data associated with the current pool config. + * @param id Id of the required pool. + */ + 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 _generateImage() private pure returns (string memory) { + /** + * @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 + 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 }); + } + + /** + * @dev Generates the name of the NFT. + */ + 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()); - + /** + * @dev Outputs all the data associated with the current pair in JSON format. + */ + 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)); - + /** + * @dev Outputs all the data associated with the current pool in JSON format. + */ + function _generatePool(Properties memory properties) + private + pure + returns (string memory) + { return string.concat( + '"asset_reserves":"', + (properties.pool.virtualX).toString(), + '",', + '"quote_reserves":"', + (properties.pool.virtualY).toString(), + '",', + '"spot_price_wad":"', + (properties.pool.spotPriceWad).toString(), + '",', '"fee_basis_points":"', - feeBasisPoints.toString(), + properties.pool.feeBasisPoints.toString(), '",', '"priority_fee_basis_points":"', - priorityFeeBasisPoints.toString(), + properties.pool.priorityFeeBasisPoints.toString(), '",', '"controller":"', - Strings.toHexString(controller), + StringsLib.toHexString(properties.pool.controller), '",', '"strategy":"', - Strings.toHexString(strategy), + StringsLib.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)); - + /** + * @dev Outputs all the data associated with the current pool config in JSON + * format. + */ + 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" + ); + } + + /** + * @dev Generates the visual representation of the NFT in HTML. + */ + function _generateHTML(Properties memory properties) + private + view + returns (string memory) + { + string memory color0 = StringsLib.toHexColor( + bytes3( + keccak256( + abi.encode(properties.pool.poolId, properties.pair.asset) + ) + ) + ); + string memory color1 = StringsLib.toHexColor( + bytes3( + keccak256( + abi.encode(properties.pool.poolId, properties.pair.quote) + ) + ) + ); + + string memory title = string.concat( + properties.pair.assetSymbol, + "-", + properties.pair.quoteSymbol, + " Portfolio LP" + ); + + return string.concat( + '', + title, + "", + STYLE_0, + color0, + ",", + color1, + STYLE_1, + "", + '
', + '', + _generateStats(properties), + _generateFooter(properties), + "" + ); + } + + /** + * @dev Generates a 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, + uint8 decimals, + string memory symbol + ) private pure returns (string memory) { + return string.concat( + '', + label, + "
", + " ", + symbol, + "" + ); + } + + /** + * @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 + ) private pure returns (string memory) { + return string.concat( + '', + label, + "
", + " %" + ); + } + + /** + * @dev Generates a element containing the title. + */ + function _generateTitle(Properties memory properties) + private + view + returns (string memory) + { + return string.concat( + '', + string.concat( + properties.pair.assetSymbol, "-", properties.pair.quoteSymbol + ), + '
', + properties.config.isPerpetual + ? "Perpetual pool" + : ( + properties.config.creationTimestamp + + properties.config.durationSeconds + ).toCountdown(), + "" + ); + } + + /** + * @dev Generates the stats element. + */ + function _generateStats(Properties memory properties) + private + view + returns (string memory) + { + return string.concat( + '
', + "", + _generateTitle(properties), + "", + _generateSpotPrice(properties), + _generateStrikePrice(properties), + "", + _generateAssetReserves(properties), + _generateQuoteReserves(properties), + "", + _generatePoolValuation(properties), + _generateSwapFee(properties), + "
", + PRIMITIVE_LOGO, + "
" + ); + } + + /** + * @dev Generates the spot price element. + */ + function _generateSpotPrice(Properties memory properties) + internal + pure + returns (string memory) + { + return string.concat( + _generateStat( + "Spot Price", + AssemblyLib.scaleFromWadDown( + properties.pool.spotPriceWad, properties.pair.quoteDecimals + ), + properties.pair.quoteDecimals, + properties.pair.quoteSymbol + ) + ); + } + + /** + * @dev Generates the strike price element. + */ + function _generateStrikePrice(Properties memory properties) + internal + pure + returns (string memory) + { + return string.concat( + _generateStat( + "Strike Price", + AssemblyLib.scaleFromWadDown( + properties.config.strikePriceWad, + properties.pair.quoteDecimals + ), + properties.pair.quoteDecimals, + properties.pair.quoteSymbol + ) + ); + } + + /** + * @dev Calculates the asset reserves and generates the element. + */ + function _generateAssetReserves(Properties memory properties) + internal + pure + returns (string memory) + { + return string.concat( + _generateStat( + "Asset Reserves", + AssemblyLib.scaleFromWadDown( + properties.pool.virtualX, properties.pair.assetDecimals + ), + properties.pair.assetDecimals, + properties.pair.assetSymbol + ) + ); + } + + /** + * @dev Calculates the quote reserves and generates the element. + */ + function _generateQuoteReserves(Properties memory properties) + internal + pure + returns (string memory) + { + return string.concat( + _generateStat( + "Asset Reserves", + AssemblyLib.scaleFromWadDown( + properties.pool.virtualY, properties.pair.quoteDecimals + ), + properties.pair.quoteDecimals, + properties.pair.quoteSymbol + ) + ); + } + + /** + * @dev Calculates the pool valuation and generates the element. + */ + function _generatePoolValuation(Properties memory properties) + internal + pure + returns (string memory) + { + uint256 poolValuation = AssemblyLib.scaleFromWadDown( + properties.pool.virtualX, properties.pair.assetDecimals + ) + * AssemblyLib.scaleFromWadDown( + properties.pool.spotPriceWad, properties.pair.quoteDecimals + ) / 10 ** properties.pair.assetDecimals + + AssemblyLib.scaleFromWadDown( + properties.pool.virtualY, properties.pair.quoteDecimals + ); + + return string.concat( + _generateStat( + "Pool Valuation", + poolValuation, + properties.pair.quoteDecimals, + properties.pair.quoteSymbol + ) + ); + } + + /** + * @dev Generates the swap fee element. + */ + function _generateSwapFee(Properties memory properties) + internal + pure + returns (string memory) + { + return string.concat( + _generatePercentStat("Swap Fee", properties.pool.feeBasisPoints) + ); + } + + /** + * @dev Generates the footer
element. + */ + function _generateFooter(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( + '

', + controlledLabel, + " and uses ", + properties.pool.hasDefaultStrategy + ? "the default strategy." + : "a custom strategy.", + "

" + ) ); } } diff --git a/contracts/libraries/StringsLib.sol b/contracts/libraries/StringsLib.sol new file mode 100644 index 00000000..9c1cb578 --- /dev/null +++ b/contracts/libraries/StringsLib.sol @@ -0,0 +1,124 @@ +// 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) + * + * Some of these functions are not really optimized, but since they are not + * supposed to be used onchain it doesn't really matter. + */ +library StringsLib { + 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)); + } + + function toCountdown(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 ", toString(daysLeft), " days")); + } + + if (hoursLeft >= 1) { + return (string.concat("Expires in ", toString(hoursLeft), " hours")); + } + + if (minutesLeft >= 1) { + return ( + string.concat("Expires in ", toString(minutesLeft), " minutes") + ); + } + + return (string.concat("Expires in ", toString(secondsLeft), " seconds")); + } +} 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 + ); + } +} diff --git a/test/TestPortfolioUri.t.sol b/test/TestPortfolioBalanceOf.t.sol similarity index 60% rename from test/TestPortfolioUri.t.sol rename to test/TestPortfolioBalanceOf.t.sol index 9dca2951..396941fc 100644 --- a/test/TestPortfolioUri.t.sol +++ b/test/TestPortfolioBalanceOf.t.sol @@ -1,48 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.4; -import "openzeppelin/utils/Base64.sol"; import "solmate/tokens/ERC1155.sol"; +import "../contracts/libraries/AssemblyLib.sol"; import "./Setup.sol"; +import "../contracts/strategies/NormalStrategy.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_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 - ); - } - +contract TestPortfolioBalanceOf is Setup { function test_balanceOf_allocating_increases_balance() public defaultConfig diff --git a/test/TestPositionRendererUri.t.sol b/test/TestPositionRendererUri.t.sol new file mode 100644 index 00000000..cf3346d7 --- /dev/null +++ b/test/TestPositionRendererUri.t.sol @@ -0,0 +1,191 @@ +// 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().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( + 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)); + } + + 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)); + } +}