Skip to content

Commit

Permalink
Introduce Group IPA Feature (#192)
Browse files Browse the repository at this point in the history
* Introduce IPA Grouping
* add GroupingModule and update IPAssetRegistry
  • Loading branch information
kingster-will authored Aug 26, 2024
1 parent 47d8fd1 commit 0c3949b
Show file tree
Hide file tree
Showing 16 changed files with 1,464 additions and 19 deletions.
138 changes: 138 additions & 0 deletions contracts/GroupNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;
import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
// solhint-disable-next-line max-line-length
import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol";

import { IIPAssetRegistry } from "./interfaces/registries/IIPAssetRegistry.sol";
import { IGroupNFT } from "./interfaces/IGroupNFT.sol";
import { Errors } from "./lib/Errors.sol";

/// @title GroupNFT
contract GroupNFT is IGroupNFT, ERC721Upgradeable, AccessManagedUpgradeable, UUPSUpgradeable {
using Strings for *;

/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
IIPAssetRegistry public immutable IP_ASSET_REGISTRY;

/// @notice Emitted for metadata updates, per EIP-4906
event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId);

/// @dev Storage structure for the GroupNFT
/// @custom:storage-location erc7201:story-protocol.GroupNFT
struct GroupNFTStorage {
string imageUrl;
uint256 totalSupply;
}

// keccak256(abi.encode(uint256(keccak256("story-protocol.GroupNFT")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant GroupNFTStorageLocation =
0x1f63c78b3808749cafddcb77c269221c148dbaa356630c2195a6ec03d7fedb00;

modifier onlyIPAssetRegistry() {
if (msg.sender != address(IP_ASSET_REGISTRY)) {
revert Errors.GroupNFT__CallerNotIPAssetRegistry(msg.sender);
}
_;
}

/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address iPAssetRegistry) {
IP_ASSET_REGISTRY = IIPAssetRegistry(iPAssetRegistry);
_disableInitializers();
}

/// @dev Initializes the GroupNFT contract
function initialize(address accessManager, string memory imageUrl) public initializer {
if (accessManager == address(0)) {
revert Errors.GroupNFT__ZeroAccessManager();
}
__ERC721_init("Programmable IP Group IP NFT", "GroupNFT");
__AccessManaged_init(accessManager);
__UUPSUpgradeable_init();
_getGroupNFTStorage().imageUrl = imageUrl;
}

/// @dev Sets the Licensing Image URL.
/// @dev Enforced to be only callable by the protocol admin
/// @param url The URL of the Licensing Image
function setLicensingImageUrl(string calldata url) external restricted {
GroupNFTStorage storage $ = _getGroupNFTStorage();
$.imageUrl = url;
emit BatchMetadataUpdate(0, $.totalSupply);
}

/// @notice Mints a Group NFT.
/// @param minter The address of the minter.
/// @param receiver The address of the receiver of the minted Group NFT.
/// @return groupNftId The ID of the minted Group NFT.
function mintGroupNft(address minter, address receiver) external onlyIPAssetRegistry returns (uint256 groupNftId) {
GroupNFTStorage storage $ = _getGroupNFTStorage();
groupNftId = $.totalSupply++;
_mint(receiver, groupNftId);
emit GroupNFTMinted(minter, receiver, groupNftId);
}

/// @notice Returns the total number of minted group IPA NFT since beginning,
/// @return The total number of minted group IPA NFT.
function totalSupply() external view returns (uint256) {
return _getGroupNFTStorage().totalSupply;
}

/// @notice ERC721 OpenSea metadata JSON representation of Group IPA NFT
function tokenURI(
uint256 id
) public view virtual override(ERC721Upgradeable, IERC721Metadata) returns (string memory) {
GroupNFTStorage storage $ = _getGroupNFTStorage();

/* solhint-disable */
// Follows the OpenSea standard for JSON metadata

// base json, open the attributes array
string memory json = string(
abi.encodePacked(
"{",
'"name": "Story Protocol IP Assets Group #',
id.toString(),
'",',
'"description": IPAsset Group",',
'"external_url": "https://protocol.storyprotocol.xyz/ipa/',
id.toString(),
'",',
'"image": "',
$.imageUrl,
'"'
)
);

// close the attributes array and the json metadata object
json = string(abi.encodePacked(json, "}"));

/* solhint-enable */

return string(abi.encodePacked("data:application/json;base64,", Base64.encode(bytes(json))));
}

/// @notice IERC165 interface support.
function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC721Upgradeable, IERC165) returns (bool) {
return interfaceId == type(IGroupNFT).interfaceId || super.supportsInterface(interfaceId);
}

