From 2fd86552b7a2b7d5143f652ce351216d34b251ec Mon Sep 17 00:00:00 2001 From: Hayden Shively <17186559+haydenshively@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:05:41 -0500 Subject: [PATCH] Fix multicall in BorrowerNFT by making mint payable, and outsource URI --- periphery/script/Deploy.s.sol | 17 +- periphery/src/boost/BoostNFT.txt | 151 ------------ periphery/src/boost/NFTDescriptor.sol | 158 ------------ periphery/src/boost/NFTSVG.sol | 269 --------------------- periphery/src/boost/SafeERC20Namer.sol | 94 ------- periphery/src/borrower-nft/BorrowerNFT.sol | 16 +- 6 files changed, 23 insertions(+), 682 deletions(-) delete mode 100644 periphery/src/boost/BoostNFT.txt delete mode 100644 periphery/src/boost/NFTDescriptor.sol delete mode 100644 periphery/src/boost/NFTSVG.sol delete mode 100644 periphery/src/boost/SafeERC20Namer.sol diff --git a/periphery/script/Deploy.s.sol b/periphery/script/Deploy.s.sol index 62412343..d8e96baa 100644 --- a/periphery/script/Deploy.s.sol +++ b/periphery/script/Deploy.s.sol @@ -9,24 +9,30 @@ import {BorrowerLens} from "src/BorrowerLens.sol"; import {LenderLens} from "src/LenderLens.sol"; import {Router, IPermit2} from "src/Router.sol"; -import {BorrowerNFT} from "src/borrower-nft/BorrowerNFT.sol"; +import {BorrowerNFT, IBorrowerURISource} from "src/borrower-nft/BorrowerNFT.sol"; import {BoostManager} from "src/managers/BoostManager.sol"; import {FrontendManager} from "src/managers/FrontendManager.sol"; import {SimpleManager} from "src/managers/SimpleManager.sol"; import {UniswapNFTManager, INFTManager} from "src/managers/UniswapNFTManager.sol"; -bytes32 constant TAG = bytes32(uint256(0xA10EBE1A2)); +bytes32 constant TAG = bytes32(uint256(0xA10EBE1A3)); contract DeployScript is Script { string[] chains = ["optimism", "arbitrum", "base"]; Factory[] factories = [ - Factory(0x3A0a11A7829bfB34400cE338a0442877FBC8582e), - Factory(0x3A0a11A7829bfB34400cE338a0442877FBC8582e), + Factory(0x000000F00500f8BC2a68D90723AC00fCf4AD4b4D), + Factory(0x000000F00500f8BC2a68D90723AC00fCf4AD4b4D), Factory(0x00000006d6C0519e0eB953CFfeb7007e5200680B) ]; + IBorrowerURISource[] uriSources = [ + IBorrowerURISource(0x0000000000000000000000000000000000000000), + IBorrowerURISource(0x0000000000000000000000000000000000000000), + IBorrowerURISource(0x0000000000000000000000000000000000000000) + ]; + INFTManager[] uniswapNfts = [ INFTManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88), INFTManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88), @@ -42,6 +48,7 @@ contract DeployScript is Script { function run() external { for (uint256 i = 0; i < chains.length; i++) { Factory factory = factories[i]; + IBorrowerURISource uriSource = uriSources[i]; INFTManager uniswapNft = uniswapNfts[i]; IPermit2 permit2 = permit2s[i]; @@ -52,7 +59,7 @@ contract DeployScript is Script { new LenderLens{salt: TAG}(); new Router{salt: TAG}(permit2); - BorrowerNFT borrowerNft = new BorrowerNFT{salt: TAG}(factory); + BorrowerNFT borrowerNft = new BorrowerNFT{salt: TAG}(factory, uriSource); new BoostManager{salt: TAG}(factory, address(borrowerNft), uniswapNft); new FrontendManager{salt: TAG}(factory); diff --git a/periphery/src/boost/BoostNFT.txt b/periphery/src/boost/BoostNFT.txt deleted file mode 100644 index 66a3cc1c..00000000 --- a/periphery/src/boost/BoostNFT.txt +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.17; - -import {ERC721} from "solmate/tokens/ERC721.sol"; -import {IUniswapV3Pool} from "v3-core/contracts/interfaces/IUniswapV3Pool.sol"; - -import {Borrower, IManager, ERC20} from "aloe-ii-core/Borrower.sol"; -import {Factory} from "aloe-ii-core/Factory.sol"; - -import {NFTDescriptor} from "./NFTDescriptor.sol"; -import {SafeERC20Namer} from "./SafeERC20Namer.sol"; - -contract BoostNFT is ERC721 { - struct NFTAttributes { - Borrower borrower; - bool isGeneralized; - } - - Factory public immutable FACTORY; - - address public owner; - - IManager public boostManager; - - mapping(uint256 => NFTAttributes) public attributesOf; - - Borrower[] internal _freeBorrowers; - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address owner_, Factory factory) ERC721("Uniswap V3 - Aloe Edition", "UNI-V3-ALOE") { - owner = owner_; - FACTORY = factory; - } - - /*////////////////////////////////////////////////////////////// - GOVERNANCE - //////////////////////////////////////////////////////////////*/ - - function setOwner(address owner_) external { - require(msg.sender == owner); - owner = owner_; - } - - function setBoostManager(IManager boostManager_) external { - require(msg.sender == owner); - boostManager = boostManager_; - } - - function createBorrower(IUniswapV3Pool pool) external { - _freeBorrowers.push(Borrower(FACTORY.createBorrower(pool, address(this), 0))); - } - - /*////////////////////////////////////////////////////////////// - MINT - //////////////////////////////////////////////////////////////*/ - - function mint(IUniswapV3Pool pool, bytes memory initializationData, uint40 oracleSeed) public payable { - uint256 id = uint256(keccak256(abi.encodePacked(msg.sender, balanceOf(msg.sender)))); - - Borrower borrower = _nextBorrower(pool); - attributesOf[id] = NFTAttributes(borrower, false); - _mint(msg.sender, id); - - initializationData = abi.encode(msg.sender, 0, initializationData); - borrower.modify{value: msg.value}(boostManager, initializationData, oracleSeed); - } - - /*////////////////////////////////////////////////////////////// - BORROWER MODIFY - //////////////////////////////////////////////////////////////*/ - - function modify(uint256 id, uint8 action, IManager manager, bytes memory data, uint40 oracleSeed) public payable { - require(msg.sender == _ownerOf[id], "Aloe: only NFT owner can modify"); - - NFTAttributes memory attributes = attributesOf[id]; - - if (address(manager) == address(0)) { - manager = boostManager; - } else if (!attributes.isGeneralized) { - attributesOf[id].isGeneralized = true; - } - - data = abi.encode(msg.sender, action, data); - attributes.borrower.modify{value: msg.value}(manager, data, oracleSeed); - } - - function modify(uint256 id, uint8 action, bytes calldata data, uint40 oracleSeed) external payable { - modify(id, action, IManager(address(0)), data, oracleSeed); - } - - /*////////////////////////////////////////////////////////////// - NFT DESCRIPTION - //////////////////////////////////////////////////////////////*/ - - function tokenURI(uint256 id) public view virtual override returns (string memory) { - NFTAttributes memory attributes = attributesOf[id]; - - int24 tickLower; - int24 tickUpper; - int24 tickCurrent; - int24 tickSpacing; - IUniswapV3Pool poolAddress = attributes.borrower.UNISWAP_POOL(); - - if (!attributes.isGeneralized) { - int24[] memory positions = attributes.borrower.getUniswapPositions(); - tickLower = positions[0]; - tickUpper = positions[1]; - (, tickCurrent, , , , , ) = poolAddress.slot0(); - tickSpacing = poolAddress.tickSpacing(); - } - - ERC20 token0 = attributes.borrower.TOKEN0(); - ERC20 token1 = attributes.borrower.TOKEN1(); - return - NFTDescriptor.constructTokenURI( - NFTDescriptor.ConstructTokenURIParams({ - tokenId: id, - token0: address(token0), - token1: address(token1), - symbol0: SafeERC20Namer.tokenSymbol(address(token0)), - symbol1: SafeERC20Namer.tokenSymbol(address(token1)), - tickLower: tickLower, - tickUpper: tickUpper, - tickCurrent: tickCurrent, - fee: poolAddress.fee(), - poolAddress: address(poolAddress), - borrowerAddress: address(attributes.borrower), - isGeneralized: attributes.isGeneralized - }) - ); - } - - /*////////////////////////////////////////////////////////////// - HELPERS - //////////////////////////////////////////////////////////////*/ - - function _nextBorrower(IUniswapV3Pool pool) private returns (Borrower borrower) { - unchecked { - uint256 count = _freeBorrowers.length; - if (count > 0) { - borrower = _freeBorrowers[count - 1]; - _freeBorrowers.pop(); - } else { - borrower = Borrower(FACTORY.createBorrower(pool, address(this), 0)); - } - } - } -} diff --git a/periphery/src/boost/NFTDescriptor.sol b/periphery/src/boost/NFTDescriptor.sol deleted file mode 100644 index 5d74d551..00000000 --- a/periphery/src/boost/NFTDescriptor.sol +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.17; - -import {Base64} from "solady/utils/Base64.sol"; -import {LibString} from "solady/utils/LibString.sol"; - -import {NFTSVG} from "./NFTSVG.sol"; - -library NFTDescriptor { - using LibString for string; - using LibString for uint256; - - struct ConstructTokenURIParams { - address poolAddress; - uint24 fee; - address token0; - address token1; - string symbol0; - string symbol1; - address borrowerAddress; - uint256 counter; - uint24 health; - bool hasAnte; - } - - function constructTokenURI(ConstructTokenURIParams memory params) internal pure returns (string memory) { - // Pool-specific parameters - string memory poolString = _addressToString(params.poolAddress); - string memory feeString = _feeToString(params.fee); - string memory token0 = _addressToString(params.token0); - string memory token1 = _addressToString(params.token1); - params.symbol0 = params.symbol0.escapeJSON(); - params.symbol1 = params.symbol1.escapeJSON(); - // Borrower-specific parameters - string memory borrowerString = _addressToString(params.borrowerAddress); - string memory counter = params.counter.toString(); - - string memory name = _generateName(params); - string memory description = _generateDescription( - poolString, - feeString, - token0, - token1, - params.symbol0, - params.symbol1, - borrowerString, - counter - ); - string memory image = Base64.encode(bytes(_generateSVGImage(params, counter, token0, token1, feeString))); - - return - string.concat( - "data:application/json;base64,", - Base64.encode( - bytes( - string.concat( - '{"name":"', - name, - '", "description":"', - description, - '", "image": "', - "data:image/svg+xml;base64,", - image, - '"}' - ) - ) - ) - ); - } - - function _generateName(ConstructTokenURIParams memory params) private pure returns (string memory) { - return string.concat(params.symbol0, "/", params.symbol1, " Borrower"); - } - - function _generateDescription( - string memory poolString, - string memory feeString, - string memory token0, - string memory token1, - string memory symbol0, - string memory symbol1, - string memory borrowerString, - string memory counter - ) private pure returns (string memory) { - return - string.concat( - "This NFT grants its owner control of an Aloe II Borrower in the ", - symbol0, - "-", - symbol1, - " market.\\n", - "\\nBorrower: ", - borrowerString, - "\\n\\n", - symbol0, - ": ", - token0, - "\\n\\n", - symbol1, - ": ", - token1, - "\\n\\nUniswap Pool: ", - poolString, - " (", - feeString, - " Fee Tier)", - "\\n\\nToken ID: ", - counter, - "\\n\\n", - unicode"⚠️ DISCLAIMER: Due diligence is imperative when assessing this NFT. Make sure token addresses match the expected tokens, as token symbols may be imitated." - ); - } - - function _generateSVGImage( - ConstructTokenURIParams memory params, - string memory tokenId, - string memory token0, - string memory token1, - string memory feeString - ) private pure returns (string memory) { - NFTSVG.SVGParams memory svgParams = NFTSVG.SVGParams( - tokenId.slice(0, 16), - token0, - token1, - params.symbol0, - params.symbol1, - feeString, - params.health, - params.hasAnte, - _tokenToColor(params.token0, 60), - _tokenToColor(params.token1, 32), - _tokenToColor(params.token0, 80), - _tokenToColor(params.token1, 36) - ); - - return NFTSVG.generateSVG(svgParams); - } - - /*////////////////////////////////////////////////////////////// - HELPERS - //////////////////////////////////////////////////////////////*/ - - function _addressToString(address addr) private pure returns (string memory) { - return LibString.toHexStringChecksummed(addr); - } - - function _feeToString(uint24 fee) private pure returns (string memory) { - if (fee == 100) return "0.01%"; - if (fee == 500) return "0.05%"; - if (fee == 3000) return "0.3%"; - if (fee == 10000) return "1.0%"; - return ""; - } - - function _tokenToColor(address token, uint256 offset) private pure returns (string memory) { - return string.concat("#", uint256((uint160(token) >> offset) % (1 << 24)).toHexStringNoPrefix(3)); - } -} diff --git a/periphery/src/boost/NFTSVG.sol b/periphery/src/boost/NFTSVG.sol deleted file mode 100644 index 6de932ad..00000000 --- a/periphery/src/boost/NFTSVG.sol +++ /dev/null @@ -1,269 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.17; - -import {LibString} from "solady/utils/LibString.sol"; - -/// @title NFTSVG -/// @notice Generates SVG for a boosted Uniswap position -library NFTSVG { - using LibString for uint256; - using LibString for int256; - - struct SVGParams { - string tokenId; - string token0; - string token1; - string symbol0; - string symbol1; - string feeTier; - uint24 health; - bool hasAnte; - string color0; - string color1; - string color2; - string color3; - } - - function generateSVG(SVGParams memory params) internal pure returns (string memory svg) { - return - string.concat( - _generateSVGDefs(), - // card effects wrappers - '', - // card background - '', - _generateSVGHeaderText(params.symbol0, params.symbol1, params.feeTier), - '" : ' filter="url(#grayscale)">', - _generate3X3Quilt(params.color0, "rgb(242,245,238)", params.color1, params.color2, params.color3), - _generateAnimatedText(params.token0, params.token1, params.symbol0, params.symbol1), - "", - _generatePositionDataText(params.tokenId, params.health, true), - "" - ); - } - - function _generateSVGDefs() private pure returns (string memory) { - return - string.concat( - '', - "", - // card drop shadow - '', - // gradient + mask so that tickers don't bleed over the edge of the card - '', - '', - // card clipping - '', - // pattern clipping - '', - // quilt clipping - '', - // paper texture (noise) - '', - '', - '', - '', - "", - // grayscale - '', - // animated text path - '', - "" - ); - } - - function _generateSVGHeaderText( - string memory quoteTokenSymbol, - string memory baseTokenSymbol, - string memory feeTier - ) private pure returns (string memory) { - return - string.concat( - '', - '', - quoteTokenSymbol, - "/", - baseTokenSymbol, - '', - feeTier, - "" - ); - } - - function _generate3X3Quilt( - string memory color0, - string memory color1, - string memory color2, - string memory color3, - string memory color4 - ) private pure returns (string memory) { - return - string.concat( - // pattern A - '', - // pattern B - '', - // pattern C - '', - // pattern C.2 - '', - // pattern B.2 - '', - // pattern A.2 - '', - // pattern D - '', - // pattern E - '', - // pattern F - '' - ); - } - - function _generateAnimatedText( - string memory quoteToken, - string memory baseToken, - string memory quoteTokenSymbol, - string memory baseTokenSymbol - ) private pure returns (string memory) { - return - string.concat( - '', - '', - baseToken, - unicode" • ", - baseTokenSymbol, - '', - '', - baseToken, - unicode" • ", - baseTokenSymbol, - '', - '', - quoteToken, - unicode" • ", - quoteTokenSymbol, - '', - '', - quoteToken, - unicode" • ", - quoteTokenSymbol, - '' - ); - } - - function _generatePositionDataText( - string memory tokenId, - uint24 health, - bool isGeneralized - ) private pure returns (string memory) { - if (isGeneralized) { - return - string.concat( - '', - '', - 'ID: ', - tokenId, - "" - ); - } - - string memory healthStr = uint256(health).toString(); - - return - string.concat( - '', - '', - 'ID: ', - tokenId, - "", - ' ', - '', - 'Min Tick: ', - healthStr, - "", - '', - '', - 'Max Tick: ', - healthStr, - "" - ); - } -} diff --git a/periphery/src/boost/SafeERC20Namer.sol b/periphery/src/boost/SafeERC20Namer.sol deleted file mode 100644 index 53943e53..00000000 --- a/periphery/src/boost/SafeERC20Namer.sol +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.17; - -interface IERC20Metadata { - function name() external view returns (string memory); - - function symbol() external view returns (string memory); - - function decimals() external view returns (uint8); -} - -/// @notice Produces token descriptors from inconsistent or absent ERC20 symbol implementations that can -/// return string or bytes32 this library will always produce a string symbol to represent the token -/// @author [Yield Protocol](https://github.com/yieldprotocol/yield-utils-v2/blob/main/src/token/SafeERC20Namer.sol) -library SafeERC20Namer { - // attempts to extract the token symbol. if it does not implement symbol, returns a symbol derived from the address - function tokenSymbol(address token) internal view returns (string memory) { - string memory symbol = _callAndParseStringReturn(token, IERC20Metadata.symbol.selector); - if (bytes(symbol).length == 0) { - // fallback to 6 uppercase hex of address - return "---"; - } - return symbol; - } - - // attempts to extract the token name. if it does not implement name, returns a name derived from the address - function tokenName(address token) internal view returns (string memory) { - string memory name = _callAndParseStringReturn(token, IERC20Metadata.name.selector); - if (bytes(name).length == 0) { - // fallback to full hex of address - return "Unknown"; - } - return name; - } - - /// @notice Provides a safe ERC20.decimals version which returns '0' as fallback value. - /// @param token The address of the ERC-20 token contract. - /// @return (uint8) Token decimals. - function tokenDecimals(address token) internal view returns (uint8) { - (bool success, bytes memory data) = token.staticcall(abi.encodeWithSelector(IERC20Metadata.decimals.selector)); - return success && data.length == 32 ? abi.decode(data, (uint8)) : 0; - } - - // calls an external view token contract method that returns a symbol or name, and parses the output into a string - function _callAndParseStringReturn(address token, bytes4 selector) private view returns (string memory) { - (bool success, bytes memory data) = token.staticcall(abi.encodeWithSelector(selector)); - // if not implemented, or returns empty data, return empty string - if (!success || data.length == 0) { - return ""; - } - // bytes32 data always has length 32 - if (data.length == 32) { - bytes32 decoded = abi.decode(data, (bytes32)); - return _bytes32ToString(decoded); - } else if (data.length > 64) { - return abi.decode(data, (string)); - } - return ""; - } - - function _bytes32ToString(bytes32 x) private pure returns (string memory) { - bytes memory bytesString = new bytes(32); - uint256 charCount = 0; - for (uint256 j = 0; j < 32; j++) { - bytes1 char = x[j]; - if (char != 0) { - bytesString[charCount] = char; - charCount++; - } - } - bytes memory bytesStringTrimmed = new bytes(charCount); - for (uint256 j = 0; j < charCount; j++) { - bytesStringTrimmed[j] = bytesString[j]; - } - return string(bytesStringTrimmed); - } - - // assumes the data is in position 2 - function _parseStringData(bytes memory b) private pure returns (string memory) { - uint256 charCount = 0; - // first parse the charCount out of the data - for (uint256 i = 32; i < 64; i++) { - charCount <<= 8; - charCount += uint8(b[i]); - } - - bytes memory bytesStringTrimmed = new bytes(charCount); - for (uint256 i = 0; i < charCount; i++) { - bytesStringTrimmed[i] = b[i + 64]; - } - - return string(bytesStringTrimmed); - } -} diff --git a/periphery/src/borrower-nft/BorrowerNFT.sol b/periphery/src/borrower-nft/BorrowerNFT.sol index b105d622..7ae5c165 100644 --- a/periphery/src/borrower-nft/BorrowerNFT.sol +++ b/periphery/src/borrower-nft/BorrowerNFT.sol @@ -8,6 +8,10 @@ import {Factory} from "aloe-ii-core/Factory.sol"; import {ERC721Z, SafeSSTORE2, BytesLib} from "./ERC721Z.sol"; +interface IBorrowerURISource { + function uriOf(Borrower borrower) external view returns (string memory); +} + contract BorrowerNFT is ERC721Z { using SafeSSTORE2 for address; using BytesLib for bytes; @@ -24,8 +28,11 @@ contract BorrowerNFT is ERC721Z { Factory public immutable FACTORY; - constructor(Factory factory) { + IBorrowerURISource public immutable URI_SOURCE; + + constructor(Factory factory, IBorrowerURISource uriSource) { FACTORY = factory; + URI_SOURCE = uriSource; } /*////////////////////////////////////////////////////////////// @@ -40,9 +47,8 @@ contract BorrowerNFT is ERC721Z { return "BORROW"; } - function tokenURI(uint256) external view override returns (string memory) { - // TODO: implement this, also could use number of leading zeros as a design parameter - return ""; + function tokenURI(uint256 tokenId) external view override returns (string memory) { + return URI_SOURCE.uriOf(_borrowerOf(tokenId)); } /// @inheritdoc ERC721Z @@ -59,7 +65,7 @@ contract BorrowerNFT is ERC721Z { MINT & MODIFY //////////////////////////////////////////////////////////////*/ - function mint(address to, IUniswapV3Pool[] calldata pools, bytes12[] calldata salts) external { + function mint(address to, IUniswapV3Pool[] calldata pools, bytes12[] calldata salts) external payable { uint256 qty = pools.length; uint256[] memory attributes = new uint256[](qty);