Skip to content

Commit 8edb5f3

Browse files
authored
feat(spg-nft): add deduplication option to mint functions (#108)
* feat(SPGNFT): add dedup option for nft minting * feat(workflows): add dedup support for workflow fns * test: add test for the new dedup feature * chore: linting and comment improvement * fix(registration): update dedup behavior * fix: rename dedup param & revert inside SPGNFT * Update BaseWorkflow.sol
1 parent 8ee3584 commit 8edb5f3

21 files changed

+500
-81
lines changed

contracts/SPGNFT.sol

+66-8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
2222
/// @param _publicMinting True if the collection is open for everyone to mint.
2323
/// @param _baseURI The base URI for the collection. If baseURI is not empty, tokenURI will be
2424
/// either baseURI + token ID (if nftMetadataURI is empty) or baseURI + nftMetadataURI.
25+
/// @param _nftMetadataHashToTokenId The mapping of nftMetadataHash to token ID.
2526
/// @custom:storage-location erc7201:story-protocol-periphery.SPGNFT
2627
struct SPGNFTStorage {
2728
uint32 _maxSupply;
@@ -33,6 +34,7 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
3334
bool _publicMinting;
3435
string _baseURI;
3536
string _contractURI;
37+
mapping(bytes32 nftMetadataHash => uint256 tokenId) _nftMetadataHashToTokenId;
3638
}
3739

3840
// keccak256(abi.encode(uint256(keccak256("story-protocol-periphery.SPGNFT")) - 1)) & ~bytes32(uint256(0xff));
@@ -148,6 +150,14 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
148150
return _getSPGNFTStorage()._contractURI;
149151
}
150152

