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(
+ '',
+ "",
+ PRIMITIVE_LOGO,
+ " ",
+ _generateTitle(properties),
+ "",
+ _generateSpotPrice(properties),
+ _generateStrikePrice(properties),
+ " ",
+ _generateAssetReserves(properties),
+ _generateQuoteReserves(properties),
+ " ",
+ _generatePoolValuation(properties),
+ _generateSwapFee(properties),
+ "
"
+ );
+ }
+
+ /**
+ * @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));
+ }
+}