Skip to content

Commit

Permalink
Merge pull request #12 from 0xsequence/clawback-metadata
Browse files Browse the repository at this point in the history
Clawback metadata
  • Loading branch information
Agusx1211 authored Jun 3, 2024
2 parents 4642544 + 48d76fa commit 17e9c7d
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 37 deletions.
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 @@ pragma solidity ^0.8.19;
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 @@ contract ClawbackMetadata is IMetadataProvider, IERC165 {
// 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 @@ -194,4 +197,18 @@ contract ClawbackMetadata is IMetadataProvider, IERC165 {
}
return false;
}

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);
}
}
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

0 comments on commit 17e9c7d

Please sign in to comment.