153+
/// @notice Returns the token ID by the metadata hash.
154+
/// @dev Returns 0 if the metadata hash has not been used in this collection.
155+
/// @param nftMetadataHash A bytes32 hash of the NFT's metadata.
156+
/// @return tokenId The token ID of the NFT with the given metadata hash.
157+
function getTokenIdByMetadataHash(bytes32 nftMetadataHash) external view returns (uint256) {
158+
return _getSPGNFTStorage()._nftMetadataHashToTokenId[nftMetadataHash];
159+
}
160+
151161
/// @notice Sets the fee to mint an NFT from the collection. Payment is in the designated currency.
152162
/// @dev Only callable by the admin role.
153163
/// @param fee The new mint fee paid in the mint token.
@@ -207,25 +217,50 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
207217
/// @notice Mints an NFT from the collection. Only callable by the minter role.
208218
/// @param to The address of the recipient of the minted NFT.
209219
/// @param nftMetadataURI OPTIONAL. The URI of the desired metadata for the newly minted NFT.
210-
/// @return tokenId The ID of the minted NFT.
211-
function mint(address to, string calldata nftMetadataURI) public virtual returns (uint256 tokenId) {
220+
/// @param nftMetadataHash OPTIONAL. A bytes32 hash of the NFT's metadata.
221+
/// This metadata is accessible via the NFT's tokenURI.
222+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
223+
/// @return tokenId The token ID of the minted NFT with the given metadata hash.
224+
function mint(
225+
address to,
226+
string calldata nftMetadataURI,
227+
bytes32 nftMetadataHash,
228+
bool allowDuplicates
229+
) public virtual returns (uint256 tokenId) {
212230
if (!_getSPGNFTStorage()._publicMinting && !hasRole(SPGNFTLib.MINTER_ROLE, msg.sender)) {
213231
revert Errors.SPGNFT__MintingDenied();
214232
}
215-
tokenId = _mintToken({ to: to, payer: msg.sender, nftMetadataURI: nftMetadataURI });
233+
tokenId = _mintToken({
234+
to: to,
235+
payer: msg.sender,
236+
nftMetadataURI: nftMetadataURI,
237+
nftMetadataHash: nftMetadataHash,
238+
allowDuplicates: allowDuplicates
239+
});
216240
}
217241

218242
/// @notice Mints an NFT from the collection. Only callable by the Periphery contracts.
219243
/// @param to The address of the recipient of the minted NFT.
220244
/// @param payer The address of the payer for the mint fee.
221245
/// @param nftMetadataURI OPTIONAL. The URI of the desired metadata for the newly minted NFT.
222-
/// @return tokenId The ID of the minted NFT.
246+
/// @param nftMetadataHash OPTIONAL. A bytes32 hash of the NFT's metadata.
247+
/// This metadata is accessible via the NFT's tokenURI.
248+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
249+
/// @return tokenId The token ID of the minted NFT with the given metadata hash.
223250
function mintByPeriphery(
224251
address to,
225252
address payer,
226-
string calldata nftMetadataURI
253+
string calldata nftMetadataURI,
254+
bytes32 nftMetadataHash,
255+
bool allowDuplicates
227256
) public virtual onlyPeriphery returns (uint256 tokenId) {
228-
tokenId = _mintToken({ to: to, payer: payer, nftMetadataURI: nftMetadataURI });
257+
tokenId = _mintToken({
258+
to: to,
259+
payer: payer,
260+
nftMetadataURI: nftMetadataURI,
261+
nftMetadataHash: nftMetadataHash,
262+
allowDuplicates: allowDuplicates
263+
});
229264
}
230265

231266
/// @dev Withdraws the contract's token balance to the fee recipient.
@@ -246,17 +281,40 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
246281
/// @param to The address of the recipient of the minted NFT.
247282
/// @param payer The address of the payer for the mint fee.
248283
/// @param nftMetadataURI OPTIONAL. The URI of the desired metadata for the newly minted NFT.
249-
/// @return tokenId The ID of the minted NFT.
250-
function _mintToken(address to, address payer, string calldata nftMetadataURI) internal returns (uint256 tokenId) {
284+
/// @param nftMetadataHash OPTIONAL. A bytes32 hash of the NFT's metadata.
285+
/// This metadata is accessible via the NFT's tokenURI.
286+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
287+
/// @return tokenId The token ID of the minted NFT with the given metadata hash.
288+
function _mintToken(
289+
address to,
290+
address payer,
291+
string calldata nftMetadataURI,
292+
bytes32 nftMetadataHash,
293+
bool allowDuplicates
294+
) internal returns (uint256 tokenId) {
251295
SPGNFTStorage storage $ = _getSPGNFTStorage();
252296
if (!$._mintOpen) revert Errors.SPGNFT__MintingClosed();
253297
if ($._totalSupply + 1 > $._maxSupply) revert Errors.SPGNFT__MaxSupplyReached();
254298

299+
tokenId = $._nftMetadataHashToTokenId[nftMetadataHash];
300+
if (!allowDuplicates && tokenId != 0) {
301+
revert Errors.SPGNFT__DuplicatedNFTMetadataHash({
302+
spgNftContract: address(this),
303+
tokenId: tokenId,
304+
nftMetadataHash: nftMetadataHash
305+
});
306+
}
307+
255308
if ($._mintFeeToken != address(0) && $._mintFee > 0) {
256309
IERC20($._mintFeeToken).transferFrom(payer, address(this), $._mintFee);
257310
}
258311

259312
tokenId = ++$._totalSupply;
313+
if ($._nftMetadataHashToTokenId[nftMetadataHash] == 0) {
314+
// only store the token ID if the metadata hash is not used
315+
$._nftMetadataHashToTokenId[nftMetadataHash] = tokenId;
316+
}
317+
260318
_mint(to, tokenId);
261319

262320
if (bytes(nftMetadataURI).length > 0) _setTokenURI(tokenId, nftMetadataURI);

contracts/interfaces/ISPGNFT.sol

+24-4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ interface ISPGNFT is IAccessControl, IERC721Metadata, IERC7572 {
6565
/// or baseURI + token ID (if nftMetadataURI is empty).
6666
function baseURI() external view returns (string memory);
6767

68+
/// @notice Returns the token ID by the metadata hash.
69+
/// @dev Returns 0 if the metadata hash has not been used in this collection.
70+
/// @param nftMetadataHash A bytes32 hash of the NFT's metadata.
71+
/// This metadata is accessible via the NFT's tokenURI.
72+
/// @return tokenId The token ID of the NFT with the given metadata hash.
73+
function getTokenIdByMetadataHash(bytes32 nftMetadataHash) external view returns (uint256);
74+
6875
/// @notice Sets the fee to mint an NFT from the collection. Payment is in the designated currency.
6976
/// @dev Only callable by the admin role.
7077
/// @param fee The new mint fee paid in the mint token.
@@ -105,18 +112,31 @@ interface ISPGNFT is IAccessControl, IERC721Metadata, IERC7572 {
105112
/// @notice Mints an NFT from the collection. Only callable by the minter role.
106113
/// @param to The address of the recipient of the minted NFT.
107114
/// @param nftMetadataURI OPTIONAL. The desired metadata for the newly minted NFT.
108-
/// @return tokenId The ID of the minted NFT.
109-
function mint(address to, string calldata nftMetadataURI) external returns (uint256 tokenId);
115+
/// @param nftMetadataHash OPTIONAL. A bytes32 hash of the NFT's metadata.
116+
/// This metadata is accessible via the NFT's tokenURI.
117+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
118+
/// @return tokenId The token ID of the minted NFT with the given metadata hash.
119+
function mint(
120+
address to,
121+
string calldata nftMetadataURI,
122+
bytes32 nftMetadataHash,
123+
bool allowDuplicates
124+
) external returns (uint256 tokenId);
110125

111126
/// @notice Mints an NFT from the collection. Only callable by Periphery contracts.
112127
/// @param to The address of the recipient of the minted NFT.
113128
/// @param payer The address of the payer for the mint fee.
114129
/// @param nftMetadataURI OPTIONAL. The desired metadata for the newly minted NFT.
115-
/// @return tokenId The ID of the minted NFT.
130+
/// @param nftMetadataHash OPTIONAL. A bytes32 hash of the NFT's metadata.
131+
/// This metadata is accessible via the NFT's tokenURI.
132+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
133+
/// @return tokenId The token ID of the minted NFT with the given metadata hash.
116134
function mintByPeriphery(
117135
address to,
118136
address payer,
119-
string calldata nftMetadataURI
137+
string calldata nftMetadataURI,
138+
bytes32 nftMetadataHash,
139+
bool allowDuplicates
120140
) external returns (uint256 tokenId);
121141

122142
/// @dev Withdraws the contract's token balance to the fee recipient.

contracts/interfaces/workflows/IDerivativeWorkflows.sol

+6-2
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ interface IDerivativeWorkflows {
1212
/// @param derivData The derivative data to be used for registerDerivative.
1313
/// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP.
1414
/// @param recipient The address to receive the minted NFT.
15+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
1516
/// @return ipId The ID of the newly registered IP.
1617
/// @return tokenId The ID of the newly minted NFT.
1718
function mintAndRegisterIpAndMakeDerivative(
1819
address spgNftContract,
1920
WorkflowStructs.MakeDerivative calldata derivData,
2021
WorkflowStructs.IPMetadata calldata ipMetadata,
21-
address recipient
22+
address recipient,
23+
bool allowDuplicates
2224
) external returns (address ipId, uint256 tokenId);
2325

2426
/// @notice Register the given NFT as a derivative IP with metadata without license tokens.
@@ -46,14 +48,16 @@ interface IDerivativeWorkflows {
4648
/// @param royaltyContext The context for royalty module, should be empty for Royalty Policy LAP.
4749
/// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP.
4850
/// @param recipient The address to receive the minted NFT.
51+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
4952
/// @return ipId The ID of the newly registered IP.
5053
/// @return tokenId The ID of the newly minted NFT.
5154
function mintAndRegisterIpAndMakeDerivativeWithLicenseTokens(
5255
address spgNftContract,
5356
uint256[] calldata licenseTokenIds,
5457
bytes calldata royaltyContext,
5558
WorkflowStructs.IPMetadata calldata ipMetadata,
56-
address recipient
59+
address recipient,
60+
bool allowDuplicates
5761
) external returns (address ipId, uint256 tokenId);
5862

5963
/// @notice Register the given NFT as a derivative IP using license tokens.

contracts/interfaces/workflows/IGroupingWorkflows.sol

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface IGroupingWorkflows {
1616
/// @param licenseTermsId The ID of the registered license terms that will be attached to the new IP.
1717
/// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP.
1818
/// @param sigAddToGroup Signature data for addIp to the group IP via the Grouping Module.
19+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
1920
/// @return ipId The ID of the newly registered IP.
2021
/// @return tokenId The ID of the newly minted NFT.
2122
function mintAndRegisterIpAndAttachLicenseAndAddToGroup(
@@ -25,7 +26,8 @@ interface IGroupingWorkflows {
2526
address licenseTemplate,
2627
uint256 licenseTermsId,
2728
WorkflowStructs.IPMetadata calldata ipMetadata,
28-
WorkflowStructs.SignatureData calldata sigAddToGroup
29+
WorkflowStructs.SignatureData calldata sigAddToGroup,
30+
bool allowDuplicates
2931
) external returns (address ipId, uint256 tokenId);
3032

3133
/// @notice Register an NFT as IP with metadata, attach license terms to the registered IP,

contracts/interfaces/workflows/ILicenseAttachmentWorkflows.sol

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ interface ILicenseAttachmentWorkflows {
2727
/// @param recipient The address of the recipient of the minted NFT.
2828
/// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP.
2929
/// @param terms The PIL terms to be registered.
30+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
3031
/// @return ipId The ID of the newly registered IP.
3132
/// @return tokenId The ID of the newly minted NFT.
3233
/// @return licenseTermsId The ID of the newly registered PIL terms.
3334
function mintAndRegisterIpAndAttachPILTerms(
3435
address spgNftContract,
3536
address recipient,
3637
WorkflowStructs.IPMetadata calldata ipMetadata,
37-
PILTerms calldata terms
38+
PILTerms calldata terms,
39+
bool allowDuplicates
3840
) external returns (address ipId, uint256 tokenId, uint256 licenseTermsId);
3941

4042
/// @notice Register a given NFT as an IP and attach Programmable IP License Terms.

contracts/interfaces/workflows/IRegistrationWorkflows.sol

+4-1
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ interface IRegistrationWorkflows {
2121
/// @param spgNftContract The address of the SPGNFT collection.
2222
/// @param recipient The address of the recipient of the minted NFT.
2323
/// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP.
24+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
25+
/// If a duplicate is found, returns existing token Id and IP Id instead of minting/registering a new one.
2426
/// @return ipId The ID of the registered IP.
2527
/// @return tokenId The ID of the newly minted NFT.
2628
function mintAndRegisterIp(
2729
address spgNftContract,
2830
address recipient,
29-
WorkflowStructs.IPMetadata calldata ipMetadata
31+
WorkflowStructs.IPMetadata calldata ipMetadata,
32+
bool allowDuplicates
3033
) external returns (address ipId, uint256 tokenId);
3134

3235
/// @notice Registers an NFT as IP with metadata.

contracts/lib/Errors.sol

+6
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,10 @@ library Errors {
7171

7272
/// @notice Caller is not one of the periphery contracts.
7373
error SPGNFT__CallerNotPeripheryContract();
74+
75+
/// @notice Error thrown when attempting to mint an NFT with a metadata hash that already exists.
76+
/// @param spgNftContract The address of the SPGNFT collection contract where the duplicate was detected.
77+
/// @param tokenId The ID of the original NFT that was first minted with this metadata hash.
78+
/// @param nftMetadataHash The hash of the NFT metadata that caused the duplication error.
79+
error SPGNFT__DuplicatedNFTMetadataHash(address spgNftContract, uint256 tokenId, bytes32 nftMetadataHash);
7480
}

contracts/workflows/DerivativeWorkflows.sol

+14-4
Original file line numberDiff line numberDiff line change
@@ -114,19 +114,24 @@ contract DerivativeWorkflows is
114114
/// @param derivData The derivative data to be used for registerDerivative.
115115
/// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP.
116116
/// @param recipient The address to receive the minted NFT.
117+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
117118
/// @return ipId The ID of the newly registered IP.
118119
/// @return tokenId The ID of the newly minted NFT.
119120
function mintAndRegisterIpAndMakeDerivative(
120121
address spgNftContract,
121122
WorkflowStructs.MakeDerivative calldata derivData,
122123
WorkflowStructs.IPMetadata calldata ipMetadata,
123-
address recipient
124+
address recipient,
125+
bool allowDuplicates
124126
) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId) {
125127
tokenId = ISPGNFT(spgNftContract).mintByPeriphery({
126128
to: address(this),
127129
payer: msg.sender,
128-
nftMetadataURI: ipMetadata.nftMetadataURI
130+
nftMetadataURI: ipMetadata.nftMetadataURI,
131+
nftMetadataHash: ipMetadata.nftMetadataHash,
132+
allowDuplicates: allowDuplicates
129133
});
134+
130135
ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId);
131136

132137
MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata);
@@ -212,22 +217,27 @@ contract DerivativeWorkflows is
212217
/// @param royaltyContext The context for royalty module, should be empty for Royalty Policy LAP.
213218
/// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and newly registered IP.
214219
/// @param recipient The address to receive the minted NFT.
220+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
215221
/// @return ipId The ID of the registered IP.
216222
/// @return tokenId The ID of the minted NFT.
217223
function mintAndRegisterIpAndMakeDerivativeWithLicenseTokens(
218224
address spgNftContract,
219225
uint256[] calldata licenseTokenIds,
220226
bytes calldata royaltyContext,
221227
WorkflowStructs.IPMetadata calldata ipMetadata,
222-
address recipient
228+
address recipient,
229+
bool allowDuplicates
223230
) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId) {
224231
_collectLicenseTokens(licenseTokenIds, address(LICENSE_TOKEN));
225232

226233
tokenId = ISPGNFT(spgNftContract).mintByPeriphery({
227234
to: address(this),
228235
payer: msg.sender,
229-
nftMetadataURI: ipMetadata.nftMetadataURI
236+
nftMetadataURI: ipMetadata.nftMetadataURI,
237+
nftMetadataHash: ipMetadata.nftMetadataHash,
238+
allowDuplicates: allowDuplicates
230239
});
240+
231241
ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId);
232242
MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata);
233243

contracts/workflows/GroupingWorkflows.sol

+7-2
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ contract GroupingWorkflows is
120120
/// @param licenseTermsId The ID of the registered license terms that will be attached to the new IP.
121121
/// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP.
122122
/// @param sigAddToGroup Signature data for addIp to the group IP via the Grouping Module.
123+
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
123124
/// @return ipId The ID of the newly registered IP.
124125
/// @return tokenId The ID of the newly minted NFT.
125126
function mintAndRegisterIpAndAttachLicenseAndAddToGroup(
@@ -129,13 +130,17 @@ contract GroupingWorkflows is
129130
address licenseTemplate,
130131
uint256 licenseTermsId,
131132
WorkflowStructs.IPMetadata calldata ipMetadata,
132-
WorkflowStructs.SignatureData calldata sigAddToGroup
133+
WorkflowStructs.SignatureData calldata sigAddToGroup,
134+
bool allowDuplicates
133135
) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId) {
134136
tokenId = ISPGNFT(spgNftContract).mintByPeriphery({
135137
to: address(this),
136138
payer: msg.sender,
137-
nftMetadataURI: ipMetadata.nftMetadataURI
139+
nftMetadataURI: ipMetadata.nftMetadataURI,
140+
nftMetadataHash: ipMetadata.nftMetadataHash,
141+
allowDuplicates: allowDuplicates
138142
});
143+
139144
ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId);
140145
MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata);
141146

0 commit comments

Comments
 (0)