Skip to content

Commit

Permalink
feat(spg-nft): add deduplication option to mint functions (#108)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sebsadface authored Oct 30, 2024
1 parent 8ee3584 commit 8edb5f3
Show file tree
Hide file tree
Showing 21 changed files with 500 additions and 81 deletions.
74 changes: 66 additions & 8 deletions contracts/SPGNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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);
Expand Down
28 changes: 24 additions & 4 deletions contracts/interfaces/ISPGNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 6 additions & 2 deletions contracts/interfaces/workflows/IDerivativeWorkflows.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -46,14 +48,16 @@ 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(
address spgNftContract,
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.
Expand Down
4 changes: 3 additions & 1 deletion contracts/interfaces/workflows/IGroupingWorkflows.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ 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.
function mintAndRegisterIpAndAttachPILTerms(
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.
Expand Down
5 changes: 4 additions & 1 deletion contracts/interfaces/workflows/IRegistrationWorkflows.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
18 changes: 14 additions & 4 deletions contracts/workflows/DerivativeWorkflows.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -212,22 +217,27 @@ 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(
address spgNftContract,
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);

Expand Down
9 changes: 7 additions & 2 deletions contracts/workflows/GroupingWorkflows.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);

Expand Down
Loading

0 comments on commit 8edb5f3

Please sign in to comment.