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(story-nft): implement IP metadata setting during mint #113

Merged
merged 1 commit into from
Nov 5, 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
9 changes: 5 additions & 4 deletions contracts/interfaces/story-nft/IOrgNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.26;

import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
import { WorkflowStructs } from "../../lib/WorkflowStructs.sol";

/// @title Organization NFT Interface
/// @notice Each organization token represents a Story ecosystem project.
Expand Down Expand Up @@ -46,23 +47,23 @@ interface IOrgNFT is IERC721Metadata {
////////////////////////////////////////////////////////////////////////////
/// @notice Mints the root organization token and register it as an IP.
/// @param recipient The address of the recipient of the root organization token.
/// @param tokenURI The URI of the root organization token.
/// @param orgIpMetadata OPTIONAL. The desired metadata for the newly minted OrgNFT and registered IP.
/// @return rootOrgTokenId The ID of the root organization token.
/// @return rootOrgIpId The ID of the root organization IP.
function mintRootOrgNft(
address recipient,
string memory tokenURI
WorkflowStructs.IPMetadata calldata orgIpMetadata
) external returns (uint256 rootOrgTokenId, address rootOrgIpId);

/// @notice Mints a organization token, register it as an IP,
/// and makes the IP as a derivative of the root organization IP.
/// @param recipient The address of the recipient of the minted organization token.
/// @param tokenURI The URI of the minted organization token.
/// @param orgIpMetadata OPTIONAL. The desired metadata for the newly minted OrgNFT and registered IP.
/// @return orgTokenId The ID of the minted organization token.
/// @return orgIpId The ID of the organization IP.
function mintOrgNft(
address recipient,
string memory tokenURI
WorkflowStructs.IPMetadata calldata orgIpMetadata
) external returns (uint256 orgTokenId, address orgIpId);

/// @notice Sets the tokenURI of `tokenId` organization token.
Expand Down
9 changes: 5 additions & 4 deletions contracts/interfaces/story-nft/IOrgStoryNFTFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.26;

import { IStoryNFT } from "./IStoryNFT.sol";
import { WorkflowStructs } from "../../lib/WorkflowStructs.sol";

/// @title Organization Story NFT Factory Interface
/// @notice Organization Story NFT Factory is the entrypoint for creating new Story NFT collections.
Expand Down Expand Up @@ -76,7 +77,7 @@ interface IOrgStoryNFTFactory {
/// @param orgStoryNftTemplate The address of a whitelisted OrgStoryNFT template to be cloned.
/// @param orgNftRecipient The address of the recipient of the organization NFT.
/// @param orgName The name of the organization.
/// @param orgTokenURI The token URI of the organization NFT.
/// @param orgIpMetadata OPTIONAL. The desired metadata for the newly minted OrgNFT and registered IP.
/// @param signature The signature from the OrgStoryNFTFactory's whitelist signer. This signautre is genreated by
/// having the whitelist signer sign the caller's address (msg.sender) for this `deployOrgStoryNft` function.
/// @param storyNftInitParams The initialization parameters for StoryNFT {see {IStoryNFT-StoryNftInitParams}}.
Expand All @@ -88,7 +89,7 @@ interface IOrgStoryNFTFactory {
address orgStoryNftTemplate,
address orgNftRecipient,
string calldata orgName,
string calldata orgTokenURI,
WorkflowStructs.IPMetadata calldata orgIpMetadata,
bytes calldata signature,
IStoryNFT.StoryNftInitParams calldata storyNftInitParams
) external returns (address orgNft, uint256 orgTokenId, address orgIpId, address orgStoryNft);
Expand All @@ -99,7 +100,7 @@ interface IOrgStoryNFTFactory {
/// @param orgStoryNftTemplate The address of a whitelisted OrgStoryNFT template to be cloned.
/// @param orgNftRecipient The address of the recipient of the organization NFT.
/// @param orgName The name of the organization.
/// @param orgTokenURI The token URI of the organization NFT.
/// @param orgIpMetadata OPTIONAL. The desired metadata for the newly minted OrgNFT and registered IP.
/// @param storyNftInitParams The initialization parameters for StoryNFT {see {IStoryNFT-StoryNftInitParams}}.
/// @param isRootOrg Whether the organization is the root organization.
/// @return orgNft The address of the organization NFT.
Expand All @@ -110,7 +111,7 @@ interface IOrgStoryNFTFactory {
address orgStoryNftTemplate,
address orgNftRecipient,
string calldata orgName,
string calldata orgTokenURI,
WorkflowStructs.IPMetadata calldata orgIpMetadata,
IStoryNFT.StoryNftInitParams calldata storyNftInitParams,
bool isRootOrg
) external returns (address orgNft, uint256 orgTokenId, address orgIpId, address orgStoryNft);
Expand Down
6 changes: 6 additions & 0 deletions contracts/interfaces/story-nft/IStoryBadgeNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ interface IStoryBadgeNFT is IStoryNFT, IERC721Metadata, IERC5192 {
/// @notice Struct for custom data for initializing the StoryBadgeNFT contract.
/// @param tokenURI The token URI for all the badges (follows OpenSea metadata standard).
/// @param signer The signer of the whitelist signatures.
/// @param ipMetadataURI The URI of the metadata for all IP from this collection.
/// @param ipMetadataHash The hash of the metadata for all IP from this collection.
/// @param nftMetadataHash The hash of the metadata for all IP NFTs from this collection.
struct CustomInitParams {
string tokenURI;
address signer;
string ipMetadataURI;
bytes32 ipMetadataHash;
bytes32 nftMetadataHash;
}

////////////////////////////////////////////////////////////////////////////
Expand Down
3 changes: 2 additions & 1 deletion contracts/story-nft/BaseOrgStoryNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ abstract contract BaseOrgStoryNFT is IOrgStoryNFT, BaseStoryNFT {
constructor(
address ipAssetRegistry,
address licensingModule,
address coreMetadataModule,
address upgradeableBeacon,
address orgNft
) BaseStoryNFT(ipAssetRegistry, licensingModule) {
) BaseStoryNFT(ipAssetRegistry, licensingModule, coreMetadataModule) {
if (orgNft == address(0)) revert StoryNFT__ZeroAddressParam();
ORG_NFT = orgNft;
UPGRADEABLE_BEACON = upgradeableBeacon;
Expand Down
31 changes: 27 additions & 4 deletions contracts/story-nft/BaseStoryNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol
/* solhint-disable-next-line max-line-length */
import { ERC721URIStorageUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { ICoreMetadataModule } from "@storyprotocol/core/interfaces/modules/metadata/ICoreMetadataModule.sol";
import { IIPAssetRegistry } from "@story-protocol/protocol-core/contracts/interfaces/registries/IIPAssetRegistry.sol";
/*solhint-disable-next-line max-line-length*/
import { ILicensingModule } from "@story-protocol/protocol-core/contracts/interfaces/modules/licensing/ILicensingModule.sol";
Expand All @@ -25,6 +26,10 @@ abstract contract BaseStoryNFT is IStoryNFT, ERC721URIStorageUpgradeable, Ownabl
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
ILicensingModule public immutable LICENSING_MODULE;

/// @notice Core Metadata Module address.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
ICoreMetadataModule public immutable CORE_METADATA_MODULE;

/// @dev Storage structure for the BaseStoryNFT
/// @param contractURI The contract URI of the collection.
/// @param baseURI The base URI of the collection.
Expand All @@ -40,10 +45,12 @@ abstract contract BaseStoryNFT is IStoryNFT, ERC721URIStorageUpgradeable, Ownabl
bytes32 private constant BaseStoryNFTStorageLocation =
0x81ed94d7560ff7bef5060a232718049e514c358c346e3254b876807a753c0e00;

constructor(address ipAssetRegistry, address licensingModule) {
if (ipAssetRegistry == address(0) || licensingModule == address(0)) revert StoryNFT__ZeroAddressParam();
constructor(address ipAssetRegistry, address licensingModule, address coreMetadataModule) {
if (ipAssetRegistry == address(0) || licensingModule == address(0) || coreMetadataModule == address(0))
revert StoryNFT__ZeroAddressParam();
IP_ASSET_REGISTRY = IIPAssetRegistry(ipAssetRegistry);
LICENSING_MODULE = ILicensingModule(licensingModule);
CORE_METADATA_MODULE = ICoreMetadataModule(coreMetadataModule);

_disableInitializers();
}
Expand Down Expand Up @@ -75,22 +82,38 @@ abstract contract BaseStoryNFT is IStoryNFT, ERC721URIStorageUpgradeable, Ownabl
/// @return tokenId The ID of the minted token.
/// @return ipId The ID of the newly created IP.
function _mintAndRegisterIp(address recipient) internal virtual returns (uint256 tokenId, address ipId) {
(tokenId, ipId) = _mintAndRegisterIp(recipient, "");
(tokenId, ipId) = _mintAndRegisterIp(recipient, "", "", bytes32(0), bytes32(0));
}

/// @notice Mints a new token and registers as an IP asset.
/// @param recipient The address to mint the token to.
/// @param tokenURI_ The token URI of the token (see {ERC721URIStorage-tokenURI} for how it is used).
/// @param ipMetadataURI The URI of the metadata for the IP.
/// @param ipMetadataHash The hash of the metadata for the IP.
/// @param nftMetadataHash The hash of the metadata for the IP NFT.
/// @return tokenId The ID of the minted token.
/// @return ipId The ID of the newly created IP.
function _mintAndRegisterIp(
address recipient,
string memory tokenURI_
string memory tokenURI_,
string memory ipMetadataURI,
bytes32 ipMetadataHash,
bytes32 nftMetadataHash
) internal virtual returns (uint256 tokenId, address ipId) {
tokenId = _getBaseStoryNFTStorage().totalSupply++;
_safeMint(recipient, tokenId);
_setTokenURI(tokenId, tokenURI_);

ipId = IP_ASSET_REGISTRY.register(block.chainid, address(this), tokenId);

// set the IP metadata if they are not empty
if (
keccak256(abi.encodePacked(ipMetadataURI)) != keccak256("") ||
ipMetadataHash != bytes32(0) ||
nftMetadataHash != bytes32(0)
) {
ICoreMetadataModule(CORE_METADATA_MODULE).setAll(ipId, ipMetadataURI, ipMetadataHash, nftMetadataHash);
}
}

/// @notice Register `ipId` as a derivative of `parentIpIds` under `licenseTemplate` with `licenseTermsIds`.
Expand Down
48 changes: 39 additions & 9 deletions contracts/story-nft/OrgNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721H
import { ERC721URIStorageUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { ICoreMetadataModule } from "@storyprotocol/core/interfaces/modules/metadata/ICoreMetadataModule.sol";
import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol";
// solhint-disable-next-line max-line-length
import { ILicensingModule } from "@story-protocol/protocol-core/contracts/interfaces/modules/licensing/ILicensingModule.sol";

import { IOrgNFT } from "../interfaces/story-nft/IOrgNFT.sol";
import { WorkflowStructs } from "../lib/WorkflowStructs.sol";

/// @title Organization NFT
/// @notice Each organization token represents a Story ecosystem project.
Expand All @@ -22,18 +24,27 @@ contract OrgNFT is IOrgNFT, ERC721URIStorageUpgradeable, AccessManagedUpgradeabl
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable

/// @notice Story Proof-of-Creativity IP Asset Registry address.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
IIPAssetRegistry public immutable IP_ASSET_REGISTRY;

/// @notice Story Proof-of-Creativity Licensing Module address.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
ILicensingModule public immutable LICENSING_MODULE;

/// @notice Core Metadata Module address.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
ICoreMetadataModule public immutable CORE_METADATA_MODULE;

/// @notice License template address.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable LICENSE_TEMPLATE;

/// @notice License terms ID.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
uint256 public immutable LICENSE_TERMS_ID;

/// @notice Story NFT Factory address.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable ORG_STORY_NFT_FACTORY;

/// @dev Storage structure for the OrgNFT
Expand All @@ -59,13 +70,15 @@ contract OrgNFT is IOrgNFT, ERC721URIStorageUpgradeable, AccessManagedUpgradeabl
constructor(
address ipAssetRegistry,
address licensingModule,
address coreMetadataModule,
address orgStoryNftFactory,
address licenseTemplate,
uint256 licenseTermsId
) {
if (
ipAssetRegistry == address(0) ||
licensingModule == address(0) ||
coreMetadataModule == address(0) ||
orgStoryNftFactory == address(0) ||
licenseTemplate == address(0)
) revert OrgNFT__ZeroAddressParam();
Expand All @@ -75,6 +88,7 @@ contract OrgNFT is IOrgNFT, ERC721URIStorageUpgradeable, AccessManagedUpgradeabl
ORG_STORY_NFT_FACTORY = orgStoryNftFactory;
LICENSE_TEMPLATE = licenseTemplate;
LICENSE_TERMS_ID = licenseTermsId;
CORE_METADATA_MODULE = ICoreMetadataModule(coreMetadataModule);

_disableInitializers();
}
Expand All @@ -93,36 +107,38 @@ contract OrgNFT is IOrgNFT, ERC721URIStorageUpgradeable, AccessManagedUpgradeabl
/// @notice Mints the root organization token and register it as an IP.
/// @dev This function is only callable by the OrgStoryNFTFactory contract.
/// @param recipient The address of the recipient of the root organization token.
/// @param tokenURI_ The URI of the root organization token.
/// @param orgIpMetadata OPTIONAL. The desired metadata for the newly minted OrgNFT and registered IP.
/// @return rootOrgTokenId The ID of the root organization token.
/// @return rootOrgIpId The ID of the root organization IP.
function mintRootOrgNft(
address recipient,
string memory tokenURI_
WorkflowStructs.IPMetadata calldata orgIpMetadata
) external onlyStoryNFTFactory returns (uint256 rootOrgTokenId, address rootOrgIpId) {
OrgNFTStorage storage $ = _getOrgNFTStorage();
if ($.rootOrgIpId != address(0)) revert OrgNFT__RootOrgNftAlreadyMinted();

(rootOrgTokenId, rootOrgIpId) = _mintAndRegisterIp(recipient, tokenURI_);
(rootOrgTokenId, rootOrgIpId) = _mintAndRegisterIp(address(this), orgIpMetadata);
$.rootOrgIpId = rootOrgIpId;

_safeTransfer(address(this), recipient, rootOrgTokenId);
}

/// @notice Mints a organization token, register it as an IP,
/// and makes the IP as a derivative of the root organization IP.
/// @dev This function is only callable by the OrgStoryNFTFactory contract.
/// @param recipient The address of the recipient of the minted organization token.
/// @param tokenURI_ The URI of the minted organization token.
/// @param orgIpMetadata OPTIONAL. The desired metadata for the newly minted OrgNFT and registered IP.
/// @return orgTokenId The ID of the minted organization token.
/// @return orgIpId The ID of the organization IP.
function mintOrgNft(
address recipient,
string memory tokenURI_
WorkflowStructs.IPMetadata calldata orgIpMetadata
) external onlyStoryNFTFactory returns (uint256 orgTokenId, address orgIpId) {
OrgNFTStorage storage $ = _getOrgNFTStorage();
if ($.rootOrgIpId == address(0)) revert OrgNFT__RootOrgNftNotMinted();

// Mint the organization token and register it as an IP.
(orgTokenId, orgIpId) = _mintAndRegisterIp(address(this), tokenURI_);
(orgTokenId, orgIpId) = _mintAndRegisterIp(address(this), orgIpMetadata);

address[] memory parentIpIds = new address[](1);
uint256[] memory licenseTermsIds = new uint256[](1);
Expand Down Expand Up @@ -152,19 +168,33 @@ contract OrgNFT is IOrgNFT, ERC721URIStorageUpgradeable, AccessManagedUpgradeabl

/// @notice Mints a organization token and register it as an IP.
/// @param recipient The address of the recipient of the minted organization token.
/// @param tokenURI_ The URI of the minted organization token.
/// @param orgIpMetadata OPTIONAL. The desired metadata for the newly minted OrgNFT and registered IP.
/// @return orgTokenId The ID of the minted organization token.
/// @return orgIpId The ID of the organization IP.
function _mintAndRegisterIp(
address recipient,
string memory tokenURI_
WorkflowStructs.IPMetadata calldata orgIpMetadata
) private returns (uint256 orgTokenId, address orgIpId) {
OrgNFTStorage storage $ = _getOrgNFTStorage();
orgTokenId = $.totalSupply++;
_safeMint(recipient, orgTokenId);
_setTokenURI(orgTokenId, tokenURI_);
_setTokenURI(orgTokenId, orgIpMetadata.nftMetadataURI);
orgIpId = IP_ASSET_REGISTRY.register(block.chainid, address(this), orgTokenId);

// set the IP metadata if they are not empty
if (
keccak256(abi.encodePacked(orgIpMetadata.ipMetadataURI)) != keccak256("") ||
orgIpMetadata.ipMetadataHash != bytes32(0) ||
orgIpMetadata.nftMetadataHash != bytes32(0)
) {
ICoreMetadataModule(CORE_METADATA_MODULE).setAll(
orgIpId,
orgIpMetadata.ipMetadataURI,
orgIpMetadata.ipMetadataHash,
orgIpMetadata.nftMetadataHash
);
}

emit OrgNFTMinted(recipient, address(this), orgTokenId, orgIpId);
}

Expand Down
Loading
Loading