/// @dev Returns the storage struct of GroupNFT.
function _getGroupNFTStorage() private pure returns (GroupNFTStorage storage $) {
assembly {
$.slot := GroupNFTStorageLocation
}
}

/// @dev Hook to authorize the upgrade according to UUPSUpgradeable
/// @param newImplementation The address of the new implementation
function _authorizeUpgrade(address newImplementation) internal override restricted {}
}
32 changes: 27 additions & 5 deletions contracts/LicenseToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,20 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag
mapping(uint256 tokenId => LicenseTokenMetadata) licenseTokenMetadatas;
}

/// @dev Storage structure for the Licensor
/// @custom:storage-location erc7201:story-protocol.Licensor
struct LicensorStorage {
mapping(address licensorIpId => uint256 totalMintedTokens) licensorIpTotalTokens;
}

// keccak256(abi.encode(uint256(keccak256("story-protocol.LicenseToken")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant LicenseTokenStorageLocation =
0x62a0d75e37bea0c3e666dc72a74112fc6af15ce635719127e380d8ca1e555d00;

// keccak256(abi.encode(uint256(keccak256("story-protocol.Licensor")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant LicensorStorageLocation =
0x23f0add89533cdf440c8f5cc9ffed2d19de5118ad74363071b8d1ea4f92f9a00;

