Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(spg-nft): add deduplication option to mint functions #108

Merged
merged 7 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading