diff --git a/contracts/SPGNFT.sol b/contracts/SPGNFT.sol index 6bbdc75..fbaa6bd 100644 --- a/contracts/SPGNFT.sol +++ b/contracts/SPGNFT.sol @@ -22,6 +22,7 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl /// @param _publicMinting True if the collection is open for everyone to mint. /// @param _baseURI The base URI for the collection. If baseURI is not empty, tokenURI will be /// either baseURI + token ID (if nftMetadataURI is empty) or baseURI + nftMetadataURI. + /// @param _nftMetadataHashToTokenId The mapping of nftMetadataHash to token ID. /// @custom:storage-location erc7201:story-protocol-periphery.SPGNFT struct SPGNFTStorage { uint32 _maxSupply; @@ -33,6 +34,7 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl bool _publicMinting; string _baseURI; string _contractURI; + mapping(bytes32 nftMetadataHash => uint256 tokenId) _nftMetadataHashToTokenId; } // keccak256(abi.encode(uint256(keccak256("story-protocol-periphery.SPGNFT")) - 1)) & ~bytes32(uint256(0xff)); @@ -148,6 +150,14 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl return _getSPGNFTStorage()._contractURI; } + /// @notice Returns the token ID by the metadata hash. + /// @dev Returns 0 if the metadata hash has not been used in this collection. + /// @param nftMetadataHash A bytes32 hash of the NFT's metadata. + /// @return tokenId The token ID of the NFT with the given metadata hash. + function getTokenIdByMetadataHash(bytes32 nftMetadataHash) external view returns (uint256) { + return _getSPGNFTStorage()._nftMetadataHashToTokenId[nftMetadataHash]; + } + /// @notice Sets the fee to mint an NFT from the collection. Payment is in the designated currency. /// @dev Only callable by the admin role. /// @param fee The new mint fee paid in the mint token. @@ -207,25 +217,50 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl /// @notice Mints an NFT from the collection. Only callable by the minter role. /// @param to The address of the recipient of the minted NFT. /// @param nftMetadataURI OPTIONAL. The URI of the desired metadata for the newly minted NFT. - /// @return tokenId The ID of the minted NFT. - function mint(address to, string calldata nftMetadataURI) public virtual returns (uint256 tokenId) { + /// @param nftMetadataHash OPTIONAL. A bytes32 hash of the NFT's metadata. + /// This metadata is accessible via the NFT's tokenURI. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. + /// @return tokenId The token ID of the minted NFT with the given metadata hash. + function mint( + address to, + string calldata nftMetadataURI, + bytes32 nftMetadataHash, + bool allowDuplicates + ) public virtual returns (uint256 tokenId) { if (!_getSPGNFTStorage()._publicMinting && !hasRole(SPGNFTLib.MINTER_ROLE, msg.sender)) { revert Errors.SPGNFT__MintingDenied(); } - tokenId = _mintToken({ to: to, payer: msg.sender, nftMetadataURI: nftMetadataURI }); + tokenId = _mintToken({ + to: to, + payer: msg.sender, + nftMetadataURI: nftMetadataURI, + nftMetadataHash: nftMetadataHash, + allowDuplicates: allowDuplicates + }); } /// @notice Mints an NFT from the collection. Only callable by the Periphery contracts. /// @param to The address of the recipient of the minted NFT. /// @param payer The address of the payer for the mint fee. /// @param nftMetadataURI OPTIONAL. The URI of the desired metadata for the newly minted NFT. - /// @return tokenId The ID of the minted NFT. + /// @param nftMetadataHash OPTIONAL. A bytes32 hash of the NFT's metadata. + /// This metadata is accessible via the NFT's tokenURI. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. + /// @return tokenId The token ID of the minted NFT with the given metadata hash. function mintByPeriphery( address to, address payer, - string calldata nftMetadataURI + string calldata nftMetadataURI, + bytes32 nftMetadataHash, + bool allowDuplicates ) public virtual onlyPeriphery returns (uint256 tokenId) { - tokenId = _mintToken({ to: to, payer: payer, nftMetadataURI: nftMetadataURI }); + tokenId = _mintToken({ + to: to, + payer: payer, + nftMetadataURI: nftMetadataURI, + nftMetadataHash: nftMetadataHash, + allowDuplicates: allowDuplicates + }); } /// @dev Withdraws the contract's token balance to the fee recipient. @@ -246,17 +281,40 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl /// @param to The address of the recipient of the minted NFT. /// @param payer The address of the payer for the mint fee. /// @param nftMetadataURI OPTIONAL. The URI of the desired metadata for the newly minted NFT. - /// @return tokenId The ID of the minted NFT. - function _mintToken(address to, address payer, string calldata nftMetadataURI) internal returns (uint256 tokenId) { + /// @param nftMetadataHash OPTIONAL. A bytes32 hash of the NFT's metadata. + /// This metadata is accessible via the NFT's tokenURI. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. + /// @return tokenId The token ID of the minted NFT with the given metadata hash. + function _mintToken( + address to, + address payer, + string calldata nftMetadataURI, + bytes32 nftMetadataHash, + bool allowDuplicates + ) internal returns (uint256 tokenId) { SPGNFTStorage storage $ = _getSPGNFTStorage(); if (!$._mintOpen) revert Errors.SPGNFT__MintingClosed(); if ($._totalSupply + 1 > $._maxSupply) revert Errors.SPGNFT__MaxSupplyReached(); + tokenId = $._nftMetadataHashToTokenId[nftMetadataHash]; + if (!allowDuplicates && tokenId != 0) { + revert Errors.SPGNFT__DuplicatedNFTMetadataHash({ + spgNftContract: address(this), + tokenId: tokenId, + nftMetadataHash: nftMetadataHash + }); + } + if ($._mintFeeToken != address(0) && $._mintFee > 0) { IERC20($._mintFeeToken).transferFrom(payer, address(this), $._mintFee); } tokenId = ++$._totalSupply; + if ($._nftMetadataHashToTokenId[nftMetadataHash] == 0) { + // only store the token ID if the metadata hash is not used + $._nftMetadataHashToTokenId[nftMetadataHash] = tokenId; + } + _mint(to, tokenId); if (bytes(nftMetadataURI).length > 0) _setTokenURI(tokenId, nftMetadataURI); diff --git a/contracts/interfaces/ISPGNFT.sol b/contracts/interfaces/ISPGNFT.sol index 75bf11b..52aa386 100644 --- a/contracts/interfaces/ISPGNFT.sol +++ b/contracts/interfaces/ISPGNFT.sol @@ -65,6 +65,13 @@ interface ISPGNFT is IAccessControl, IERC721Metadata, IERC7572 { /// or baseURI + token ID (if nftMetadataURI is empty). function baseURI() external view returns (string memory); + /// @notice Returns the token ID by the metadata hash. + /// @dev Returns 0 if the metadata hash has not been used in this collection. + /// @param nftMetadataHash A bytes32 hash of the NFT's metadata. + /// This metadata is accessible via the NFT's tokenURI. + /// @return tokenId The token ID of the NFT with the given metadata hash. + function getTokenIdByMetadataHash(bytes32 nftMetadataHash) external view returns (uint256); + /// @notice Sets the fee to mint an NFT from the collection. Payment is in the designated currency. /// @dev Only callable by the admin role. /// @param fee The new mint fee paid in the mint token. @@ -105,18 +112,31 @@ interface ISPGNFT is IAccessControl, IERC721Metadata, IERC7572 { /// @notice Mints an NFT from the collection. Only callable by the minter role. /// @param to The address of the recipient of the minted NFT. /// @param nftMetadataURI OPTIONAL. The desired metadata for the newly minted NFT. - /// @return tokenId The ID of the minted NFT. - function mint(address to, string calldata nftMetadataURI) external returns (uint256 tokenId); + /// @param nftMetadataHash OPTIONAL. A bytes32 hash of the NFT's metadata. + /// This metadata is accessible via the NFT's tokenURI. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. + /// @return tokenId The token ID of the minted NFT with the given metadata hash. + function mint( + address to, + string calldata nftMetadataURI, + bytes32 nftMetadataHash, + bool allowDuplicates + ) external returns (uint256 tokenId); /// @notice Mints an NFT from the collection. Only callable by Periphery contracts. /// @param to The address of the recipient of the minted NFT. /// @param payer The address of the payer for the mint fee. /// @param nftMetadataURI OPTIONAL. The desired metadata for the newly minted NFT. - /// @return tokenId The ID of the minted NFT. + /// @param nftMetadataHash OPTIONAL. A bytes32 hash of the NFT's metadata. + /// This metadata is accessible via the NFT's tokenURI. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. + /// @return tokenId The token ID of the minted NFT with the given metadata hash. function mintByPeriphery( address to, address payer, - string calldata nftMetadataURI + string calldata nftMetadataURI, + bytes32 nftMetadataHash, + bool allowDuplicates ) external returns (uint256 tokenId); /// @dev Withdraws the contract's token balance to the fee recipient. diff --git a/contracts/interfaces/workflows/IDerivativeWorkflows.sol b/contracts/interfaces/workflows/IDerivativeWorkflows.sol index 1d40160..047935f 100644 --- a/contracts/interfaces/workflows/IDerivativeWorkflows.sol +++ b/contracts/interfaces/workflows/IDerivativeWorkflows.sol @@ -12,13 +12,15 @@ interface IDerivativeWorkflows { /// @param derivData The derivative data to be used for registerDerivative. /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. /// @param recipient The address to receive the minted NFT. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. /// @return ipId The ID of the newly registered IP. /// @return tokenId The ID of the newly minted NFT. function mintAndRegisterIpAndMakeDerivative( address spgNftContract, WorkflowStructs.MakeDerivative calldata derivData, WorkflowStructs.IPMetadata calldata ipMetadata, - address recipient + address recipient, + bool allowDuplicates ) external returns (address ipId, uint256 tokenId); /// @notice Register the given NFT as a derivative IP with metadata without license tokens. @@ -46,6 +48,7 @@ interface IDerivativeWorkflows { /// @param royaltyContext The context for royalty module, should be empty for Royalty Policy LAP. /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. /// @param recipient The address to receive the minted NFT. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. /// @return ipId The ID of the newly registered IP. /// @return tokenId The ID of the newly minted NFT. function mintAndRegisterIpAndMakeDerivativeWithLicenseTokens( @@ -53,7 +56,8 @@ interface IDerivativeWorkflows { uint256[] calldata licenseTokenIds, bytes calldata royaltyContext, WorkflowStructs.IPMetadata calldata ipMetadata, - address recipient + address recipient, + bool allowDuplicates ) external returns (address ipId, uint256 tokenId); /// @notice Register the given NFT as a derivative IP using license tokens. diff --git a/contracts/interfaces/workflows/IGroupingWorkflows.sol b/contracts/interfaces/workflows/IGroupingWorkflows.sol index ef78758..34d21fb 100644 --- a/contracts/interfaces/workflows/IGroupingWorkflows.sol +++ b/contracts/interfaces/workflows/IGroupingWorkflows.sol @@ -16,6 +16,7 @@ interface IGroupingWorkflows { /// @param licenseTermsId The ID of the registered license terms that will be attached to the new IP. /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. /// @param sigAddToGroup Signature data for addIp to the group IP via the Grouping Module. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. /// @return ipId The ID of the newly registered IP. /// @return tokenId The ID of the newly minted NFT. function mintAndRegisterIpAndAttachLicenseAndAddToGroup( @@ -25,7 +26,8 @@ interface IGroupingWorkflows { address licenseTemplate, uint256 licenseTermsId, WorkflowStructs.IPMetadata calldata ipMetadata, - WorkflowStructs.SignatureData calldata sigAddToGroup + WorkflowStructs.SignatureData calldata sigAddToGroup, + bool allowDuplicates ) external returns (address ipId, uint256 tokenId); /// @notice Register an NFT as IP with metadata, attach license terms to the registered IP, diff --git a/contracts/interfaces/workflows/ILicenseAttachmentWorkflows.sol b/contracts/interfaces/workflows/ILicenseAttachmentWorkflows.sol index 007ed51..4a6614f 100644 --- a/contracts/interfaces/workflows/ILicenseAttachmentWorkflows.sol +++ b/contracts/interfaces/workflows/ILicenseAttachmentWorkflows.sol @@ -27,6 +27,7 @@ interface ILicenseAttachmentWorkflows { /// @param recipient The address of the recipient of the minted NFT. /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. /// @param terms The PIL terms to be registered. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. /// @return ipId The ID of the newly registered IP. /// @return tokenId The ID of the newly minted NFT. /// @return licenseTermsId The ID of the newly registered PIL terms. @@ -34,7 +35,8 @@ interface ILicenseAttachmentWorkflows { address spgNftContract, address recipient, WorkflowStructs.IPMetadata calldata ipMetadata, - PILTerms calldata terms + PILTerms calldata terms, + bool allowDuplicates ) external returns (address ipId, uint256 tokenId, uint256 licenseTermsId); /// @notice Register a given NFT as an IP and attach Programmable IP License Terms. diff --git a/contracts/interfaces/workflows/IRegistrationWorkflows.sol b/contracts/interfaces/workflows/IRegistrationWorkflows.sol index 0d80b17..cefecd1 100644 --- a/contracts/interfaces/workflows/IRegistrationWorkflows.sol +++ b/contracts/interfaces/workflows/IRegistrationWorkflows.sol @@ -21,12 +21,15 @@ interface IRegistrationWorkflows { /// @param spgNftContract The address of the SPGNFT collection. /// @param recipient The address of the recipient of the minted NFT. /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. + /// If a duplicate is found, returns existing token Id and IP Id instead of minting/registering a new one. /// @return ipId The ID of the registered IP. /// @return tokenId The ID of the newly minted NFT. function mintAndRegisterIp( address spgNftContract, address recipient, - WorkflowStructs.IPMetadata calldata ipMetadata + WorkflowStructs.IPMetadata calldata ipMetadata, + bool allowDuplicates ) external returns (address ipId, uint256 tokenId); /// @notice Registers an NFT as IP with metadata. diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index ffb4ca0..02f509c 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -71,4 +71,10 @@ library Errors { /// @notice Caller is not one of the periphery contracts. error SPGNFT__CallerNotPeripheryContract(); + + /// @notice Error thrown when attempting to mint an NFT with a metadata hash that already exists. + /// @param spgNftContract The address of the SPGNFT collection contract where the duplicate was detected. + /// @param tokenId The ID of the original NFT that was first minted with this metadata hash. + /// @param nftMetadataHash The hash of the NFT metadata that caused the duplication error. + error SPGNFT__DuplicatedNFTMetadataHash(address spgNftContract, uint256 tokenId, bytes32 nftMetadataHash); } diff --git a/contracts/workflows/DerivativeWorkflows.sol b/contracts/workflows/DerivativeWorkflows.sol index 780d8dc..4f8bf2d 100644 --- a/contracts/workflows/DerivativeWorkflows.sol +++ b/contracts/workflows/DerivativeWorkflows.sol @@ -114,19 +114,24 @@ contract DerivativeWorkflows is /// @param derivData The derivative data to be used for registerDerivative. /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. /// @param recipient The address to receive the minted NFT. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. /// @return ipId The ID of the newly registered IP. /// @return tokenId The ID of the newly minted NFT. function mintAndRegisterIpAndMakeDerivative( address spgNftContract, WorkflowStructs.MakeDerivative calldata derivData, WorkflowStructs.IPMetadata calldata ipMetadata, - address recipient + address recipient, + bool allowDuplicates ) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId) { tokenId = ISPGNFT(spgNftContract).mintByPeriphery({ to: address(this), payer: msg.sender, - nftMetadataURI: ipMetadata.nftMetadataURI + nftMetadataURI: ipMetadata.nftMetadataURI, + nftMetadataHash: ipMetadata.nftMetadataHash, + allowDuplicates: allowDuplicates }); + ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); @@ -212,6 +217,7 @@ contract DerivativeWorkflows is /// @param royaltyContext The context for royalty module, should be empty for Royalty Policy LAP. /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and newly registered IP. /// @param recipient The address to receive the minted NFT. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. /// @return ipId The ID of the registered IP. /// @return tokenId The ID of the minted NFT. function mintAndRegisterIpAndMakeDerivativeWithLicenseTokens( @@ -219,15 +225,19 @@ contract DerivativeWorkflows is uint256[] calldata licenseTokenIds, bytes calldata royaltyContext, WorkflowStructs.IPMetadata calldata ipMetadata, - address recipient + address recipient, + bool allowDuplicates ) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId) { _collectLicenseTokens(licenseTokenIds, address(LICENSE_TOKEN)); tokenId = ISPGNFT(spgNftContract).mintByPeriphery({ to: address(this), payer: msg.sender, - nftMetadataURI: ipMetadata.nftMetadataURI + nftMetadataURI: ipMetadata.nftMetadataURI, + nftMetadataHash: ipMetadata.nftMetadataHash, + allowDuplicates: allowDuplicates }); + ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); diff --git a/contracts/workflows/GroupingWorkflows.sol b/contracts/workflows/GroupingWorkflows.sol index 8589080..4af45de 100644 --- a/contracts/workflows/GroupingWorkflows.sol +++ b/contracts/workflows/GroupingWorkflows.sol @@ -120,6 +120,7 @@ contract GroupingWorkflows is /// @param licenseTermsId The ID of the registered license terms that will be attached to the new IP. /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. /// @param sigAddToGroup Signature data for addIp to the group IP via the Grouping Module. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. /// @return ipId The ID of the newly registered IP. /// @return tokenId The ID of the newly minted NFT. function mintAndRegisterIpAndAttachLicenseAndAddToGroup( @@ -129,13 +130,17 @@ contract GroupingWorkflows is address licenseTemplate, uint256 licenseTermsId, WorkflowStructs.IPMetadata calldata ipMetadata, - WorkflowStructs.SignatureData calldata sigAddToGroup + WorkflowStructs.SignatureData calldata sigAddToGroup, + bool allowDuplicates ) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId) { tokenId = ISPGNFT(spgNftContract).mintByPeriphery({ to: address(this), payer: msg.sender, - nftMetadataURI: ipMetadata.nftMetadataURI + nftMetadataURI: ipMetadata.nftMetadataURI, + nftMetadataHash: ipMetadata.nftMetadataHash, + allowDuplicates: allowDuplicates }); + ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); diff --git a/contracts/workflows/LicenseAttachmentWorkflows.sol b/contracts/workflows/LicenseAttachmentWorkflows.sol index 63b9f3b..96306e1 100644 --- a/contracts/workflows/LicenseAttachmentWorkflows.sol +++ b/contracts/workflows/LicenseAttachmentWorkflows.sol @@ -123,6 +123,7 @@ contract LicenseAttachmentWorkflows is /// @param recipient The address of the recipient of the minted NFT. /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. /// @param terms The PIL terms to be registered. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. /// @return ipId The ID of the newly registered IP. /// @return tokenId The ID of the newly minted NFT. /// @return licenseTermsId The ID of the newly registered PIL terms. @@ -130,13 +131,17 @@ contract LicenseAttachmentWorkflows is address spgNftContract, address recipient, WorkflowStructs.IPMetadata calldata ipMetadata, - PILTerms calldata terms + PILTerms calldata terms, + bool allowDuplicates ) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId, uint256 licenseTermsId) { tokenId = ISPGNFT(spgNftContract).mintByPeriphery({ to: address(this), payer: msg.sender, - nftMetadataURI: ipMetadata.nftMetadataURI + nftMetadataURI: ipMetadata.nftMetadataURI, + nftMetadataHash: ipMetadata.nftMetadataHash, + allowDuplicates: allowDuplicates }); + ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); diff --git a/contracts/workflows/RegistrationWorkflows.sol b/contracts/workflows/RegistrationWorkflows.sol index f8ec69c..7cb366e 100644 --- a/contracts/workflows/RegistrationWorkflows.sol +++ b/contracts/workflows/RegistrationWorkflows.sol @@ -107,18 +107,23 @@ contract RegistrationWorkflows is /// @param spgNftContract The address of the SPGNFT collection. /// @param recipient The address of the recipient of the minted NFT. /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. /// @return ipId The ID of the registered IP. /// @return tokenId The ID of the newly minted NFT. function mintAndRegisterIp( address spgNftContract, address recipient, - WorkflowStructs.IPMetadata calldata ipMetadata + WorkflowStructs.IPMetadata calldata ipMetadata, + bool allowDuplicates ) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId) { tokenId = ISPGNFT(spgNftContract).mintByPeriphery({ to: address(this), payer: msg.sender, - nftMetadataURI: ipMetadata.nftMetadataURI + nftMetadataURI: ipMetadata.nftMetadataURI, + nftMetadataHash: ipMetadata.nftMetadataHash, + allowDuplicates: allowDuplicates }); + ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); diff --git a/test/SPGNFT.t.sol b/test/SPGNFT.t.sol index 44db22d..2a11e75 100644 --- a/test/SPGNFT.t.sol +++ b/test/SPGNFT.t.sol @@ -138,7 +138,12 @@ contract SPGNFTTest is BaseTest { uint256 mintFee = nftContract.mintFee(); uint256 balanceBeforeAlice = mockToken.balanceOf(u.alice); uint256 balanceBeforeContract = mockToken.balanceOf(address(nftContract)); - uint256 tokenId = nftContract.mint(u.bob, ipMetadataEmpty.nftMetadataURI); + uint256 tokenId = nftContract.mint({ + to: u.bob, + nftMetadataURI: ipMetadataEmpty.nftMetadataURI, + nftMetadataHash: ipMetadataEmpty.nftMetadataHash, + allowDuplicates: true + }); assertEq(nftContract.totalSupply(), 1); assertEq(nftContract.balanceOf(u.bob), 1); @@ -149,7 +154,14 @@ contract SPGNFTTest is BaseTest { balanceBeforeAlice = mockToken.balanceOf(u.alice); balanceBeforeContract = mockToken.balanceOf(address(nftContract)); - tokenId = nftContract.mint(u.bob, ipMetadataDefault.nftMetadataURI); + tokenId = nftContract.mint({ + to: u.bob, + nftMetadataURI: ipMetadataDefault.nftMetadataURI, + nftMetadataHash: ipMetadataDefault.nftMetadataHash, + allowDuplicates: true + }); + + assertEq(nftContract.getTokenIdByMetadataHash(ipMetadataDefault.nftMetadataHash), tokenId); assertEq(nftContract.totalSupply(), 2); assertEq(nftContract.balanceOf(u.bob), 2); assertEq(nftContract.ownerOf(tokenId), u.bob); @@ -163,7 +175,14 @@ contract SPGNFTTest is BaseTest { nftContract.setMintFee(200 * 10 ** mockToken.decimals()); mintFee = nftContract.mintFee(); - tokenId = nftContract.mint(u.carl, ipMetadataDefault.nftMetadataURI); + tokenId = nftContract.mint({ + to: u.carl, + nftMetadataURI: ipMetadataDefault.nftMetadataURI, + nftMetadataHash: ipMetadataDefault.nftMetadataHash, + allowDuplicates: true + }); + assertEq(tokenId, 3); + assertEq(nftContract.getTokenIdByMetadataHash(ipMetadataDefault.nftMetadataHash), 2); assertEq(mockToken.balanceOf(address(nftContract)), 400 * 10 ** mockToken.decimals()); assertEq(nftContract.totalSupply(), 3); assertEq(nftContract.balanceOf(u.carl), 1); @@ -175,6 +194,37 @@ contract SPGNFTTest is BaseTest { vm.stopPrank(); } + function test_SPGNFT_mint_revert_DuplicatedNFTMetadataHash() public { + vm.startPrank(u.alice); + mockToken.mint(address(u.alice), 1000 * 10 ** mockToken.decimals()); + mockToken.approve(address(nftContract), 1000 * 10 ** mockToken.decimals()); + + // turn on dedup + uint256 tokenId = nftContract.mint({ + to: u.carl, + nftMetadataURI: ipMetadataDefault.nftMetadataURI, + nftMetadataHash: ipMetadataDefault.nftMetadataHash, + allowDuplicates: false + }); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.SPGNFT__DuplicatedNFTMetadataHash.selector, + address(nftContract), + tokenId, + ipMetadataDefault.nftMetadataHash + ) + ); + nftContract.mint({ + to: u.carl, + nftMetadataURI: ipMetadataDefault.nftMetadataURI, + nftMetadataHash: ipMetadataDefault.nftMetadataHash, + allowDuplicates: false + }); + + vm.stopPrank(); + } + function test_SPGNFT_setBaseURI() public { vm.startPrank(u.alice); mockToken.mint(address(u.alice), 1000 * 10 ** mockToken.decimals()); @@ -182,19 +232,34 @@ contract SPGNFTTest is BaseTest { // non empty baseURI assertEq(nftContract.baseURI(), testBaseURI); - uint256 tokenId1 = nftContract.mint(u.alice, ipMetadataDefault.nftMetadataURI); + uint256 tokenId1 = nftContract.mint({ + to: u.alice, + nftMetadataURI: ipMetadataDefault.nftMetadataURI, + nftMetadataHash: ipMetadataDefault.nftMetadataHash, + allowDuplicates: true + }); assertEq(nftContract.tokenURI(tokenId1), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); nftContract.setBaseURI("test"); assertEq(nftContract.baseURI(), "test"); - uint256 tokenId2 = nftContract.mint(u.alice, ipMetadataEmpty.nftMetadataURI); + uint256 tokenId2 = nftContract.mint({ + to: u.alice, + nftMetadataURI: ipMetadataEmpty.nftMetadataURI, + nftMetadataHash: ipMetadataEmpty.nftMetadataHash, + allowDuplicates: true + }); assertEq(nftContract.tokenURI(tokenId1), string.concat("test", ipMetadataDefault.nftMetadataURI)); assertEq(nftContract.tokenURI(tokenId2), string.concat("test", tokenId2.toString())); // empty baseURI nftContract.setBaseURI(""); assertEq(nftContract.baseURI(), ""); - uint256 tokenId3 = nftContract.mint(u.alice, ipMetadataDefault.nftMetadataURI); + uint256 tokenId3 = nftContract.mint({ + to: u.alice, + nftMetadataURI: ipMetadataDefault.nftMetadataURI, + nftMetadataHash: ipMetadataDefault.nftMetadataHash, + allowDuplicates: true + }); assertEq(nftContract.tokenURI(tokenId1), ipMetadataDefault.nftMetadataURI); assertEq(nftContract.tokenURI(tokenId2), ipMetadataEmpty.nftMetadataURI); assertEq(nftContract.tokenURI(tokenId3), ipMetadataDefault.nftMetadataURI); @@ -222,7 +287,7 @@ contract SPGNFTTest is BaseTest { abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(nftContract), 0, mintFee) ); vm.prank(u.alice); - nftContract.mint(u.bob, ipMetadataDefault.nftMetadataURI); + nftContract.mint(u.bob, ipMetadataDefault.nftMetadataURI, ipMetadataDefault.nftMetadataHash, false); } function test_SPGNFT_revert_mint_erc20InsufficientBalance() public { @@ -237,7 +302,7 @@ contract SPGNFTTest is BaseTest { nftContract.mintFee() ) ); - nftContract.mint(u.bob, ipMetadataDefault.nftMetadataURI); + nftContract.mint(u.bob, ipMetadataDefault.nftMetadataURI, ipMetadataDefault.nftMetadataHash, false); vm.stopPrank(); } @@ -291,7 +356,7 @@ contract SPGNFTTest is BaseTest { uint256 mintFee = nftContract.mintFee(); - nftContract.mint(feeRecipient, ipMetadataDefault.nftMetadataURI); + nftContract.mint(feeRecipient, ipMetadataDefault.nftMetadataURI, ipMetadataDefault.nftMetadataHash, false); assertEq(mockToken.balanceOf(address(nftContract)), mintFee); diff --git a/test/integration/workflows/DerivativeIntegration.t.sol b/test/integration/workflows/DerivativeIntegration.t.sol index e244a5a..1aaa7d8 100644 --- a/test/integration/workflows/DerivativeIntegration.t.sol +++ b/test/integration/workflows/DerivativeIntegration.t.sol @@ -55,7 +55,8 @@ contract DerivativeIntegration is BaseIntegration { maxMintingFee: 0 }), ipMetadata: testIpMetadata, - recipient: testSender + recipient: testSender, + allowDuplicates: true }); assertTrue(ipAssetRegistry.isRegistered(childIpId)); @@ -84,7 +85,12 @@ contract DerivativeIntegration is BaseIntegration { StoryUSD.mint(testSender, testMintFee); StoryUSD.approve(address(spgNftContract), testMintFee); // for nft minting fee - uint256 childTokenId = spgNftContract.mint(testSender, testIpMetadata.nftMetadataURI); + uint256 childTokenId = spgNftContract.mint({ + to: testSender, + nftMetadataURI: testIpMetadata.nftMetadataURI, + nftMetadataHash: testIpMetadata.nftMetadataHash, + allowDuplicates: true + }); address childIpId = ipAssetRegistry.ipId(block.chainid, address(spgNftContract), childTokenId); uint256 deadline = block.timestamp + 1000; @@ -183,7 +189,8 @@ contract DerivativeIntegration is BaseIntegration { licenseTokenIds: licenseTokenIds, royaltyContext: "", ipMetadata: testIpMetadata, - recipient: testSender + recipient: testSender, + allowDuplicates: true }); assertTrue(ipAssetRegistry.isRegistered(childIpId)); @@ -212,7 +219,12 @@ contract DerivativeIntegration is BaseIntegration { { StoryUSD.mint(testSender, testMintFee); StoryUSD.approve(address(spgNftContract), testMintFee); // for nft minting fee - uint256 childTokenId = spgNftContract.mint(testSender, testIpMetadata.nftMetadataURI); + uint256 childTokenId = spgNftContract.mint({ + to: testSender, + nftMetadataURI: testIpMetadata.nftMetadataURI, + nftMetadataHash: testIpMetadata.nftMetadataHash, + allowDuplicates: true + }); address childIpId = ipAssetRegistry.ipId(block.chainid, address(spgNftContract), childTokenId); uint256 deadline = block.timestamp + 1000; @@ -370,7 +382,8 @@ contract DerivativeIntegration is BaseIntegration { commercialRevShare: 10 * 10 ** 6, // 10% royaltyPolicy: royaltyPolicyLRPAddr, currencyToken: testMintFeeToken - }) + }), + allowDuplicates: true }); parentIpIds = new address[](1); diff --git a/test/integration/workflows/GroupingIntegration.t.sol b/test/integration/workflows/GroupingIntegration.t.sol index 5bb926a..84a3bad 100644 --- a/test/integration/workflows/GroupingIntegration.t.sol +++ b/test/integration/workflows/GroupingIntegration.t.sol @@ -79,7 +79,8 @@ contract GroupingIntegration is BaseIntegration { signer: testSender, deadline: deadline, signature: sigAddToGroup - }) + }), + allowDuplicates: true }); assertEq(IIPAccount(payable(groupId)).state(), expectedState); @@ -98,7 +99,12 @@ contract GroupingIntegration is BaseIntegration { { StoryUSD.mint(testSender, testMintFee); StoryUSD.approve(address(spgNftContract), testMintFee); - uint256 tokenId = spgNftContract.mint(testSender, testIpMetadata.nftMetadataURI); + uint256 tokenId = spgNftContract.mint({ + to: testSender, + nftMetadataURI: testIpMetadata.nftMetadataURI, + nftMetadataHash: testIpMetadata.nftMetadataHash, + allowDuplicates: true + }); // get the expected IP ID address expectedIpId = ipAssetRegistry.ipId(block.chainid, address(spgNftContract), tokenId); @@ -231,7 +237,8 @@ contract GroupingIntegration is BaseIntegration { maxMintingFee: 0 }), ipMetadata: testIpMetadata, - recipient: testSender + recipient: testSender, + allowDuplicates: true }); StoryUSD.mint(testSender, testMintFee); @@ -246,7 +253,8 @@ contract GroupingIntegration is BaseIntegration { maxMintingFee: 0 }), ipMetadata: testIpMetadata, - recipient: testSender + recipient: testSender, + allowDuplicates: true }); uint256 amount1 = 1_000 * 10 ** StoryUSD.decimals(); // 1,000 tokens @@ -367,7 +375,12 @@ contract GroupingIntegration is BaseIntegration { // mint a NFT from the spgNftContract uint256[] memory tokenIds = new uint256[](numCalls); for (uint256 i = 0; i < numCalls; i++) { - tokenIds[i] = spgNftContract.mint(testSender, testIpMetadata.nftMetadataURI); + tokenIds[i] = spgNftContract.mint({ + to: testSender, + nftMetadataURI: testIpMetadata.nftMetadataURI, + nftMetadataHash: testIpMetadata.nftMetadataHash, + allowDuplicates: true + }); } // get the expected IP ID diff --git a/test/integration/workflows/LicenseAttachmentIntegration.t.sol b/test/integration/workflows/LicenseAttachmentIntegration.t.sol index 2f42139..6ba9084 100644 --- a/test/integration/workflows/LicenseAttachmentIntegration.t.sol +++ b/test/integration/workflows/LicenseAttachmentIntegration.t.sol @@ -45,7 +45,8 @@ contract LicenseAttachmentIntegration is BaseIntegration { (address ipId, ) = registrationWorkflows.mintAndRegisterIp({ spgNftContract: address(spgNftContract), recipient: testSender, - ipMetadata: testIpMetadata + ipMetadata: testIpMetadata, + allowDuplicates: true }); uint256 deadline = block.timestamp + 1000; @@ -82,7 +83,8 @@ contract LicenseAttachmentIntegration is BaseIntegration { spgNftContract: address(spgNftContract), recipient: testSender, ipMetadata: testIpMetadata, - terms: commUseTerms + terms: commUseTerms, + allowDuplicates: true }); assertTrue(ipAssetRegistry.isRegistered(ipId1)); assertEq(tokenId1, spgNftContract.totalSupply()); @@ -104,7 +106,8 @@ contract LicenseAttachmentIntegration is BaseIntegration { spgNftContract: address(spgNftContract), recipient: testSender, ipMetadata: testIpMetadata, - terms: commUseTerms + terms: commUseTerms, + allowDuplicates: true }); assertTrue(ipAssetRegistry.isRegistered(ipId2)); assertEq(tokenId2, spgNftContract.totalSupply()); @@ -124,7 +127,12 @@ contract LicenseAttachmentIntegration is BaseIntegration { StoryUSD.mint(testSender, testMintFee); StoryUSD.approve(address(spgNftContract), testMintFee); - uint256 tokenId = spgNftContract.mint(testSender, ""); + uint256 tokenId = spgNftContract.mint({ + to: testSender, + nftMetadataURI: "", + nftMetadataHash: bytes32(0), + allowDuplicates: true + }); address expectedIpId = ipAssetRegistry.ipId(block.chainid, address(spgNftContract), tokenId); uint256 deadline = block.timestamp + 1000; diff --git a/test/integration/workflows/RegistrationIntegration.t.sol b/test/integration/workflows/RegistrationIntegration.t.sol index fb5ebea..30d3f23 100644 --- a/test/integration/workflows/RegistrationIntegration.t.sol +++ b/test/integration/workflows/RegistrationIntegration.t.sol @@ -79,7 +79,8 @@ contract RegistrationIntegration is BaseIntegration { (address ipId, uint256 tokenId) = registrationWorkflows.mintAndRegisterIp({ spgNftContract: address(spgNftContract), recipient: testSender, - ipMetadata: testIpMetadata + ipMetadata: testIpMetadata, + allowDuplicates: true }); assertEq(tokenId, 1); @@ -91,7 +92,7 @@ contract RegistrationIntegration is BaseIntegration { function _test_RegistrationIntegration_registerIp() private logTest("test_RegistrationIntegration_registerIp") { StoryUSD.mint(testSender, testMintFee); StoryUSD.approve(address(spgNftContract), testMintFee); - uint256 tokenId = spgNftContract.mint(testSender, ""); + uint256 tokenId = spgNftContract.mint(testSender, "", bytes32(0), true); // get signature for setting IP metadata uint256 deadline = block.timestamp + 1000; diff --git a/test/integration/workflows/RoyaltyIntegration.t.sol b/test/integration/workflows/RoyaltyIntegration.t.sol index b47dd25..06132aa 100644 --- a/test/integration/workflows/RoyaltyIntegration.t.sol +++ b/test/integration/workflows/RoyaltyIntegration.t.sol @@ -284,11 +284,11 @@ contract RoyaltyIntegration is BaseIntegration { /// - `grandChildIp`: It has all 3 license terms attached. It has 3 parents and 1 grandparent IPs. /// @param numSnapshots The number of snapshots to take of the ancestor IP's royalty vault. function _setupIpGraph(uint256 numSnapshots) private { - uint256 ancestorTokenId = spgNftContract.mint(testSender, ""); - uint256 childTokenIdA = spgNftContract.mint(testSender, ""); - uint256 childTokenIdB = spgNftContract.mint(testSender, ""); - uint256 childTokenIdC = spgNftContract.mint(testSender, ""); - uint256 grandChildTokenId = spgNftContract.mint(testSender, ""); + uint256 ancestorTokenId = spgNftContract.mint(testSender, "", bytes32(0), true); + uint256 childTokenIdA = spgNftContract.mint(testSender, "", bytes32(0), true); + uint256 childTokenIdB = spgNftContract.mint(testSender, "", bytes32(0), true); + uint256 childTokenIdC = spgNftContract.mint(testSender, "", bytes32(0), true); + uint256 grandChildTokenId = spgNftContract.mint(testSender, "", bytes32(0), true); WorkflowStructs.IPMetadata memory emptyIpMetadata = WorkflowStructs.IPMetadata({ ipMetadataURI: "", diff --git a/test/workflows/DerivativeWorkflows.t.sol b/test/workflows/DerivativeWorkflows.t.sol index 3f3ba8c..76c22e8 100644 --- a/test/workflows/DerivativeWorkflows.t.sol +++ b/test/workflows/DerivativeWorkflows.t.sol @@ -10,6 +10,7 @@ import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensi import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol"; // contracts +import { Errors } from "../../contracts/lib/Errors.sol"; import { WorkflowStructs } from "../../contracts/lib/WorkflowStructs.sol"; // test @@ -29,7 +30,8 @@ contract DerivativeWorkflowsTest is BaseTest { spgNftContract: address(nftContract), recipient: caller, ipMetadata: ipMetadataDefault, - terms: PILFlavors.nonCommercialSocialRemixing() + terms: PILFlavors.nonCommercialSocialRemixing(), + allowDuplicates: true }); _; } @@ -44,11 +46,69 @@ contract DerivativeWorkflowsTest is BaseTest { commercialRevShare: 10 * 10 ** 6, // 10% royaltyPolicy: address(royaltyPolicyLAP), currencyToken: address(mockToken) - }) + }), + allowDuplicates: true }); _; } + function test_DerivativeWorkflows_revert_DuplicatedNFTMetadataHash() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(derivativeWorkflows)) + withNonCommercialParentIp + { + // First, create an derivative with the same NFT metadata hash but with dedup turned off + (address licenseTemplateParent, uint256 licenseTermsIdParent) = licenseRegistry.getAttachedLicenseTerms( + ipIdParent, + 0 + ); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipIdParent; + + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = licenseTermsIdParent; + + (address ipIdChild, ) = derivativeWorkflows.mintAndRegisterIpAndMakeDerivative({ + spgNftContract: address(nftContract), + derivData: WorkflowStructs.MakeDerivative({ + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + royaltyContext: "", + maxMintingFee: 0 + }), + ipMetadata: ipMetadataDefault, + recipient: caller, + allowDuplicates: true + }); + + // Now attempt to create another derivative with the same NFT metadata hash but with dedup turned on + vm.expectRevert( + abi.encodeWithSelector( + Errors.SPGNFT__DuplicatedNFTMetadataHash.selector, + address(nftContract), + 1, + ipMetadataDefault.nftMetadataHash + ) + ); + derivativeWorkflows.mintAndRegisterIpAndMakeDerivative({ + spgNftContract: address(nftContract), + derivData: WorkflowStructs.MakeDerivative({ + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + royaltyContext: "", + maxMintingFee: 0 + }), + ipMetadata: ipMetadataDefault, + recipient: caller, + allowDuplicates: false + }); + } + function test_DerivativeWorkflows_mintAndRegisterIpAndMakeDerivative_withNonCommercialLicense() public withCollection @@ -123,7 +183,8 @@ contract DerivativeWorkflowsTest is BaseTest { licenseTokenIds: licenseTokenIds, royaltyContext: "", ipMetadata: ipMetadataDefault, - recipient: caller + recipient: caller, + allowDuplicates: true }); assertTrue(ipAssetRegistry.isRegistered(ipIdChild)); assertEq(tokenIdChild, 2); @@ -157,7 +218,12 @@ contract DerivativeWorkflowsTest is BaseTest { 0 ); - uint256 tokenIdChild = nftContract.mint(caller, ipMetadataDefault.nftMetadataURI); + uint256 tokenIdChild = nftContract.mint({ + to: caller, + nftMetadataURI: ipMetadataDefault.nftMetadataURI, + nftMetadataHash: ipMetadataDefault.nftMetadataHash, + allowDuplicates: true + }); address ipIdChild = ipAssetRegistry.ipId(block.chainid, address(nftContract), tokenIdChild); uint256 deadline = block.timestamp + 1000; @@ -252,7 +318,8 @@ contract DerivativeWorkflowsTest is BaseTest { maxMintingFee: 0 }), ipMetadataDefault, - caller + caller, + true ); } @@ -302,7 +369,8 @@ contract DerivativeWorkflowsTest is BaseTest { maxMintingFee: 0 }), ipMetadata: ipMetadataDefault, - recipient: caller + recipient: caller, + allowDuplicates: true }); assertTrue(ipAssetRegistry.isRegistered(ipIdChild)); assertEq(tokenIdChild, 2); @@ -330,7 +398,12 @@ contract DerivativeWorkflowsTest is BaseTest { 0 ); - uint256 tokenIdChild = nftContract.mint(address(caller), ipMetadataDefault.nftMetadataURI); + uint256 tokenIdChild = nftContract.mint({ + to: caller, + nftMetadataURI: ipMetadataDefault.nftMetadataURI, + nftMetadataHash: ipMetadataDefault.nftMetadataHash, + allowDuplicates: true + }); address ipIdChild = ipAssetRegistry.ipId(block.chainid, address(nftContract), tokenIdChild); uint256 deadline = block.timestamp + 1000; diff --git a/test/workflows/GroupingWorkflows.t.sol b/test/workflows/GroupingWorkflows.t.sol index 0b070bb..ae008dd 100644 --- a/test/workflows/GroupingWorkflows.t.sol +++ b/test/workflows/GroupingWorkflows.t.sol @@ -61,6 +61,45 @@ contract GroupingWorkflowsTest is BaseTest { _setupIPs(); } + function test_GroupingWorkflows_revert_DuplicatedNFTMetadataHash() public { + uint256 deadline = block.timestamp + 1000; + + (bytes memory sigAddToGroup, bytes32 expectedState, ) = _getSetPermissionSigForPeriphery({ + ipId: groupId, + to: address(groupingWorkflows), + module: address(groupingModule), + selector: IGroupingModule.addIp.selector, + deadline: deadline, + state: IIPAccount(payable(groupId)).state(), + signerSk: groupOwnerSk + }); + + vm.startPrank(minter); + vm.expectRevert( + abi.encodeWithSelector( + Errors.SPGNFT__DuplicatedNFTMetadataHash.selector, + address(spgNftPublic), + 1, + ipMetadataDefault.nftMetadataHash + ) + ); + groupingWorkflows.mintAndRegisterIpAndAttachLicenseAndAddToGroup({ + spgNftContract: address(spgNftPublic), + groupId: groupId, + recipient: minter, + ipMetadata: ipMetadataDefault, + licenseTemplate: address(pilTemplate), + licenseTermsId: testLicenseTermsId, + sigAddToGroup: WorkflowStructs.SignatureData({ + signer: groupOwner, + deadline: deadline, + signature: sigAddToGroup + }), + allowDuplicates: false + }); + vm.stopPrank(); + } + // Mint → Register IP → Attach license terms → Add new IP to group IPA function test_GroupingWorkflows_mintAndRegisterIpAndAttachLicenseAndAddToGroup() public { uint256 deadline = block.timestamp + 1000; @@ -89,7 +128,8 @@ contract GroupingWorkflowsTest is BaseTest { signer: groupOwner, deadline: deadline, signature: sigAddToGroup - }) + }), + allowDuplicates: true }); vm.stopPrank(); @@ -277,7 +317,8 @@ contract GroupingWorkflowsTest is BaseTest { maxMintingFee: 0 }), ipMetadata: ipMetadataDefault, - recipient: ipOwner1 + recipient: ipOwner1, + allowDuplicates: true }); vm.stopPrank(); @@ -296,7 +337,8 @@ contract GroupingWorkflowsTest is BaseTest { maxMintingFee: 0 }), ipMetadata: ipMetadataDefault, - recipient: ipOwner2 + recipient: ipOwner2, + allowDuplicates: true }); vm.stopPrank(); @@ -399,7 +441,8 @@ contract GroupingWorkflowsTest is BaseTest { pilTemplate, testLicenseTermsId, ipMetadataDefault, - WorkflowStructs.SignatureData({ signer: groupOwner, deadline: deadline, signature: sigsAddToGroup[i] }) + WorkflowStructs.SignatureData({ signer: groupOwner, deadline: deadline, signature: sigsAddToGroup[i] }), + true ); } @@ -542,7 +585,8 @@ contract GroupingWorkflowsTest is BaseTest { registrationWorkflows.mintAndRegisterIp.selector, address(spgNftPublic), minter, - ipMetadataDefault + ipMetadataDefault, + true ); } diff --git a/test/workflows/LicenseAttachmentWorkflows.t.sol b/test/workflows/LicenseAttachmentWorkflows.t.sol index 2ae6909..3f9e667 100644 --- a/test/workflows/LicenseAttachmentWorkflows.t.sol +++ b/test/workflows/LicenseAttachmentWorkflows.t.sol @@ -11,8 +11,8 @@ import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensi import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol"; // contracts +import { Errors } from "../../contracts/lib/Errors.sol"; import { WorkflowStructs } from "../../contracts/lib/WorkflowStructs.sol"; - // test import { BaseTest } from "../utils/BaseTest.t.sol"; @@ -38,13 +38,46 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { (address ipId, uint256 tokenId) = registrationWorkflows.mintAndRegisterIp({ spgNftContract: address(nftContract), recipient: owner, - ipMetadata: ipMetadataDefault + ipMetadata: ipMetadataDefault, + allowDuplicates: true }); ipAsset[1] = IPAsset({ ipId: payable(ipId), tokenId: tokenId, owner: owner }); vm.stopPrank(); _; } + function test_LicenseAttachmentWorkflows_revert_DuplicatedNFTMetadataHash() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(licenseAttachmentWorkflows)) + { + (address ipId1, uint256 tokenId1, uint256 licenseTermsId1) = licenseAttachmentWorkflows + .mintAndRegisterIpAndAttachPILTerms({ + spgNftContract: address(nftContract), + recipient: caller, + ipMetadata: ipMetadataDefault, + terms: PILFlavors.nonCommercialSocialRemixing(), + allowDuplicates: true + }); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.SPGNFT__DuplicatedNFTMetadataHash.selector, + address(nftContract), + 1, + ipMetadataDefault.nftMetadataHash + ) + ); + licenseAttachmentWorkflows.mintAndRegisterIpAndAttachPILTerms({ + spgNftContract: address(nftContract), + recipient: caller, + ipMetadata: ipMetadataDefault, + terms: PILFlavors.nonCommercialSocialRemixing(), + allowDuplicates: false + }); + } + function test_LicenseAttachmentWorkflows_registerPILTermsAndAttach() public withCollection withIp(u.alice) { address payable ipId = ipAsset[1].ipId; uint256 deadline = block.timestamp + 1000; @@ -85,7 +118,8 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { spgNftContract: address(nftContract), recipient: caller, ipMetadata: ipMetadataEmpty, - terms: PILFlavors.nonCommercialSocialRemixing() + terms: PILFlavors.nonCommercialSocialRemixing(), + allowDuplicates: true }); assertTrue(ipAssetRegistry.isRegistered(ipId1)); assertEq(tokenId1, 1); @@ -101,7 +135,8 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { spgNftContract: address(nftContract), recipient: caller, ipMetadata: ipMetadataDefault, - terms: PILFlavors.nonCommercialSocialRemixing() + terms: PILFlavors.nonCommercialSocialRemixing(), + allowDuplicates: true }); assertTrue(ipAssetRegistry.isRegistered(ipId2)); assertEq(tokenId2, 2); @@ -116,7 +151,12 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { whenCallerHasMinterRole withEnoughTokens(address(licenseAttachmentWorkflows)) { - uint256 tokenId = nftContract.mint(address(caller), ipMetadataEmpty.nftMetadataURI); + uint256 tokenId = nftContract.mint({ + to: caller, + nftMetadataURI: ipMetadataEmpty.nftMetadataURI, + nftMetadataHash: ipMetadataEmpty.nftMetadataHash, + allowDuplicates: true + }); address payable ipId = payable(ipAssetRegistry.ipId(block.chainid, address(nftContract), tokenId)); uint256 deadline = block.timestamp + 1000; @@ -214,7 +254,8 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { spgNftContract: address(nftContract), recipient: caller, ipMetadata: ipMetadataDefault, - terms: PILFlavors.nonCommercialSocialRemixing() + terms: PILFlavors.nonCommercialSocialRemixing(), + allowDuplicates: true }); address[] memory parentIpIds = new address[](1); @@ -233,7 +274,8 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { maxMintingFee: 0 }), ipMetadata: ipMetadataDefault, - recipient: caller + recipient: caller, + allowDuplicates: true }); uint256 deadline = block.timestamp + 1000; diff --git a/test/workflows/RegistrationWorkflows.t.sol b/test/workflows/RegistrationWorkflows.t.sol index 6b0c83e..8a26b69 100644 --- a/test/workflows/RegistrationWorkflows.t.sol +++ b/test/workflows/RegistrationWorkflows.t.sol @@ -62,7 +62,43 @@ contract RegistrationWorkflowsTest is BaseTest { registrationWorkflows.mintAndRegisterIp({ spgNftContract: address(nftContract), recipient: u.bob, - ipMetadata: ipMetadataEmpty + ipMetadata: ipMetadataEmpty, + allowDuplicates: true + }); + } + + function test_RegistrationWorkflows_revert_duplicatedNftMetadataHash() + public + withCollection + whenCallerHasMinterRole + { + mockToken.mint(address(caller), 1000 * 10 ** mockToken.decimals()); + mockToken.approve(address(nftContract), 1000 * 10 ** mockToken.decimals()); + + (address ipId1, uint256 tokenId1) = registrationWorkflows.mintAndRegisterIp({ + spgNftContract: address(nftContract), + recipient: u.bob, + ipMetadata: ipMetadataDefault, + allowDuplicates: false + }); + assertEq(tokenId1, 1); + assertTrue(ipAssetRegistry.isRegistered(ipId1)); + assertEq(nftContract.tokenURI(tokenId1), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); + assertMetadata(ipId1, ipMetadataDefault); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.SPGNFT__DuplicatedNFTMetadataHash.selector, + address(nftContract), + tokenId1, + ipMetadataDefault.nftMetadataHash + ) + ); + registrationWorkflows.mintAndRegisterIp({ + spgNftContract: address(nftContract), + recipient: u.bob, + ipMetadata: ipMetadataDefault, + allowDuplicates: false }); } @@ -94,7 +130,8 @@ contract RegistrationWorkflowsTest is BaseTest { (address ipId, uint256 tokenId) = registrationWorkflows.mintAndRegisterIp({ spgNftContract: address(nftContract), recipient: u.bob, - ipMetadata: ipMetadataEmpty + ipMetadata: ipMetadataEmpty, + allowDuplicates: true }); vm.stopPrank(); @@ -111,7 +148,8 @@ contract RegistrationWorkflowsTest is BaseTest { (address ipId1, uint256 tokenId1) = registrationWorkflows.mintAndRegisterIp({ spgNftContract: address(nftContract), recipient: u.bob, - ipMetadata: ipMetadataEmpty + ipMetadata: ipMetadataEmpty, + allowDuplicates: true }); assertEq(tokenId1, 1); assertTrue(ipAssetRegistry.isRegistered(ipId1)); @@ -121,7 +159,8 @@ contract RegistrationWorkflowsTest is BaseTest { (address ipId2, uint256 tokenId2) = registrationWorkflows.mintAndRegisterIp({ spgNftContract: address(nftContract), recipient: u.bob, - ipMetadata: ipMetadataDefault + ipMetadata: ipMetadataDefault, + allowDuplicates: true }); assertEq(tokenId2, 2); assertTrue(ipAssetRegistry.isRegistered(ipId2)); @@ -209,7 +248,8 @@ contract RegistrationWorkflowsTest is BaseTest { registrationWorkflows.mintAndRegisterIp.selector, address(nftContract), u.bob, - ipMetadataDefault + ipMetadataDefault, + true ); } bytes[] memory results = registrationWorkflows.multicall(data);