modifier onlyLicensingModule() {
if (msg.sender != address(LICENSING_MODULE)) {
revert Errors.LicenseToken__CallerNotLicensingModule();
Expand Down Expand Up @@ -98,8 +108,10 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag

LicenseTokenStorage storage $ = _getLicenseTokenStorage();
startLicenseTokenId = $.totalMintedTokens;
$.totalMintedTokens += amount;
_getLicensorStorage().licensorIpTotalTokens[licensorIpId] += amount;
for (uint256 i = 0; i < amount; i++) {
uint256 tokenId = $.totalMintedTokens++;
uint256 tokenId = startLicenseTokenId + i;
$.licenseTokenMetadatas[tokenId] = ltm;
_mint(receiver, tokenId);
emit LicenseTokenMinted(minter, receiver, tokenId);
Expand Down Expand Up @@ -191,6 +203,13 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag
return _getLicenseTokenStorage().licenseTokenMetadatas[tokenId].licenseTemplate;
}

/// @notice Retrieves the total number of License Tokens minted for a given licensor IP.
/// @param licensorIpId The ID of the licensor IP.
/// @return The total number of License Tokens minted for the licensor IP.
function getTotalTokensByLicensor(address licensorIpId) external view returns (uint256) {
return _getLicensorStorage().licensorIpTotalTokens[licensorIpId];
}

/// @notice Returns true if the license has been revoked (licensor IP tagged after a dispute in
/// the dispute module). If the tag is removed, the license is not revoked anymore.
/// @return isRevoked True if the license is revoked
Expand Down Expand Up @@ -281,17 +300,20 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag
return super._update(to, tokenId, auth);
}

////////////////////////////////////////////////////////////////////////////
// Upgrades related //
////////////////////////////////////////////////////////////////////////////

/// @dev Returns the storage struct of LicenseToken.
function _getLicenseTokenStorage() private pure returns (LicenseTokenStorage storage $) {
assembly {
$.slot := LicenseTokenStorageLocation
}
}

/// @dev Returns the storage struct of Licensor.
function _getLicensorStorage() private pure returns (LicensorStorage storage $) {
assembly {
$.slot := LicensorStorageLocation
}
}

/// @dev Hook to authorize the upgrade according to UUPSUpgradeable
/// @param newImplementation The address of the new implementation
function _authorizeUpgrade(address newImplementation) internal override restricted {}
Expand Down
22 changes: 22 additions & 0 deletions contracts/interfaces/IGroupNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

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

/// @title IGroupNFT
/// @notice Interface for the IP Group (ERC721) NFT collection that manages Group NFTs representing IP Group.
/// Each Group NFT may represent a IP Group.
/// Group NFTs are ERC721 NFTs that can be minted, transferred, but cannot be burned.
interface IGroupNFT is IERC721Metadata {
/// @notice Emitted when a IP Group NFT minted.
/// @param minter The address of the minter of the IP Group NFT
/// @param receiver The address of the receiver of the Group NFT.
/// @param tokenId The ID of the minted IP Group NFT.
event GroupNFTMinted(address indexed minter, address indexed receiver, uint256 indexed tokenId);

/// @notice Mints a Group NFT.
/// @param minter The address of the minter.
/// @param receiver The address of the receiver of the minted Group NFT.
/// @return groupNftId The ID of the minted Group NFT.
function mintGroupNft(address minter, address receiver) external returns (uint256 groupNftId);
}
5 changes: 5 additions & 0 deletions contracts/interfaces/ILicenseToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ interface ILicenseToken is IERC721Metadata, IERC721Enumerable {
/// @return A `LicenseTokenMetadata` struct containing the metadata of the specified License Token.
function getLicenseTokenMetadata(uint256 tokenId) external view returns (LicenseTokenMetadata memory);

/// @notice Retrieves the total number of License Tokens minted for a given licensor IP.
/// @param licensorIpId The ID of the licensor IP.
/// @return The total number of License Tokens minted for the licensor IP.
function getTotalTokensByLicensor(address licensorIpId) external view returns (uint256);

/// @notice Validates License Tokens for registering a derivative IP.
/// @dev This function checks if the License Tokens are valid for the derivative IP registration process.
/// The function will be called by LicensingModule when registering a derivative IP with license tokens.
Expand Down
42 changes: 42 additions & 0 deletions contracts/interfaces/modules/grouping/IGroupRewardPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

/// @title IGroupingPolicy
/// @notice Interface for grouping policies
interface IGroupRewardPool {
/// @notice Distributes rewards to the given IP accounts in pool
/// @param groupId The group ID
/// @param token The reward tokens
/// @param ipIds The IP IDs
function distributeRewards(
address groupId,
address token,
address[] calldata ipIds
) external returns (uint256[] memory rewards);

/// @notice Collects royalty revenue to the group pool through royalty module
/// @param groupId The group ID
/// @param token The reward token
function collectRoyalties(address groupId, address token) external;

/// @notice Adds an IP to the group pool
/// @param groupId The group ID
/// @param ipId The IP ID
function addIp(address groupId, address ipId) external;

/// @notice Removes an IP from the group pool
/// @param groupId The group ID
/// @param ipId The IP ID
function removeIp(address groupId, address ipId) external;

/// @notice Returns the available reward for each IP in the group
/// @param groupId The group ID
/// @param token The reward token
/// @param ipIds The IP IDs
/// @return The rewards for each IP
function getAvailableReward(
address groupId,
address token,
address[] calldata ipIds
) external view returns (uint256[] memory);
}
70 changes: 70 additions & 0 deletions contracts/interfaces/modules/grouping/IGroupingModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

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

/// @title IGroupingModule
/// @notice This interface defines the entry point for users to manage group in the Story Protocol.
/// It defines the workflow of grouping actions and coordinates among all grouping components.
/// The Grouping Module is responsible for adding ip to group, removing ip from group and claiming reward.
interface IGroupingModule is IModule {
/// @notice Emitted when a group is registered.
/// @param groupId The address of the group.
/// @param groupPool The address of the group pool.
event IPGroupRegistered(address indexed groupId, address indexed groupPool);

/// @notice Emitted when added ip to group.
/// @param groupId The address of the group.
/// @param ipIds The IP ID.
event AddedIpToGroup(address indexed groupId, address[] ipIds);

/// @notice Emitted when removed ip from group.
/// @param groupId The address of the group.
/// @param ipIds The IP ID.
event RemovedIpFromGroup(address indexed groupId, address[] ipIds);

/// @notice Emitted when claimed reward.
/// @param groupId The address of the group.
/// @param token The address of the token.
/// @param ipId The IP ID.
/// @param amount The amount of reward.
event ClaimedReward(address indexed groupId, address indexed token, address[] ipId, uint256[] amount);

/// @notice Registers a Group IPA.
/// @param groupPool The address of the group pool.
/// @return groupId The address of the newly registered Group IPA.
function registerGroup(address groupPool) external returns (address groupId);

/// @notice Whitelists a group reward pool.
/// @param rewardPool The address of the group reward pool.
function whitelistGroupRewardPool(address rewardPool) external;

/// @notice Adds IP to group.
/// the function must be called by the Group IP owner or an authorized operator.
/// @param groupIpId The address of the group IP.
/// @param ipIds The IP IDs.
function addIp(address groupIpId, address[] calldata ipIds) external;

/// @notice Removes IP from group.
/// the function must be called by the Group IP owner or an authorized operator.
/// @param groupIpId The address of the group IP.
/// @param ipIds The IP IDs.
function removeIp(address groupIpId, address[] calldata ipIds) external;

/// @notice Claims reward.
/// @param groupId The address of the group.
/// @param token The address of the token.
/// @param ipIds The IP IDs.
function claimReward(address groupId, address token, address[] calldata ipIds) external;

/// @notice Returns the available reward for each IP in the group.
/// @param groupId The address of the group.
/// @param token The address of the token.
/// @param ipIds The IP IDs.
/// @return The rewards for each IP.
function getClaimableReward(
address groupId,
address token,
address[] calldata ipIds
) external view returns (uint256[] memory);
}
Loading

0 comments on commit 0c3949b

Please sign in to comment.