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

Clawback metadata #12

Merged
merged 3 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
49 changes: 33 additions & 16 deletions src/tokens/wrappers/clawback/ClawbackMetadata.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import {IMetadataProvider} from "../../common/IMetadataProvider.sol";
import {IClawbackFunctions} from "./IClawback.sol";

import {Duration} from "../../../utils/Duration.sol";

import {LibString} from "solady/utils/LibString.sol";
import {Base64} from "solady/utils/Base64.sol";

Expand Down Expand Up @@ -71,49 +73,50 @@
// From clawback
bool hasTokenId = details.tokenType == IClawbackFunctions.TokenType.ERC721
|| details.tokenType == IClawbackFunctions.TokenType.ERC1155;
properties = new MetadataProperty[](hasTokenId ? 8 : 7);
properties[0] = MetadataProperty("tokenType", _toTokenTypeStr(details.tokenType));
properties[1] = MetadataProperty("tokenAddress", details.tokenAddr.toHexStringChecksummed());
properties[2] = MetadataProperty("templateId", details.templateId.toString());
properties[3] = MetadataProperty("lockedAt", details.lockedAt.toString());
properties[4] = MetadataProperty("duration", template.duration.toString());
properties[5] = MetadataProperty("destructionOnly", _boolToString(template.destructionOnly));
properties[6] = MetadataProperty("transferOpen", _boolToString(template.transferOpen));
properties = new MetadataProperty[](hasTokenId ? 9 : 8);
properties[0] = MetadataProperty("token_type", _toTokenTypeStr(details.tokenType));
properties[1] = MetadataProperty("token_address", details.tokenAddr.toHexStringChecksummed());
properties[2] = MetadataProperty("template_id", details.templateId.toString());
properties[3] = MetadataProperty("locked_at", details.lockedAt.toString());
properties[4] = MetadataProperty("unlocks_in", _formatUnlocksIn(details.lockedAt, template.duration));
properties[5] = MetadataProperty("duration", Duration.format(template.duration));
properties[6] = MetadataProperty("destruction_only", _boolToString(template.destructionOnly));
properties[7] = MetadataProperty("transfer_open", _boolToString(template.transferOpen));
if (hasTokenId) {
properties[7] = MetadataProperty("tokenId", details.tokenId.toString());
properties[8] = MetadataProperty("token_id", details.tokenId.toString());
}

// From contract
if (details.tokenType == IClawbackFunctions.TokenType.ERC20) {
properties = _safeAddStringProperty(
properties, "originalName", details.tokenAddr, abi.encodeWithSelector(IERC20Metadata.name.selector)
properties, "original_name", details.tokenAddr, abi.encodeWithSelector(IERC20Metadata.name.selector)
);
properties = _safeAddStringProperty(
properties, "originalSymbol", details.tokenAddr, abi.encodeWithSelector(IERC20Metadata.symbol.selector)
properties, "original_symbol", details.tokenAddr, abi.encodeWithSelector(IERC20Metadata.symbol.selector)
);
properties = _safeAddUint256Property(
properties,
"originalDecimals",
"original_decimals",
details.tokenAddr,
abi.encodeWithSelector(IERC20Metadata.decimals.selector)
);
} else if (details.tokenType == IClawbackFunctions.TokenType.ERC721) {
properties = _safeAddStringProperty(
properties, "originalName", details.tokenAddr, abi.encodeWithSelector(IERC721Metadata.name.selector)
properties, "original_name", details.tokenAddr, abi.encodeWithSelector(IERC721Metadata.name.selector)
);
properties = _safeAddStringProperty(
properties, "originalSymbol", details.tokenAddr, abi.encodeWithSelector(IERC721Metadata.symbol.selector)
properties, "original_symbol", details.tokenAddr, abi.encodeWithSelector(IERC721Metadata.symbol.selector)
);
properties = _safeAddStringProperty(
properties,
"originalURI",
"original_URI",
details.tokenAddr,
abi.encodeWithSelector(IERC721Metadata.tokenURI.selector, details.tokenId)
);
} else if (details.tokenType == IClawbackFunctions.TokenType.ERC1155) {
properties = _safeAddStringProperty(
properties,
"originalURI",
"original_URI",
details.tokenAddr,
abi.encodeWithSelector(IERC1155MetadataURI.uri.selector, details.tokenId)
);
Expand Down Expand Up @@ -158,7 +161,7 @@
return MetadataProperty(key, abi.decode(prop, (string)));
}
// Unable to get property
revert();

