Skip to content

Commit

Permalink
perf: optimize NFTDescriptor contract (#1113)
Browse files Browse the repository at this point in the history
* test: update token uri tests

* test: remove tokenURI tests for each model

* perf: optimize NFTDescriptor contract
feat: remove backward compatibility

Co-authored-by: Andrei Vlad Birgaoanu <[email protected]>

* perf: remove no longer needed low level call

---------

Co-authored-by: andreivladbrg <[email protected]>
Co-authored-by: Andrei Vlad Birgaoanu <[email protected]>
  • Loading branch information
3 people authored Dec 12, 2024
1 parent 9ef51a3 commit b3ea122
Show file tree
Hide file tree
Showing 18 changed files with 85 additions and 647 deletions.
1 change: 0 additions & 1 deletion script/GenerateSVG.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ contract GenerateSVG is BaseScript, LockupNFTDescriptor {
progress: stringifyPercentage(progress),
progressNumerical: progress,
lockupAddress: LOCKUP.toHexString(),
lockupModel: "Lockup Linear",
status: status
})
);
Expand Down
67 changes: 8 additions & 59 deletions src/LockupNFTDescriptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { ILockupNFTDescriptor } from "./interfaces/ILockupNFTDescriptor.sol";
import { ISablierLockup } from "./interfaces/ISablierLockup.sol";
import { ISablierLockupBase } from "./interfaces/ISablierLockupBase.sol";
import { Errors } from "./libraries/Errors.sol";
import { NFTSVG } from "./libraries/NFTSVG.sol";
import { SVGElements } from "./libraries/SVGElements.sol";
import { Lockup } from "./types/DataTypes.sol";

