diff --git a/src/erc-1155/ERC1155TL.sol b/src/erc-1155/ERC1155TL.sol index f24f028..8481647 100644 --- a/src/erc-1155/ERC1155TL.sol +++ b/src/erc-1155/ERC1155TL.sol @@ -1,11 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.22; -import { - ERC1155Upgradeable, - IERC1155, - IERC165 -} from "openzeppelin-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; +import {Strings} from "openzeppelin/utils/Strings.sol"; +import {ERC1155Upgradeable, IERC1155, IERC165} from "openzeppelin-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; import {EIP2981TLUpgradeable} from "tl-sol-tools/upgradeable/royalties/EIP2981TLUpgradeable.sol"; import {OwnableAccessControlUpgradeable} from "tl-sol-tools/upgradeable/access/OwnableAccessControlUpgradeable.sol"; import {IStory} from "src/interfaces/IStory.sol"; @@ -18,7 +15,21 @@ import {ITLNftDelegationRegistry} from "src/interfaces/ITLNftDelegationRegistry. /// @notice Transient Labs ERC-1155 Creator Contract /// @author transientlabs.xyz /// @custom:version 3.0.0 -contract ERC1155TL is ERC1155Upgradeable, EIP2981TLUpgradeable, OwnableAccessControlUpgradeable, ICreatorBase, IERC1155TL, IStory { +contract ERC1155TL is + ERC1155Upgradeable, + EIP2981TLUpgradeable, + OwnableAccessControlUpgradeable, + ICreatorBase, + IERC1155TL, + IStory +{ + /*////////////////////////////////////////////////////////////////////////// + Custom Types + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev String representation for address + using Strings for address; + /*////////////////////////////////////////////////////////////////////////// State Variables //////////////////////////////////////////////////////////////////////////*/ @@ -29,6 +40,9 @@ contract ERC1155TL is ERC1155Upgradeable, EIP2981TLUpgradeable, OwnableAccessCon uint256 private _counter; string public name; string public symbol; + bool public storyEnabled; + ITLNftDelegationRegistry public tlNftDelegationRegistry; + IBlockListRegistry public blocklistRegistry; mapping(uint256 => Token) private _tokens; /*////////////////////////////////////////////////////////////////////////// @@ -107,13 +121,12 @@ contract ERC1155TL is ERC1155Upgradeable, EIP2981TLUpgradeable, OwnableAccessCon General Functions //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc IERC1155TL + /// @inheritdoc ICreatorBase function totalSupply() external view returns (uint256) { return _counter; } - /// @notice Function to get token creation details - /// @param tokenId The token to lookup + /// @inheritdoc IERC1155TL function getTokenDetails(uint256 tokenId) external view returns (Token memory) { return _tokens[tokenId]; } @@ -122,7 +135,7 @@ contract ERC1155TL is ERC1155Upgradeable, EIP2981TLUpgradeable, OwnableAccessCon Access Control Functions //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc IERC1155TL + /// @inheritdoc ICreatorBase function setApprovedMintContracts(address[] calldata minters, bool status) external onlyRoleOrOwner(ADMIN_ROLE) { _setRole(APPROVED_MINT_CONTRACT, minters, status); } @@ -212,12 +225,12 @@ contract ERC1155TL is ERC1155Upgradeable, EIP2981TLUpgradeable, OwnableAccessCon Royalty Functions //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc IERC1155TL + /// @inheritdoc ICreatorBase function setDefaultRoyalty(address newRecipient, uint256 newPercentage) external onlyOwner { _setDefaultRoyaltyInfo(newRecipient, newPercentage); } - /// @inheritdoc IERC1155TL + /// @inheritdoc ICreatorBase function setTokenRoyalty(uint256 tokenId, address newRecipient, uint256 newPercentage) external onlyOwner { _overrideTokenRoyaltyInfo(tokenId, newRecipient, newPercentage); } @@ -244,19 +257,37 @@ contract ERC1155TL is ERC1155Upgradeable, EIP2981TLUpgradeable, OwnableAccessCon Story Inscriptions //////////////////////////////////////////////////////////////////////////*/ - + /// @inheritdoc IStory + function addCollectionStory(string calldata creatorName, string calldata story) + external + onlyRoleOrOwner(ADMIN_ROLE) + {} - /*////////////////////////////////////////////////////////////////////////// - BlockList Functions - //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc IStory + function addCreatorStory(uint256 tokenId, string calldata creatorName, string calldata story) + external + onlyRoleOrOwner(ADMIN_ROLE) + {} + /// @inheritdoc IStory + function addStory(uint256 tokenId, string calldata collectorName, string calldata story) external {} + /// @inheritdoc ICreatorBase + function setStoryStatus(bool status) external {} /*////////////////////////////////////////////////////////////////////////// - TL NFT Delegation Registry + BlockList //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ICreatorBase + function setBlockListRegistry(address newBlockListRegistry) external {} + + /*////////////////////////////////////////////////////////////////////////// + NFT Delegation Registry + //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ICreatorBase + function setNftDelegationRegistry(address newNftDelegationRegistry) external {} /*////////////////////////////////////////////////////////////////////////// ERC-165 Support @@ -271,7 +302,8 @@ contract ERC1155TL is ERC1155Upgradeable, EIP2981TLUpgradeable, OwnableAccessCon { return ( ERC1155Upgradeable.supportsInterface(interfaceId) || EIP2981TLUpgradeable.supportsInterface(interfaceId) - || interfaceId == type(IStory).interfaceId || interfaceId == 0x0d23ecb9 // previous story contract version that is still supported + || interfaceId == type(ICreatorBase).interfaceId || interfaceId == type(IStory).interfaceId + || interfaceId == 0x0d23ecb9 // previous story contract version that is still supported || interfaceId == type(IERC1155TL).interfaceId ); } diff --git a/src/erc-1155/IERC1155TL.sol b/src/erc-1155/IERC1155TL.sol index 14b314c..9404c6e 100644 --- a/src/erc-1155/IERC1155TL.sol +++ b/src/erc-1155/IERC1155TL.sol @@ -93,4 +93,10 @@ interface IERC1155TL { /// @param tokenIds Array of tokens to burn /// @param amounts Amount of each token to burn function burn(address from, uint256[] calldata tokenIds, uint256[] calldata amounts) external; + + /// @notice Function to set a token uri + /// @dev Requires owner or admin + /// @param tokenId The token to mint + /// @param newUri The new token uri + function setTokenUri(uint256 tokenId, string calldata newUri) external; } diff --git a/src/erc-721/ERC721TL.sol b/src/erc-721/ERC721TL.sol index 0c29e69..35c5f1c 100644 --- a/src/erc-721/ERC721TL.sol +++ b/src/erc-721/ERC721TL.sol @@ -1,12 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.22; -import {IERC2309} from "openzeppelin/interfaces/IERC2309.sol"; import {IERC4906} from "openzeppelin/interfaces/IERC4906.sol"; import {Strings} from "openzeppelin/utils/Strings.sol"; -import { - ERC721Upgradeable, IERC165 -} from "openzeppelin-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {ERC721Upgradeable, IERC165} from "openzeppelin-upgradeable/token/ERC721/ERC721Upgradeable.sol"; import {OwnableAccessControlUpgradeable} from "tl-sol-tools/upgradeable/access/OwnableAccessControlUpgradeable.sol"; import {EIP2981TLUpgradeable} from "tl-sol-tools/upgradeable/royalties/EIP2981TLUpgradeable.sol"; import {IERC721TL} from "src/erc-721/IERC721TL.sol"; @@ -28,7 +25,6 @@ contract ERC721TL is IERC721TL, ISynergy, IStory, - IERC2309, IERC4906 { /*////////////////////////////////////////////////////////////////////////// @@ -46,6 +42,9 @@ contract ERC721TL is /// @dev String representation of uint256 using Strings for uint256; + /// @dev String representation for address + using Strings for address; + /*////////////////////////////////////////////////////////////////////////// State Variables //////////////////////////////////////////////////////////////////////////*/ @@ -195,24 +194,6 @@ contract ERC721TL is } } - /// @inheritdoc IERC721TL - function batchMintUltra(address recipient, uint128 numTokens, string calldata baseUri) - external - onlyRoleOrOwner(ADMIN_ROLE) - { - if (recipient == address(0)) revert MintToZeroAddress(); - if (bytes(baseUri).length == 0) revert EmptyTokenURI(); - if (numTokens < 2) revert BatchSizeTooSmall(); - uint256 start = _counter + 1; - uint256 end = start + numTokens - 1; - _counter += numTokens; - _batchMints.push(BatchMint(recipient, start, end, baseUri)); - - _increaseBalance(recipient, numTokens); // this function adds the number of tokens to the recipient address - - emit ConsecutiveTransfer(start, end, address(0), recipient); - } - /// @inheritdoc IERC721TL function airdrop(address[] calldata addresses, string calldata baseUri) external onlyRoleOrOwner(ADMIN_ROLE) { if (bytes(baseUri).length == 0) revert EmptyTokenURI(); @@ -251,12 +232,12 @@ contract ERC721TL is Royalty Functions //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc IERC721TL + /// @inheritdoc ICreatorBase function setDefaultRoyalty(address newRecipient, uint256 newPercentage) external onlyRoleOrOwner(ADMIN_ROLE) { _setDefaultRoyaltyInfo(newRecipient, newPercentage); } - /// @inheritdoc IERC721TL + /// @inheritdoc ICreatorBase function setTokenRoyalty(uint256 tokenId, address newRecipient, uint256 newPercentage) external onlyRoleOrOwner(ADMIN_ROLE) @@ -268,7 +249,7 @@ contract ERC721TL is Synergy Functions //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc IERC721TL + /// @inheritdoc ISynergy function proposeNewTokenUri(uint256 tokenId, string calldata newUri) external onlyRoleOrOwner(ADMIN_ROLE) { if (!_exists(tokenId)) revert TokenDoesntExist(); if (bytes(newUri).length == 0) revert EmptyTokenURI(); @@ -283,7 +264,7 @@ contract ERC721TL is } } - /// @inheritdoc IERC721TL + /// @inheritdoc ISynergy function acceptTokenUriUpdate(uint256 tokenId) external { if (ownerOf(tokenId) != msg.sender) revert CallerNotTokenOwner(); string memory uri = _proposedTokenUris[tokenId]; @@ -294,9 +275,7 @@ contract ERC721TL is emit SynergyStatusChange(msg.sender, tokenId, SynergyAction.Accepted, uri); } - /// @notice function to reject a proposed token uri update for a specific token - /// @dev requires owner of the token to call the function - /// @param tokenId the token to reject the metadata update for + /// @inheritdoc ISynergy function rejectTokenUriUpdate(uint256 tokenId) external { if (ownerOf(tokenId) != msg.sender) revert CallerNotTokenOwner(); string memory uri = _proposedTokenUris[tokenId]; @@ -323,14 +302,38 @@ contract ERC721TL is Story Inscriptions //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc IStory + function addCollectionStory(string calldata creatorName, string calldata story) + external + onlyRoleOrOwner(ADMIN_ROLE) + {} + + /// @inheritdoc IStory + function addCreatorStory(uint256 tokenId, string calldata creatorName, string calldata story) + external + onlyRoleOrOwner(ADMIN_ROLE) + {} + + /// @inheritdoc IStory + function addStory(uint256 tokenId, string calldata collectorName, string calldata story) external {} + + /// @inheritdoc ICreatorBase + function setStoryStatus(bool status) external {} + /*////////////////////////////////////////////////////////////////////////// - BlockList + BlockList //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ICreatorBase + function setBlockListRegistry(address newBlockListRegistry) external {} + /*////////////////////////////////////////////////////////////////////////// - TL NFT Delegation Registry + NFT Delegation Registry //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ICreatorBase + function setNftDelegationRegistry(address newNftDelegationRegistry) external {} + /*////////////////////////////////////////////////////////////////////////// ERC-165 Support //////////////////////////////////////////////////////////////////////////*/ @@ -344,9 +347,10 @@ contract ERC721TL is { return ( ERC721Upgradeable.supportsInterface(interfaceId) || EIP2981TLUpgradeable.supportsInterface(interfaceId) - || interfaceId == type(IERC2309).interfaceId - || interfaceId == type(IERC4906).interfaceId - || interfaceId == type(IStory).interfaceId || interfaceId == 0x0d23ecb9 // previous story contract version that is still supported + || interfaceId == 0x49064906 // ERC-4906 + || interfaceId == type(ICreatorBase).interfaceId + || interfaceId == type(ISynergy).interfaceId || interfaceId == type(IStory).interfaceId + || interfaceId == 0x0d23ecb9 // previous story contract version that is still supported || interfaceId == type(IERC721TL).interfaceId ); } diff --git a/src/erc-721/IERC721TL.sol b/src/erc-721/IERC721TL.sol index 092a70e..d41cc7d 100644 --- a/src/erc-721/IERC721TL.sol +++ b/src/erc-721/IERC721TL.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.22; /// @title IERC721TL.sol /// @notice Interface for ERC721TL -/// @dev Interface id = +/// @dev Interface id = 0xc74089ae0 /// @author transientlabs.xyz /// @custom:version 3.0.0 interface IERC721TL { @@ -34,16 +34,6 @@ interface IERC721TL { /// @param baseUri The base uri for the batch, expecting json to be in order, starting at file name 0, and SHOULD NOT have a trailing `/` function batchMint(address recipient, uint128 numTokens, string calldata baseUri) external; - /// @notice Function to batch mint tokens with ultra gas savings using ERC-2309 - /// @dev Requires owner or admin - /// @dev Usage of ERC-2309 MAY NOT be supported on all platforms - /// @dev The `baseUri` folder should have the same number of json files in it as `numTokens` - /// @dev The `baseUri` folder should have files named without any file extension - /// @param recipient The recipient of the token - assumed as able to receive 721 tokens - /// @param numTokens Number of tokens in the batch mint - /// @param baseUri The base uri for the batch, expecting json to be in order, starting at file name 0, and SHOULD NOT have a trailing `/` - function batchMintUltra(address recipient, uint128 numTokens, string calldata baseUri) external; - /// @notice Function to airdrop tokens to addresses /// @dev Requires owner or admin /// @dev Utilizes batch mint token uri values to save some gas but still ultimately mints individual tokens to people diff --git a/src/erc-721/multi-metadata/ERC7160TL.sol b/src/erc-721/multi-metadata/ERC7160TL.sol index 1b61a54..79f6bce 100644 --- a/src/erc-721/multi-metadata/ERC7160TL.sol +++ b/src/erc-721/multi-metadata/ERC7160TL.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.22; -import {IERC2309} from "openzeppelin/interfaces/IERC2309.sol"; import {IERC4906} from "openzeppelin/interfaces/IERC4906.sol"; import {Strings} from "openzeppelin/utils/Strings.sol"; import {ERC721Upgradeable, IERC165} from "openzeppelin-upgradeable/token/ERC721/ERC721Upgradeable.sol"; @@ -12,7 +11,6 @@ import {IBlockListRegistry} from "src/interfaces/IBlockListRegistry.sol"; import {ICreatorBase} from "src/interfaces/ICreatorBase.sol"; import {IERC7160} from "src/interfaces/IERC7160.sol"; import {IStory} from "src/interfaces/IStory.sol"; -import {ISynergy} from "src/interfaces/ISynergy.sol"; import {ITLNftDelegationRegistry} from "src/interfaces/ITLNftDelegationRegistry.sol"; /// @title ERC7160TL.sol @@ -26,9 +24,7 @@ contract ERC7160TL is OwnableAccessControlUpgradeable, ICreatorBase, IERC721TL, - ISynergy, IStory, - IERC2309, IERC4906, IERC7160 { @@ -60,6 +56,9 @@ contract ERC7160TL is /// @dev String representation of uint256 using Strings for uint256; + /// @dev String representation for address + using Strings for address; + /*////////////////////////////////////////////////////////////////////////// State Variables //////////////////////////////////////////////////////////////////////////*/ @@ -213,24 +212,6 @@ contract ERC7160TL is } } - /// @inheritdoc IERC721TL - function batchMintUltra(address recipient, uint128 numTokens, string calldata baseUri) - external - onlyRoleOrOwner(ADMIN_ROLE) - { - if (recipient == address(0)) revert MintToZeroAddress(); - if (bytes(baseUri).length == 0) revert EmptyTokenURI(); - if (numTokens < 2) revert BatchSizeTooSmall(); - uint256 start = _counter + 1; - uint256 end = start + numTokens - 1; - _counter += numTokens; - _batchMints.push(BatchMint(recipient, start, end, baseUri)); - - _increaseBalance(recipient, numTokens); // this function adds the number of tokens to the recipient address - - emit ConsecutiveTransfer(start, end, address(0), recipient); - } - /// @inheritdoc IERC721TL function airdrop(address[] calldata addresses, string calldata baseUri) external onlyRoleOrOwner(ADMIN_ROLE) { if (bytes(baseUri).length == 0) revert EmptyTokenURI(); @@ -372,14 +353,38 @@ contract ERC7160TL is Story Inscriptions //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc IStory + function addCollectionStory(string calldata creatorName, string calldata story) + external + onlyRoleOrOwner(ADMIN_ROLE) + {} + + /// @inheritdoc IStory + function addCreatorStory(uint256 tokenId, string calldata creatorName, string calldata story) + external + onlyRoleOrOwner(ADMIN_ROLE) + {} + + /// @inheritdoc IStory + function addStory(uint256 tokenId, string calldata collectorName, string calldata story) external {} + + /// @inheritdoc ICreatorBase + function setStoryStatus(bool status) external {} + /*////////////////////////////////////////////////////////////////////////// - BlockList + BlockList //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ICreatorBase + function setBlockListRegistry(address newBlockListRegistry) external {} + /*////////////////////////////////////////////////////////////////////////// - TL NFT Delegation Registry + NFT Delegation Registry //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ICreatorBase + function setNftDelegationRegistry(address newNftDelegationRegistry) external {} + /*////////////////////////////////////////////////////////////////////////// ERC-165 Support //////////////////////////////////////////////////////////////////////////*/ @@ -393,10 +398,10 @@ contract ERC7160TL is { return ( ERC721Upgradeable.supportsInterface(interfaceId) || EIP2981TLUpgradeable.supportsInterface(interfaceId) - || interfaceId == type(IERC2309).interfaceId - || interfaceId == type(IERC4906).interfaceId + || interfaceId == 0x49064906 // ERC-4906 || interfaceId == type(IERC7160).interfaceId - || interfaceId == type(IStory).interfaceId || interfaceId == 0x0d23ecb9 // previous story contract version that is still supported + || interfaceId == type(ICreatorBase).interfaceId || interfaceId == type(IStory).interfaceId + || interfaceId == 0x0d23ecb9 // previous story contract version that is still supported || interfaceId == type(IERC721TL).interfaceId ); } diff --git a/src/erc-721/shatter/IShatter.sol b/src/erc-721/shatter/IShatter.sol index bf00c8a..e72c0af 100644 --- a/src/erc-721/shatter/IShatter.sol +++ b/src/erc-721/shatter/IShatter.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; /// @title IShatter.sol /// @notice interface defining the Shatter standard -/// @dev shatter turns a 1/1 into a multiple sub-pieces. +/// Interface id = 0x01c5bfc1 /// @author transientlabs.xyz /// @custom:version 2.4.0 interface IShatter { diff --git a/src/erc-721/shatter/Shatter.sol b/src/erc-721/shatter/Shatter.sol index 89fe523..6bb41aa 100644 --- a/src/erc-721/shatter/Shatter.sol +++ b/src/erc-721/shatter/Shatter.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.22; -import {IERC2309} from "openzeppelin/interfaces/IERC2309.sol"; import {IERC4906} from "openzeppelin/interfaces/IERC4906.sol"; import {Strings} from "openzeppelin/utils/Strings.sol"; import {ERC721Upgradeable, IERC165} from "openzeppelin-upgradeable/token/ERC721/ERC721Upgradeable.sol"; @@ -26,7 +25,6 @@ contract Shatter is ICreatorBase, ISynergy, IStory, - IERC2309, IERC4906 { /*////////////////////////////////////////////////////////////////////////// @@ -36,6 +34,9 @@ contract Shatter is /// @dev string representation of uint256 using Strings for uint256; + /// @dev String representation for address + using Strings for address; + /*////////////////////////////////////////////////////////////////////////// State Variables //////////////////////////////////////////////////////////////////////////*/ @@ -145,7 +146,7 @@ contract Shatter is //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ICreatorBase - function setApprovedMintContracts(address[] calldata /*minters*/, bool /*status*/) external { + function setApprovedMintContracts(address[] calldata, /*minters*/ bool /*status*/ ) external pure { revert("N/A"); } @@ -157,7 +158,7 @@ contract Shatter is /// @dev Requires contract owner or admin /// @dev Requires that shatters is equal to 0 -> meaning no piece has been minted /// @param recipient The address to mint to token to - /// @param uri The base uri to be used for the shatter folder WITHOUT trailing "/" + /// @param uri The base uri to be used for the shatter folder WITHOUT trailing "/" /// @param min The minimum number of shatters /// @param max The maximum number of shatters /// @param time Time after which shatter can occur @@ -194,30 +195,12 @@ contract Shatter is if (numShatters > 1) { _burn(0); - _batchMint(msg.sender, numShatters, false); - emit Shattered(msg.sender, numShatters, block.timestamp); - } else { - isFused = true; - emit Shattered(msg.sender, numShatters, block.timestamp); - emit Fused(msg.sender, block.timestamp); - } - // no reentrancy so can set these after burning and minting - // needs to be called here since _burn relies on ownership check - isShattered = true; - shatters = numShatters; - } - - /// @inheritdoc IShatter - /// @dev Uses ERC-2309. BEWARE - this may not be supported by all platforms. - function shatterUltra(uint128 numShatters) external { - if (isShattered) revert IsShattered(); - if (msg.sender != ownerOf(0)) revert CallerNotTokenOwner(); - if (numShatters < minShatters || numShatters > maxShatters) revert InvalidNumShatters(); - if (block.timestamp < shatterTime) revert CallPriorToShatterTime(); + _shatterAddress = msg.sender; + _increaseBalance(_shatterAddress, numShatters); - if (numShatters > 1) { - _burn(0); - _batchMint(msg.sender, numShatters, true); + for (uint256 id = 1; id < numShatters + 1; ++id) { + emit Transfer(address(0), msg.sender, id); + } emit Shattered(msg.sender, numShatters, block.timestamp); } else { isFused = true; @@ -333,10 +316,38 @@ contract Shatter is Story Inscriptions //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc IStory + function addCollectionStory(string calldata creatorName, string calldata story) + external + onlyRoleOrOwner(ADMIN_ROLE) + {} + + /// @inheritdoc IStory + function addCreatorStory(uint256 tokenId, string calldata creatorName, string calldata story) + external + onlyRoleOrOwner(ADMIN_ROLE) + {} + + /// @inheritdoc IStory + function addStory(uint256 tokenId, string calldata collectorName, string calldata story) external {} + + /// @inheritdoc ICreatorBase + function setStoryStatus(bool status) external {} + /*////////////////////////////////////////////////////////////////////////// - BlockList + BlockList //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ICreatorBase + function setBlockListRegistry(address newBlockListRegistry) external {} + + /*////////////////////////////////////////////////////////////////////////// + NFT Delegation Registry + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ICreatorBase + function setNftDelegationRegistry(address newNftDelegationRegistry) external {} + /*////////////////////////////////////////////////////////////////////////// ERC-165 Support //////////////////////////////////////////////////////////////////////////*/ @@ -350,9 +361,10 @@ contract Shatter is { return ( ERC721Upgradeable.supportsInterface(interfaceId) || EIP2981TLUpgradeable.supportsInterface(interfaceId) - || interfaceId == type(IERC2309).interfaceId - || interfaceId == type(IERC4906).interfaceId - || interfaceId == type(IStory).interfaceId || interfaceId == 0x0d23ecb9 // previous story contract version that is still supported + || interfaceId == 0x49064906 // ERC-4906 + || interfaceId == type(ICreatorBase).interfaceId + || interfaceId == type(ISynergy).interfaceId || interfaceId == type(IStory).interfaceId + || interfaceId == 0x0d23ecb9 // previous story contract version that is still supported || interfaceId == type(IShatter).interfaceId ); } @@ -361,25 +373,6 @@ contract Shatter is Internal Functions //////////////////////////////////////////////////////////////////////////*/ - /// @notice Function to batch mint upon shatter - /// @dev Only mints tokenIds 1 -> quantity to recipient - /// @dev Does not check if the recipient ifs the zero address or can receive ERC-721 tokens - /// @param recipient Address to receive the tokens - /// @param quantity Amount of tokens to batch mint - /// @param ultra Bool specifying to use ERC-2309 or the regular `Transfer` event - function _batchMint(address recipient, uint128 quantity, bool ultra) internal { - _shatterAddress = recipient; - _increaseBalance(_shatterAddress, quantity); - - if (ultra) { - emit ConsecutiveTransfer(1, quantity, address(0), recipient); - } else { - for (uint256 id = 1; id < quantity + 1; ++id) { - emit Transfer(address(0), recipient, id); - } - } - } - /// @inheritdoc ERC721Upgradeable /// @notice function to override { ERC721Upgradeable._ownerOf } to allow for batch minting/shatter function _ownerOf(uint256 tokenId) internal view virtual override returns (address) { diff --git a/src/erc-721/TRACE/ITRACE.sol b/src/erc-721/trace/ITRACE.sol similarity index 71% rename from src/erc-721/TRACE/ITRACE.sol rename to src/erc-721/trace/ITRACE.sol index 80ed7fa..65a208a 100644 --- a/src/erc-721/TRACE/ITRACE.sol +++ b/src/erc-721/trace/ITRACE.sol @@ -3,10 +3,16 @@ pragma solidity 0.8.22; /// @title ITRACE.sol /// @notice Interface for TRACE -/// @dev Interface id = +/// @dev Interface id = 0xcfec4f64 /// @author transientlabs.xyz /// @custom:version 3.0.0 interface ITRACE { + /*////////////////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////////////////*/ + + event TRACERSRegistryUpdated(address indexed sender, address indexed oldTracersRegistry, address indexed newTracersRegistry); + /*////////////////////////////////////////////////////////////////////////// Functions //////////////////////////////////////////////////////////////////////////*/ @@ -34,6 +40,12 @@ interface ITRACE { /// @param baseUri The base uri for the batch, expecting json to be in order, starting at file name 0, and SHOULD NOT have a trailing `/` function airdrop(address[] calldata addresses, string calldata baseUri) external; + /// @notice Function to allow an approved mint contract to mint + /// @dev Requires the caller to be an approved mint contract + /// @param recipient The recipient of the token - assumed as able to receive 721 tokens + /// @param uri The token uri to mint + function externalMint(address recipient, string calldata uri) external; + /// @notice Function to transfer token to another wallet /// @dev Callable only by owner or admin /// @dev Useful if a chip fails or an alteration damages a chip in some way @@ -47,22 +59,18 @@ interface ITRACE { /// @param newTracersRegistry The new TRACERS Registry function setTracersRegistry(address newTracersRegistry) external; - /// @notice Function to write a story for a token + /// @notice Function to write stories for tokens /// @dev Requires that the passed signature is signed by the token owner, which is the ARX Halo Chip (physical) /// @dev Uses EIP-712 for the signature - /// @param tokenId The token to add a story to - /// @param story The story text - /// @param signature The signtature from the chip to verify physical presence - function addVerifiedStory(uint256 tokenId, string calldata story, bytes calldata signature) external; - - /// @notice Function to return the nonce for a token - /// @param tokenId The token to query - /// @return uint256 The token nonce - function getTokenNonce(uint256 tokenId) external view returns (uint256); + /// @param tokenIds The tokens to add a stories to + /// @param stories The story text + /// @param signatures The signtatures from the chip to verify physical presence + function addVerifiedStory(uint256[] calldata tokenIds, string[] calldata stories, bytes[] calldata signatures) + external; /// @notice Function to update a token uri for a specific token /// @dev Requires owner or admin /// @param tokenId The token to propose new metadata for /// @param newUri The new token uri proposed - function updateTokenUri(uint256 tokenId, string calldata newUri) external; -} \ No newline at end of file + function setTokenUri(uint256 tokenId, string calldata newUri) external; +} diff --git a/src/erc-721/TRACE/TRACE.sol b/src/erc-721/trace/TRACE.sol similarity index 68% rename from src/erc-721/TRACE/TRACE.sol rename to src/erc-721/trace/TRACE.sol index 551e4fc..66bb0bb 100644 --- a/src/erc-721/TRACE/TRACE.sol +++ b/src/erc-721/trace/TRACE.sol @@ -1,23 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.22; -import {IERC2309} from "openzeppelin/interfaces/IERC2309.sol"; import {IERC4906} from "openzeppelin/interfaces/IERC4906.sol"; import {Strings} from "openzeppelin/utils/Strings.sol"; import {ECDSA} from "openzeppelin/utils/cryptography/ECDSA.sol"; -import { - ERC721Upgradeable, IERC165 -} from "openzeppelin-upgradeable/token/ERC721/ERC721Upgradeable.sol"; -import {OwnableAccessControlUpgradeable} from "tl-sol-tools/upgradeable/access/OwnableAccessControlUpgradeable.sol"; +import {ERC721Upgradeable, IERC165} from "openzeppelin-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {EIP712Upgradeable} from "openzeppelin-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; import {EIP2981TLUpgradeable} from "tl-sol-tools/upgradeable/royalties/EIP2981TLUpgradeable.sol"; -import {ITRACE} from "src/trace/ITRACE.sol"; +import {OwnableAccessControlUpgradeable} from "tl-sol-tools/upgradeable/access/OwnableAccessControlUpgradeable.sol"; +import {ITRACE} from "src/erc-721/trace/ITRACE.sol"; import {IBlockListRegistry} from "src/interfaces/IBlockListRegistry.sol"; import {ICreatorBase} from "src/interfaces/ICreatorBase.sol"; import {IStory} from "src/interfaces/IStory.sol"; import {ITLNftDelegationRegistry} from "src/interfaces/ITLNftDelegationRegistry.sol"; -import {EIP712Upgradeable} from "openzeppelin-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; -import {EIP2981TLUpgradeable} from "tl-sol-tools/upgradeable/royalties/EIP2981TLUpgradeable.sol"; -import {OwnableAccessControlUpgradeable} from "tl-sol-tools/upgradeable/access/OwnableAccessControlUpgradeable.sol"; import {ITRACERSRegistry} from "src/interfaces/ITRACERSRegistry.sol"; /// @title TRACE.sol @@ -32,32 +27,30 @@ contract TRACE is ICreatorBase, ITRACE, IStory, - IERC2309, IERC4906 { /*////////////////////////////////////////////////////////////////////////// Custom Types //////////////////////////////////////////////////////////////////////////*/ - /// @dev struct defining a batch mint - used for airdrops + /// @dev Struct defining a batch mint - used for airdrops struct BatchMint { uint256 fromTokenId; uint256 toTokenId; string baseUri; } - /// @dev struct for verified story & signed EIP-712 message + /// @dev Struct for verified story & signed EIP-712 message struct VerifiedStory { - uint256 nonce; + address nftContract; uint256 tokenId; - address sender; string story; } - /// @dev string representation of uint256 + /// @dev String representation of uint256 using Strings for uint256; - /// @dev string representation for address + /// @dev String representation for address using Strings for address; /*////////////////////////////////////////////////////////////////////////// @@ -66,47 +59,36 @@ contract TRACE is string public constant VERSION = "3.0.0"; bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + bytes32 public constant APPROVED_MINT_CONTRACT = keccak256("APPROVED_MINT_CONTRACT"); ITRACERSRegistry public tracersRegistry; uint256 private _counter; // token ids mapping(uint256 => string) private _tokenUris; // established token uris - mapping(uint256 => uint256) private _tokenNonces; // token nonces to prevent replay attacks + mapping(bytes32 => bool) private _verifiedStoryHashUsed; // prevent replay attacks BatchMint[] private _batchMints; // dynamic array for batch mints /*////////////////////////////////////////////////////////////////////////// Custom Errors //////////////////////////////////////////////////////////////////////////*/ - /// @dev token uri is an empty string + /// @dev Token uri is an empty string error EmptyTokenURI(); - /// @dev batch mint to zero address - error MintToZeroAddress(); - - /// @dev batch size too small - error BatchSizeTooSmall(); - - /// @dev airdrop to too few addresses + /// @dev Airdrop to too few addresses error AirdropTooFewAddresses(); - /// @dev token not owned by the owner of the contract - error TokenNotOwnedByOwner(); - - /// @dev caller is not the owner of the specific token - error CallerNotTokenOwner(); - - /// @dev caller is not approved or owner - error CallerNotApprovedOrOwner(); - - /// @dev token does not exist + /// @dev Token does not exist error TokenDoesntExist(); - /// @dev no proposed token uri to change to - error NoTokenUriUpdateAvailable(); + /// @dev Verified story already written for token + error VerifiedStoryAlreadyWritten(); + + /// @dev Array length mismatch + error ArrayLengthMismatch(); - /// @dev invalid signature + /// @dev Invalid signature error InvalidSignature(); - /// @dev unauthorized to add a verified story + /// @dev Unauthorized to add a verified story error Unauthorized(); /*////////////////////////////////////////////////////////////////////////// @@ -124,6 +106,7 @@ contract TRACE is /// @param name The name of the contract /// @param symbol The symbol of the contract + /// @param personalization A string to emit as a collection story. Can be ASCII art or something else that is a personalizaiton of the contract. /// @param defaultRoyaltyRecipient The default address for royalty payments /// @param defaultRoyaltyPercentage The default royalty percentage of basis points (out of 10,000) /// @param initOwner The owner of the contract @@ -132,6 +115,7 @@ contract TRACE is function initialize( string memory name, string memory symbol, + string memory personalization, address defaultRoyaltyRecipient, uint256 defaultRoyaltyPercentage, address initOwner, @@ -142,13 +126,18 @@ contract TRACE is __ERC721_init(name, symbol); __EIP2981TL_init(defaultRoyaltyRecipient, defaultRoyaltyPercentage); __OwnableAccessControl_init(initOwner); - __EIP712_init("T.R.A.C.E.", "1"); + __EIP712_init("T.R.A.C.E.", "3"); // add admins _setRole(ADMIN_ROLE, admins, true); // set TRACERS Registry tracersRegistry = ITRACERSRegistry(defaultTracersRegistry); + + // emit personalization as collection story + if (bytes(personalization).length > 0) { + emit CollectionStory(msg.sender, msg.sender.toHexString(), personalization); + } } /*////////////////////////////////////////////////////////////////////////// @@ -165,8 +154,8 @@ contract TRACE is //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ICreatorBase - function setApprovedMintContracts(address[] calldata /*minters*/, bool /*status*/) external { - revert("N/A"); + function setApprovedMintContracts(address[] calldata minters, bool status) external onlyRoleOrOwner(ADMIN_ROLE) { + _setRole(APPROVED_MINT_CONTRACT, minters, status); } /*////////////////////////////////////////////////////////////////////////// @@ -210,6 +199,14 @@ contract TRACE is } } + /// @inheritdoc ITRACE + function externalMint(address recipient, string calldata uri) external onlyRole(APPROVED_MINT_CONTRACT) { + if (bytes(uri).length == 0) revert EmptyTokenURI(); + _counter++; + _tokenUris[_counter] = uri; + _mint(recipient, _counter); + } + /*////////////////////////////////////////////////////////////////////////// T.R.A.C.E. Functions //////////////////////////////////////////////////////////////////////////*/ @@ -222,52 +219,27 @@ contract TRACE is /// @inheritdoc ITRACE function setTracersRegistry(address newTracersRegistry) external onlyRoleOrOwner(ADMIN_ROLE) { + address oldTracersRegistry = address(tracersRegistry); tracersRegistry = ITRACERSRegistry(newTracersRegistry); + emit TRACERSRegistryUpdated(msg.sender, oldTracersRegistry, newTracersRegistry); } /// @inheritdoc ITRACE - function addVerifiedStory(uint256 tokenId, string calldata story, bytes calldata signature) external { - // default name to hex address - string memory registeredAgentName = msg.sender.toHexString(); - - // only check registered agent if the registry is not the zero address - if (address(tracersRegistry) != address(0)) { - if (address(tracersRegistry).code.length == 0) revert Unauthorized(); - bool isRegisteredAgent; - (isRegisteredAgent, registeredAgentName) = tracersRegistry.isRegisteredAgent(msg.sender); - if (!isRegisteredAgent) revert Unauthorized(); - } - - // verify signature - address tokenOwner = ownerOf(tokenId); - bytes32 digest = _hashTypedDataV4(_hashVerifiedStory(tokenId, _tokenNonces[tokenId]++, msg.sender, story)); - if (tokenOwner != ECDSA.recover(digest, signature)) revert InvalidSignature(); - - // emit story - emit Story(tokenId, msg.sender, registeredAgentName, story); - } - - /// @inheritdoc ITRACE - function getTokenNonce(uint256 tokenId) external view returns (uint256) { - return _tokenNonces[tokenId]; - } - - /// @notice function to hash the typed data - function _hashVerifiedStory(uint256 tokenId, uint256 nonce, address sender, string memory story) - internal - pure - returns (bytes32) + function addVerifiedStory(uint256[] calldata tokenIds, string[] calldata stories, bytes[] calldata signatures) + external { - return keccak256( - abi.encode( - // keccak256("VerifiedStory(uint256 nonce,uint256 tokenId,address sender,string story)"), - 0x3ea278f3e0e25a71281e489b82695f448ae01ef3fc312598f1e61ac9956ab954, - nonce, - tokenId, - sender, - keccak256(bytes(story)) - ) - ); + if (tokenIds.length != stories.length && stories.length != signatures.length) { + revert ArrayLengthMismatch(); + } + for (uint256 i = 0; i < tokenIds.length; i++) { + // get variables + uint256 tokenId = tokenIds[i]; + string memory story = stories[i]; + bytes memory signature = signatures[i]; + + // add verified story + _addVerifiedStory(tokenId, story, signature); + } } /*////////////////////////////////////////////////////////////////////////// @@ -292,7 +264,7 @@ contract TRACE is //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ITRACE - function updateTokenUri(uint256 tokenId, string calldata newUri) external onlyRoleOrOwner(ADMIN_ROLE) { + function setTokenUri(uint256 tokenId, string calldata newUri) external onlyRoleOrOwner(ADMIN_ROLE) { if (!_exists(tokenId)) revert TokenDoesntExist(); if (bytes(newUri).length == 0) revert EmptyTokenURI(); _tokenUris[tokenId] = newUri; @@ -317,6 +289,68 @@ contract TRACE is Story Inscriptions //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc IStory + /// @dev ignores the creator name to avoid sybil + function addCollectionStory(string calldata, /*creatorName*/ string calldata story) + external + onlyRoleOrOwner(ADMIN_ROLE) + { + emit CollectionStory(msg.sender, msg.sender.toHexString(), story); + } + + /// @inheritdoc IStory + /// @dev ignores the creator name to avoid sybil + function addCreatorStory(uint256 tokenId, string calldata /*creatorName*/, string calldata story) + external + onlyRoleOrOwner(ADMIN_ROLE) + { + if (!_exists(tokenId)) revert TokenDoesntExist(); + emit CreatorStory(tokenId, msg.sender, msg.sender.toHexString(), story); + } + + /// @inheritdoc IStory + function addStory(uint256 tokenId, string calldata collectorName, string calldata story) external { + revert(); + } + + /// @inheritdoc ICreatorBase + function setStoryStatus(bool status) external { + revert(); + } + + /// @inheritdoc ICreatorBase + function storyEnabled() external view returns (bool) { + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + BlockList + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ICreatorBase + function setBlockListRegistry(address newBlockListRegistry) external { + revert(); + } + + /// @inheritdoc ICreatorBase + function blocklistRegistry() external view returns (IBlockListRegistry) { + return IBlockListRegistry(address(0)); + } + + /*////////////////////////////////////////////////////////////////////////// + NFT Delegation Registry + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ICreatorBase + function setNftDelegationRegistry(address newNftDelegationRegistry) external { + revert(); + } + + /// @inheritdoc ICreatorBase + function tlNftDelegationRegistry() external view returns (ITLNftDelegationRegistry) { + return ITLNftDelegationRegistry(address(0)); + } + /*////////////////////////////////////////////////////////////////////////// ERC-165 Support //////////////////////////////////////////////////////////////////////////*/ @@ -330,8 +364,8 @@ contract TRACE is { return ( ERC721Upgradeable.supportsInterface(interfaceId) || EIP2981TLUpgradeable.supportsInterface(interfaceId) - || interfaceId == type(IERC2309).interfaceId - || interfaceId == type(IERC4906).interfaceId + || interfaceId == 0x49064906 // ERC-4906 + || interfaceId == type(ICreatorBase).interfaceId || interfaceId == type(IStory).interfaceId || interfaceId == 0x0d23ecb9 // previous story contract version that is still supported || interfaceId == type(ITRACE).interfaceId ); @@ -364,4 +398,45 @@ contract TRACE is function _exists(uint256 tokenId) internal view returns (bool) { return _ownerOf(tokenId) != address(0); } + + /// @notice Function to add a verified story in a reusable way + function _addVerifiedStory(uint256 tokenId, string memory story, bytes memory signature) internal { + // default name to hex address + string memory registeredAgentName = msg.sender.toHexString(); + + // only check registered agent if the registry is not the zero address + if (address(tracersRegistry) != address(0)) { + bool isRegisteredAgent; + (isRegisteredAgent, registeredAgentName) = tracersRegistry.isRegisteredAgent(msg.sender); + if (!isRegisteredAgent) revert Unauthorized(); + } + + // verify signature + bytes32 verifiedStoryHash = _hashVerifiedStory(address(this), tokenId, story); + if (_verifiedStoryHashUsed[verifiedStoryHash]) revert VerifiedStoryAlreadyWritten(); + _verifiedStoryHashUsed[verifiedStoryHash] = true; + address tokenOwner = ownerOf(tokenId); + bytes32 digest = _hashTypedDataV4(verifiedStoryHash); + if (tokenOwner != ECDSA.recover(digest, signature)) revert InvalidSignature(); + + // emit story + emit Story(tokenId, msg.sender, registeredAgentName, story); + } + + /// @notice Function to hash the typed data + function _hashVerifiedStory(address nftContract, uint256 tokenId, string memory story) + internal + pure + returns (bytes32) + { + return keccak256( + abi.encode( + // keccak256("VerifiedStory(address nftContract,uint256 tokenId,string story)"), + 0x76b12200216600191228eb643bc7cba6e319d03951a863e3306595415759682b, + nftContract, + tokenId, + keccak256(bytes(story)) + ) + ); + } } diff --git a/src/interfaces/ICreatorBase.sol b/src/interfaces/ICreatorBase.sol index 3d154d5..b258e1f 100644 --- a/src/interfaces/ICreatorBase.sol +++ b/src/interfaces/ICreatorBase.sol @@ -1,22 +1,28 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.22; +import {IBlockListRegistry} from "src/interfaces/IBlockListRegistry.sol"; +import {ITLNftDelegationRegistry} from "src/interfaces/ITLNftDelegationRegistry.sol"; + /// @title ICreatorBase.sol /// @notice Base interface for creator contracts -/// @dev Interface id = +/// @dev Interface id = 0x1c8e024d /// @author transientlabs.xyz /// @custom:version 3.0.0 interface ICreatorBase { - /*////////////////////////////////////////////////////////////////////////// Events //////////////////////////////////////////////////////////////////////////*/ /// @dev Event for changing the BlockList registry - event BlockListRegistryUpdate(address indexed sender, address indexed prevBlockListRegistry, address indexed newBlockListRegistry); + event BlockListRegistryUpdate( + address indexed sender, address indexed prevBlockListRegistry, address indexed newBlockListRegistry + ); /// @dev Event for changing the NFT Delegation registry - event NftDelegationRegistryUpdate(address indexed sender, address indexed prevNftDelegationRegistry, address indexed newNftDelegationRegistry); + event NftDelegationRegistryUpdate( + address indexed sender, address indexed prevNftDelegationRegistry, address indexed newNftDelegationRegistry + ); /*////////////////////////////////////////////////////////////////////////// Functions @@ -36,11 +42,17 @@ interface ICreatorBase { /// @param newBlockListRegistry The new blocklist registry function setBlockListRegistry(address newBlockListRegistry) external; + /// @notice Function to get the blocklist registry + function blocklistRegistry() external view returns (IBlockListRegistry); + /// @notice Function to change the TL NFT delegation registry /// @dev Access to owner or admin /// @param newNftDelegationRegistry The new blocklist registry function setNftDelegationRegistry(address newNftDelegationRegistry) external; + /// @notice Function to get the delegation registry + function tlNftDelegationRegistry() external view returns (ITLNftDelegationRegistry); + /// @notice Function to set the default royalty specification /// @dev Requires owner or admin /// @param newRecipient The new royalty payout address @@ -58,4 +70,8 @@ interface ICreatorBase { /// @dev Requires owner or admin /// @param status The status to set for collector story inscriptions function setStoryStatus(bool status) external; + + /// @notice Function to get the status of collector stories + /// @return bool Status of collector stories being enabled + function storyEnabled() external view returns (bool); } diff --git a/src/interfaces/ISynergy.sol b/src/interfaces/ISynergy.sol index a5705e3..f57977a 100644 --- a/src/interfaces/ISynergy.sol +++ b/src/interfaces/ISynergy.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.22; /// @title ISynergy.sol /// @notice Interface for Synergy -/// @dev Interface id = +/// @dev Interface id = 0x8193ebea /// @author transientlabs.xyz /// @custom:version 3.0.0 interface ISynergy { diff --git a/test/TRACE/TRACE.t.sol b/test/TRACE/TRACE.t.sol deleted file mode 100644 index 76248f1..0000000 --- a/test/TRACE/TRACE.t.sol +++ /dev/null @@ -1,848 +0,0 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity 0.8.22; - -// import "forge-std/Test.sol"; -// import { -// TRACE, -// EmptyTokenURI, -// MintToZeroAddress, -// BatchSizeTooSmall, -// TokenDoesntExist, -// AirdropTooFewAddresses, -// CallerNotApprovedOrOwner, -// CallerNotTokenOwner -// } from "tl-creator-contracts/TRACE/TRACE.sol"; -// import {TRACERSRegistry} from "tl-creator-contracts/TRACE/TRACERSRegistry.sol"; -// import {NotRoleOrOwner} from "tl-sol-tools/upgradeable/access/OwnableAccessControlUpgradeable.sol"; -// import {TRACESigUtils} from "../utils/TRACESigUtils.sol"; -// import {Strings} from "openzeppelin/utils/Strings.sol"; - -// contract TRACETest is Test { -// using Strings for address; -// using Strings for uint256; - -// event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); -// event RoleChange(address indexed from, address indexed user, bool indexed approved, bytes32 role); -// event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); -// event MetadataUpdate(uint256 tokenId); -// event CreatorStory(uint256 indexed tokenId, address indexed creatorAddress, string creatorName, string story); -// event Story(uint256 indexed tokenId, address indexed senderAddress, string senderName, string story); - -// error InvalidSignature(); -// error Unauthorized(); -// error NotCreatorOrAdmin(); - -// TRACE public trace; -// address public royaltyRecipient = makeAddr("royaltyRecipient"); -// address public creatorAdmin = makeAddr("admin"); -// uint256 public chipPrivateKey = 0x007; -// address public chip; -// address public agent = makeAddr("agent"); - -// TRACESigUtils public sigUtils; - -// TRACERSRegistry public registry; - -// function setUp() public { -// // create registry -// registry = new TRACERSRegistry(); -// TRACERSRegistry.RegisteredAgent memory ra = TRACERSRegistry.RegisteredAgent(true, 0, "agent"); -// registry.setRegisteredAgent(agent, ra); - -// // chip -// chip = vm.addr(chipPrivateKey); - -// // create TRACE -// address[] memory admins = new address[](0); -// trace = new TRACE(false); -// trace.initialize("Test TRACE", "TRACE", royaltyRecipient, 1000, address(this), admins, address(registry)); - -// // sig utils -// sigUtils = new TRACESigUtils("1", address(trace)); -// } - -// /// @notice Initialization Tests -// function testInitialization( -// string memory name, -// string memory symbol, -// address defaultRoyaltyRecipient, -// uint256 defaultRoyaltyPercentage, -// address initOwner, -// address[] memory admins, -// address tracersRegistry -// ) public { -// // ensure royalty guards enabled -// vm.assume(defaultRoyaltyRecipient != address(0)); -// if (defaultRoyaltyPercentage >= 10_000) { -// defaultRoyaltyPercentage = defaultRoyaltyPercentage % 10_000; -// } - -// // create contract -// trace = new TRACE(false); -// // initialize and verify events thrown (order matters) -// vm.expectEmit(true, true, false, false); -// emit OwnershipTransferred(address(0), address(this)); -// vm.expectEmit(true, true, false, false); -// emit OwnershipTransferred(address(this), initOwner); -// for (uint256 i = 0; i < admins.length; i++) { -// vm.expectEmit(true, true, true, true); -// emit RoleChange(address(this), admins[i], true, trace.ADMIN_ROLE()); -// } -// trace.initialize( -// name, symbol, defaultRoyaltyRecipient, defaultRoyaltyPercentage, initOwner, admins, tracersRegistry -// ); -// assertEq(trace.name(), name); -// assertEq(trace.symbol(), symbol); -// (address recp, uint256 amt) = trace.royaltyInfo(1, 10000); -// assertEq(recp, defaultRoyaltyRecipient); -// assertEq(amt, defaultRoyaltyPercentage); -// assertEq(trace.owner(), initOwner); -// for (uint256 i = 0; i < admins.length; i++) { -// assertTrue(trace.hasRole(trace.ADMIN_ROLE(), admins[i])); -// } -// assertEq(trace.storyEnabled(), true); - -// // can't initialize again -// vm.expectRevert(bytes("Initializable: contract is already initialized")); -// trace.initialize( -// name, symbol, defaultRoyaltyRecipient, defaultRoyaltyPercentage, initOwner, admins, tracersRegistry -// ); -// } - -// /// @notice test non-existent token ownership -// function testNonExistentTokens(uint8 mintNum, uint8 numTokens) public { -// for (uint256 i = 0; i < mintNum; i++) { -// trace.mint(address(this), "uri"); -// } -// uint256 nonexistentTokenId = uint256(mintNum) + uint256(numTokens) + 1; -// vm.expectRevert(abi.encodePacked("ERC721: invalid token ID")); -// trace.ownerOf(nonexistentTokenId); -// vm.expectRevert(TokenDoesntExist.selector); -// trace.tokenURI(nonexistentTokenId); -// } - -// /// @notice test mint -// // - access control ✅ -// // - proper recipient ✅ -// // - transfer event ✅ -// // - proper token id ✅ -// // - ownership ✅ -// // - balance ✅ -// // - transfer to another address ✅ -// // - safe transfer to another address ✅ -// // - token uri ✅ - -// function testMintCustomErrors() public { -// vm.expectRevert(EmptyTokenURI.selector); -// trace.mint(address(this), ""); -// } - -// function testMintAccessControl(address user) public { -// vm.assume(user != address(this)); -// vm.assume(user != address(0)); -// // ensure user can't call the mint function -// vm.startPrank(user, user); -// vm.expectRevert(abi.encodeWithSelector(NotRoleOrOwner.selector, trace.ADMIN_ROLE())); -// trace.mint(address(this), "uriOne"); -// vm.stopPrank(); - -// // grant admin access and ensure that the user can call the mint function -// address[] memory admins = new address[](1); -// admins[0] = user; -// trace.setRole(trace.ADMIN_ROLE(), admins, true); -// vm.startPrank(user, user); -// vm.expectEmit(true, true, true, false); -// emit Transfer(address(0), address(this), 1); -// trace.mint(address(this), "uriOne"); -// vm.stopPrank(); -// assertEq(trace.balanceOf(address(this)), 1); -// assertEq(trace.ownerOf(1), address(this)); -// assertEq(trace.tokenURI(1), "uriOne"); - -// // revoke admin access and ensure that the user can't call the mint function -// trace.setRole(trace.ADMIN_ROLE(), admins, false); -// vm.startPrank(user, user); -// vm.expectRevert(abi.encodeWithSelector(NotRoleOrOwner.selector, trace.ADMIN_ROLE())); -// trace.mint(address(this), "uriOne"); -// vm.stopPrank(); -// } - -// function testMint(uint16 tokenId, address recipient) public { -// vm.assume(tokenId != 0); -// vm.assume(recipient != address(0)); -// if (tokenId > 1000) { -// tokenId = tokenId % 1000 + 1; // map to 1000 -// } -// for (uint256 i = 1; i <= tokenId; i++) { -// string memory uri = string(abi.encodePacked("uri_", i.toString())); -// vm.expectEmit(true, true, true, false); -// emit Transfer(address(0), recipient, i); -// vm.expectEmit(true, true, false, false); -// emit CreatorStory(i, address(this), "", "{\n\"trace\": {\"type\": \"trace_authentication\"}\n}"); -// trace.mint(recipient, uri); -// assertEq(trace.balanceOf(recipient), i); -// assertEq(trace.ownerOf(i), recipient); -// assertEq(trace.tokenURI(i), uri); -// } -// } - -// function testMintTokenRoyalty(uint16 tokenId, address recipient, address royaltyAddress, uint16 royaltyPercent) -// public -// { -// vm.assume(tokenId != 0); -// vm.assume(recipient != address(0)); -// vm.assume(royaltyAddress != royaltyRecipient); -// vm.assume(royaltyAddress != address(0)); -// if (royaltyPercent >= 10_000) royaltyPercent = royaltyPercent % 10_000; -// if (tokenId > 1000) { -// tokenId = tokenId % 1000 + 1; // map to 1000 -// } -// for (uint256 i = 1; i <= tokenId; i++) { -// string memory uri = string(abi.encodePacked("uri_", i.toString())); -// vm.expectEmit(true, true, true, false); -// emit Transfer(address(0), recipient, i); -// vm.expectEmit(true, true, false, false); -// emit CreatorStory(i, address(this), "", "{\n\"trace\": {\"type\": \"trace_authentication\"}\n}"); -// trace.mint(recipient, uri, royaltyAddress, royaltyPercent); -// assertEq(trace.balanceOf(recipient), i); -// assertEq(trace.ownerOf(i), recipient); -// assertEq(trace.tokenURI(i), uri); -// (address recp, uint256 amt) = trace.royaltyInfo(i, 10_000); -// assertEq(recp, royaltyAddress); -// assertEq(amt, royaltyPercent); -// } -// } - -// function testMintTransfers(uint16 tokenId, address recipient, address secondRecipient) public { -// vm.assume(recipient != address(0)); -// vm.assume(secondRecipient != address(0)); -// vm.assume(recipient != secondRecipient); -// vm.assume(recipient.code.length == 0); -// vm.assume(secondRecipient.code.length == 0); -// vm.assume(tokenId != 0); -// if (tokenId > 1000) { -// tokenId = tokenId % 1000 + 1; // map to 1000 -// } -// for (uint256 i = 1; i <= tokenId; i++) { -// // mint -// trace.mint(address(this), "uri"); -// // transfer to recipient with transferFrom -// vm.expectEmit(true, true, true, false); -// emit Transfer(address(this), recipient, i); -// trace.transferFrom(address(this), recipient, i); -// assertEq(trace.balanceOf(recipient), 1); -// assertEq(trace.ownerOf(i), recipient); -// // transfer to second recipient with safeTransferFrom -// vm.startPrank(recipient, recipient); -// vm.expectEmit(true, true, true, false); -// emit Transfer(recipient, secondRecipient, i); -// trace.safeTransferFrom(recipient, secondRecipient, i); -// assertEq(trace.balanceOf(secondRecipient), i); -// assertEq(trace.ownerOf(i), secondRecipient); -// vm.stopPrank(); -// } -// } - -// /// @notice test airdrop -// // - access control ✅ -// // - proper recipients ✅ -// // - transfer events ✅ -// // - proper token ids ✅ -// // - ownership ✅ -// // - balances ✅ -// // - transfer to another address ✅ -// // - safe transfer to another address ✅ -// // - token uris ✅ - -// function testAirdropCustomErrors() public { -// address[] memory addresses = new address[](1); -// addresses[0] = address(1); -// vm.expectRevert(EmptyTokenURI.selector); -// trace.airdrop(addresses, ""); - -// vm.expectRevert(AirdropTooFewAddresses.selector); -// trace.airdrop(addresses, "baseUri"); -// } - -// function testAirdropAccessControl(address user) public { -// vm.assume(user != address(this)); -// vm.assume(user != address(0)); -// address[] memory addresses = new address[](2); -// addresses[0] = address(1); -// addresses[1] = address(2); -// // ensure user can't call the airdrop function -// vm.startPrank(user, user); -// vm.expectRevert(abi.encodeWithSelector(NotRoleOrOwner.selector, trace.ADMIN_ROLE())); -// trace.airdrop(addresses, "baseUri"); -// vm.stopPrank(); - -// // grant admin access and ensure that the user can call the airdrop function -// address[] memory admins = new address[](1); -// admins[0] = user; -// trace.setRole(trace.ADMIN_ROLE(), admins, true); -// vm.startPrank(user, user); -// vm.expectEmit(true, true, true, false); -// emit Transfer(address(0), address(1), 1); -// vm.expectEmit(true, true, true, false); -// emit Transfer(address(0), address(2), 2); -// trace.airdrop(addresses, "baseUri"); -// vm.stopPrank(); -// assertEq(trace.balanceOf(address(1)), 1); -// assertEq(trace.balanceOf(address(2)), 1); -// assertEq(trace.ownerOf(1), address(1)); -// assertEq(trace.ownerOf(2), address(2)); -// assertEq(trace.tokenURI(1), "baseUri/0"); -// assertEq(trace.tokenURI(2), "baseUri/1"); - -// // revoke admin access and ensure that the user can't call the airdrop function -// trace.setRole(trace.ADMIN_ROLE(), admins, false); -// vm.startPrank(user, user); -// vm.expectRevert(abi.encodeWithSelector(NotRoleOrOwner.selector, trace.ADMIN_ROLE())); -// trace.airdrop(addresses, "baseUri"); -// vm.stopPrank(); -// } - -// function testAirdrop(uint16 numAddresses) public { -// vm.assume(numAddresses > 1); -// if (numAddresses > 1000) { -// numAddresses = numAddresses % 999 + 2; // map to 300 -// } -// address[] memory addresses = new address[](numAddresses); -// for (uint256 i = 0; i < numAddresses; i++) { -// addresses[i] = makeAddr(i.toString()); -// } -// for (uint256 i = 1; i <= numAddresses; i++) { -// vm.expectEmit(true, true, true, false); -// emit Transfer(address(0), addresses[i - 1], i); -// vm.expectEmit(true, true, false, false); -// emit CreatorStory(i, address(this), "", "{\n\"trace\": {\"type\": \"trace_authentication\"}\n}"); -// } -// trace.airdrop(addresses, "baseUri"); -// for (uint256 i = 1; i <= numAddresses; i++) { -// string memory uri = string(abi.encodePacked("baseUri/", (i - 1).toString())); -// assertEq(trace.balanceOf(addresses[i - 1]), 1); -// assertEq(trace.ownerOf(i), addresses[i - 1]); -// assertEq(trace.tokenURI(i), uri); -// } - -// // test mint after metadata -// trace.mint(address(this), "newUri"); -// assertEq(trace.ownerOf(numAddresses + 1), address(this)); -// assertEq(trace.tokenURI(numAddresses + 1), "newUri"); -// } - -// function testAirdropTransfers(uint16 numAddresses, address recipient) public { -// vm.assume(numAddresses > 1); -// vm.assume(recipient != address(0)); -// vm.assume(recipient.code.length == 0); -// if (numAddresses > 1000) { -// numAddresses = numAddresses % 999 + 2; // map to 300 -// } -// address[] memory addresses = new address[](numAddresses); -// for (uint256 i = 0; i < numAddresses; i++) { -// if (makeAddr(i.toString()) == recipient) { -// addresses[i] = makeAddr("hello"); -// } else { -// addresses[i] = makeAddr(i.toString()); -// } -// } -// trace.airdrop(addresses, "baseUri"); -// for (uint256 i = 1; i < numAddresses / 2; i++) { -// vm.startPrank(addresses[i - 1], addresses[i - 1]); -// vm.expectEmit(true, true, true, false); -// emit Transfer(addresses[i - 1], recipient, i); -// trace.transferFrom(addresses[i - 1], recipient, i); -// vm.stopPrank(); -// assertEq(trace.balanceOf(recipient), i); -// assertEq(trace.balanceOf(addresses[i - 1]), 0); -// assertEq(trace.ownerOf(i), recipient); -// } -// for (uint256 i = numAddresses / 2; i <= numAddresses; i++) { -// vm.startPrank(addresses[i - 1], addresses[i - 1]); -// vm.expectEmit(true, true, true, false); -// emit Transfer(addresses[i - 1], recipient, i); -// trace.safeTransferFrom(addresses[i - 1], recipient, i); -// vm.stopPrank(); -// assertEq(trace.balanceOf(recipient), i); -// assertEq(trace.balanceOf(addresses[i - 1]), 0); -// assertEq(trace.ownerOf(i), recipient); -// } -// } - -// /// @notice test TRACE functions -// // - access control ✅ -// // - transfer tokens -// // - set tracers registry ✅ -// // - add verified story - -// function testTransferToken(address user, uint256 numAddresses) public { -// vm.assume(user != address(this) && user != address(0)); -// address[] memory users = new address[](1); -// users[0] = user; - -// vm.assume(numAddresses > 1); -// if (numAddresses > 1000) { -// numAddresses = numAddresses % 999 + 2; // map to 300 -// } -// address[] memory addresses = new address[](numAddresses); -// for (uint256 i = 0; i < numAddresses; i++) { -// addresses[i] = makeAddr(i.toString()); -// } - -// // regular mint -// trace.mint(address(1), "hiii"); -// assert(trace.ownerOf(1) == address(1)); - -// // airdrop -// trace.airdrop(addresses, "baseUri"); -// for (uint256 i = 2; i <= numAddresses + 1; i++) { -// assertEq(trace.ownerOf(i), addresses[i - 2]); -// } - -// // transfer tokens -// vm.expectEmit(true, true, true, false); -// emit Transfer(address(1), address(this), 1); -// vm.expectEmit(true, true, false, false); -// emit CreatorStory(1, address(this), "", "{\n\"trace\": {\"type\": \"trace_authentication\"}\n}"); -// trace.transferToken(address(1), address(this), 1); -// assert(trace.ownerOf(1) == address(this)); - -// for (uint256 i = 2; i <= numAddresses + 1; i++) { -// vm.expectEmit(true, true, true, false); -// emit Transfer(addresses[i - 2], address(this), i); -// vm.expectEmit(true, true, false, false); -// emit CreatorStory(i, address(this), "", "{\n\"trace\": {\"type\": \"trace_authentication\"}\n}"); -// trace.transferToken(addresses[i - 2], address(this), i); -// assert(trace.ownerOf(i) == address(this)); -// } -// } - -// function testSetTracersRegistry(address user, address newRegistryOne, address newRegistryTwo) public { -// vm.assume(user != address(this)); -// address[] memory users = new address[](1); -// users[0] = user; - -// // expect revert from hacker -// vm.expectRevert(); -// vm.prank(user); -// trace.setTracersRegistry(newRegistryOne); - -// // expect creator pass -// trace.setTracersRegistry(newRegistryOne); -// assert(address(trace.tracersRegistry()) == newRegistryOne); - -// // set admin -// trace.setRole(trace.ADMIN_ROLE(), users, true); - -// // expect admin pass -// vm.prank(user); -// trace.setTracersRegistry(newRegistryTwo); -// assert(address(trace.tracersRegistry()) == newRegistryTwo); -// } - -// function testAddVerifiedStoryRegularMint(uint256 badSignerPrivateKey, address notAgent) public { -// vm.assume( -// badSignerPrivateKey != chipPrivateKey && badSignerPrivateKey != 0 -// && badSignerPrivateKey < 115792089237316195423570985008687907852837564279074904382605163141518161494337 -// ); -// vm.assume(notAgent != agent); - -// uint256 nonce = trace.getTokenNonce(1); - -// // mint -// trace.mint(chip, "https://arweave.net/tx_id"); - -// // sender not registered agent fails -// bytes32 digest = sigUtils.getTypedDataHash(1, nonce, notAgent, "This is a story!"); -// (uint8 v, bytes32 r, bytes32 s) = vm.sign(chipPrivateKey, digest); -// bytes memory sig = abi.encodePacked(r, s, v); -// vm.expectRevert(Unauthorized.selector); -// vm.prank(notAgent); -// trace.addVerifiedStory(1, "This is a story!", sig); - -// // registry is an EOA fails -// trace.setTracersRegistry(address(0xC0FFEE)); - -// vm.expectRevert(Unauthorized.selector); -// vm.prank(notAgent); -// trace.addVerifiedStory(1, "This is a story!", sig); - -// trace.setTracersRegistry(address(registry)); - -// // bad signer fails -// digest = sigUtils.getTypedDataHash(1, nonce, agent, "This is a story!"); -// (v, r, s) = vm.sign(badSignerPrivateKey, digest); -// sig = abi.encodePacked(r, s, v); -// vm.expectRevert(InvalidSignature.selector); -// vm.prank(agent); -// trace.addVerifiedStory(1, "This is a story!", sig); - -// // non-existent token fails -// digest = sigUtils.getTypedDataHash(2, nonce, agent, "This is a story!"); -// (v, r, s) = vm.sign(chipPrivateKey, digest); -// sig = abi.encodePacked(r, s, v); -// vm.expectRevert(InvalidSignature.selector); -// vm.prank(agent); -// trace.addVerifiedStory(1, "This is a story!", sig); - -// // right signer + registered agent passes -// digest = sigUtils.getTypedDataHash(1, nonce, agent, "This is a story!"); -// (v, r, s) = vm.sign(chipPrivateKey, digest); -// sig = abi.encodePacked(r, s, v); -// vm.expectEmit(true, true, false, true); -// emit Story(1, agent, "agent", "This is a story!"); -// vm.prank(agent); -// trace.addVerifiedStory(1, "This is a story!", sig); -// assert(trace.getTokenNonce(1) == nonce + 1); - -// // fails with replay -// vm.expectRevert(InvalidSignature.selector); -// vm.prank(agent); -// trace.addVerifiedStory(1, "This is a story!", sig); -// } - -// function testAddVerifiedStoryAirdrop(uint256 badSignerPrivateKey, address notAgent) public { -// vm.assume( -// badSignerPrivateKey != chipPrivateKey && badSignerPrivateKey != 0 -// && badSignerPrivateKey < 115792089237316195423570985008687907852837564279074904382605163141518161494337 -// ); -// vm.assume(notAgent != agent); - -// uint256 nonce = trace.getTokenNonce(1); - -// // airdrop -// address[] memory addresses = new address[](2); -// addresses[0] = chip; -// addresses[1] = chip; -// trace.airdrop(addresses, "baseUri"); - -// // sender not registered agent fails -// bytes32 digest = sigUtils.getTypedDataHash(1, nonce, notAgent, "This is a story!"); -// (uint8 v, bytes32 r, bytes32 s) = vm.sign(chipPrivateKey, digest); -// bytes memory sig = abi.encodePacked(r, s, v); -// vm.expectRevert(Unauthorized.selector); -// vm.prank(notAgent); -// trace.addVerifiedStory(1, "This is a story!", sig); - -// // registry is an EOA fails -// trace.setTracersRegistry(address(0xC0FFEE)); - -// vm.expectRevert(Unauthorized.selector); -// vm.prank(notAgent); -// trace.addVerifiedStory(1, "This is a story!", sig); - -// trace.setTracersRegistry(address(registry)); - -// // bad signer fails -// digest = sigUtils.getTypedDataHash(1, nonce, agent, "This is a story!"); -// (v, r, s) = vm.sign(badSignerPrivateKey, digest); -// sig = abi.encodePacked(r, s, v); -// vm.expectRevert(InvalidSignature.selector); -// vm.prank(agent); -// trace.addVerifiedStory(1, "This is a story!", sig); - -// // non-existent token fails -// digest = sigUtils.getTypedDataHash(3, nonce, agent, "This is a story!"); -// (v, r, s) = vm.sign(chipPrivateKey, digest); -// sig = abi.encodePacked(r, s, v); -// vm.expectRevert(InvalidSignature.selector); -// vm.prank(agent); -// trace.addVerifiedStory(1, "This is a story!", sig); - -// // right signer + registered agent passes -// digest = sigUtils.getTypedDataHash(1, nonce, agent, "This is a story!"); -// (v, r, s) = vm.sign(chipPrivateKey, digest); -// sig = abi.encodePacked(r, s, v); -// vm.expectEmit(true, true, false, true); -// emit Story(1, agent, "agent", "This is a story!"); -// vm.prank(agent); -// trace.addVerifiedStory(1, "This is a story!", sig); -// assert(trace.getTokenNonce(1) == nonce + 1); - -// // fails with replay -// vm.expectRevert(InvalidSignature.selector); -// vm.prank(agent); -// trace.addVerifiedStory(1, "This is a story!", sig); -// } - -// function testAddVerifiedStoryDiffNames(string memory name) public { -// // trace -// address[] memory admins = new address[](1); -// admins[0] = creatorAdmin; -// trace = new TRACE(false); -// trace.initialize(name, "COA", royaltyRecipient, 1000, address(this), admins, address(registry)); - -// // mint token -// trace.mint(chip, "https://arweave.net/tx_id"); - -// // set TRACE registry -// trace.setTracersRegistry(address(registry)); - -// // sig utils -// sigUtils = new TRACESigUtils("1", address(trace)); - -// // get nonce -// uint256 nonce = trace.getTokenNonce(1); - -// // add story -// bytes32 digest = sigUtils.getTypedDataHash(1, nonce, agent, "This is a story!"); -// (uint8 v, bytes32 r, bytes32 s) = vm.sign(chipPrivateKey, digest); -// bytes memory sig = abi.encodePacked(r, s, v); -// vm.expectEmit(true, true, false, true); -// emit Story(1, agent, "agent", "This is a story!"); -// vm.prank(agent); -// trace.addVerifiedStory(1, "This is a story!", sig); -// assert(trace.getTokenNonce(1) == nonce + 1); -// } - -// function testAddVerifiedStoryNoRegisteredAgents(address sender) public { -// // change registry to zero address -// trace.setTracersRegistry(address(0)); - -// // mint -// trace.mint(chip, "https://arweave.net/tx_id"); - -// // test add story -// uint256 nonce = trace.getTokenNonce(1); -// bytes32 digest = sigUtils.getTypedDataHash(1, nonce, sender, "This is a story!"); -// (uint8 v, bytes32 r, bytes32 s) = vm.sign(chipPrivateKey, digest); -// bytes memory sig = abi.encodePacked(r, s, v); -// vm.expectEmit(true, true, false, true); -// emit Story(1, sender, sender.toHexString(), "This is a story!"); -// vm.prank(sender); -// trace.addVerifiedStory(1, "This is a story!", sig); -// assert(trace.getTokenNonce(1) == nonce + 1); - -// // test replay protection -// vm.expectRevert(InvalidSignature.selector); -// vm.prank(sender); -// trace.addVerifiedStory(1, "This is a story!", sig); -// } - -// /// @notice test royalty functions -// // - access control ✅ - -// function testRoyaltyAccessControl(address user) public { -// vm.assume(user != address(this) && user != address(0)); -// address[] memory users = new address[](1); -// users[0] = user; - -// trace.mint(user, "https://arweave.net/tx_id"); - -// // verify user can't set default royalty -// vm.prank(user); -// vm.expectRevert(); -// trace.setDefaultRoyalty(user, 1000); - -// // verify user can't set token royalty -// vm.prank(user); -// vm.expectRevert(); -// trace.setTokenRoyalty(1, user, 1000); - -// // verify owner can set default royalty -// trace.setDefaultRoyalty(address(1), 100); -// (address recp, uint256 amt) = trace.royaltyInfo(1, 10000); -// assert(recp == address(1)); -// assert(amt == 100); - -// // set admins -// trace.setRole(trace.ADMIN_ROLE(), users, true); - -// // verify admin can set default royalty -// vm.prank(user); -// trace.setDefaultRoyalty(address(2), 1000); -// (recp, amt) = trace.royaltyInfo(1, 10000); -// assert(recp == address(2)); -// assert(amt == 1000); - -// // verify owner can set token royalty -// trace.setTokenRoyalty(1, address(1), 100); -// (recp, amt) = trace.royaltyInfo(1, 10000); -// assert(recp == address(1)); -// assert(amt == 100); - -// // verify admin can set token royalty -// vm.prank(user); -// trace.setTokenRoyalty(1, address(2), 1000); -// (recp, amt) = trace.royaltyInfo(1, 10000); -// assert(recp == address(2)); -// assert(amt == 1000); -// } - -// /// @notice test metadata update function -// // - custom errors -// // - access control -// // - regular mint -// // - airdrop - -// function testMetadataUpdateCustomErrors() public { -// // token doesn't exist -// vm.expectRevert(TokenDoesntExist.selector); -// trace.updateTokenUri(1, "hiiii"); - -// // empty uri -// trace.mint(address(1), "hiii"); -// vm.expectRevert(EmptyTokenURI.selector); -// trace.updateTokenUri(1, ""); -// } - -// function testMetadataUpdateAccessControl(address user) public { -// vm.assume(user != address(this) && user != address(0)); -// address[] memory users = new address[](1); -// users[0] = user; - -// trace.mint(user, "https://arweave.net/tx_id"); - -// // verify user can't update metadata -// vm.prank(user); -// vm.expectRevert(); -// trace.updateTokenUri(1, "ipfs://cid"); - -// // verify owner can update metadata -// vm.expectEmit(false, false, false, true); -// emit MetadataUpdate(1); -// trace.updateTokenUri(1, "ipfs://cid"); -// assertEq(trace.tokenURI(1), "ipfs://cid"); - -// // set admins -// trace.setRole(trace.ADMIN_ROLE(), users, true); - -// // verify admin can update metadata -// vm.expectEmit(false, false, false, true); -// emit MetadataUpdate(1); -// vm.prank(user); -// trace.updateTokenUri(1, "https://arweave.net/tx_id"); -// assertEq(trace.tokenURI(1), "https://arweave.net/tx_id"); -// } - -// function testMetadataUpdateAirdrop(uint256 numAddresses) public { -// vm.assume(numAddresses > 1); -// if (numAddresses > 1000) { -// numAddresses = numAddresses % 999 + 2; // map to 300 -// } - -// address[] memory addresses = new address[](numAddresses); -// for (uint256 i = 0; i < numAddresses; i++) { -// addresses[i] = makeAddr(i.toString()); -// } - -// trace.airdrop(addresses, "baseUri"); - -// for (uint256 i = 1; i <= numAddresses; i++) { -// string memory uri = string(abi.encodePacked("baseUri/", (i - 1).toString())); -// assertEq(trace.tokenURI(i), uri); -// } - -// for (uint256 i = 1; i <= numAddresses; i++) { -// string memory uri = string(abi.encodePacked("ipfs://", (i - 1).toString())); -// trace.updateTokenUri(i, uri); -// assertEq(trace.tokenURI(i), uri); -// } -// } - -// /// @notice test story functions -// // - enable/disable story access control ✅ -// // - regular mint ✅ -// // - airdrop ✅ -// // - write creator story to existing token w/ proper acccess ✅ -// // - write collector story to existing token w/ proper access ✅ -// // - write creator story to non-existent token (reverts) ✅ -// // - write collector story to non-existent token (reverts) ✅ - -// function testStoryAccessControl(address user) public { -// vm.assume(user != address(this)); -// address[] memory users = new address[](1); -// users[0] = user; - -// // verify user can't enable/disable -// vm.startPrank(user, user); -// vm.expectRevert(); -// trace.setStoryEnabled(false); -// vm.stopPrank(); - -// // verify admin can enable/disable -// trace.setRole(trace.ADMIN_ROLE(), users, true); -// vm.startPrank(user, user); -// trace.setStoryEnabled(false); -// vm.stopPrank(); -// trace.setRole(trace.ADMIN_ROLE(), users, false); - -// // verify owner can enable/disable -// trace.setStoryEnabled(false); -// assertFalse(trace.storyEnabled()); -// trace.setStoryEnabled(true); -// assertTrue(trace.storyEnabled()); -// } - -// function testStoryNonExistentTokens() public { -// vm.expectRevert(); -// trace.addCreatorStory(1, "XCOPY", "I AM XCOPY"); -// vm.expectRevert(); -// trace.addStory(1, "NOT XCOPY", "I AM NOT XCOPY"); -// } - -// function testStoryWithMint(address collector) public { -// vm.assume(collector != address(0)); -// vm.assume(collector != address(this)); -// trace.mint(collector, "uri"); - -// // test creator story -// vm.expectEmit(true, true, true, true); -// emit CreatorStory(1, address(this), "XCOPY", "I AM XCOPY"); -// trace.addCreatorStory(1, "XCOPY", "I AM XCOPY"); - -// // test collector can't add creator story -// vm.startPrank(collector, collector); -// vm.expectRevert(); -// trace.addCreatorStory(1, "XCOPY", "I AM XCOPY"); - -// // test collector story -// vm.expectEmit(true, true, true, true); -// emit Story(1, collector, "NOT XCOPY", "I AM NOT XCOPY"); -// trace.addStory(1, "NOT XCOPY", "I AM NOT XCOPY"); -// vm.stopPrank(); - -// // test that owner can't add collector story -// vm.expectRevert(); -// trace.addStory(1, "NOT XCOPY", "I AM NOT XCOPY"); -// } - -// function testStoryWithAirdrop(address collector) public { -// vm.assume(collector != address(0)); -// vm.assume(collector != address(this)); -// address[] memory addresses = new address[](2); -// addresses[0] = collector; -// addresses[1] = collector; -// trace.airdrop(addresses, "uri"); - -// // test creator story -// vm.expectEmit(true, true, true, true); -// emit CreatorStory(1, address(this), "XCOPY", "I AM XCOPY"); -// trace.addCreatorStory(1, "XCOPY", "I AM XCOPY"); -// vm.expectEmit(true, true, true, true); -// emit CreatorStory(2, address(this), "XCOPY", "I AM XCOPY"); -// trace.addCreatorStory(2, "XCOPY", "I AM XCOPY"); - -// // test collector can't add creator story -// vm.startPrank(collector, collector); -// vm.expectRevert(); -// trace.addCreatorStory(1, "XCOPY", "I AM XCOPY"); -// vm.expectRevert(); -// trace.addCreatorStory(2, "XCOPY", "I AM XCOPY"); - -// // test collector story -// vm.expectEmit(true, true, true, true); -// emit Story(1, collector, "NOT XCOPY", "I AM NOT XCOPY"); -// trace.addStory(1, "NOT XCOPY", "I AM NOT XCOPY"); -// vm.expectEmit(true, true, true, true); -// emit Story(2, collector, "NOT XCOPY", "I AM NOT XCOPY"); -// trace.addStory(2, "NOT XCOPY", "I AM NOT XCOPY"); -// vm.stopPrank(); - -// // test that owner can't add collector story -// vm.expectRevert(); -// trace.addStory(1, "NOT XCOPY", "I AM NOT XCOPY"); -// vm.expectRevert(); -// trace.addStory(2, "NOT XCOPY", "I AM NOT XCOPY"); -// } -// } diff --git a/test/core/ERC1155TL.t.sol b/test/erc-721/ERC1155TL.t.sol similarity index 100% rename from test/core/ERC1155TL.t.sol rename to test/erc-721/ERC1155TL.t.sol diff --git a/test/core/ERC721TL.t.sol b/test/erc-721/ERC721TL.t.sol similarity index 100% rename from test/core/ERC721TL.t.sol rename to test/erc-721/ERC721TL.t.sol diff --git a/test/erc-721/trace/TRACE.t.sol b/test/erc-721/trace/TRACE.t.sol new file mode 100644 index 0000000..5b5ad1c --- /dev/null +++ b/test/erc-721/trace/TRACE.t.sol @@ -0,0 +1,1274 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.22; + +import "forge-std/Test.sol"; +import {TRACE} from "src/erc-721/trace/TRACE.sol"; +import {ITRACERSRegistry} from "src/interfaces/ITRACERSRegistry.sol"; +import {IERC721Errors} from "openzeppelin/interfaces/draft-IERC6093.sol"; +import {Initializable} from "openzeppelin/proxy/utils/Initializable.sol"; +import {OwnableAccessControlUpgradeable} from "tl-sol-tools/upgradeable/access/OwnableAccessControlUpgradeable.sol"; +import {TRACESigUtils} from "test/utils/TRACESigUtils.sol"; +import {Strings} from "openzeppelin/utils/Strings.sol"; + +contract TRACETest is Test { + using Strings for address; + using Strings for uint256; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event RoleChange(address indexed from, address indexed user, bool indexed approved, bytes32 role); + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event MetadataUpdate(uint256 tokenId); + event CollectionStory(address indexed creatorAddress, string creatorName, string story); + event CreatorStory(uint256 indexed tokenId, address indexed creatorAddress, string creatorName, string story); + event Story(uint256 indexed tokenId, address indexed senderAddress, string senderName, string story); + event TRACERSRegistryUpdated(address indexed sender, address indexed oldTracersRegistry, address indexed newTracersRegistry); + + TRACE public trace; + address public royaltyRecipient = makeAddr("royaltyRecipient"); + address public creatorAdmin = makeAddr("admin"); + uint256 public chipPrivateKey = 0x007; + address public chip; + address public agent = makeAddr("agent"); + address tracersRegistry = makeAddr("tracersRegistry"); + + TRACESigUtils public sigUtils; + + function setUp() public { + // chip + chip = vm.addr(chipPrivateKey); + + // create TRACE + address[] memory admins = new address[](0); + trace = new TRACE(false); + trace.initialize("Test TRACE", "TRACE", "", royaltyRecipient, 1000, address(this), admins, tracersRegistry); + + // sig utils + sigUtils = new TRACESigUtils("3", address(trace)); + } + + /// @notice initialization Tests + function test_initialize( + string memory name, + string memory symbol, + string memory personalization, + address defaultRoyaltyRecipient, + uint256 defaultRoyaltyPercentage, + address initOwner, + address[] memory admins, + address tracersRegistry_ + ) public { + // ensure royalty guards enabled + vm.assume(defaultRoyaltyRecipient != address(0)); + if (defaultRoyaltyPercentage >= 10_000) { + defaultRoyaltyPercentage = defaultRoyaltyPercentage % 10_000; + } + + vm.assume(initOwner != address(0)); + + // create contract + trace = new TRACE(false); + + // initialize and verify events thrown (order matters) + vm.expectEmit(true, true, true, true); + emit OwnershipTransferred(address(0), initOwner); + for (uint256 i = 0; i < admins.length; i++) { + vm.expectEmit(true, true, true, true); + emit RoleChange(address(this), admins[i], true, trace.ADMIN_ROLE()); + } + if (bytes(personalization).length > 0) { + vm.expectEmit(true, true, true, true); + emit CollectionStory(address(this), address(this).toHexString(), personalization); + } + trace.initialize( + name, symbol, personalization, defaultRoyaltyRecipient, defaultRoyaltyPercentage, initOwner, admins, tracersRegistry_ + ); + + // assert intial values + assertEq(trace.name(), name); + assertEq(trace.symbol(), symbol); + (address recp, uint256 amt) = trace.royaltyInfo(1, 10000); + assertEq(recp, defaultRoyaltyRecipient); + assertEq(amt, defaultRoyaltyPercentage); + assertEq(trace.owner(), initOwner); + for (uint256 i = 0; i < admins.length; i++) { + assertTrue(trace.hasRole(trace.ADMIN_ROLE(), admins[i])); + } + assertEq(trace.storyEnabled(), true); + assertEq(address(trace.blocklistRegistry()), address(0)); + assertEq(address(trace.tlNftDelegationRegistry()), address(0)); + assertEq(address(trace.tracersRegistry()), tracersRegistry_); + + // can't initialize again + vm.expectRevert(Initializable.InvalidInitialization.selector); + trace.initialize( + name, symbol, personalization, defaultRoyaltyRecipient, defaultRoyaltyPercentage, initOwner, admins, tracersRegistry + ); + } + + /// @notice test ERC-165 support + function test_supportsInterface() public { + assertTrue(trace.supportsInterface(0x1c8e024d)); // ICreatorBase + assertTrue(trace.supportsInterface(0xcfec4f64)); // ITRACE + assertTrue(trace.supportsInterface(0x2464f17b)); // IStory + assertTrue(trace.supportsInterface(0x0d23ecb9)); // IStory (old) + assertTrue(trace.supportsInterface(0x01ffc9a7)); // ERC-165 + assertTrue(trace.supportsInterface(0x80ac58cd)); // ERC-721 + assertTrue(trace.supportsInterface(0x2a55205a)); // ERC-2981 + assertTrue(trace.supportsInterface(0x49064906)); // ERC-4906 + } + + /// @notice test mint + // - access control ✅ + // - proper recipient ✅ + // - transfer event ✅ + // - proper token id ✅ + // - ownership ✅ + // - balance ✅ + // - transfer to another address ✅ + // - safe transfer to another address ✅ + // - token uri ✅ + + function test_mint_customErrors() public { + vm.expectRevert(TRACE.EmptyTokenURI.selector); + trace.mint(address(this), ""); + + vm.expectRevert(TRACE.EmptyTokenURI.selector); + trace.mint(address(this), "", address(1), 10); + } + + function test_mint_accessControl(address user) public { + // limit fuzz input + vm.assume(user != address(this)); + vm.assume(user != address(0)); + + // ensure user can't call the mint function + vm.startPrank(user, user); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + trace.mint(address(this), "uriOne"); + vm.stopPrank(); + + // grant admin access and ensure that the user can call the mint function + address[] memory admins = new address[](1); + admins[0] = user; + trace.setRole(trace.ADMIN_ROLE(), admins, true); + vm.startPrank(user, user); + vm.expectEmit(true, true, true, false); + emit Transfer(address(0), address(this), 1); + trace.mint(address(this), "uriOne"); + vm.stopPrank(); + assertEq(trace.balanceOf(address(this)), 1); + assertEq(trace.ownerOf(1), address(this)); + assertEq(trace.tokenURI(1), "uriOne"); + + // revoke admin access and ensure that the user can't call the mint function + trace.setRole(trace.ADMIN_ROLE(), admins, false); + vm.startPrank(user, user); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + trace.mint(address(this), "uriOne"); + vm.stopPrank(); + } + + function test_mint(uint16 tokenId, address recipient) public { + // limit fuzz input + vm.assume(tokenId != 0); + vm.assume(recipient != address(0)); + if (tokenId > 1000) { + tokenId = tokenId % 1000 + 1; // map to 1000 + } + + // mint token and check ownership + for (uint256 i = 1; i <= tokenId; i++) { + string memory uri = string(abi.encodePacked("uri_", i.toString())); + vm.expectEmit(true, true, true, false); + emit Transfer(address(0), recipient, i); + vm.expectEmit(true, true, false, false); + emit CreatorStory(i, address(this), "", "{\n\"trace\": {\"type\": \"trace_authentication\"}\n}"); + trace.mint(recipient, uri); + assertEq(trace.balanceOf(recipient), i); + assertEq(trace.ownerOf(i), recipient); + assertEq(trace.tokenURI(i), uri); + } + + // ensure ownership throws for non-existent token + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, tokenId + 1)); + trace.ownerOf(tokenId + 1); + vm.expectRevert(TRACE.TokenDoesntExist.selector); + trace.tokenURI(tokenId + 1); + } + + function test_mint_withTokenRoyalty(uint16 tokenId, address recipient, address royaltyAddress, uint16 royaltyPercent) + public + { + // limit fuzz input + vm.assume(tokenId != 0); + vm.assume(recipient != address(0)); + vm.assume(royaltyAddress != royaltyRecipient); + vm.assume(royaltyAddress != address(0)); + if (royaltyPercent >= 10_000) royaltyPercent = royaltyPercent % 10_000; + if (tokenId > 1000) { + tokenId = tokenId % 1000 + 1; // map to 1000 + } + + // mint token and check ownership + for (uint256 i = 1; i <= tokenId; i++) { + string memory uri = string(abi.encodePacked("uri_", i.toString())); + vm.expectEmit(true, true, true, false); + emit Transfer(address(0), recipient, i); + vm.expectEmit(true, true, false, false); + emit CreatorStory(i, address(this), "", "{\n\"trace\": {\"type\": \"trace_authentication\"}\n}"); + trace.mint(recipient, uri, royaltyAddress, royaltyPercent); + assertEq(trace.balanceOf(recipient), i); + assertEq(trace.ownerOf(i), recipient); + assertEq(trace.tokenURI(i), uri); + (address recp, uint256 amt) = trace.royaltyInfo(i, 10_000); + assertEq(recp, royaltyAddress); + assertEq(amt, royaltyPercent); + } + + // ensure ownership throws for non-existent token + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, tokenId + 1)); + trace.ownerOf(tokenId + 1); + vm.expectRevert(TRACE.TokenDoesntExist.selector); + trace.tokenURI(tokenId + 1); + } + + function test_mint_thenTransfer(uint16 tokenId, address recipient, address secondRecipient) public { + vm.assume(recipient != address(0)); + vm.assume(secondRecipient != address(0)); + vm.assume(recipient != secondRecipient); + vm.assume(recipient.code.length == 0); + vm.assume(secondRecipient.code.length == 0); + vm.assume(tokenId != 0); + if (tokenId > 1000) { + tokenId = tokenId % 1000 + 1; // map to 1000 + } + for (uint256 i = 1; i <= tokenId; i++) { + // mint + trace.mint(address(this), "uri"); + // transfer to recipient with transferFrom + vm.expectEmit(true, true, true, false); + emit Transfer(address(this), recipient, i); + trace.transferFrom(address(this), recipient, i); + assertEq(trace.balanceOf(recipient), 1); + assertEq(trace.ownerOf(i), recipient); + // transfer to second recipient with safeTransferFrom + vm.startPrank(recipient, recipient); + vm.expectEmit(true, true, true, false); + emit Transfer(recipient, secondRecipient, i); + trace.safeTransferFrom(recipient, secondRecipient, i); + assertEq(trace.balanceOf(secondRecipient), i); + assertEq(trace.ownerOf(i), secondRecipient); + vm.stopPrank(); + } + } + + /// @notice test airdrop + // - access control ✅ + // - proper recipients ✅ + // - transfer events ✅ + // - proper token ids ✅ + // - ownership ✅ + // - balances ✅ + // - transfer to another address ✅ + // - safe transfer to another address ✅ + // - token uris ✅ + + function test_airdrop_customErrors() public { + address[] memory addresses = new address[](1); + addresses[0] = address(1); + vm.expectRevert(TRACE.EmptyTokenURI.selector); + trace.airdrop(addresses, ""); + + vm.expectRevert(TRACE.AirdropTooFewAddresses.selector); + trace.airdrop(addresses, "baseUri"); + } + + function test_airdrop_accessControl(address user) public { + vm.assume(user != address(this)); + vm.assume(user != address(0)); + address[] memory addresses = new address[](2); + addresses[0] = address(1); + addresses[1] = address(2); + // ensure user can't call the airdrop function + vm.startPrank(user, user); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + trace.airdrop(addresses, "baseUri"); + vm.stopPrank(); + + // grant admin access and ensure that the user can call the airdrop function + address[] memory admins = new address[](1); + admins[0] = user; + trace.setRole(trace.ADMIN_ROLE(), admins, true); + vm.startPrank(user, user); + vm.expectEmit(true, true, true, false); + emit Transfer(address(0), address(1), 1); + vm.expectEmit(true, true, true, false); + emit Transfer(address(0), address(2), 2); + trace.airdrop(addresses, "baseUri"); + vm.stopPrank(); + assertEq(trace.balanceOf(address(1)), 1); + assertEq(trace.balanceOf(address(2)), 1); + assertEq(trace.ownerOf(1), address(1)); + assertEq(trace.ownerOf(2), address(2)); + assertEq(trace.tokenURI(1), "baseUri/0"); + assertEq(trace.tokenURI(2), "baseUri/1"); + + // revoke admin access and ensure that the user can't call the airdrop function + trace.setRole(trace.ADMIN_ROLE(), admins, false); + vm.startPrank(user, user); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + trace.airdrop(addresses, "baseUri"); + vm.stopPrank(); + } + + function test_airdrop(uint16 numAddresses) public { + // limit fuzz + vm.assume(numAddresses > 1); + if (numAddresses > 1000) { + numAddresses = numAddresses % 999 + 2; // map to 300 + } + + // create addresses + address[] memory addresses = new address[](numAddresses); + for (uint256 i = 0; i < numAddresses; i++) { + addresses[i] = makeAddr(i.toString()); + } + + // verify airdrop + for (uint256 i = 1; i <= numAddresses; i++) { + vm.expectEmit(true, true, true, false); + emit Transfer(address(0), addresses[i - 1], i); + vm.expectEmit(true, true, false, false); + emit CreatorStory(i, address(this), "", "{\n\"trace\": {\"type\": \"trace_authentication\"}\n}"); + } + trace.airdrop(addresses, "baseUri"); + for (uint256 i = 1; i <= numAddresses; i++) { + string memory uri = string(abi.encodePacked("baseUri/", (i - 1).toString())); + assertEq(trace.balanceOf(addresses[i - 1]), 1); + assertEq(trace.ownerOf(i), addresses[i - 1]); + assertEq(trace.tokenURI(i), uri); + } + + // ensure ownership throws for non-existent token + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, numAddresses + 1)); + trace.ownerOf(numAddresses + 1); + vm.expectRevert(TRACE.TokenDoesntExist.selector); + trace.tokenURI(numAddresses + 1); + + // test mint after metadata + trace.mint(address(this), "newUri"); + assertEq(trace.ownerOf(numAddresses + 1), address(this)); + assertEq(trace.tokenURI(numAddresses + 1), "newUri"); + } + + function test_airdrop_thenTransfer(uint16 numAddresses, address recipient) public { + // limit fuzz + vm.assume(numAddresses > 1); + vm.assume(recipient != address(0)); + vm.assume(recipient.code.length == 0); + if (numAddresses > 1000) { + numAddresses = numAddresses % 999 + 2; // map to 300 + } + + // create addresses + address[] memory addresses = new address[](numAddresses); + for (uint256 i = 0; i < numAddresses; i++) { + if (makeAddr(i.toString()) == recipient) { + addresses[i] = makeAddr("hello"); + } else { + addresses[i] = makeAddr(i.toString()); + } + } + + // airdrop + trace.airdrop(addresses, "baseUri"); + for (uint256 i = 1; i < numAddresses / 2; i++) { + vm.startPrank(addresses[i - 1], addresses[i - 1]); + vm.expectEmit(true, true, true, false); + emit Transfer(addresses[i - 1], recipient, i); + trace.transferFrom(addresses[i - 1], recipient, i); + vm.stopPrank(); + assertEq(trace.balanceOf(recipient), i); + assertEq(trace.balanceOf(addresses[i - 1]), 0); + assertEq(trace.ownerOf(i), recipient); + } + + // transfer + for (uint256 i = numAddresses / 2; i <= numAddresses; i++) { + vm.startPrank(addresses[i - 1], addresses[i - 1]); + vm.expectEmit(true, true, true, false); + emit Transfer(addresses[i - 1], recipient, i); + trace.safeTransferFrom(addresses[i - 1], recipient, i); + vm.stopPrank(); + assertEq(trace.balanceOf(recipient), i); + assertEq(trace.balanceOf(addresses[i - 1]), 0); + assertEq(trace.ownerOf(i), recipient); + } + } + + /// @notice test externalMint + // - access control ✅ + // - proper recipient ✅ + // - transfer event ✅ + // - proper token id ✅ + // - ownership ✅ + // - balance ✅ + // - transfer to another address ✅ + // - safe transfer to another address ✅ + // - token uris ✅ + + function test_externalMint_customErrors() public { + address[] memory minters = new address[](1); + minters[0] = address(this); + trace.setRole(trace.APPROVED_MINT_CONTRACT(), minters, true); + vm.expectRevert(TRACE.EmptyTokenURI.selector); + trace.externalMint(address(this), ""); + } + + function test_externalMint_accessControl(address user) public { + // limit fuzz + vm.assume(user != address(this)); + vm.assume(user != address(0)); + address[] memory addresses = new address[](2); + addresses[0] = address(1); + addresses[1] = address(2); + + // ensure user can't call the airdrop function + vm.startPrank(user, user); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotSpecifiedRole.selector, trace.APPROVED_MINT_CONTRACT())); + trace.externalMint(address(this), "uri"); + vm.stopPrank(); + + // grant minter access and ensure that the user can call the external mint function + address[] memory minters = new address[](1); + minters[0] = user; + trace.setRole(trace.APPROVED_MINT_CONTRACT(), minters, true); + vm.startPrank(user, user); + vm.expectEmit(true, true, true, false); + emit Transfer(address(0), address(this), 1); + trace.externalMint(address(this), "uri"); + vm.stopPrank(); + assertEq(trace.balanceOf(address(this)), 1); + assertEq(trace.ownerOf(1), address(this)); + assertEq(trace.tokenURI(1), "uri"); + + // revoke mint access and ensure that the user can't call the external mint function + trace.setRole(trace.APPROVED_MINT_CONTRACT(), minters, false); + vm.startPrank(user, user); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotSpecifiedRole.selector, trace.APPROVED_MINT_CONTRACT())); + trace.externalMint(address(this), "uri"); + vm.stopPrank(); + + // grant admin role and ensure that the user can't call the external mint function + trace.setRole(trace.ADMIN_ROLE(), minters, true); + vm.startPrank(user, user); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotSpecifiedRole.selector, trace.APPROVED_MINT_CONTRACT())); + trace.externalMint(address(this), "uri"); + vm.stopPrank(); + + // revoke admin role and ensure that the user can't call the external mint function + trace.setRole(trace.ADMIN_ROLE(), minters, false); + vm.startPrank(user, user); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotSpecifiedRole.selector, trace.APPROVED_MINT_CONTRACT())); + trace.externalMint(address(this), "uri"); + vm.stopPrank(); + } + + function test_externalMint(address recipient, string memory uri, uint16 numTokens) public { + // limit fuzz + vm.assume(recipient != address(0)); + vm.assume(recipient != address(1)); + vm.assume(bytes(uri).length > 0); + vm.assume(numTokens > 0); + if (numTokens > 1000) { + numTokens = numTokens % 1000 + 1; + } + + // set mint contract + address[] memory minters = new address[](1); + minters[0] = address(1); + trace.setRole(trace.APPROVED_MINT_CONTRACT(), minters, true); + + // mint + for (uint256 i = 1; i <= numTokens; i++) { + vm.startPrank(address(1), address(1)); + vm.expectEmit(true, true, true, false); + emit Transfer(address(0), recipient, i); + trace.externalMint(recipient, uri); + vm.stopPrank(); + assertEq(trace.balanceOf(recipient), i); + assertEq(trace.ownerOf(i), recipient); + assertEq(trace.tokenURI(i), uri); + } + + // ensure ownership throws for non-existent token + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, numTokens + 1)); + trace.ownerOf(numTokens + 1); + vm.expectRevert(TRACE.TokenDoesntExist.selector); + trace.tokenURI(numTokens + 1); + } + + function test_externalMint_thenTransfer( + address recipient, + string memory uri, + uint16 numTokens, + address transferRecipient + ) public { + // limit fuzz + vm.assume(recipient != address(0)); + vm.assume(recipient != address(1)); + vm.assume(transferRecipient != address(0)); + vm.assume(transferRecipient != address(1)); + vm.assume(transferRecipient.code.length == 0); + vm.assume(bytes(uri).length > 0); + vm.assume(numTokens > 0); + if (numTokens > 1000) { + numTokens = numTokens % 1000 + 1; + } + + // approve mint contract + address[] memory minters = new address[](1); + minters[0] = address(1); + trace.setRole(trace.APPROVED_MINT_CONTRACT(), minters, true); + + // mint + for (uint256 i = 1; i <= numTokens; i++) { + vm.startPrank(address(1), address(1)); + trace.externalMint(recipient, uri); + vm.stopPrank(); + vm.startPrank(recipient, recipient); + vm.expectEmit(true, true, true, false); + emit Transfer(recipient, transferRecipient, i); + trace.transferFrom(recipient, transferRecipient, i); + vm.stopPrank(); + assertEq(trace.balanceOf(transferRecipient), 1); + assertEq(trace.ownerOf(i), transferRecipient); + vm.startPrank(transferRecipient, transferRecipient); + vm.expectEmit(true, true, true, false); + emit Transfer(transferRecipient, address(1), i); + trace.safeTransferFrom(transferRecipient, address(1), i); + vm.stopPrank(); + assertEq(trace.balanceOf(address(1)), i); + assertEq(trace.ownerOf(i), address(1)); + } + } + + /// @notice test TRACE functions + // - access control ✅ + // - transfer tokens ✅ + // - set tracers registry ✅ + // - add verified story ✅ + // - add verified story batch ✅ + + function test_transferToken_accessControl(address user) public { + // limit fuzz + vm.assume(user != address(this)); + + // mint token + trace.mint(address(this), "hii"); + + // ensure the user can't transfer the token + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + vm.prank(user); + trace.transferToken(address(this), user, 1); + } + + function test_transferToken(address user, uint256 numAddresses) public { + // limit fuzz + vm.assume(user != address(this) && user != address(0)); + address[] memory users = new address[](1); + users[0] = user; + vm.assume(numAddresses > 1); + if (numAddresses > 1000) { + numAddresses = numAddresses % 999 + 2; // map to 300 + } + + // create addresses + address[] memory addresses = new address[](numAddresses); + for (uint256 i = 0; i < numAddresses; i++) { + addresses[i] = makeAddr(i.toString()); + } + + // regular mint + trace.mint(user, "hiii"); + assert(trace.ownerOf(1) == user); + + // airdrop + trace.airdrop(addresses, "baseUri"); + for (uint256 i = 2; i <= numAddresses + 1; i++) { + assertEq(trace.ownerOf(i), addresses[i - 2]); + } + + // transfer tokens + vm.expectEmit(true, true, true, false); + emit Transfer(user, address(this), 1); + vm.expectEmit(true, true, false, false); + emit CreatorStory(1, address(this), "", "{\n\"trace\": {\"type\": \"trace_authentication\"}\n}"); + trace.transferToken(user, address(this), 1); + assert(trace.ownerOf(1) == address(this)); + + for (uint256 i = 2; i <= numAddresses + 1; i++) { + vm.expectEmit(true, true, true, false); + emit Transfer(addresses[i - 2], address(this), i); + vm.expectEmit(true, true, false, false); + emit CreatorStory(i, address(this), "", "{\n\"trace\": {\"type\": \"trace_authentication\"}\n}"); + trace.transferToken(addresses[i - 2], address(this), i); + assert(trace.ownerOf(i) == address(this)); + } + } + + function test_setTracersRegistry(address user, address newRegistryOne, address newRegistryTwo) public { + vm.assume(user != address(this)); + address[] memory users = new address[](1); + users[0] = user; + + // expect revert from user + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + vm.prank(user); + trace.setTracersRegistry(newRegistryOne); + + // expect creator pass + vm.expectEmit(true, true, true, true); + emit TRACERSRegistryUpdated(address(this), tracersRegistry, newRegistryOne); + trace.setTracersRegistry(newRegistryOne); + assert(address(trace.tracersRegistry()) == newRegistryOne); + + // set admin + trace.setRole(trace.ADMIN_ROLE(), users, true); + + // expect admin pass + vm.expectEmit(true, true, true, true); + emit TRACERSRegistryUpdated(user, newRegistryOne, newRegistryTwo); + vm.prank(user); + trace.setTracersRegistry(newRegistryTwo); + assert(address(trace.tracersRegistry()) == newRegistryTwo); + } + + function test_addVerifiedStory_customErrors(uint256 len1, uint256 len2, uint256 len3) public { + // limit fuzz + if (len1 > 200) { + len1 = len1 % 200; + } + if (len2 > 200) { + len2 = len2 % 200; + } + if (len3 > 200) { + len3 = len3 % 200; + } + uint256[] memory tokenIds = new uint256[](len1); + string[] memory stories = new string[](len2); + bytes[] memory sigs = new bytes[](len3); + if (len1 != len2 && len2 != len3) { + vm.expectRevert(TRACE.ArrayLengthMismatch.selector); + trace.addVerifiedStory(tokenIds, stories, sigs); + } + } + + function test_addVerifiedStory_mint(uint256 badSignerPrivateKey, address notAgent) public { + // limit fuzz + vm.assume( + badSignerPrivateKey != chipPrivateKey && badSignerPrivateKey != 0 + && badSignerPrivateKey < 115792089237316195423570985008687907852837564279074904382605163141518161494337 + ); + vm.assume(notAgent != agent); + + // set mocks + vm.mockCall( + tracersRegistry, + abi.encodeWithSelector( + ITRACERSRegistry.isRegisteredAgent.selector, + notAgent + ), + abi.encode(false, "") + ); + vm.mockCall( + tracersRegistry, + abi.encodeWithSelector( + ITRACERSRegistry.isRegisteredAgent.selector, + agent + ), + abi.encode(true, "agent") + ); + + // variables + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 1; + string[] memory stories = new string[](1); + stories[0] = "This is a story!"; + bytes[] memory sigs = new bytes[](1); + + // mint + trace.mint(chip, "https://arweave.net/tx_id"); + + // sender not registered agent fails + bytes32 digest = sigUtils.getTypedDataHash(address(trace), 1, "This is a story!"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + vm.expectRevert(TRACE.Unauthorized.selector); + vm.prank(notAgent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // registry is an EOA fails + trace.setTracersRegistry(address(0xC0FFEE)); + + // expect EOA registry to fail + vm.expectRevert(); + vm.prank(notAgent); + trace.addVerifiedStory(tokenIds, stories, sigs); + vm.expectRevert(); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + trace.setTracersRegistry(tracersRegistry); + + // bad signer fails + (v, r, s) = vm.sign(badSignerPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + vm.expectRevert(TRACE.InvalidSignature.selector); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // sign for wrong token + digest = sigUtils.getTypedDataHash(address(trace), 2, "This is a story!"); + (v, r, s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + vm.expectRevert(TRACE.InvalidSignature.selector); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // sign for wrong nft contract + digest = sigUtils.getTypedDataHash(tracersRegistry, 1, "This is a story!"); + (v, r, s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + vm.expectRevert(TRACE.InvalidSignature.selector); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // sign for non-existent token + digest = sigUtils.getTypedDataHash(address(trace), 2, "This is a story!"); + (v, r, s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + tokenIds[0] = 2; + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, 2)); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // right signer + registered agent passes + digest = sigUtils.getTypedDataHash(address(trace), 1, "This is a story!"); + (v, r, s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + tokenIds[0] = 1; + vm.expectEmit(true, true, true, true); + emit Story(1, agent, "agent", "This is a story!"); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // fails with replay + vm.expectRevert(TRACE.VerifiedStoryAlreadyWritten.selector); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // clear mocks + vm.clearMockedCalls(); + } + + function test_addVerifiedStory_airdrop(uint256 badSignerPrivateKey, address notAgent) public { + // limit fuzz + vm.assume( + badSignerPrivateKey != chipPrivateKey && badSignerPrivateKey != 0 + && badSignerPrivateKey < 115792089237316195423570985008687907852837564279074904382605163141518161494337 + ); + vm.assume(notAgent != agent); + + // set mocks + vm.mockCall( + tracersRegistry, + abi.encodeWithSelector( + ITRACERSRegistry.isRegisteredAgent.selector, + notAgent + ), + abi.encode(false, "") + ); + vm.mockCall( + tracersRegistry, + abi.encodeWithSelector( + ITRACERSRegistry.isRegisteredAgent.selector, + agent + ), + abi.encode(true, "agent") + ); + + // variables + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 1; + string[] memory stories = new string[](1); + stories[0] = "This is a story!"; + bytes[] memory sigs = new bytes[](1); + + // airdrop + address[] memory addresses = new address[](2); + addresses[0] = chip; + addresses[1] = chip; + trace.airdrop(addresses, "baseUri"); + + // sender not registered agent fails + bytes32 digest = sigUtils.getTypedDataHash(address(trace), 1, "This is a story!"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + vm.expectRevert(TRACE.Unauthorized.selector); + vm.prank(notAgent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // registry is an EOA fails + trace.setTracersRegistry(address(0xC0FFEE)); + + // expect EOA registry to fail + vm.expectRevert(); + vm.prank(notAgent); + trace.addVerifiedStory(tokenIds, stories, sigs); + vm.expectRevert(); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + trace.setTracersRegistry(tracersRegistry); + + // bad signer fails + (v, r, s) = vm.sign(badSignerPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + vm.expectRevert(TRACE.InvalidSignature.selector); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // sign for wrong token + digest = sigUtils.getTypedDataHash(address(trace), 2, "This is a story!"); + (v, r, s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + vm.expectRevert(TRACE.InvalidSignature.selector); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // sign for wrong nft contract + digest = sigUtils.getTypedDataHash(tracersRegistry, 1, "This is a story!"); + (v, r, s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + vm.expectRevert(TRACE.InvalidSignature.selector); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // sign for non-existent token + digest = sigUtils.getTypedDataHash(address(trace), 3, "This is a story!"); + (v, r, s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + tokenIds[0] = 3; + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, 3)); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // right signer + registered agent passes + digest = sigUtils.getTypedDataHash(address(trace), 1, "This is a story!"); + (v, r, s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + tokenIds[0] = 1; + vm.expectEmit(true, true, true, true); + emit Story(1, agent, "agent", "This is a story!"); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // fails with replay + vm.expectRevert(TRACE.VerifiedStoryAlreadyWritten.selector); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // clear mocks + vm.clearMockedCalls(); + } + + function test_addVerifiedStory_multipleForSameToken() public { + // set mock + vm.mockCall( + tracersRegistry, + abi.encodeWithSelector( + ITRACERSRegistry.isRegisteredAgent.selector, + agent + ), + abi.encode(true, "agent") + ); + + // variables + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 1; + tokenIds[1] = 1; + string[] memory stories = new string[](2); + stories[0] = "This is the first story!"; + stories[1] = "This is the second story!"; + bytes[] memory sigs = new bytes[](2); + + // mint + trace.mint(chip, "https://arweave.net/tx_id1"); + + // sender not registered agent fails + bytes32 digest = sigUtils.getTypedDataHash(address(trace), tokenIds[0], stories[0]); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + digest = sigUtils.getTypedDataHash(address(trace), tokenIds[1], stories[1]); + (v, r, s) = vm.sign(chipPrivateKey, digest); + sigs[1] = abi.encodePacked(r, s, v); + vm.expectEmit(true, true, true, true); + emit Story(tokenIds[0], agent, "agent", stories[0]); + vm.expectEmit(true, true, true, true); + emit Story(tokenIds[1], agent, "agent", stories[1]); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // fails with replay + vm.expectRevert(TRACE.VerifiedStoryAlreadyWritten.selector); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // clear mocks + vm.clearMockedCalls(); + } + + function test_addVerifiedStory_multipleForDiffTokens() public { + // set mock + vm.mockCall( + tracersRegistry, + abi.encodeWithSelector( + ITRACERSRegistry.isRegisteredAgent.selector, + agent + ), + abi.encode(true, "agent") + ); + + // variables + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 1; + tokenIds[1] = 2; + string[] memory stories = new string[](2); + stories[0] = "This is the first story!"; + stories[1] = "This is the second story!"; + bytes[] memory sigs = new bytes[](2); + + // mint + trace.mint(chip, "https://arweave.net/tx_id1"); + trace.mint(chip, "https://arweave.net/tx_id2"); + + // sender not registered agent fails + bytes32 digest = sigUtils.getTypedDataHash(address(trace), tokenIds[0], stories[0]); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + digest = sigUtils.getTypedDataHash(address(trace), tokenIds[1], stories[1]); + (v, r, s) = vm.sign(chipPrivateKey, digest); + sigs[1] = abi.encodePacked(r, s, v); + vm.expectEmit(true, true, true, true); + emit Story(tokenIds[0], agent, "agent", stories[0]); + vm.expectEmit(true, true, true, true); + emit Story(tokenIds[1], agent, "agent", stories[1]); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // fails with replay + vm.expectRevert(TRACE.VerifiedStoryAlreadyWritten.selector); + vm.prank(agent); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // clear mocks + vm.clearMockedCalls(); + } + + function test_addVerifiedStory_traceRegistryZeroAddress(address sender) public { + // change registry to zero address + trace.setTracersRegistry(address(0)); + + // mint + trace.mint(chip, "https://arweave.net/tx_id"); + + // variables + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 1; + string[] memory stories = new string[](1); + stories[0] = "This is a story!"; + bytes[] memory sigs = new bytes[](1); + + // test add story + bytes32 digest = sigUtils.getTypedDataHash(address(trace), 1, "This is a story!"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(chipPrivateKey, digest); + sigs[0] = abi.encodePacked(r, s, v); + vm.expectEmit(true, true, true, true); + emit Story(1, sender, sender.toHexString(), "This is a story!"); + vm.prank(sender); + trace.addVerifiedStory(tokenIds, stories, sigs); + + // test replay protection + vm.expectRevert(TRACE.VerifiedStoryAlreadyWritten.selector); + vm.prank(sender); + trace.addVerifiedStory(tokenIds, stories, sigs); + } + + /// @notice test royalty functions + // - access control ✅ + + function test_royalty_accessControl(address user) public { + // limit fuzz + vm.assume(user != address(this) && user != address(0)); + address[] memory users = new address[](1); + users[0] = user; + + // mint token + trace.mint(user, "https://arweave.net/tx_id"); + + // verify user can't set default royalty + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + vm.prank(user); + trace.setDefaultRoyalty(user, 1000); + + // verify user can't set token royalty + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + vm.prank(user); + trace.setTokenRoyalty(1, user, 1000); + + // verify owner can set default royalty + trace.setDefaultRoyalty(address(1), 100); + (address recp, uint256 amt) = trace.royaltyInfo(1, 10000); + assert(recp == address(1)); + assert(amt == 100); + + // set admins + trace.setRole(trace.ADMIN_ROLE(), users, true); + + // verify admin can set default royalty + vm.prank(user); + trace.setDefaultRoyalty(address(2), 1000); + (recp, amt) = trace.royaltyInfo(1, 10000); + assert(recp == address(2)); + assert(amt == 1000); + + // verify owner can set token royalty + trace.setTokenRoyalty(1, address(1), 100); + (recp, amt) = trace.royaltyInfo(1, 10000); + assert(recp == address(1)); + assert(amt == 100); + + // verify admin can set token royalty + vm.prank(user); + trace.setTokenRoyalty(1, address(2), 1000); + (recp, amt) = trace.royaltyInfo(1, 10000); + assert(recp == address(2)); + assert(amt == 1000); + } + + /// @notice test metadata update function + // - custom errors ✅ + // - access control + // - regular mint + // - airdrop + + function test_setTokenUri_customErrors() public { + // token doesn't exist + vm.expectRevert(TRACE.TokenDoesntExist.selector); + trace.setTokenUri(1, "hiiii"); + + // empty uri + trace.mint(address(1), "hiii"); + vm.expectRevert(TRACE.EmptyTokenURI.selector); + trace.setTokenUri(1, ""); + } + + function test_setTokenUri_accessControl(address user) public { + vm.assume(user != address(this) && user != address(0)); + address[] memory users = new address[](1); + users[0] = user; + + trace.mint(user, "https://arweave.net/tx_id"); + + // verify user can't update metadata + vm.prank(user); + vm.expectRevert(); + trace.setTokenUri(1, "ipfs://cid"); + + // verify owner can update metadata + vm.expectEmit(true, true, true, true); + emit MetadataUpdate(1); + trace.setTokenUri(1, "ipfs://cid"); + assertEq(trace.tokenURI(1), "ipfs://cid"); + + // set admins + trace.setRole(trace.ADMIN_ROLE(), users, true); + + // verify admin can update metadata + vm.expectEmit(true, true, true, true); + emit MetadataUpdate(1); + vm.prank(user); + trace.setTokenUri(1, "https://arweave.net/tx_id"); + assertEq(trace.tokenURI(1), "https://arweave.net/tx_id"); + } + + function test_setTokenUri_airdrop(uint256 numAddresses) public { + // limit fuzz + vm.assume(numAddresses > 1); + if (numAddresses > 1000) { + numAddresses = numAddresses % 999 + 2; // map to 300 + } + + // create addresses + address[] memory addresses = new address[](numAddresses); + for (uint256 i = 0; i < numAddresses; i++) { + addresses[i] = makeAddr(i.toString()); + } + + // airdrop + trace.airdrop(addresses, "baseUri"); + + + // create new uris + for (uint256 i = 1; i <= numAddresses; i++) { + string memory uri = string(abi.encodePacked("baseUri/", (i - 1).toString())); + assertEq(trace.tokenURI(i), uri); + } + + // set token uris + for (uint256 i = 1; i <= numAddresses; i++) { + string memory uri = string(abi.encodePacked("ipfs://", (i - 1).toString())); + trace.setTokenUri(i, uri); + assertEq(trace.tokenURI(i), uri); + } + } + + /// @notice test story functions + // - regular mint ✅ + // - airdrop ✅ + // - write creator story to existing token w/ proper acccess ✅ + // - write collection story to existing token w/ proper access ✅ + // - write creator story to non-existent token (reverts) ✅ + // - write collection story to non-existent token (reverts) ✅ + // - write collector story reverts + // - set story status reverts + + function test_story_always_reverts(address user) public { + vm.expectRevert(); + vm.prank(user); + trace.setStoryStatus(false); + + vm.expectRevert(); + vm.prank(user); + trace.addStory(1, "", "hello"); + } + + function test_addCreatorStory_nonExistentTokens() public { + vm.expectRevert(TRACE.TokenDoesntExist.selector); + trace.addCreatorStory(1, "XCOPY", "I AM XCOPY"); + } + + function test_story_mint(address collector, address hacker) public { + // limit fuzz + vm.assume(collector != address(this) && collector != address(0)); + vm.assume(hacker != address(this)); + trace.mint(collector, "uri"); + + // test creator story + vm.expectEmit(true, true, true, true); + emit CreatorStory(1, address(this), address(this).toHexString(), "I AM XCOPY"); + trace.addCreatorStory(1, "XCOPY", "I AM XCOPY"); + + // test collector can't add creator story + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + vm.prank(collector); + trace.addCreatorStory(1, "XCOPY", "I AM XCOPY"); + + // test hacker can't add creator story + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + vm.prank(hacker); + trace.addCreatorStory(1, "XCOPY", "I AM XCOPY"); + + // test collection story + vm.expectEmit(true, true, true, true); + emit CollectionStory(address(this), address(this).toHexString(), "I AM NOT XCOPY"); + trace.addCollectionStory("NOT XCOPY", "I AM NOT XCOPY"); + + // test that collector can't add collection story + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + vm.prank(collector); + trace.addCollectionStory("NOT XCOPY", "I AM NOT XCOPY"); + + // test that hacker can't add collection story + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + vm.prank(hacker); + trace.addCollectionStory("NOT XCOPY", "I AM NOT XCOPY"); + } + + function test_story_airdrop(address collector, address hacker) public { + // limit fuzz + vm.assume(collector != address(this) && collector != address(0)); + vm.assume(hacker != address(this)); + address[] memory addresses = new address[](2); + addresses[0] = collector; + addresses[1] = collector; + trace.airdrop(addresses, "uri"); + + // test creator story + vm.expectEmit(true, true, true, true); + emit CreatorStory(1, address(this), address(this).toHexString(), "I AM XCOPY"); + trace.addCreatorStory(1, "XCOPY", "I AM XCOPY"); + vm.expectEmit(true, true, true, true); + emit CreatorStory(2, address(this), address(this).toHexString(), "I AM XCOPY"); + trace.addCreatorStory(2, "XCOPY", "I AM XCOPY"); + + // test collector can't add creator story + vm.startPrank(collector); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + trace.addCreatorStory(1, "XCOPY", "I AM XCOPY"); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + trace.addCreatorStory(2, "XCOPY", "I AM XCOPY"); + vm.stopPrank(); + + // test hacker can't add creator story + vm.startPrank(hacker); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + trace.addCreatorStory(1, "XCOPY", "I AM XCOPY"); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + trace.addCreatorStory(2, "XCOPY", "I AM XCOPY"); + vm.stopPrank(); + + // test collection story + vm.expectEmit(true, true, true, true); + emit CollectionStory(address(this), address(this).toHexString(), "I AM NOT XCOPY"); + trace.addCollectionStory("XCOPY", "I AM NOT XCOPY"); + vm.expectEmit(true, true, true, true); + emit CollectionStory(address(this), address(this).toHexString(), "I AM NOT XCOPY"); + trace.addCollectionStory("XCOPY", "I AM NOT XCOPY"); + + // test that collector can't add collection story + vm.startPrank(collector); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + trace.addCollectionStory("NOT XCOPY", "I AM NOT XCOPY"); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + trace.addCollectionStory("NOT XCOPY", "I AM NOT XCOPY"); + vm.stopPrank(); + + // test that hacker can't add collection story + vm.startPrank(hacker); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + trace.addCollectionStory("NOT XCOPY", "I AM NOT XCOPY"); + vm.expectRevert(abi.encodeWithSelector(OwnableAccessControlUpgradeable.NotRoleOrOwner.selector, trace.ADMIN_ROLE())); + trace.addCollectionStory("NOT XCOPY", "I AM NOT XCOPY"); + vm.stopPrank(); + } + + /// @notice blocklist and delegation registry tests + + function test_blocklist(address user, address registry) public { + vm.expectRevert(); + vm.prank(user); + trace.setBlockListRegistry(registry); + + assertEq(address(trace.blocklistRegistry()), address(0)); + } + + function test_tlNftDelegationRegistry(address user, address registry) public { + vm.expectRevert(); + vm.prank(user); + trace.setNftDelegationRegistry(registry); + + assertEq(address(trace.tlNftDelegationRegistry()), address(0)); + } +} diff --git a/test/utils/TRACESigUtils.sol b/test/utils/TRACESigUtils.sol index 61ec330..f2931c1 100644 --- a/test/utils/TRACESigUtils.sol +++ b/test/utils/TRACESigUtils.sol @@ -16,31 +16,30 @@ contract TRACESigUtils { ); } - /// @notice function to hash the typed data - function _hashVerifiedStory(uint256 tokenId, uint256 nonce, address sender, string memory story) + /// @notice Function to hash the typed data + function _hashVerifiedStory(address nftContract, uint256 tokenId, string memory story) internal pure returns (bytes32) { return keccak256( abi.encode( - // keccak256("VerifiedStory(uint256 nonce,uint256 tokenId,address sender,string story)"), - 0x3ea278f3e0e25a71281e489b82695f448ae01ef3fc312598f1e61ac9956ab954, - nonce, + // keccak256("VerifiedStory(address nftContract,uint256 tokenId,string story)"), + 0x76b12200216600191228eb643bc7cba6e319d03951a863e3306595415759682b, + nftContract, tokenId, - sender, keccak256(bytes(story)) ) ); } // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer - function getTypedDataHash(uint256 tokenId, uint256 nonce, address sender, string memory story) + function getTypedDataHash(address nftContract, uint256 tokenId, string memory story) public view returns (bytes32) { - bytes32 hash = _hashVerifiedStory(tokenId, nonce, sender, story); + bytes32 hash = _hashVerifiedStory(nftContract, tokenId, story); return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, hash)); } }