Skip to content

Commit

Permalink
Introducing Core Metadata Module and View Module (#15)
Browse files Browse the repository at this point in the history
* Implement CoreMetadataModule
* add error and module name
* add functions to set and get all metadata
* add metadataURI
* add immutable flag
* add events and more tests
  • Loading branch information
kingster-will authored Mar 29, 2024
1 parent ee1030d commit 5832c61
Show file tree
Hide file tree
Showing 8 changed files with 808 additions and 0 deletions.
54 changes: 54 additions & 0 deletions contracts/interfaces/modules/metadata/ICoreMetadataModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import { IModule } from "../../../../contracts/interfaces/modules/base/IModule.sol";

/// @title CoreMetadataModule
/// @notice Manages the core metadata for IP assets within the Story Protocol.
/// @dev This contract allows setting and updating core metadata attributes for IP assets.
interface ICoreMetadataModule is IModule {
/// @notice Emitted when the nftTokenURI for an IP asset is set.
event NFTTokenURISet(address indexed ipId, string nftTokenURI, bytes32 nftMetadataHash);

/// @notice Emitted when the metadataURI for an IP asset is set.
event MetadataURISet(address indexed ipId, string metadataURI, bytes32 metadataHash);

/// @notice Emitted when all metadata for an IP asset are frozen.
event MetadataFrozen(address indexed ipId);

/// @notice Update the nftTokenURI for an IP asset,
/// by retrieve the latest TokenURI from IP NFT to which the IP Asset bound.
/// @dev Will revert if IP asset's metadata is frozen.
/// @param ipId The address of the IP asset.
/// @param nftMetadataHash A bytes32 hash representing the metadata of the NFT.
/// This metadata is associated with the IP Asset and is accessible via the NFT's TokenURI.
/// Use bytes32(0) to indicate that the metadata is not available.
function updateNftTokenURI(address ipId, bytes32 nftMetadataHash) external;

/// @notice Sets the metadataURI for an IP asset.
/// @dev Will revert if IP asset's metadata is frozen.
/// @param ipId The address of the IP asset.
/// @param metadataURI The metadataURI to set for the IP asset.
/// @param metadataHash The hash of metadata at metadataURI.
/// Use bytes32(0) to indicate that the metadata is not available.
function setMetadataURI(address ipId, string memory metadataURI, bytes32 metadataHash) external;

/// @notice Sets all core metadata for an IP asset.
/// @dev Will revert if IP asset's metadata is frozen.
/// @param ipId The address of the IP asset.
/// @param metadataURI The metadataURI to set for the IP asset.
/// @param metadataHash The hash of metadata at metadataURI.
/// Use bytes32(0) to indicate that the metadata is not available.
/// @param nftMetadataHash A bytes32 hash representing the metadata of the NFT.
/// This metadata is associated with the IP Asset and is accessible via the NFT's TokenURI.
/// Use bytes32(0) to indicate that the metadata is not available.
function setAll(address ipId, string memory metadataURI, bytes32 metadataHash, bytes32 nftMetadataHash) external;

/// @notice make all metadata of the IP Asset immutable.
/// @param ipId The address of the IP asset.
function freezeMetadata(address ipId) external;

/// @notice Check if the metadata of the IP Asset is immutable.
/// @param ipId The address of the IP asset.
function isMetadataFrozen(address ipId) external view returns (bool);
}
64 changes: 64 additions & 0 deletions contracts/interfaces/modules/metadata/ICoreMetadataViewModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import { IViewModule } from "../base/IViewModule.sol";

/// @title CoreMetadataViewModule
/// @notice This view module provides getter functions to access all core metadata
/// and generate json string of all core metadata returned by tokenURI().
/// The view module consolidates core metadata for IPAssets from both IPAssetRegistry and CoreMetadataModule.
/// @dev The "name" from CoreMetadataModule overrides the "name" from IPAssetRegistry if set.
interface ICoreMetadataViewModule is IViewModule {
/// @notice Core metadata struct for IPAssets.
struct CoreMetadata {
string nftTokenURI;
bytes32 nftMetadataHash;
string metadataURI;
bytes32 metadataHash;
uint256 registrationDate;
address owner;
}

/// @notice Retrieves the metadataURI of the IPAsset from CoreMetadataModule.
/// @param ipId The address of the IPAsset.
/// @return The metadataURI of the IPAsset.
function getMetadataURI(address ipId) external view returns (string memory);

/// @notice Retrieves the metadata hash of the IPAsset from CoreMetadataModule.
/// @param ipId The address of the IPAsset.
/// @return The metadata hash of the IPAsset.
function getMetadataHash(address ipId) external view returns (bytes32);

/// @notice Retrieves the registration date of the IPAsset from IPAssetRegistry.
/// @param ipId The address of the IPAsset.
/// @return The registration date of the IPAsset.
function getRegistrationDate(address ipId) external view returns (uint256);

/// @notice Retrieves the TokenURI of the NFT to which IP Asset bound.
/// @param ipId The address of the IPAsset.
/// @return The NFT TokenURI of the IPAsset.
function getNftTokenURI(address ipId) external view returns (string memory);

/// @notice Retrieves the metadata hash of the NFT to which IP Asset bound.
/// @param ipId The address of the IPAsset.
/// @return The NFT metadata hash of the IPAsset.
function getNftMetadataHash(address ipId) external view returns (bytes32);

/// @notice Retrieves the owner of the IPAsset.
/// @param ipId The address of the IPAsset.
/// @return The address of the owner of the IPAsset.
function getOwner(address ipId) external view returns (address);

/// @notice Retrieves all core metadata of the IPAsset.
/// @param ipId The address of the IPAsset.
/// @return The CoreMetadata struct of the IPAsset.
function getCoreMetadata(address ipId) external view returns (CoreMetadata memory);

/// @notice Generates a JSON string formatted according to the standard NFT metadata schema for the IPAsset,
//// including all relevant metadata fields.
/// @dev This function consolidates metadata from both IPAssetRegistry
/// and CoreMetadataModule, with "name" from CoreMetadataModule taking precedence.
/// @param ipId The address of the IPAsset.
/// @return A JSON string representing all metadata of the IPAsset.
function getJsonString(address ipId) external view returns (string memory);
}
5 changes: 5 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,9 @@ library Errors {
error AccessControlled__ZeroAddress();
error AccessControlled__NotIpAccount(address ipAccount);
error AccessControlled__CallerIsNotIpAccount(address caller);

////////////////////////////////////////////////////////////////////////////
// CoreMetadataModule //
////////////////////////////////////////////////////////////////////////////
error CoreMetadataModule__MetadataAlreadyFrozen();
}
4 changes: 4 additions & 0 deletions contracts/lib/modules/Module.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ string constant DISPUTE_MODULE_KEY = "DISPUTE_MODULE";
string constant ROYALTY_MODULE_KEY = "ROYALTY_MODULE";

string constant TOKEN_WITHDRAWAL_MODULE_KEY = "TOKEN_MANAGEMENT_MODULE";

string constant CORE_METADATA_MODULE_KEY = "CORE_METADATA_MODULE";

string constant CORE_METADATA_VIEW_MODULE_KEY = "CORE_METADATA_VIEW_MODULE";
116 changes: 116 additions & 0 deletions contracts/modules/metadata/CoreMetadataModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
import { IIPAccount } from "../../interfaces/IIPAccount.sol";
import { AccessControlled } from "../../access/AccessControlled.sol";
import { BaseModule } from "../BaseModule.sol";
import { Errors } from "../../lib/Errors.sol";
import { IPAccountStorageOps } from "../../lib/IPAccountStorageOps.sol";
import { CORE_METADATA_MODULE_KEY } from "../../lib/modules/Module.sol";
import { ICoreMetadataModule } from "../../interfaces/modules/metadata/ICoreMetadataModule.sol";

/// @title CoreMetadataModule
/// @notice Manages the core metadata for IP assets within the Story Protocol.
/// @dev This contract allows setting core metadata attributes for IP assets.
/// It implements the ICoreMetadataModule interface.
/// The metadata can be set and updated by the owner of the IP asset.
/// The metadata can be frozen to prevent further changes.
contract CoreMetadataModule is BaseModule, AccessControlled, ICoreMetadataModule {
using IPAccountStorageOps for IIPAccount;

string public override name = CORE_METADATA_MODULE_KEY;

/// @notice Modifier to ensure that metadata can only be changed when mutable.
modifier onlyMutable(address ipId) {
if (IIPAccount(payable(ipId)).getBool("IMMUTABLE")) {
revert Errors.CoreMetadataModule__MetadataAlreadyFrozen();
}
_;
}

/// @notice Creates a new CoreMetadataModule instance.
/// @param accessController The address of the AccessController contract.
/// @param ipAccountRegistry The address of the IPAccountRegistry contract.
constructor(
address accessController,
address ipAccountRegistry
) AccessControlled(accessController, ipAccountRegistry) {}

/// @notice Update the nftTokenURI for an IP asset,
/// by retrieve the latest TokenURI from IP NFT to which the IP Asset bound.
/// @dev Will revert if IP asset's metadata is frozen.
/// @param ipId The address of the IP asset.
/// @param nftMetadataHash A bytes32 hash representing the metadata of the NFT.
/// This metadata is associated with the IP Asset and is accessible via the NFT's TokenURI.
/// Use bytes32(0) to indicate that the metadata is not available.
function updateNftTokenURI(address ipId, bytes32 nftMetadataHash) external verifyPermission(ipId) {
_updateNftTokenURI(ipId, nftMetadataHash);
}

/// @notice Sets the metadataURI for an IP asset.
/// @dev Will revert if IP asset's metadata is frozen.
/// @param ipId The address of the IP asset.
/// @param metadataURI The metadataURI to set for the IP asset.
/// @param metadataHash The hash of metadata at metadataURI.
/// Use bytes32(0) to indicate that the metadata is not available.
function setMetadataURI(
address ipId,
string memory metadataURI,
bytes32 metadataHash
) external verifyPermission(ipId) {
_setMetadataURI(ipId, metadataURI, metadataHash);
}

/// @notice Sets all core metadata for an IP asset.
/// @dev Will revert if IP asset's metadata is frozen.
/// @param ipId The address of the IP asset.
/// @param metadataURI The metadataURI to set for the IP asset.
/// @param metadataHash The hash of metadata at metadataURI.
/// Use bytes32(0) to indicate that the metadata is not available.
/// @param nftMetadataHash A bytes32 hash representing the metadata of the NFT.
/// This metadata is associated with the IP Asset and is accessible via the NFT's TokenURI.
/// Use bytes32(0) to indicate that the metadata is not available.
function setAll(
address ipId,
string memory metadataURI,
bytes32 metadataHash,
bytes32 nftMetadataHash
) external verifyPermission(ipId) {
_updateNftTokenURI(ipId, nftMetadataHash);
_setMetadataURI(ipId, metadataURI, metadataHash);
}

/// @notice make all metadata of the IP Asset immutable.
/// @param ipId The address of the IP asset.
function freezeMetadata(address ipId) external verifyPermission(ipId) {
IIPAccount(payable(ipId)).setBool("IMMUTABLE", true);
emit MetadataFrozen(ipId);
}

/// @notice Check if the metadata of the IP Asset is immutable.
/// @param ipId The address of the IP asset.
function isMetadataFrozen(address ipId) external view returns (bool) {
return IIPAccount(payable(ipId)).getBool("IMMUTABLE");
}

/// @dev Implements the IERC165 interface.
function supportsInterface(bytes4 interfaceId) public view virtual override(BaseModule, IERC165) returns (bool) {
return interfaceId == type(ICoreMetadataModule).interfaceId || super.supportsInterface(interfaceId);
}

function _updateNftTokenURI(address ipId, bytes32 nftMetadataHash) internal onlyMutable(ipId) {
(, address tokenAddress, uint256 tokenId) = IIPAccount(payable(ipId)).token();
string memory nftTokenURI = IERC721Metadata(tokenAddress).tokenURI(tokenId);
IIPAccount(payable(ipId)).setString("NFT_TOKEN_URI", nftTokenURI);
IIPAccount(payable(ipId)).setBytes32("NFT_METADATA_HASH", nftMetadataHash);
emit NFTTokenURISet(ipId, nftTokenURI, nftMetadataHash);
}

function _setMetadataURI(address ipId, string memory metadataURI, bytes32 metadataHash) internal onlyMutable(ipId) {
IIPAccount(payable(ipId)).setString("METADATA_URI", metadataURI);
IIPAccount(payable(ipId)).setBytes32("METADATA_HASH", metadataHash);
emit MetadataURISet(ipId, metadataURI, metadataHash);
}
}
Loading

0 comments on commit 5832c61

Please sign in to comment.