Check warning on line 164 in src/tokens/wrappers/clawback/ClawbackMetadata.sol

View workflow job for this annotation

GitHub Actions / Solidity lint

Provide an error message for revert
}

function getUint256Property(string memory key, address tokenAddr, bytes calldata callData)
Expand All @@ -171,7 +174,7 @@
return MetadataProperty(key, abi.decode(prop, (uint256)).toString());
}
// Unable to get property
revert();

Check warning on line 177 in src/tokens/wrappers/clawback/ClawbackMetadata.sol

View workflow job for this annotation

GitHub Actions / Solidity lint

Provide an error message for revert
}

function _appendProperty(MetadataProperty[] memory properties, MetadataProperty memory prop)
Expand All @@ -194,4 +197,18 @@
}
return false;
}

function _formatUnlocksIn(uint256 lockedAt, uint256 duration) internal view returns (string memory) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intentionally chose not to add this as most platforms cache the metadata. Thought it would be out of date and confusing for non-native/tech people.
Happy to include if you disagree.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Middle ground would be to reduce the granularity to days and exclude hours etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is a big issue, we can update it later if it gives us trouble.

uint256 unlocksAt = lockedAt + duration;
if (block.timestamp >= unlocksAt) {
return "Unlocked";
}

uint256 remaining = unlocksAt - block.timestamp;
if (remaining >= 999999 days) {
return "Never";
}

return Duration.format(remaining);
}
}
39 changes: 39 additions & 0 deletions src/utils/Duration.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.19;

import {LibString} from "solady/utils/LibString.sol";


library Duration {
using LibString for *;

function format(uint256 totalSeconds) internal pure returns (string memory) {
uint256 d = totalSeconds / (24 * 60 * 60);
uint256 h = (totalSeconds % (24 * 60 * 60)) / (60 * 60);
uint256 m = (totalSeconds % (60 * 60)) / 60;
uint256 s = totalSeconds % 60;

string memory result;

if (d > 0) {
result = string(abi.encodePacked(d.toString(), " days"));
}
if (h > 0) {
result = bytes(result).length > 0
? string(abi.encodePacked(result, ", ", h.toString(), " hours"))
: string(abi.encodePacked(h.toString(), " hours"));
}
if (m > 0) {
result = bytes(result).length > 0
? string(abi.encodePacked(result, ", ", m.toString(), " minutes"))
: string(abi.encodePacked(m.toString(), " minutes"));
}
if (s > 0) {
result = bytes(result).length > 0
? string(abi.encodePacked(result, ", ", s.toString(), " seconds"))
: string(abi.encodePacked(s.toString(), " seconds"));
}

return result;
}
}
130 changes: 109 additions & 21 deletions test/tokens/wrappers/clawback/ClawbackMetadata.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {ClawbackTestBase, IClawbackFunctions, ClawbackMetadata} from "./Clawback
import {console, stdError} from "forge-std/Test.sol";

import {IMetadataProvider} from "src/tokens/common/IMetadataProvider.sol";
import {Duration} from "src/utils/Duration.sol";

import {IERC165} from "@0xsequence/erc-1155/contracts/interfaces/IERC165.sol";

Expand Down Expand Up @@ -51,13 +52,13 @@ contract ClawbackMetadataTest is ClawbackTestBase {
details.tokenAddr = address(erc20);

ClawbackMetadata.MetadataProperty[] memory properties = clawbackMetadata.metadataProperties(details, template);
assertEq(properties.length, 10);
assertEq(properties.length, 11);

_checkCommonProperties(properties, details, template, "ERC-20");

_hasProperty(properties, "originalName", erc20.name());
_hasProperty(properties, "originalSymbol", erc20.symbol());
_hasProperty(properties, "originalDecimals", erc20.decimals().toString());
_hasProperty(properties, "original_name", erc20.name());
_hasProperty(properties, "original_symbol", erc20.symbol());
_hasProperty(properties, "original_decimals", erc20.decimals().toString());
}