/*
██╗ ██████╗ ██████╗██╗ ██╗██╗ ██╗██████╗ ███╗ ██╗███████╗████████╗
Expand Down Expand Up @@ -47,16 +46,12 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
address token;
string tokenSymbol;
uint128 depositedAmount;
bool isTransferable;
string json;
ISablierLockup lockup;
string lockupModel;
string lockupStringified;
bytes returnData;
string status;
string svg;
uint256 streamedPercentage;
bool success;
}

/// @inheritdoc ILockupNFTDescriptor
Expand All @@ -65,22 +60,12 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {

// Load the contracts.
vars.lockup = ISablierLockup(address(lockup));
vars.lockupModel = mapSymbol(lockup);
vars.lockupStringified = address(lockup).toHexString();
vars.tokenSymbol = safeTokenSymbol(vars.token);
vars.depositedAmount = vars.lockup.getDepositedAmount(streamId);

// Retrieve the underlying token contract's address.
if (vars.lockupModel.equal("Sablier Lockup")) {
// For Lockup contract versions v2.0.0 and later, use the `getUnderlyingToken` function.
vars.token = address(vars.lockup.getUnderlyingToken(streamId));
}
// For Lockup contract versions earlier than v2.0.0, use the `getAsset` function.
else {
(, bytes memory returnData) =
address(lockup).staticcall(abi.encodeWithSignature("getAsset(uint256)", streamId));
vars.token = abi.decode(returnData, (address));
}
vars.token = address(vars.lockup.getUnderlyingToken(streamId));
vars.tokenSymbol = safeTokenSymbol(vars.token);

// Load the stream's data.
vars.status = stringifyStatus(vars.lockup.statusOf(streamId));
Expand All @@ -103,18 +88,10 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
lockupAddress: vars.lockupStringified,
progress: stringifyPercentage(vars.streamedPercentage),
progressNumerical: vars.streamedPercentage,
status: vars.status,
lockupModel: vars.lockupModel
status: vars.status
})
);

// Performs a low-level call to handle older deployments that miss the `isTransferable` function.
(vars.success, vars.returnData) =
address(vars.lockup).staticcall(abi.encodeCall(ISablierLockupBase.isTransferable, (streamId)));

// When the call has failed, the stream NFT is assumed to be transferable.
vars.isTransferable = vars.success ? abi.decode(vars.returnData, (bool)) : true;

// Generate the JSON metadata.
vars.json = string.concat(
'{"attributes":',
Expand All @@ -125,15 +102,14 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
}),
',"description":"',
generateDescription({
lockupModel: vars.lockupModel,
tokenSymbol: vars.tokenSymbol,
lockupStringified: vars.lockupStringified,
tokenAddress: vars.token.toHexString(),
streamId: streamId.toString(),
isTransferable: vars.isTransferable
isTransferable: vars.lockup.isTransferable(streamId)
}),
'","external_url":"https://sablier.com","name":"',
generateName({ lockupModel: vars.lockupModel, streamId: streamId.toString() }),
string.concat("Sablier Lockup #", streamId.toString()),
'","image":"data:image/svg+xml;base64,',
Base64.encode(bytes(vars.svg)),
'"}'
Expand Down Expand Up @@ -286,7 +262,6 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {

/// @notice Generates a string with the NFT's JSON metadata description, which provides a high-level overview.
function generateDescription(
string memory lockupModel,
string memory tokenSymbol,
string memory lockupStringified,
string memory tokenAddress,
Expand All @@ -304,15 +279,12 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
: unicode"❕INFO: This NFT is non-transferable. It cannot be sold or transferred to another account.";

return string.concat(
"This NFT represents a payment stream in a Sablier Lockup ",
lockupModel,
" contract. The owner of this NFT can withdraw the streamed tokens, which are denominated in ",
"This NFT represents a stream in Sablier Lockup contract. The owner of this NFT can withdraw the streamed tokens, which are denominated in ",
tokenSymbol,
".\\n\\n- Stream ID: ",
streamId,
"\\n- ",
lockupModel,
" Address: ",
"Sablier Lockup Address: ",
lockupStringified,
"\\n- ",
tokenSymbol,
Expand All @@ -323,12 +295,6 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
);
}

/// @notice Generates a string with the NFT's JSON metadata name, which is unique for each stream.
/// @dev The `streamId` is equivalent to the ERC-721 `tokenId`.
function generateName(string memory lockupModel, string memory streamId) internal pure returns (string memory) {
return string.concat("Sablier ", lockupModel, " #", streamId);
}

/// @notice Checks whether the provided string contains only alphanumeric characters, spaces, and dashes.
/// @dev Note that this returns true for empty strings.
function isAllowedCharacter(string memory str) internal pure returns (bool) {
Expand All @@ -352,23 +318,6 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
return true;
}

/// @notice Maps ERC-721 symbols to human-readable model names.
/// @dev Reverts if the symbol is unknown.
function mapSymbol(IERC721Metadata sablier) internal view returns (string memory) {
string memory symbol = sablier.symbol();
if (symbol.equal("SAB-LOCKUP")) {
return "Sablier Lockup";
} else if (symbol.equal("SAB-LOCKUP-LIN") || symbol.equal("SAB-V2-LOCKUP-LIN")) {
return "Sablier Lockup Linear";
} else if (symbol.equal("SAB-LOCKUP-DYN") || symbol.equal("SAB-V2-LOCKUP-DYN")) {
return "Sablier Lockup Dynamic";
} else if (symbol.equal("SAB-LOCKUP-TRA") || symbol.equal("SAB-V2-LOCKUP-TRA")) {
return "Sablier Lockup Tranched";
} else {
revert Errors.LockupNFTDescriptor_UnknownNFT(sablier, symbol);
}
}

/// @notice Retrieves the token's decimals safely, defaulting to "0" if an error occurs.
/// @dev Performs a low-level call to handle tokens in which the decimals are not implemented.
function safeTokenDecimals(address token) internal view returns (uint8) {
Expand Down
11 changes: 3 additions & 8 deletions src/libraries/NFTSVG.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ library NFTSVG {
string tokenSymbol;
string duration;
string lockupAddress;
string lockupModel;
string progress;
uint256 progressNumerical;
string status;
Expand Down Expand Up @@ -89,7 +88,7 @@ library NFTSVG {
'<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 1000 1000">',
SVGElements.BACKGROUND,
generateDefs(params.accentColor, params.status, vars.cards),
generateFloatingText(params.lockupAddress, params.lockupModel, params.tokenAddress, params.tokenSymbol),
generateFloatingText(params.lockupAddress, params.tokenAddress, params.tokenSymbol),
generateHrefs(vars.progressXPosition, vars.statusXPosition, vars.amountXPosition, vars.durationXPosition),
"</svg>"
);
Expand Down Expand Up @@ -119,7 +118,6 @@ library NFTSVG {

function generateFloatingText(
string memory lockupAddress,
string memory lockupModel,
string memory tokenAddress,
string memory tokenSymbol
)
Expand All @@ -131,12 +129,9 @@ library NFTSVG {
'<text text-rendering="optimizeSpeed">',
SVGElements.floatingText({
offset: "-100%",
text: string.concat(lockupAddress, unicode"", "Sablier ", lockupModel)
}),
SVGElements.floatingText({
offset: "0%",
text: string.concat(lockupAddress, unicode"", "Sablier ", lockupModel)
text: string.concat(lockupAddress, unicode"", "Sablier Lockup")
}),
SVGElements.floatingText({ offset: "0%", text: string.concat(lockupAddress, unicode"", "Sablier Lockup") }),
SVGElements.floatingText({ offset: "-50%", text: string.concat(tokenAddress, unicode"", tokenSymbol) }),
SVGElements.floatingText({ offset: "50%", text: string.concat(tokenAddress, unicode"", tokenSymbol) }),
"</text>"
Expand Down
Loading

0 comments on commit b3ea122

Please sign in to comment.