Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor asset to token #1097

Merged
merged 5 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ In-depth documentation is available at [docs.sablier.com](https://docs.sablier.c
## Background

Sablier Lockup Protocol is a token distribution protocol used by DAOs and businesses for vesting, payroll, airdrops, and
more. Our flagship model is the linear stream, which distributes assets on a continuous, by-the-second basis.
more. Our flagship model is the linear stream, which distributes tokens on a continuous, by-the-second basis.

The way it works is that the sender of a payment stream first deposits a specific amount of ERC-20 tokens in a contract.
Then, the contract progressively allocates the funds to the recipient, who can access them as they become available over
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ publicly for their contribution if they so choose.

To qualify for a reward under this Program, you must adhere to the following criteria:

- Identify a previously unreported, non-public vulnerability that could result in the loss or freeze of any ERC-20 asset
- Identify a previously unreported, non-public vulnerability that could result in the loss or freeze of any ERC-20 token
in Sablier Lockup (but not on any third-party platform interacting with Sablier Lockup) and that is within the scope
of this Program.
- The vulnerability must be distinct from the issues covered in the [Audits](https://github.com/sablier-labs/audits).
Expand Down
4 changes: 2 additions & 2 deletions script/GenerateSVG.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ contract GenerateSVG is BaseScript, LockupNFTDescriptor {
NFTSVG.SVGParams({
accentColor: generateAccentColor({ sablier: LOCKUP, streamId: uint256(keccak256(msg.data)) }),
amount: string.concat(SVGElements.SIGN_GE, " ", amount),
assetAddress: DAI.toHexString(),
assetSymbol: "DAI",
tokenAddress: DAI.toHexString(),
tokenSymbol: "DAI",
duration: calculateDurationInDays({ startTime: 0, endTime: duration * 1 days }),
progress: stringifyPercentage(progress),
progressNumerical: progress,
Expand Down
14 changes: 7 additions & 7 deletions script/Init.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ interface IERC20Mint {

/// @notice Initializes the protocol by creating some streams.
contract Init is BaseScript {
function run(ISablierLockup lockup, IERC20 asset) public broadcast {
function run(ISablierLockup lockup, IERC20 token) public broadcast {
address sender = broadcaster;
address recipient = vm.addr(vm.deriveKey({ mnemonic: mnemonic, index: 1 }));

/*//////////////////////////////////////////////////////////////////////////
LOCKUP-LINEAR
//////////////////////////////////////////////////////////////////////////*/

// Mint enough assets to the sender.
IERC20Mint(address(asset)).mint({ beneficiary: sender, value: 131_601.1e18 + 10_000e18 });
// Mint enough tokens to the sender.
IERC20Mint(address(token)).mint({ beneficiary: sender, value: 131_601.1e18 + 10_000e18 });

// Approve the Lockup contracts to transfer the ERC-20 assets from the sender.
asset.approve({ spender: address(lockup), value: type(uint256).max });
// Approve the Lockup contracts to transfer the ERC-20 tokens from the sender.
token.approve({ spender: address(lockup), value: type(uint256).max });

// Create 7 Lockup Linear streams with various amounts and durations.
//
Expand All @@ -48,7 +48,7 @@ contract Init is BaseScript {
sender: sender,
recipient: recipient,
totalAmount: totalAmounts[i],
asset: asset,
token: token,
cancelable: true,
transferable: true,
broker: Broker(address(0), ud60x18(0)),
Expand Down Expand Up @@ -80,7 +80,7 @@ contract Init is BaseScript {
sender: sender,
recipient: recipient,
totalAmount: 10_000e18,
asset: asset,
token: token,
cancelable: true,
transferable: true,
broker: Broker(address(0), ud60x18(0)),
Expand Down
69 changes: 40 additions & 29 deletions src/LockupNFTDescriptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {

/// @dev Needed to avoid Stack Too Deep.
struct TokenURIVars {
address asset;
string assetSymbol;
address token;
string tokenSymbol;
uint128 depositedAmount;
bool isTransferable;
string json;
Expand All @@ -67,10 +67,21 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
vars.lockup = ISablierLockup(address(lockup));
vars.lockupModel = mapSymbol(lockup);
vars.lockupStringified = address(lockup).toHexString();
vars.asset = address(vars.lockup.getAsset(streamId));
vars.assetSymbol = safeAssetSymbol(vars.asset);
vars.tokenSymbol = safeTokenSymbol(vars.token);
vars.depositedAmount = vars.lockup.getDepositedAmount(streamId);

// Load the token's address based on the contract version.
if (vars.lockupModel.equal("Sablier Lockup")) {
// For the latest version of the Lockup contract, use the `getToken` function.
smol-ninja marked this conversation as resolved.
Show resolved Hide resolved
vars.token = address(vars.lockup.getToken(streamId));
}
// For older versions of the Lockup contract, use the `getAsset` function.
smol-ninja marked this conversation as resolved.
Show resolved Hide resolved
else {
(, bytes memory returnData) =
address(lockup).staticcall(abi.encodeWithSelector(bytes4(keccak256("getAsset(uint256)")), streamId));
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
vars.token = abi.decode(returnData, (address));
}

// Load the stream's data.
vars.status = stringifyStatus(vars.lockup.statusOf(streamId));
vars.streamedPercentage = calculateStreamedPercentage({
Expand All @@ -82,9 +93,9 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
vars.svg = NFTSVG.generateSVG(
NFTSVG.SVGParams({
accentColor: generateAccentColor(address(lockup), streamId),
amount: abbreviateAmount({ amount: vars.depositedAmount, decimals: safeAssetDecimals(vars.asset) }),
assetAddress: vars.asset.toHexString(),
assetSymbol: vars.assetSymbol,
amount: abbreviateAmount({ amount: vars.depositedAmount, decimals: safeTokenDecimals(vars.token) }),
tokenAddress: vars.token.toHexString(),
tokenSymbol: vars.tokenSymbol,
duration: calculateDurationInDays({
startTime: vars.lockup.getStartTime(streamId),
endTime: vars.lockup.getEndTime(streamId)
Expand All @@ -108,16 +119,16 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
vars.json = string.concat(
'{"attributes":',
generateAttributes({
assetSymbol: vars.assetSymbol,
tokenSymbol: vars.tokenSymbol,
sender: vars.lockup.getSender(streamId).toHexString(),
status: vars.status
}),
',"description":"',
generateDescription({
lockupModel: vars.lockupModel,
assetSymbol: vars.assetSymbol,
tokenSymbol: vars.tokenSymbol,
lockupStringified: vars.lockupStringified,
assetAddress: vars.asset.toHexString(),
tokenAddress: vars.token.toHexString(),
streamId: streamId.toString(),
isTransferable: vars.isTransferable
}),
Expand Down Expand Up @@ -249,12 +260,12 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
}

/// @notice Generates an array of JSON objects that represent the NFT's attributes:
/// - Asset symbol
/// - Token symbol
/// - Sender address
/// - Status
/// @dev These attributes are useful for filtering and sorting the NFTs.
function generateAttributes(
string memory assetSymbol,
string memory tokenSymbol,
string memory sender,
string memory status
)
Expand All @@ -263,8 +274,8 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
returns (string memory)
{
return string.concat(
'[{"trait_type":"Asset","value":"',
assetSymbol,
'[{"trait_type":"Token","value":"',
tokenSymbol,
'"},{"trait_type":"Sender","value":"',
sender,
'"},{"trait_type":"Status","value":"',
Expand All @@ -276,9 +287,9 @@ 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 assetSymbol,
string memory tokenSymbol,
string memory lockupStringified,
string memory assetAddress,
string memory tokenAddress,
string memory streamId,
bool isTransferable
)
Expand All @@ -295,18 +306,18 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
return string.concat(
"This NFT represents a payment stream in a Sablier Lockup ",
lockupModel,
" contract. The owner of this NFT can withdraw the streamed assets, which are denominated in ",
assetSymbol,
" contract. The owner of this NFT can withdraw the streamed tokens, which are denominated in ",
tokenSymbol,
".\\n\\n- Stream ID: ",
streamId,
"\\n- ",
lockupModel,
" Address: ",
lockupStringified,
"\\n- ",
assetSymbol,
tokenSymbol,
" Address: ",
assetAddress,
tokenAddress,
"\\n\\n",
info
);
Expand Down Expand Up @@ -358,22 +369,22 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
}
}

/// @notice Retrieves the asset's decimals safely, defaulting to "0" if an error occurs.
/// @dev Performs a low-level call to handle assets in which the decimals are not implemented.
function safeAssetDecimals(address asset) internal view returns (uint8) {
(bool success, bytes memory returnData) = asset.staticcall(abi.encodeCall(IERC20Metadata.decimals, ()));
/// @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) {
(bool success, bytes memory returnData) = token.staticcall(abi.encodeCall(IERC20Metadata.decimals, ()));
if (success && returnData.length == 32) {
return abi.decode(returnData, (uint8));
} else {
return 0;
}
}

/// @notice Retrieves the asset's symbol safely, defaulting to a hard-coded value if an error occurs.
/// @dev Performs a low-level call to handle assets in which the symbol is not implemented or it is a bytes32
/// @notice Retrieves the token's symbol safely, defaulting to a hard-coded value if an error occurs.
/// @dev Performs a low-level call to handle tokens in which the symbol is not implemented or it is a bytes32
/// instead of a string.
function safeAssetSymbol(address asset) internal view returns (string memory) {
(bool success, bytes memory returnData) = asset.staticcall(abi.encodeCall(IERC20Metadata.symbol, ()));
function safeTokenSymbol(address token) internal view returns (string memory) {
(bool success, bytes memory returnData) = token.staticcall(abi.encodeCall(IERC20Metadata.symbol, ()));

// Non-empty strings have a length greater than 64, and bytes32 has length 32.
if (!success || returnData.length <= 64) {
Expand All @@ -383,7 +394,7 @@ contract LockupNFTDescriptor is ILockupNFTDescriptor {
string memory symbol = abi.decode(returnData, (string));

// Check if the symbol is too long or contains disallowed characters. This measure helps mitigate potential
// security threats from malicious assets injecting scripts in the symbol string.
// security threats from malicious tokens injecting scripts in the symbol string.
if (bytes(symbol).length > 30) {
return "Long Symbol";
} else {
Expand Down
Loading
Loading