function testMetadataPropertiesERC721(DetailsParam memory detailsParam, IClawbackFunctions.Template memory template)
Expand All @@ -71,14 +72,14 @@ contract ClawbackMetadataTest is ClawbackTestBase {
erc721.mint(address(this), details.tokenId, 1);

ClawbackMetadata.MetadataProperty[] memory properties = clawbackMetadata.metadataProperties(details, template);
assertEq(properties.length, 11);
assertEq(properties.length, 12);

_checkCommonProperties(properties, details, template, "ERC-721");

_hasProperty(properties, "tokenId", details.tokenId.toString());
_hasProperty(properties, "originalName", erc721.name());
_hasProperty(properties, "originalSymbol", erc721.symbol());
_hasProperty(properties, "originalURI", erc721.tokenURI(details.tokenId));
_hasProperty(properties, "token_id", details.tokenId.toString());
_hasProperty(properties, "original_name", erc721.name());
_hasProperty(properties, "original_symbol", erc721.symbol());
_hasProperty(properties, "original_URI", erc721.tokenURI(details.tokenId));
}

function testMetadataPropertiesERC1155(
Expand All @@ -90,12 +91,84 @@ contract ClawbackMetadataTest is ClawbackTestBase {
details.tokenAddr = address(erc1155);

ClawbackMetadata.MetadataProperty[] memory properties = clawbackMetadata.metadataProperties(details, template);
assertEq(properties.length, 9);
assertEq(properties.length, 10);

_checkCommonProperties(properties, details, template, "ERC-1155");

_hasProperty(properties, "tokenId", details.tokenId.toString());
_hasProperty(properties, "originalURI", erc1155.uri(details.tokenId));
_hasProperty(properties, "token_id", details.tokenId.toString());
_hasProperty(properties, "original_URI", erc1155.uri(details.tokenId));
}

function testDurationAndUnlocksAt() public {
IClawbackFunctions.TokenDetails memory details;
IClawbackFunctions.Template memory template;

vm.warp(1688184000);

details.lockedAt = uint56(block.timestamp - 1 days);
template.duration = uint56(1 days);

ClawbackMetadata.MetadataProperty[] memory properties;

// Test when details.lockedAt is set to more than duration ago (unlocked)
details.lockedAt = uint56(block.timestamp - 200 days);
properties = clawbackMetadata.metadataProperties(details, template);
_hasProperty(properties, "unlocks_in", "Unlocked");
_hasProperty(properties, "duration", "1 days");

// Test when details.lockedAt is set to just now (locked for 1 day)
details.lockedAt = uint56(block.timestamp);
properties = clawbackMetadata.metadataProperties(details, template);
_hasProperty(properties, "unlocks_in", "1 days");
_hasProperty(properties, "duration", "1 days");

// Test when template.duration is set to a very high value (never unlocks)
template.duration = uint56(999999 days);
properties = clawbackMetadata.metadataProperties(details, template);
_hasProperty(properties, "unlocks_in", "Never");
_hasProperty(properties, "duration", "999999 days");

// Test when template.duration is set to a small value and almost unlocked
template.duration = uint56(12);
details.lockedAt = uint56(block.timestamp - 11);
properties = clawbackMetadata.metadataProperties(details, template);
_hasProperty(properties, "unlocks_in", "1 seconds");
_hasProperty(properties, "duration", "12 seconds");

// Test when template.duration is 0 (should be unlocked)
template.duration = uint56(0);
details.lockedAt = uint56(block.timestamp);
properties = clawbackMetadata.metadataProperties(details, template);
_hasProperty(properties, "unlocks_in", "Unlocked");
_hasProperty(properties, "duration", "");

// Test for multiple units (e.g., 1 day, 3 hours, and 30 minutes)
template.duration = uint56(1 days + 3 hours + 30 minutes);
details.lockedAt = uint56(block.timestamp - 30 minutes);
properties = clawbackMetadata.metadataProperties(details, template);
_hasProperty(properties, "unlocks_in", "1 days, 3 hours");
_hasProperty(properties, "duration", "1 days, 3 hours, 30 minutes");

// Test for a very short duration (e.g., 5 seconds)
template.duration = uint56(5 seconds + 1 minutes);
details.lockedAt = uint56(block.timestamp - 1 minutes);
properties = clawbackMetadata.metadataProperties(details, template);
_hasProperty(properties, "unlocks_in", "5 seconds");
_hasProperty(properties, "duration", "1 minutes, 5 seconds");

// Test when duration includes all units (days, hours, minutes, and seconds)
template.duration = uint56(2 days + 5 hours + 10 minutes + 15 seconds);
details.lockedAt = uint56(block.timestamp);
properties = clawbackMetadata.metadataProperties(details, template);
_hasProperty(properties, "unlocks_in", "2 days, 5 hours, 10 minutes, 15 seconds");
_hasProperty(properties, "duration", "2 days, 5 hours, 10 minutes, 15 seconds");

// Test for unlocking after a period less than a day (e.g., 10 hours)
template.duration = uint56(10 hours);
details.lockedAt = uint56(block.timestamp - 8 hours);
properties = clawbackMetadata.metadataProperties(details, template);
_hasProperty(properties, "unlocks_in", "2 hours");
_hasProperty(properties, "duration", "10 hours");
}

function _checkCommonProperties(
Expand All @@ -104,21 +177,36 @@ contract ClawbackMetadataTest is ClawbackTestBase {
IClawbackFunctions.Template memory template,
string memory tokenTypeStr
) internal {
_hasProperty(properties, "tokenType", tokenTypeStr);
_hasProperty(properties, "tokenAddress", details.tokenAddr.toHexStringChecksummed());
_hasProperty(properties, "templateId", details.templateId.toString());
_hasProperty(properties, "lockedAt", details.lockedAt.toString());
_hasProperty(properties, "destructionOnly", template.destructionOnly ? "true" : "false");
_hasProperty(properties, "transferOpen", template.transferOpen ? "true" : "false");
_hasProperty(properties, "duration", template.duration.toString());
_hasProperty(properties, "token_type", tokenTypeStr);
_hasProperty(properties, "token_address", details.tokenAddr.toHexStringChecksummed());
_hasProperty(properties, "template_id", details.templateId.toString());
_hasProperty(properties, "locked_at", details.lockedAt.toString());
_hasProperty(properties, "destruction_only", template.destructionOnly ? "true" : "false");
_hasProperty(properties, "transfer_open", template.transferOpen ? "true" : "false");
_hasProperty(properties, "duration", Duration.format(template.duration));
_hasProperty(properties, "unlocks_in", _formatUnlocksIn(details.lockedAt, template.duration));
}

function _formatUnlocksIn(uint256 lockedAt, uint256 duration) internal view returns (string memory) {
uint256 unlocksAt = lockedAt + duration;
if (block.timestamp >= unlocksAt) {
return "Unlocked";
}

uint256 remaining = unlocksAt - block.timestamp;
if (remaining >= 999999 days) {
return "Never";
}

return Duration.format(remaining);
}

function _hasProperty(ClawbackMetadata.MetadataProperty[] memory properties, string memory key, string memory value)
internal
{
bytes32 hasedKey = keccak256(abi.encodePacked(key));
bytes32 hashedKey = keccak256(abi.encodePacked(key));
for (uint256 i = 0; i < properties.length; i++) {
if (keccak256(abi.encodePacked(properties[i].key)) == hasedKey) {
if (keccak256(abi.encodePacked(properties[i].key)) == hashedKey) {
assertEq(properties[i].value, value, key);
return;
}
Expand Down
Loading