From 31233ddba1551f009820c865825ca4cde17ebf6f Mon Sep 17 00:00:00 2001 From: Sebastian Liu Date: Tue, 27 Aug 2024 19:36:33 -0700 Subject: [PATCH 1/2] chore: spec bump and update change log --- .env.example | 4 +-- CHANGELOG.md | 14 ++++++++ foundry.toml | 2 +- package.json | 2 +- script/utils/BroadcastManager.s.sol | 6 ++-- script/utils/upgrades/ERC7201Helper.s.sol | 2 +- yarn.lock | 44 +++++++++++------------ 7 files changed, 44 insertions(+), 30 deletions(-) diff --git a/.env.example b/.env.example index 9bd729a..7b0371a 100644 --- a/.env.example +++ b/.env.example @@ -3,8 +3,8 @@ MAINNET_RPC_URL = https://eth-mainnet.g.alchemy.com/v2/1234123412341234 # TODO: Remove private key in favor of forge cast wallet MAINNET_PRIVATEKEY= 12341234123412341234123412341234 -# SEPOLIA -SEPOLIA_RPC_URL = https://eth-mainnet.g.alchemy.com/v2/1234123412341234 +# TESTNET +TESTNET_RPC_URL = https://eth-mainnet.g.alchemy.com/v2/1234123412341234 # TODO: Remove private key in favor of forge cast wallet # ETHSCAN diff --git a/CHANGELOG.md b/CHANGELOG.md index e5cccb3..0f004f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,22 @@ # CHANGELOG +## v1.1.0 +- Migrate periphery contracts from protocol core repo (#1) +- Revamped SPG with NFT collection and mint token logic. (#5, #6) +- Added support for batch transactions via `multicall` (#38) +- Added functionality for registering IP with metadata and supporting metadata for SPG NFT. (#8, #20, #37) +- Addressed ownership transfer issues in deployment script. (#18, #39) +- Fixed issues with derivative registration, including minting fees for commercial licenses, license token flow, and making register and attach PIL terms idempotent. (#23, #25, #30) +- Added SPG & SPG NFT upgrade scripts (#10) +- Added IP Graph, Solady's ERC6551 integration, and core protocol package bumps. (#30) +- Enhance CI/CD, repo, and misc.(#2, #3, #11, #32) + +**Full Changelog**: [v1.1.0](https://github.com/storyprotocol/protocol-periphery-v1/commits/v1.1.0) + ## v1.0.0-beta-rc1 This is the first release of the Story Protocol Gateway - Adds the SPG, a convenient wrapper around the core contracts for registration - Includes NFT minting management tooling for registering and minting in one-shot + diff --git a/foundry.toml b/foundry.toml index 098f105..681c47e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,7 +14,7 @@ fs_permissions = [{ access = 'read', path = './' }, { access = 'read-write', pat # Comment out for local development (testing requires 0xSplit forks — will add Mock soon) # pin fork by using --fork-block-number 19042069 to reduce wait time mainnet = "https://rpc.ankr.com/eth" -sepolia = "${SEPOLIA_RPC_URL}" +testnet = "${TESTNET_RPC_URL}" # See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/package.json b/package.json index 044efa0..6de87a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@story-protocol/protocol-periphery", - "version": "v1.0.0-rc1", + "version": "v1.1.0", "description": "Story Protocol periphery smart contracts", "main": "", "directories": { diff --git a/script/utils/BroadcastManager.s.sol b/script/utils/BroadcastManager.s.sol index ba9261d..5bbcca5 100644 --- a/script/utils/BroadcastManager.s.sol +++ b/script/utils/BroadcastManager.s.sol @@ -17,9 +17,9 @@ contract BroadcastManager is Script { multisig = vm.envAddress("MAINNET_MULTISIG_ADDRESS"); vm.startBroadcast(deployerPrivateKey); } else if (block.chainid == 1513 || block.chainid == 11155111) { - deployerPrivateKey = vm.envUint("SEPOLIA_PRIVATEKEY"); - deployer = vm.envAddress("SEPOLIA_DEPLOYER_ADDRESS"); - multisig = vm.envAddress("SEPOLIA_MULTISIG_ADDRESS"); + deployerPrivateKey = vm.envUint("TESTNET_PRIVATEKEY"); + deployer = vm.envAddress("TESTNET_DEPLOYER_ADDRESS"); + multisig = vm.envAddress("TESTNET_MULTISIG_ADDRESS"); vm.startBroadcast(deployerPrivateKey); } else if (block.chainid == 31337) { multisig = address(0x456); diff --git a/script/utils/upgrades/ERC7201Helper.s.sol b/script/utils/upgrades/ERC7201Helper.s.sol index 3eb69f4..f30967f 100644 --- a/script/utils/upgrades/ERC7201Helper.s.sol +++ b/script/utils/upgrades/ERC7201Helper.s.sol @@ -14,7 +14,7 @@ contract ERC7201HelperScript is Script { string constant NAMESPACE = "story-protocol-periphery"; string constant CONTRACT_NAME = "SPG"; - function run() external { + function run() external pure { bytes memory erc7201Key = abi.encodePacked(NAMESPACE, ".", CONTRACT_NAME); bytes32 hash = keccak256(abi.encode(uint256(keccak256(erc7201Key)) - 1)) & ~bytes32(uint256(0xff)); diff --git a/yarn.lock b/yarn.lock index 6e29777..cc280b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -436,7 +436,7 @@ "@story-protocol/protocol-core@github:storyprotocol/protocol-core-v1#main": version "1.1.0" - resolved "https://codeload.github.com/storyprotocol/protocol-core-v1/tar.gz/8133cd54641341b7efd116318ff93cf3d8a17a1a" + resolved "https://codeload.github.com/storyprotocol/protocol-core-v1/tar.gz/4d961e5f4dc4605013d4237d9705236fd5e2fba2" dependencies: "@openzeppelin/contracts" "5.0.2" "@openzeppelin/contracts-upgradeable" "5.0.2" @@ -490,9 +490,9 @@ integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== "@types/node@*": - version "22.4.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.4.0.tgz#c295fe1d6f5f58916cc61dbef8cf65b5b9b71de9" - integrity sha512-49AbMDwYUz7EXxKU/r7mXOsxwFr4BYbvB7tWYxVuLdb2ibd30ijjXINSMAHiEEZk5PCRBmW1gUeisn2VMKt3cQ== + version "22.5.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.1.tgz#de01dce265f6b99ed32b295962045d10b5b99560" + integrity sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw== dependencies: undici-types "~6.19.2" @@ -1529,9 +1529,9 @@ http2-wrapper@^2.1.10: resolve-alpn "^1.2.0" husky@^9.0.11: - version "9.1.4" - resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.4.tgz#926fd19c18d345add5eab0a42b2b6d9a80259b34" - integrity sha512-bho94YyReb4JV7LYWRWxZ/xr6TtOTt8cMfmQ39MQYJ7f/YE268s3GdghGwi+y4zAeqewE5zYLvuhV0M0ijsDEA== + version "9.1.5" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.5.tgz#2b6edede53ee1adbbd3a3da490628a23f5243b83" + integrity sha512-rowAVRUBfI0b4+niA4SJMhfQwc107VLkBUgEYYAOQAbqDCnra1nYh83hF/MDmhYs9t9n1E3DuKOrs2LYNC+0Ag== ignore@^5.1.1, ignore@^5.2.0, ignore@^5.2.4: version "5.3.2" @@ -1587,9 +1587,9 @@ is-binary-path@~2.1.0: binary-extensions "^2.0.0" is-core-module@^2.13.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" - integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== dependencies: hasown "^2.0.2" @@ -1805,9 +1805,9 @@ micro-ftch@^0.3.1: integrity sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg== micromatch@^4.0.4: - version "4.0.7" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" - integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" picomatch "^2.3.1" @@ -2086,9 +2086,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier-plugin-solidity@^1.1.3: - version "1.4.0" - resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.4.0.tgz#9eaa6e1d380c8d2b58e4c533ee36eda5c65870c1" - integrity sha512-XXEOjKaY4nC0Hjqv+DMo+A7ZNbS70jil0phl1mdMAbKf45pkxfhPXrNBMDSWsTYTldwSX+8JOwsUynO3enVc5A== + version "1.4.1" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.4.1.tgz#8060baf18853a9e34d2e09e47e87b4f19e15afe9" + integrity sha512-Mq8EtfacVZ/0+uDKTtHZGW3Aa7vEbX/BNx63hmVg6YTiTXSiuKP0amj0G6pGwjmLaOfymWh3QgXEZkjQbU8QRg== dependencies: "@solidity-parser/parser" "^0.18.0" semver "^7.5.4" @@ -2545,9 +2545,9 @@ tslib@2.4.0: integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== tslib@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" - integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -2600,9 +2600,9 @@ uglify-js@^3.1.4: integrity sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ== undici-types@~6.19.2: - version "6.19.6" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.6.tgz#e218c3df0987f4c0e0008ca00d6b6472d9b89b36" - integrity sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org== + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== universalify@^0.1.0: version "0.1.2" From 087f63e2b19378cd54066d4e007de5919bf2c71b Mon Sep 17 00:00:00 2001 From: Sebastian Liu Date: Tue, 27 Aug 2024 19:40:11 -0700 Subject: [PATCH 2/2] feat(spg): add group IPA features --- contracts/StoryProtocolGateway.sol | 252 ++++++++++++---- .../interfaces/IStoryProtocolGateway.sol | 110 +++++-- script/Main.s.sol | 8 +- script/UpgradeSPG.s.sol | 4 +- .../utils/StoryProtocolCoreAddressManager.sol | 4 + test/StoryProtocolGateway.t.sol | 275 +++++++++++++++++- test/mocks/MockEvenSplitGroupPool.sol | 138 +++++++++ test/utils/BaseTest.t.sol | 101 ++++++- 8 files changed, 785 insertions(+), 107 deletions(-) create mode 100644 test/mocks/MockEvenSplitGroupPool.sol diff --git a/contracts/StoryProtocolGateway.sol b/contracts/StoryProtocolGateway.sol index b52bb8c..358fb7e 100644 --- a/contracts/StoryProtocolGateway.sol +++ b/contracts/StoryProtocolGateway.sol @@ -13,12 +13,14 @@ import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; import { Licensing } from "@storyprotocol/core/lib/Licensing.sol"; +import { GroupNFT } from "@storyprotocol/core/GroupNFT.sol"; import { IIPAccount } from "@storyprotocol/core/interfaces/IIPAccount.sol"; import { AccessPermission } from "@storyprotocol/core/lib/AccessPermission.sol"; import { ILicenseToken } from "@storyprotocol/core/interfaces/ILicenseToken.sol"; import { IAccessController } from "@storyprotocol/core/interfaces/access/IAccessController.sol"; import { ILicenseRegistry } from "@storyprotocol/core/interfaces/registries/ILicenseRegistry.sol"; import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol"; +import { IGroupingModule } from "@storyprotocol/core/interfaces/modules/grouping/IGroupingModule.sol"; import { IRoyaltyModule } from "@storyprotocol/core/interfaces/modules/royalty/IRoyaltyModule.sol"; import { ILicensingHook } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingHook.sol"; import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol"; @@ -76,6 +78,12 @@ contract StoryProtocolGateway is /// @notice The address of the License Token. ILicenseToken public immutable LICENSE_TOKEN; + /// @notice The address of the Grouping Module. + IGroupingModule public immutable GROUPING_MODULE; + + /// @notice The address of the Group NFT contract. + GroupNFT public immutable GROUP_NFT; + /// @notice Check that the caller has the minter role for the provided SPG NFT. /// @param nftContract The address of the SPG NFT. modifier onlyCallerWithMinterRole(address nftContract) { @@ -92,7 +100,9 @@ contract StoryProtocolGateway is address royaltyModule, address coreMetadataModule, address pilTemplate, - address licenseToken + address licenseToken, + address groupingModule, + address groupNft ) { if ( accessController == address(0) || @@ -101,7 +111,9 @@ contract StoryProtocolGateway is licenseRegistry == address(0) || royaltyModule == address(0) || coreMetadataModule == address(0) || - licenseToken == address(0) + licenseToken == address(0) || + groupingModule == address(0) || + groupNft == address(0) ) revert Errors.SPG__ZeroAddressParam(); ACCESS_CONTROLLER = IAccessController(accessController); @@ -112,6 +124,8 @@ contract StoryProtocolGateway is CORE_METADATA_MODULE = ICoreMetadataModule(coreMetadataModule); PIL_TEMPLATE = IPILicenseTemplate(pilTemplate); LICENSE_TOKEN = ILicenseToken(licenseToken); + GROUPING_MODULE = IGroupingModule(groupingModule); + GROUP_NFT = GroupNFT(groupNft); _disableInitializers(); } @@ -160,26 +174,26 @@ contract StoryProtocolGateway is emit CollectionCreated(nftContract); } - /// @notice Mint an NFT from a collection and register it with metadata as an IP. + /// @notice Mint an NFT from a SPGNFT collection and register it with metadata as an IP. /// @dev Caller must have the minter role for the provided SPG NFT. - /// @param nftContract The address of the NFT collection. + /// @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. /// @return ipId The ID of the registered IP. - /// @return tokenId The ID of the minted NFT. + /// @return tokenId The ID of the newly minted NFT. function mintAndRegisterIp( - address nftContract, + address spgNftContract, address recipient, IPMetadata calldata ipMetadata - ) external onlyCallerWithMinterRole(nftContract) returns (address ipId, uint256 tokenId) { - tokenId = ISPGNFT(nftContract).mintBySPG({ + ) external onlyCallerWithMinterRole(spgNftContract) returns (address ipId, uint256 tokenId) { + tokenId = ISPGNFT(spgNftContract).mintBySPG({ to: address(this), payer: msg.sender, nftMetadataURI: ipMetadata.nftMetadataURI }); - ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); + ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); _setMetadata(ipMetadata, ipId); - ISPGNFT(nftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); } /// @notice Registers an NFT as IP with metadata. @@ -187,7 +201,7 @@ contract StoryProtocolGateway is /// @param tokenId The ID of the NFT. /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. /// @param sigMetadata OPTIONAL. Signature data for setAll (metadata) for the IP via the Core Metadata Module. - /// @return ipId The ID of the registered IP. + /// @return ipId The ID of the newly registered IP function registerIp( address nftContract, uint256 tokenId, @@ -201,7 +215,7 @@ contract StoryProtocolGateway is /// @notice Register Programmable IP License Terms (if unregistered) and attach it to IP. /// @param ipId The ID of the IP. /// @param terms The PIL terms to be registered. - /// @return licenseTermsId The ID of the registered PIL terms. + /// @return licenseTermsId The ID of the newly registered PIL terms. function registerPILTermsAndAttach( address ipId, PILTerms calldata terms @@ -209,32 +223,37 @@ contract StoryProtocolGateway is licenseTermsId = _registerPILTermsAndAttach(ipId, terms); } - /// @notice Mint an NFT from a collection, register it with metadata as an IP, register Programmable IP License + /// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, + /// register Programmable IP License /// Terms (if unregistered), and attach it to the registered IP. /// @dev Caller must have the minter role for the provided SPG NFT. - /// @param nftContract The address of the NFT collection. + /// @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 terms The PIL terms to be registered. - /// @return ipId The ID of the registered IP. - /// @return tokenId The ID of the minted NFT. - /// @return licenseTermsId The ID of the registered PIL terms. + /// @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 nftContract, + address spgNftContract, address recipient, IPMetadata calldata ipMetadata, PILTerms calldata terms - ) external onlyCallerWithMinterRole(nftContract) returns (address ipId, uint256 tokenId, uint256 licenseTermsId) { - tokenId = ISPGNFT(nftContract).mintBySPG({ + ) + external + onlyCallerWithMinterRole(spgNftContract) + returns (address ipId, uint256 tokenId, uint256 licenseTermsId) + { + tokenId = ISPGNFT(spgNftContract).mintBySPG({ to: address(this), payer: msg.sender, nftMetadataURI: ipMetadata.nftMetadataURI }); - ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); + ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); _setMetadata(ipMetadata, ipId); licenseTermsId = _registerPILTermsAndAttach(ipId, terms); - ISPGNFT(nftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); } /// @notice Register a given NFT as an IP and attach Programmable IP License Terms. @@ -245,10 +264,9 @@ contract StoryProtocolGateway is /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. /// @param terms The PIL terms to be registered. /// @param sigMetadata OPTIONAL. Signature data for setAll (metadata) for the IP via the Core Metadata Module. - /// @param sigAttach Signature data for attachLicenseTerms to the IP via the Licensing Module. The nonce of this - /// signature must be one above `sigMetadata` if the metadata is being set, ie. `sigMetadata` is non-empty. - /// @return ipId The ID of the registered IP. - /// @return licenseTermsId The ID of the registered PIL terms. + /// @param sigAttach Signature data for attachLicenseTerms to the IP via the Licensing Module. + /// @return ipId The ID of the newly registered IP. + /// @return licenseTermsId The ID of the newly registered PIL terms. function registerIpAndAttachPILTerms( address nftContract, uint256 tokenId, @@ -268,33 +286,33 @@ contract StoryProtocolGateway is licenseTermsId = _registerPILTermsAndAttach(ipId, terms); } - /// @notice Mint an NFT from a collection and register it as a derivative IP without license tokens. + /// @notice Mint an NFT from a SPGNFT collection and register it as a derivative IP without license tokens. /// @dev Caller must have the minter role for the provided SPG NFT. - /// @param nftContract The address of the NFT collection. + /// @param spgNftContract The address of the SPGNFT collection. /// @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. - /// @return ipId The ID of the registered IP. - /// @return tokenId The ID of the minted NFT. + /// @return ipId The ID of the newly registered IP. + /// @return tokenId The ID of the newly minted NFT. function mintAndRegisterIpAndMakeDerivative( - address nftContract, + address spgNftContract, MakeDerivative calldata derivData, IPMetadata calldata ipMetadata, address recipient - ) external onlyCallerWithMinterRole(nftContract) returns (address ipId, uint256 tokenId) { - tokenId = ISPGNFT(nftContract).mintBySPG({ + ) external onlyCallerWithMinterRole(spgNftContract) returns (address ipId, uint256 tokenId) { + tokenId = ISPGNFT(spgNftContract).mintBySPG({ to: address(this), payer: msg.sender, nftMetadataURI: ipMetadata.nftMetadataURI }); - ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); + ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); _setMetadata(ipMetadata, ipId); _collectMintFeesAndSetApproval( msg.sender, ipId, - derivData.parentIpIds, derivData.licenseTemplate, + derivData.parentIpIds, derivData.licenseTermsIds ); @@ -306,17 +324,17 @@ contract StoryProtocolGateway is royaltyContext: derivData.royaltyContext }); - ISPGNFT(nftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); } - /// @notice Register the given NFT as a derivative IP with metadata without using license tokens. + /// @notice Register the given NFT as a derivative IP with metadata without license tokens. /// @param nftContract The address of the NFT collection. /// @param tokenId The ID of the NFT. /// @param derivData The derivative data to be used for registerDerivative. /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. /// @param sigMetadata OPTIONAL. Signature data for setAll (metadata) for the IP via the Core Metadata Module. /// @param sigRegister Signature data for registerDerivative for the IP via the Licensing Module. - /// @return ipId The ID of the registered IP. + /// @return ipId The ID of the newly registered IP. function registerIpAndMakeDerivative( address nftContract, uint256 tokenId, @@ -337,8 +355,8 @@ contract StoryProtocolGateway is _collectMintFeesAndSetApproval( msg.sender, ipId, - derivData.parentIpIds, derivData.licenseTemplate, + derivData.parentIpIds, derivData.licenseTermsIds ); @@ -354,7 +372,7 @@ contract StoryProtocolGateway is /// @notice Mint an NFT from a collection and register it as a derivative IP using license tokens /// @dev Caller must have the minter role for the provided SPG NFT. Caller must own the license tokens and have /// approved SPG to transfer them. - /// @param nftContract The address of the NFT collection. + /// @param spgNftContract The address of the NFT collection. /// @param licenseTokenIds The IDs of the license tokens to be burned for linking the IP to parent IPs. /// @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. @@ -362,25 +380,25 @@ contract StoryProtocolGateway is /// @return ipId The ID of the registered IP. /// @return tokenId The ID of the minted NFT. function mintAndRegisterIpAndMakeDerivativeWithLicenseTokens( - address nftContract, + address spgNftContract, uint256[] calldata licenseTokenIds, bytes calldata royaltyContext, IPMetadata calldata ipMetadata, address recipient - ) external onlyCallerWithMinterRole(nftContract) returns (address ipId, uint256 tokenId) { + ) external onlyCallerWithMinterRole(spgNftContract) returns (address ipId, uint256 tokenId) { _collectLicenseTokens(licenseTokenIds); - tokenId = ISPGNFT(nftContract).mintBySPG({ + tokenId = ISPGNFT(spgNftContract).mintBySPG({ to: address(this), payer: msg.sender, nftMetadataURI: ipMetadata.nftMetadataURI }); - ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); + ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); _setMetadata(ipMetadata, ipId); LICENSING_MODULE.registerDerivativeWithLicenseTokens(ipId, licenseTokenIds, royaltyContext); - ISPGNFT(nftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); } /// @notice Register the given NFT as a derivative IP using license tokens. @@ -392,7 +410,7 @@ contract StoryProtocolGateway is /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. /// @param sigMetadata OPTIONAL. Signature data for setAll (metadata) for the IP via the Core Metadata Module. /// @param sigRegister Signature data for registerDerivativeWithLicenseTokens for the IP via the Licensing Module. - /// @return ipId The ID of the registered IP. + /// @return ipId The ID of the newly registered IP. function registerIpAndMakeDerivativeWithLicenseTokens( address nftContract, uint256 tokenId, @@ -415,6 +433,108 @@ contract StoryProtocolGateway is LICENSING_MODULE.registerDerivativeWithLicenseTokens(ipId, licenseTokenIds, royaltyContext); } + /// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, attach + /// Programmable IP License Terms to the registered IP, and add it to a group IP. + /// @dev Caller must have the minter role for the provided SPG NFT. + /// @param spgNftContract The address of the SPGNFT collection. + /// @param groupId The ID of the group IP to add the newly registered IP. + /// @param recipient The address of the recipient of the minted NFT. + /// @param licenseTermsId The ID of the registered PIL terms that will be attached to the newly registered 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. + /// @return ipId The ID of the newly registered IP. + /// @return tokenId The ID of the newly minted NFT. + function mintAndRegisterIpAndAttachPILTermsAndAddToGroup( + address spgNftContract, + address groupId, + address recipient, + uint256 licenseTermsId, + IPMetadata calldata ipMetadata, + SignatureData calldata sigAddToGroup + ) external onlyCallerWithMinterRole(spgNftContract) returns (address ipId, uint256 tokenId) { + tokenId = ISPGNFT(spgNftContract).mintBySPG({ + to: address(this), + payer: msg.sender, + nftMetadataURI: ipMetadata.nftMetadataURI + }); + ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); + _setMetadata(ipMetadata, ipId); + + LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId); + + _setPermissionForModule(groupId, sigAddToGroup, address(GROUPING_MODULE), IGroupingModule.addIp.selector); + + address[] memory ipIds = new address[](1); + ipIds[0] = ipId; + GROUPING_MODULE.addIp(groupId, ipIds); + + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + } + + /// @notice Register an NFT as IP with metadata, attach Programmable IP License Terms to the registered IP, + /// and add it to a group IP. + /// @param nftContract The address of the NFT collection. + /// @param tokenId The ID of the NFT. + /// @param groupId The ID of the group IP to add the newly registered IP. + /// @param licenseTermsId The ID of the registered PIL terms that will be attached to the newly registered IP. + /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. + /// @param sigMetadataAndAttach Signature data for setAll (metadata) and attachLicenseTerms to the IP + /// via the Core Metadata Module and Licensing Module. + /// @param sigAddToGroup Signature data for addIp to the group IP via the Grouping Module. + /// @return ipId The ID of the newly registered IP. + function registerIpAndAttachPILTermsAndAddToGroup( + address nftContract, + uint256 tokenId, + address groupId, + uint256 licenseTermsId, + IPMetadata calldata ipMetadata, + SignatureData calldata sigMetadataAndAttach, + SignatureData calldata sigAddToGroup + ) external returns (address ipId) { + ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); + + address[] memory modules = new address[](2); + bytes4[] memory selectors = new bytes4[](2); + modules[0] = address(CORE_METADATA_MODULE); + modules[1] = address(LICENSING_MODULE); + selectors[0] = ICoreMetadataModule.setAll.selector; + selectors[1] = ILicensingModule.attachLicenseTerms.selector; + + _setBatchPermissionForModules(ipId, sigMetadataAndAttach, modules, selectors); + + _setMetadata(ipMetadata, ipId); + + LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId); + + _setPermissionForModule(groupId, sigAddToGroup, address(GROUPING_MODULE), IGroupingModule.addIp.selector); + + address[] memory ipIds = new address[](1); + ipIds[0] = ipId; + GROUPING_MODULE.addIp(groupId, ipIds); + } + + /// @notice Register a group IP with a group reward pool, register Programmable IP License Terms, + /// attach it to the group IP, and add individual IPs to the group IP. + /// @dev ipIds must be have the same PIL terms as the group IP. + /// @param groupPool The address of the group reward pool. + /// @param ipIds The IDs of the IPs to add to the newly registered group IP. + /// @param groupIpTerms The PIL terms to be registered and attached to the newly registered group IP. + /// @return groupId The ID of the newly registered group IP. + /// @return groupLicenseTermsId The ID of the newly registered PIL terms. + function registerGroupAndAttachPILTermsAndAddIps( + address groupPool, + address[] calldata ipIds, + PILTerms calldata groupIpTerms + ) external returns (address groupId, uint256 groupLicenseTermsId) { + groupId = GROUPING_MODULE.registerGroup(groupPool); + + groupLicenseTermsId = _registerPILTermsAndAttach(groupId, groupIpTerms); + + GROUPING_MODULE.addIp(groupId, ipIds); + + GROUP_NFT.safeTransferFrom(address(this), msg.sender, GROUP_NFT.totalSupply() - 1); + } + /// @dev Registers PIL License Terms and attaches them to the given IP. /// @param ipId The ID of the IP. /// @param terms The PIL terms to be registered. @@ -461,6 +581,40 @@ contract StoryProtocolGateway is ); } + /// @dev Sets batch permission via signature to allow this contract to interact with mutiple modules + /// on behalf of the provided IP Account. + /// @param ipId The ID of the IP. + /// @param sigData Signature data for setting the batch permission. + /// @param modules The addresses of the modules to set the permission for. + /// @param selectors The selectors of the functions to be permitted for execution. + function _setBatchPermissionForModules( + address ipId, + SignatureData calldata sigData, + address[] memory modules, + bytes4[] memory selectors + ) internal { + // assumes modules and selectors must have a 1:1 mapping + AccessPermission.Permission[] memory permissionList = new AccessPermission.Permission[](modules.length); + for (uint256 i = 0; i < modules.length; i++) { + permissionList[i] = AccessPermission.Permission({ + ipAccount: ipId, + signer: address(this), + to: modules[i], + func: selectors[i], + permission: AccessPermission.ALLOW + }); + } + + IIPAccount(payable(ipId)).executeWithSig( + address(ACCESS_CONTROLLER), + 0, + abi.encodeWithSelector(IAccessController.setBatchPermissions.selector, permissionList), + sigData.signer, + sigData.deadline, + sigData.signature + ); + } + /// @dev Sets the metadata for the given IP if metadata is non-empty. /// @dev Sets the metadata for the given IP if metadata is non-empty. /// @param ipMetadata The metadata to set. @@ -514,14 +668,14 @@ contract StoryProtocolGateway is /// @dev Collect mint fees for all parent IPs from the payer and set approval for Royalty Module to spend mint fees. /// @param payerAddress The address of the payer for the license mint fees. /// @param childIpId The ID of the derivative IP. - /// @param parentIpIds The IDs of all the parent IPs. /// @param licenseTemplate The address of the license template. + /// @param parentIpIds The IDs of all the parent IPs. /// @param licenseTermsIds The IDs of the license terms for each corresponding parent IP. function _collectMintFeesAndSetApproval( address payerAddress, address childIpId, - address[] calldata parentIpIds, address licenseTemplate, + address[] calldata parentIpIds, uint256[] calldata licenseTermsIds ) internal { // Get currency token and royalty policy, assumes all parent IPs have the same currency token. @@ -537,7 +691,7 @@ contract StoryProtocolGateway is IERC20(mintFeeCurrencyToken).safeTransferFrom(payerAddress, address(this), totalMintFee); // Approve Royalty Policy to spend mint fee - IERC20(mintFeeCurrencyToken).forceApprove(royaltyPolicy, totalMintFee); + IERC20(mintFeeCurrencyToken).forceApprove(address(ROYALTY_MODULE), totalMintFee); } } } diff --git a/contracts/interfaces/IStoryProtocolGateway.sol b/contracts/interfaces/IStoryProtocolGateway.sol index 002ec3e..235acdf 100644 --- a/contracts/interfaces/IStoryProtocolGateway.sol +++ b/contracts/interfaces/IStoryProtocolGateway.sol @@ -60,15 +60,15 @@ interface IStoryProtocolGateway { address owner ) external returns (address nftContract); - /// @notice Mint an NFT from a collection and register it with metadata as an IP. + /// @notice Mint an NFT from a SPGNFT collection and register it with metadata as an IP. /// @dev Caller must have the minter role for the provided SPG NFT. - /// @param nftContract The address of the NFT collection. + /// @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. /// @return ipId The ID of the registered IP. - /// @return tokenId The ID of the minted NFT. + /// @return tokenId The ID of the newly minted NFT. function mintAndRegisterIp( - address nftContract, + address spgNftContract, address recipient, IPMetadata calldata ipMetadata ) external returns (address ipId, uint256 tokenId); @@ -78,7 +78,7 @@ interface IStoryProtocolGateway { /// @param tokenId The ID of the NFT. /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. /// @param sigMetadata OPTIONAL. Signature data for setAll (metadata) for the IP via the Core Metadata Module. - /// @return ipId The ID of the registered IP. + /// @return ipId The ID of the newly registered IP. function registerIp( address nftContract, uint256 tokenId, @@ -89,21 +89,22 @@ interface IStoryProtocolGateway { /// @notice Register Programmable IP License Terms (if unregistered) and attach it to IP. /// @param ipId The ID of the IP. /// @param terms The PIL terms to be registered. - /// @return licenseTermsId The ID of the registered PIL terms. + /// @return licenseTermsId The ID of the newly registered PIL terms. function registerPILTermsAndAttach(address ipId, PILTerms calldata terms) external returns (uint256 licenseTermsId); - /// @notice Mint an NFT from a collection, register it with metadata as an IP, register Programmable IP License + /// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, + /// register Programmable IPLicense /// Terms (if unregistered), and attach it to the registered IP. /// @dev Caller must have the minter role for the provided SPG NFT. - /// @param nftContract The address of the NFT collection. + /// @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 terms The PIL terms to be registered. - /// @return ipId The ID of the registered IP. - /// @return tokenId The ID of the minted NFT. - /// @return licenseTermsId The ID of the registered PIL terms. + /// @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 nftContract, + address spgNftContract, address recipient, IPMetadata calldata ipMetadata, PILTerms calldata terms @@ -118,8 +119,8 @@ interface IStoryProtocolGateway { /// @param terms The PIL terms to be registered. /// @param sigMetadata OPTIONAL. Signature data for setAll (metadata) for the IP via the Core Metadata Module. /// @param sigAttach Signature data for attachLicenseTerms to the IP via the Licensing Module. - /// @return ipId The ID of the registered IP. - /// @return licenseTermsId The ID of the registered PIL terms. + /// @return ipId The ID of the newly registered IP. + /// @return licenseTermsId The ID of the newly registered PIL terms. function registerIpAndAttachPILTerms( address nftContract, uint256 tokenId, @@ -129,29 +130,29 @@ interface IStoryProtocolGateway { SignatureData calldata sigAttach ) external returns (address ipId, uint256 licenseTermsId); - /// @notice Mint an NFT from a collection and register it as a derivative IP without license tokens. + /// @notice Mint an NFT from a SPGNFT collection and register it as a derivative IP without license tokens. /// @dev Caller must have the minter role for the provided SPG NFT. - /// @param nftContract The address of the NFT collection. + /// @param spgNftContract The address of the SPGNFT collection. /// @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. - /// @return ipId The ID of the registered IP. - /// @return tokenId The ID of the minted NFT. + /// @return ipId The ID of the newly registered IP. + /// @return tokenId The ID of the newly minted NFT. function mintAndRegisterIpAndMakeDerivative( - address nftContract, + address spgNftContract, MakeDerivative calldata derivData, IPMetadata calldata ipMetadata, address recipient ) external returns (address ipId, uint256 tokenId); - /// @notice Register the given NFT as a derivative IP with metadata without using license tokens. + /// @notice Register the given NFT as a derivative IP with metadata without license tokens. /// @param nftContract The address of the NFT collection. /// @param tokenId The ID of the NFT. /// @param derivData The derivative data to be used for registerDerivative. /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. /// @param sigMetadata OPTIONAL. Signature data for setAll (metadata) for the IP via the Core Metadata Module. /// @param sigRegister Signature data for registerDerivative for the IP via the Licensing Module. - /// @return ipId The ID of the registered IP. + /// @return ipId The ID of the newly registered IP. function registerIpAndMakeDerivative( address nftContract, uint256 tokenId, @@ -161,17 +162,17 @@ interface IStoryProtocolGateway { SignatureData calldata sigRegister ) external returns (address ipId); - /// @notice Mint an NFT from a collection and register it as a derivative IP using license tokens. + /// @notice Mint an NFT from a SPGNFT collection and register it as a derivative IP using license tokens. /// @dev Caller must have the minter role for the provided SPG NFT. - /// @param nftContract The address of the NFT collection. + /// @param spgNftContract The address of the SPGNFT collection. /// @param licenseTokenIds The IDs of the license tokens to be burned for linking the IP to parent IPs. /// @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. - /// @return ipId The ID of the registered IP. - /// @return tokenId The ID of the minted NFT. + /// @return ipId The ID of the newly registered IP. + /// @return tokenId The ID of the newly minted NFT. function mintAndRegisterIpAndMakeDerivativeWithLicenseTokens( - address nftContract, + address spgNftContract, uint256[] calldata licenseTokenIds, bytes calldata royaltyContext, IPMetadata calldata ipMetadata, @@ -186,7 +187,7 @@ interface IStoryProtocolGateway { /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. /// @param sigMetadata OPTIONAL. Signature data for setAll (metadata) for the IP via the Core Metadata Module. /// @param sigRegister Signature data for registerDerivativeWithLicenseTokens for the IP via the Licensing Module. - /// @return ipId The ID of the registered IP. + /// @return ipId The ID of the newly registered IP. function registerIpAndMakeDerivativeWithLicenseTokens( address nftContract, uint256 tokenId, @@ -196,4 +197,59 @@ interface IStoryProtocolGateway { SignatureData calldata sigMetadata, SignatureData calldata sigRegister ) external returns (address ipId); + + /// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, + /// attach Programmable IP License Terms to the registered IP, and add it to a group IP. + /// @dev Caller must have the minter role for the provided SPG NFT. + /// @param spgNftContract The address of the SPGNFT collection. + /// @param groupId The ID of the group IP to add the newly registered IP. + /// @param recipient The address of the recipient of the minted NFT. + /// @param licenseTermsId The ID of the registered PIL terms that will be attached to the newly registered 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. + /// @return ipId The ID of the newly registered IP. + /// @return tokenId The ID of the newly minted NFT. + function mintAndRegisterIpAndAttachPILTermsAndAddToGroup( + address spgNftContract, + address groupId, + address recipient, + uint256 licenseTermsId, + IPMetadata calldata ipMetadata, + SignatureData calldata sigAddToGroup + ) external returns (address ipId, uint256 tokenId); + + /// @notice Register an NFT as IP with metadata, attach Programmable IP License Terms to the registered IP, + /// and add it to a group IP. + /// @param nftContract The address of the NFT collection. + /// @param tokenId The ID of the NFT. + /// @param groupId The ID of the group IP to add the newly registered IP. + /// @param licenseTermsId The ID of the registered PIL terms that will be attached to the newly registered IP. + /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. + /// @param sigMetadataAndAttach Signature data for setAll (metadata) and attachLicenseTerms to the IP + /// via the Core Metadata Module and Licensing Module. + /// @param sigAddToGroup Signature data for addIp to the group IP via the Grouping Module. + /// @return ipId The ID of the newly registered IP. + function registerIpAndAttachPILTermsAndAddToGroup( + address nftContract, + uint256 tokenId, + address groupId, + uint256 licenseTermsId, + IPMetadata calldata ipMetadata, + SignatureData calldata sigMetadataAndAttach, + SignatureData calldata sigAddToGroup + ) external returns (address ipId); + + /// @notice Register a group IP with a group reward pool, register Programmable IP License Terms, + /// attach it to the group IP, and add individual IPs to the group IP. + /// @dev ipIds must be have the same PIL terms as the group IP. + /// @param groupPool The address of the group reward pool. + /// @param ipIds The IDs of the IPs to add to the newly registered group IP. + /// @param groupIpTerms The PIL terms to be registered and attached to the newly registered group IP. + /// @return groupId The ID of the newly registered group IP. + /// @return groupLicenseTermsId The ID of the newly registered PIL terms. + function registerGroupAndAttachPILTermsAndAddIps( + address groupPool, + address[] calldata ipIds, + PILTerms calldata groupIpTerms + ) external returns (address groupId, uint256 groupLicenseTermsId); } diff --git a/script/Main.s.sol b/script/Main.s.sol index e46322a..35a458d 100644 --- a/script/Main.s.sol +++ b/script/Main.s.sol @@ -4,11 +4,9 @@ pragma solidity ^0.8.23; import { console2 } from "forge-std/console2.sol"; import { Script } from "forge-std/Script.sol"; -import { stdJson } from "forge-std/StdJson.sol"; import { ICreate3Deployer } from "@create3-deployer/contracts/Create3Deployer.sol"; import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; -import { IPAssetRegistry } from "@storyprotocol/core/registries/IPAssetRegistry.sol"; import { StoryProtocolGateway } from "../contracts/StoryProtocolGateway.sol"; import { SPGNFT } from "../contracts/SPGNFT.sol"; @@ -61,7 +59,9 @@ contract Main is Script, StoryProtocolCoreAddressManager, BroadcastManager, Json royaltyModuleAddr, coreMetadataModuleAddr, pilTemplateAddr, - licenseTokenAddr + licenseTokenAddr, + groupingModuleAddr, + groupNFTAddr ) ); spg = StoryProtocolGateway( @@ -103,7 +103,7 @@ contract Main is Script, StoryProtocolCoreAddressManager, BroadcastManager, Json console2.log(string.concat(contractKey, " deployed to:"), newAddress); } - function _getSalt(string memory name) private view returns (bytes32 salt) { + function _getSalt(string memory name) private pure returns (bytes32 salt) { salt = keccak256(abi.encode(name, create3SaltSeed)); } } diff --git a/script/UpgradeSPG.s.sol b/script/UpgradeSPG.s.sol index 4031dfd..85b0db7 100644 --- a/script/UpgradeSPG.s.sol +++ b/script/UpgradeSPG.s.sol @@ -60,7 +60,9 @@ contract UpgradeSPG is royaltyModuleAddr, coreMetadataModuleAddr, pilTemplateAddr, - licenseTokenAddr + licenseTokenAddr, + groupingModuleAddr, + groupNFTAddr ) ); console2.log("New SPG Implementation", newSpgImpl); diff --git a/script/utils/StoryProtocolCoreAddressManager.sol b/script/utils/StoryProtocolCoreAddressManager.sol index b0c4ba5..13ba184 100644 --- a/script/utils/StoryProtocolCoreAddressManager.sol +++ b/script/utils/StoryProtocolCoreAddressManager.sol @@ -16,6 +16,8 @@ contract StoryProtocolCoreAddressManager is Script { address internal accessControllerAddr; address internal pilTemplateAddr; address internal licenseTokenAddr; + address internal groupingModuleAddr; + address internal groupNFTAddr; function _readStoryProtocolCoreAddresses() internal { string memory root = vm.projectRoot(); @@ -39,5 +41,7 @@ contract StoryProtocolCoreAddressManager is Script { accessControllerAddr = json.readAddress(".main.AccessController"); pilTemplateAddr = json.readAddress(".main.PILicenseTemplate"); licenseTokenAddr = json.readAddress(".main.LicenseToken"); + groupingModuleAddr = json.readAddress(".main.GroupingModule"); + groupNFTAddr = json.readAddress(".main.GroupNFT"); } } diff --git a/test/StoryProtocolGateway.t.sol b/test/StoryProtocolGateway.t.sol index 508c28b..a6664a3 100644 --- a/test/StoryProtocolGateway.t.sol +++ b/test/StoryProtocolGateway.t.sol @@ -7,6 +7,8 @@ import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol"; import { MetaTx } from "@storyprotocol/core/lib/MetaTx.sol"; import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol"; import { ICoreMetadataModule } from "@storyprotocol/core/interfaces/modules/metadata/ICoreMetadataModule.sol"; +import { IGroupingModule } from "@storyprotocol/core/interfaces/modules/grouping/IGroupingModule.sol"; +import { IAccessController } from "@storyprotocol/core/interfaces/access/IAccessController.sol"; import { IStoryProtocolGateway as ISPG } from "../contracts/interfaces/IStoryProtocolGateway.sol"; import { ISPGNFT } from "../contracts/interfaces/ISPGNFT.sol"; @@ -25,6 +27,7 @@ contract StoryProtocolGatewayTest is BaseTest { ISPGNFT internal nftContract; ISPGNFT[] internal nftContracts; + address internal groupId; address internal minter; address internal caller; mapping(uint256 index => IPAsset) internal ipAsset; @@ -87,7 +90,7 @@ contract StoryProtocolGatewayTest is BaseTest { { vm.expectRevert(Errors.SPG__CallerNotMinterRole.selector); vm.prank(caller); - spg.mintAndRegisterIp({ nftContract: address(nftContract), recipient: bob, ipMetadata: ipMetadataEmpty }); + spg.mintAndRegisterIp({ spgNftContract: address(nftContract), recipient: bob, ipMetadata: ipMetadataEmpty }); } modifier whenCallerHasMinterRole() { @@ -101,7 +104,7 @@ contract StoryProtocolGatewayTest is BaseTest { mockToken.approve(address(nftContract), 1000 * 10 ** mockToken.decimals()); (address ipId1, uint256 tokenId1) = spg.mintAndRegisterIp({ - nftContract: address(nftContract), + spgNftContract: address(nftContract), recipient: bob, ipMetadata: ipMetadataEmpty }); @@ -111,7 +114,7 @@ contract StoryProtocolGatewayTest is BaseTest { assertMetadata(ipId1, ipMetadataEmpty); (address ipId2, uint256 tokenId2) = spg.mintAndRegisterIp({ - nftContract: address(nftContract), + spgNftContract: address(nftContract), recipient: bob, ipMetadata: ipMetadataDefault }); @@ -155,7 +158,7 @@ contract StoryProtocolGatewayTest is BaseTest { mockToken.mint(address(owner), 100 * 10 ** mockToken.decimals()); mockToken.approve(address(nftContract), 100 * 10 ** mockToken.decimals()); (address ipId, uint256 tokenId) = spg.mintAndRegisterIp({ - nftContract: address(nftContract), + spgNftContract: address(nftContract), recipient: owner, ipMetadata: ipMetadataDefault }); @@ -212,7 +215,7 @@ contract StoryProtocolGatewayTest is BaseTest { withEnoughTokens { (address ipId1, uint256 tokenId1, uint256 licenseTermsId1) = spg.mintAndRegisterIpAndAttachPILTerms({ - nftContract: address(nftContract), + spgNftContract: address(nftContract), recipient: caller, ipMetadata: ipMetadataEmpty, terms: PILFlavors.nonCommercialSocialRemixing() @@ -227,7 +230,7 @@ contract StoryProtocolGatewayTest is BaseTest { assertEq(licenseTermsId, licenseTermsId1); (address ipId2, uint256 tokenId2, uint256 licenseTermsId2) = spg.mintAndRegisterIpAndAttachPILTerms({ - nftContract: address(nftContract), + spgNftContract: address(nftContract), recipient: caller, ipMetadata: ipMetadataDefault, terms: PILFlavors.nonCommercialSocialRemixing() @@ -275,7 +278,7 @@ contract StoryProtocolGatewayTest is BaseTest { modifier withNonCommercialParentIp() { (ipIdParent, , ) = spg.mintAndRegisterIpAndAttachPILTerms({ - nftContract: address(nftContract), + spgNftContract: address(nftContract), recipient: caller, ipMetadata: ipMetadataDefault, terms: PILFlavors.nonCommercialSocialRemixing() @@ -305,7 +308,7 @@ contract StoryProtocolGatewayTest is BaseTest { modifier withCommercialParentIp() { (ipIdParent, , ) = spg.mintAndRegisterIpAndAttachPILTerms({ - nftContract: address(nftContract), + spgNftContract: address(nftContract), recipient: caller, ipMetadata: ipMetadataDefault, terms: PILFlavors.commercialUse({ @@ -365,7 +368,7 @@ contract StoryProtocolGatewayTest is BaseTest { licenseTokenIds[0] = startLicenseTokenId; (address ipIdChild, uint256 tokenIdChild) = spg.mintAndRegisterIpAndMakeDerivativeWithLicenseTokens({ - nftContract: address(nftContract), + spgNftContract: address(nftContract), licenseTokenIds: licenseTokenIds, royaltyContext: "", ipMetadata: ipMetadataDefault, @@ -579,6 +582,170 @@ contract StoryProtocolGatewayTest is BaseTest { } } + modifier withGroup() { + groupId = groupingModule.registerGroup(address(rewardPool)); + uint256 deadline = block.timestamp + 1000; + + (bytes memory signature, , bytes memory data) = _getSetPermissionSignatureForSPG({ + ipId: groupId, + module: address(licensingModule), + selector: ILicensingModule.attachLicenseTerms.selector, + deadline: deadline, + state: IIPAccount(payable(groupId)).state(), + signerPk: alicePk + }); + + IIPAccount(payable(groupId)).executeWithSig({ + to: address(accessController), + value: 0, + data: data, + signer: alice, + deadline: deadline, + signature: signature + }); + + uint256 licenseTermsId = spg.registerPILTermsAndAttach({ + ipId: groupId, + terms: PILFlavors.nonCommercialSocialRemixing() + }); + _; + } + + function test_SPG_mintAndRegisterIpAndAddToGroup() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens + withGroup + { + uint256 deadline = block.timestamp + 1000; + + (bytes memory sigAddToGroup, bytes32 expectedState, ) = _getSetPermissionSignatureForSPG({ + ipId: groupId, + module: address(groupingModule), + selector: IGroupingModule.addIp.selector, + deadline: deadline, + state: IIPAccount(payable(groupId)).state(), + signerPk: alicePk + }); + + (address ipId, uint256 tokenId) = spg.mintAndRegisterIpAndAttachPILTermsAndAddToGroup({ + spgNftContract: address(nftContract), + groupId: groupId, + recipient: caller, + ipMetadata: ipMetadataEmpty, + licenseTermsId: 1, + sigAddToGroup: ISPG.SignatureData({ signer: alice, deadline: deadline, signature: sigAddToGroup }) + }); + + assertEq(expectedState, IIPAccount(payable(groupId)).state()); + assertTrue(ipAssetRegistry.isRegistered(ipId)); + assertTrue(ipAssetRegistry.isRegisteredGroup(groupId)); + assertTrue(ipAssetRegistry.containsIp(groupId, ipId)); + assertEq(ipAssetRegistry.totalMembers(groupId), 1); + assertEq(tokenId, 1); + assertSPGNFTMetadata(tokenId, ipMetadataEmpty.nftMetadataURI); + assertMetadata(ipId, ipMetadataEmpty); + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, 1); + } + + function test_SPG_registerIpAndAddToGroup() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens + withGroup + { + uint256 tokenId = nftContract.mint(address(caller), ipMetadataEmpty.nftMetadataURI); + address expectedIpId = ipAssetRegistry.ipId(block.chainid, address(nftContract), tokenId); + + uint256 deadline = block.timestamp + 1000; + + (bytes memory sigMetadataAndAttach, , ) = _getSetBatchPermissionSignatureForSPG({ + ipId: expectedIpId, + permissionList: _getMetadataAndAttachTermsPermissionList(expectedIpId), + deadline: deadline, + state: bytes32(0), + signerPk: alicePk + }); + + (bytes memory sigAddToGroup, , ) = _getSetPermissionSignatureForSPG({ + ipId: groupId, + module: address(groupingModule), + selector: IGroupingModule.addIp.selector, + deadline: deadline, + state: IIPAccount(payable(groupId)).state(), + signerPk: alicePk + }); + + address ipId = spg.registerIpAndAttachPILTermsAndAddToGroup({ + nftContract: address(nftContract), + tokenId: tokenId, + groupId: groupId, + ipMetadata: ipMetadataEmpty, + licenseTermsId: 1, + sigMetadataAndAttach: ISPG.SignatureData({ + signer: alice, + deadline: deadline, + signature: sigMetadataAndAttach + }), + sigAddToGroup: ISPG.SignatureData({ signer: alice, deadline: deadline, signature: sigAddToGroup }) + }); + + assertEq(expectedIpId, ipId); + assertTrue(ipAssetRegistry.isRegistered(expectedIpId)); + assertTrue(ipAssetRegistry.isRegisteredGroup(groupId)); + assertTrue(ipAssetRegistry.containsIp(groupId, expectedIpId)); + assertEq(ipAssetRegistry.totalMembers(groupId), 1); + assertSPGNFTMetadata(tokenId, ipMetadataEmpty.nftMetadataURI); + assertMetadata(expectedIpId, ipMetadataEmpty); + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(expectedIpId, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, 1); + } + + function test_SPG_registerGroupAndAddIps() public withCollection whenCallerHasMinterRole { + mockToken.mint(address(caller), 1000 * 10 * 10 ** mockToken.decimals()); + mockToken.approve(address(nftContract), 1000 * 10 * 10 ** mockToken.decimals()); + + bytes[] memory data = new bytes[](10); + for (uint256 i = 0; i < 10; i++) { + data[i] = abi.encodeWithSelector( + spg.mintAndRegisterIpAndAttachPILTerms.selector, + address(nftContract), + bob, + ipMetadataDefault, + PILFlavors.nonCommercialSocialRemixing() + ); + } + bytes[] memory results = spg.multicall(data); + address[] memory ipIds = new address[](10); + + for (uint256 i = 0; i < 10; i++) { + (ipIds[i], ) = abi.decode(results[i], (address, uint256)); + } + + uint256 groupLicenseTermsId; + (groupId, groupLicenseTermsId) = spg.registerGroupAndAttachPILTermsAndAddIps( + address(rewardPool), + ipIds, + PILFlavors.nonCommercialSocialRemixing() + ); + + assertTrue(ipAssetRegistry.isRegisteredGroup(groupId)); + assertEq(groupLicenseTermsId, 1); + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(groupId, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, groupLicenseTermsId); + + assertEq(ipAssetRegistry.totalMembers(groupId), 10); + for (uint256 i = 0; i < 10; i++) { + assertTrue(ipAssetRegistry.containsIp(groupId, ipIds[i])); + } + } + /// @dev Assert metadata for the SPGNFT. function assertSPGNFTMetadata(uint256 tokenId, string memory expectedMetadata) internal { assertEq(nftContract.tokenURI(tokenId), expectedMetadata); @@ -613,6 +780,8 @@ contract StoryProtocolGatewayTest is BaseTest { /// @param state IPAccount's internal nonce /// @param signerPk The private key of the signer. /// @return signature The signature for setting the permission. + /// @return expectedState The expected IPAccount's state after setting the permission. + /// @return data The call data for executing the setPermission function. function _getSetPermissionSignatureForSPG( address ipId, address module, @@ -624,12 +793,12 @@ contract StoryProtocolGatewayTest is BaseTest { expectedState = keccak256( abi.encode( state, // ipAccount.state() - abi.encodeWithSignature( - "execute(address,uint256,bytes)", + abi.encodeWithSelector( + IIPAccount.execute.selector, address(accessController), 0, // amount of ether to send - abi.encodeWithSignature( - "setPermission(address,address,address,bytes4,uint8)", + abi.encodeWithSelector( + IAccessController.setPermission.selector, ipId, address(spg), address(module), @@ -640,8 +809,8 @@ contract StoryProtocolGatewayTest is BaseTest { ) ); - data = abi.encodeWithSignature( - "setPermission(address,address,address,bytes4,uint8)", + data = abi.encodeWithSelector( + IAccessController.setPermission.selector, ipId, address(spg), address(module), @@ -666,6 +835,80 @@ contract StoryProtocolGatewayTest is BaseTest { signature = abi.encodePacked(r, s, v); } + /// @dev Get the permission list for setting metadata and attaching license terms for the IP. + /// @param ipId The ID of the IP that the permissions are for. + /// @return permissionList The list of permissions for setting metadata and attaching license terms. + function _getMetadataAndAttachTermsPermissionList( + address ipId + ) internal view returns (AccessPermission.Permission[] memory permissionList) { + address[] memory modules = new address[](2); + bytes4[] memory selectors = new bytes4[](2); + permissionList = new AccessPermission.Permission[](modules.length); + + modules[0] = address(coreMetadataModule); + modules[1] = address(licensingModule); + selectors[0] = ICoreMetadataModule.setAll.selector; + selectors[1] = ILicensingModule.attachLicenseTerms.selector; + + for (uint256 i = 0; i < 2; i++) { + permissionList[i] = AccessPermission.Permission({ + ipAccount: ipId, + signer: address(spg), + to: modules[i], + func: selectors[i], + permission: AccessPermission.ALLOW + }); + } + } + + /// @dev Get the signature for setting batch permission for the IP by the SPG. + /// @param ipId The ID of the IP to set the permissions for. + /// @param permissionList A list of permissions to set. + /// @param deadline The deadline for the signature. + /// @param state IPAccount's internal state + /// @param signerPk The private key of the signer. + /// @return signature The signature for setting the batch permission. + /// @return expectedState The expected IPAccount's state after setting batch permission. + /// @return data The call data for executing the setBatchPermissions function. + function _getSetBatchPermissionSignatureForSPG( + address ipId, + AccessPermission.Permission[] memory permissionList, + uint256 deadline, + bytes32 state, + uint256 signerPk + ) internal returns (bytes memory signature, bytes32 expectedState, bytes memory data) { + assertEq(state, bytes32(0)); + expectedState = keccak256( + abi.encode( + state, // ipAccount.state() + abi.encodeWithSelector( + IIPAccount.execute.selector, + address(accessController), + 0, // amount of ether to send + abi.encodeWithSelector(IAccessController.setBatchPermissions.selector, permissionList) + ) + ) + ); + + data = abi.encodeWithSelector(IAccessController.setBatchPermissions.selector, permissionList); + + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(ipId), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(accessController), + value: 0, + data: data, + nonce: expectedState, + deadline: deadline + }) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, digest); + signature = abi.encodePacked(r, s, v); + } + function _mintAndRegisterIpAndMakeDerivativeBaseTest() internal { (address licenseTemplateParent, uint256 licenseTermsIdParent) = licenseRegistry.getAttachedLicenseTerms( ipIdParent, @@ -679,7 +922,7 @@ contract StoryProtocolGatewayTest is BaseTest { licenseTermsIds[0] = licenseTermsIdParent; (address ipIdChild, uint256 tokenIdChild) = spg.mintAndRegisterIpAndMakeDerivative({ - nftContract: address(nftContract), + spgNftContract: address(nftContract), derivData: ISPG.MakeDerivative({ parentIpIds: parentIpIds, licenseTemplate: address(pilTemplate), diff --git a/test/mocks/MockEvenSplitGroupPool.sol b/test/mocks/MockEvenSplitGroupPool.sol new file mode 100644 index 0000000..7ca02ed --- /dev/null +++ b/test/mocks/MockEvenSplitGroupPool.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +import { IGroupRewardPool } from "@storyprotocol/core/interfaces/modules/grouping/IGroupRewardPool.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +contract MockEvenSplitGroupPool is IGroupRewardPool { + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; + + struct IpRewardInfo { + uint256 startPoolBalance; // balance of pool when IP added to pool + uint256 rewardDebt; // pending reward = (PoolInfo.accBalance - startPoolBalance) / totalIp - ip.rewardDebt + } + + struct PoolInfo { + uint256 accBalance; + uint256 availableBalance; + } + + mapping(address groupId => mapping(address ipId => uint256 addedTime)) public ipAddedTime; + mapping(address groupId => uint256 totalMemberIPs) public totalMemberIPs; + mapping(address groupId => EnumerableSet.AddressSet tokens) internal groupTokens; + // Info of each token pool. groupId => { token => PoolInfo} + mapping(address groupId => mapping(address token => PoolInfo)) public poolInfo; + // Info of each user that stakes LP tokens. groupId => { token => { ipId => IpInfo}} + mapping(address groupId => mapping(address tokenId => mapping(address ipId => IpRewardInfo))) public ipRewardInfo; + + function addIp(address groupId, address ipId) external { + // ignore if IP is already added to pool + if (ipAddedTime[groupId][ipId] != 0) return; + ipAddedTime[groupId][ipId] = block.timestamp; + // set rewardDebt of IP to current availableReward of the IP + totalMemberIPs[groupId] += 1; + + EnumerableSet.AddressSet storage tokens = groupTokens[groupId]; + uint256 length = tokens.length(); + for (uint256 i = 0; i < length; i++) { + address token = tokens.at(i); + _collectRoyalties(groupId, token); + uint256 totalReward = poolInfo[groupId][token].accBalance; + ipRewardInfo[groupId][token][ipId].startPoolBalance = totalReward; + ipRewardInfo[groupId][token][ipId].rewardDebt = 0; + } + } + + function removeIp(address groupId, address ipId) external { + EnumerableSet.AddressSet storage tokens = groupTokens[groupId]; + uint256 length = tokens.length(); + address[] memory ipIds = new address[](1); + ipIds[0] = ipId; + for (uint256 i = 0; i < length; i++) { + address token = tokens.at(i); + _collectRoyalties(groupId, token); + _distributeRewards(groupId, token, ipIds); + ipAddedTime[groupId][ipId] = 0; + } + totalMemberIPs[groupId] -= 1; + } + + /// @notice Returns the 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) { + return _getAvailableReward(groupId, token, ipIds); + } + + function distributeRewards( + address groupId, + address token, + address[] calldata ipIds + ) external returns (uint256[] memory rewards) { + return _distributeRewards(groupId, token, ipIds); + } + + function collectRoyalties(address groupId, address token) external { + _collectRoyalties(groupId, token); + } + + function _getAvailableReward( + address groupId, + address token, + address[] memory ipIds + ) internal view returns (uint256[] memory) { + uint256 totalAccumulatePoolBalance = poolInfo[groupId][token].accBalance; + uint256[] memory rewards = new uint256[](ipIds.length); + for (uint256 i = 0; i < ipIds.length; i++) { + // ignore if IP is not added to pool + if (ipAddedTime[groupId][ipIds[i]] == 0) { + rewards[i] = 0; + revert("IP not added to pool"); + continue; + } + uint256 poolBalanceBeforeIpAdded = ipRewardInfo[groupId][token][ipIds[i]].startPoolBalance; + uint256 rewardPerIP = (totalAccumulatePoolBalance - poolBalanceBeforeIpAdded) / totalMemberIPs[groupId]; + rewards[i] = rewardPerIP - ipRewardInfo[groupId][token][ipIds[i]].rewardDebt; + } + return rewards; + } + + function _distributeRewards( + address groupId, + address token, + address[] memory ipIds + ) internal returns (uint256[] memory rewards) { + rewards = _getAvailableReward(groupId, token, ipIds); + for (uint256 i = 0; i < ipIds.length; i++) { + // calculate pending reward for each IP + ipRewardInfo[groupId][token][ipIds[i]].rewardDebt += rewards[i]; + poolInfo[groupId][token].availableBalance -= rewards[i]; + // call royalty module to transfer reward to IP as royalty + IERC20(token).safeTransfer(ipIds[i], rewards[i]); + } + } + + function _collectRoyalties(address groupId, address token) internal { + // call royalty module to collect revenue of token + uint256 royalties = 0; + poolInfo[groupId][token].availableBalance += royalties; + poolInfo[groupId][token].accBalance += royalties; + groupTokens[groupId].add(token); + } + + function depositReward(address groupId, address token, uint256 amount) external { + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + poolInfo[groupId][token].accBalance += amount; + poolInfo[groupId][token].availableBalance += amount; + groupTokens[groupId].add(token); + } +} diff --git a/test/utils/BaseTest.t.sol b/test/utils/BaseTest.t.sol index 5bd1894..8ed3d5a 100644 --- a/test/utils/BaseTest.t.sol +++ b/test/utils/BaseTest.t.sol @@ -16,10 +16,15 @@ import { PILicenseTemplate } from "@storyprotocol/core/modules/licensing/PILicen import { LicensingModule } from "@storyprotocol/core/modules/licensing/LicensingModule.sol"; import { DisputeModule } from "@storyprotocol/core/modules/dispute/DisputeModule.sol"; import { RoyaltyModule } from "@storyprotocol/core/modules/royalty/RoyaltyModule.sol"; -import { RoyaltyPolicyLAP } from "@storyprotocol/core/modules/royalty/policies/RoyaltyPolicyLAP.sol"; +import { RoyaltyPolicyLAP } from "@storyprotocol/core/modules/royalty/policies/LAP/RoyaltyPolicyLAP.sol"; import { IpRoyaltyVault } from "@storyprotocol/core/modules/royalty/policies/IpRoyaltyVault.sol"; import { CoreMetadataModule } from "@storyprotocol/core/modules/metadata/CoreMetadataModule.sol"; import { CoreMetadataViewModule } from "@storyprotocol/core/modules/metadata/CoreMetadataViewModule.sol"; +import { LicenseToken } from "@storyprotocol/core/LicenseToken.sol"; +import { GroupNFT } from "@storyprotocol/core/GroupNFT.sol"; +import { GroupingModule } from "@storyprotocol/core/modules/grouping/GroupingModule.sol"; +import { MockEvenSplitGroupPool } from "../mocks/MockEvenSplitGroupPool.sol"; +import { IPGraphACL } from "@storyprotocol/core/access/IPGraphACL.sol"; import { StoryProtocolGateway } from "../../contracts/StoryProtocolGateway.sol"; import { SPGNFT } from "../../contracts/SPGNFT.sol"; @@ -50,6 +55,10 @@ contract BaseTest is Test { CoreMetadataViewModule internal coreMetadataViewModule; PILicenseTemplate internal pilTemplate; LicenseToken internal licenseToken; + GroupingModule internal groupingModule; + GroupNFT internal groupNFT; + IPGraphACL internal ipGraphACL; + MockEvenSplitGroupPool public rewardPool; StoryProtocolGateway internal spg; SPGNFT internal spgNftImpl; @@ -81,7 +90,7 @@ contract BaseTest is Test { } function setUp_test_Core() public { - address impl; + address impl = address(0); // Make sure we don't deploy wrong impl ERC6551Registry erc6551Registry = new ERC6551Registry(); @@ -111,7 +120,14 @@ contract BaseTest is Test { ); require(_loadProxyImpl(address(moduleRegistry)) == impl, "ModuleRegistry Proxy Implementation Mismatch"); - impl = address(new IPAssetRegistry(address(erc6551Registry), _getDeployedAddress(type(IPAccountImpl).name))); + impl = address(0); // Make sure we don't deploy wrong impl + impl = address( + new IPAssetRegistry( + address(erc6551Registry), + _getDeployedAddress(type(IPAccountImpl).name), + _getDeployedAddress(type(GroupingModule).name) + ) + ); ipAssetRegistry = IPAssetRegistry( TestProxyHelper.deployUUPSProxy( create3Deployer, @@ -126,6 +142,7 @@ contract BaseTest is Test { ); require(_loadProxyImpl(address(ipAssetRegistry)) == impl, "IPAssetRegistry Proxy Implementation Mismatch"); + impl = address(0); // Make sure we don't deploy wrong impl address ipAccountRegistry = address(ipAssetRegistry); impl = address(new AccessController(address(ipAssetRegistry), address(moduleRegistry))); @@ -143,10 +160,16 @@ contract BaseTest is Test { ); require(_loadProxyImpl(address(accessController)) == impl, "AccessController Proxy Implementation Mismatch"); + ipGraphACL = new IPGraphACL(address(protocolAccessManager)); + ipGraphACL.whitelistAddress(_getDeployedAddress(type(RoyaltyPolicyLAP).name)); + ipGraphACL.whitelistAddress(_getDeployedAddress(type(LicenseRegistry).name)); + + impl = address(0); // Make sure we don't deploy wrong impl impl = address( new LicenseRegistry( _getDeployedAddress(type(LicensingModule).name), - _getDeployedAddress(type(DisputeModule).name) + _getDeployedAddress(type(DisputeModule).name), + address(ipGraphACL) ) ); licenseRegistry = LicenseRegistry( @@ -180,6 +203,7 @@ contract BaseTest is Test { "Deploy: IP Account Impl Address Mismatch" ); + impl = address(0); // Make sure we don't deploy wrong impl impl = address( new DisputeModule(address(accessController), address(ipAssetRegistry), address(licenseRegistry)) ); @@ -197,11 +221,13 @@ contract BaseTest is Test { ); require(_loadProxyImpl(address(disputeModule)) == impl, "DisputeModule Proxy Implementation Mismatch"); + impl = address(0); // Make sure we don't deploy wrong impl impl = address( new RoyaltyModule( _getDeployedAddress(type(LicensingModule).name), address(disputeModule), - address(licenseRegistry) + address(licenseRegistry), + address(ipAssetRegistry) ) ); royaltyModule = RoyaltyModule( @@ -209,7 +235,7 @@ contract BaseTest is Test { create3Deployer, _getSalt(type(RoyaltyModule).name), impl, - abi.encodeCall(RoyaltyModule.initialize, address(protocolAccessManager)) + abi.encodeCall(RoyaltyModule.initialize, (address(protocolAccessManager), 1024, 1024, 10)) ) ); require( @@ -218,6 +244,7 @@ contract BaseTest is Test { ); require(_loadProxyImpl(address(royaltyModule)) == impl, "RoyaltyModule Proxy Implementation Mismatch"); + impl = address(0); // Make sure we don't deploy wrong impl impl = address( new LicensingModule( address(accessController), @@ -243,7 +270,8 @@ contract BaseTest is Test { ); require(_loadProxyImpl(address(licensingModule)) == impl, "LicensingModule Proxy Implementation Mismatch"); - impl = address(new RoyaltyPolicyLAP(address(royaltyModule), address(licensingModule))); + impl = address(0); // Make sure we don't deploy wrong impl + impl = address(new RoyaltyPolicyLAP(address(royaltyModule), address(licensingModule), address(ipGraphACL))); royaltyPolicyLAP = RoyaltyPolicyLAP( TestProxyHelper.deployUUPSProxy( create3Deployer, @@ -263,7 +291,7 @@ contract BaseTest is Test { _getSalt(type(IpRoyaltyVault).name), abi.encodePacked( type(IpRoyaltyVault).creationCode, - abi.encode(address(royaltyPolicyLAP), address(disputeModule)) + abi.encode(address(disputeModule), address(royaltyModule)) ) ) ); @@ -278,6 +306,7 @@ contract BaseTest is Test { ) ); + impl = address(0); // Make sure we don't deploy wrong impl impl = address(new LicenseToken(address(licensingModule), address(disputeModule))); licenseToken = LicenseToken( TestProxyHelper.deployUUPSProxy( @@ -299,6 +328,7 @@ contract BaseTest is Test { ); require(_loadProxyImpl(address(licenseToken)) == impl, "LicenseToken Proxy Implementation Mismatch"); + impl = address(0); // Make sure we don't deploy wrong impl impl = address( new PILicenseTemplate( address(accessController), @@ -348,17 +378,61 @@ contract BaseTest is Test { ) ); + impl = address(0); // Make sure we don't deploy wrong impl + impl = address(new GroupNFT(_getDeployedAddress(type(GroupingModule).name))); + groupNFT = GroupNFT( + TestProxyHelper.deployUUPSProxy( + create3Deployer, + _getSalt(type(GroupNFT).name), + impl, + abi.encodeCall( + GroupNFT.initialize, + ( + address(protocolAccessManager), + "https://github.com/storyprotocol/protocol-core/blob/main/assets/license-image.gif" + ) + ) + ) + ); + require(_getDeployedAddress(type(GroupNFT).name) == address(groupNFT), "Deploy: Group NFT Address Mismatch"); + require(_loadProxyImpl(address(groupNFT)) == impl, "GroupNFT Proxy Implementation Mismatch"); + + impl = address(0); // Make sure we don't deploy wrong impl + impl = address( + new GroupingModule( + address(accessController), + address(ipAssetRegistry), + address(licenseRegistry), + address(licenseToken), + address(groupNFT) + ) + ); + groupingModule = GroupingModule( + TestProxyHelper.deployUUPSProxy( + create3Deployer, + _getSalt(type(GroupingModule).name), + impl, + abi.encodeCall(GroupingModule.initialize, address(protocolAccessManager)) + ) + ); + require( + _getDeployedAddress(type(GroupingModule).name) == address(groupingModule), + "Deploy: Grouping Module Address Mismatch" + ); + require(_loadProxyImpl(address(groupingModule)) == impl, "GroupingModule Proxy Implementation Mismatch"); + moduleRegistry.registerModule("DISPUTE_MODULE", address(disputeModule)); moduleRegistry.registerModule("LICENSING_MODULE", address(licensingModule)); moduleRegistry.registerModule("ROYALTY_MODULE", address(royaltyModule)); moduleRegistry.registerModule("CORE_METADATA_MODULE", address(coreMetadataModule)); moduleRegistry.registerModule("CORE_METADATA_VIEW_MODULE", address(coreMetadataViewModule)); + moduleRegistry.registerModule("GROUPING_MODULE", address(groupingModule)); coreMetadataViewModule.updateCoreMetadataModule(); licenseRegistry.registerLicenseTemplate(address(pilTemplate)); royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), true); - royaltyPolicyLAP.setIpRoyaltyVaultBeacon(address(ipRoyaltyVaultBeacon)); + royaltyModule.setIpRoyaltyVaultBeacon(address(ipRoyaltyVaultBeacon)); ipRoyaltyVaultBeacon.transferOwnership(address(royaltyPolicyLAP)); } @@ -372,7 +446,9 @@ contract BaseTest is Test { address(royaltyModule), address(coreMetadataModule), address(pilTemplate), - address(licenseToken) + address(licenseToken), + address(groupingModule), + address(groupNFT) ) ); spg = StoryProtocolGateway( @@ -409,6 +485,11 @@ contract BaseTest is Test { function setUp_test_Misc() public { mockToken = new MockERC20(); royaltyModule.whitelistRoyaltyToken(address(mockToken), true); + rewardPool = new MockEvenSplitGroupPool(); + + licenseRegistry.setDefaultLicenseTerms(address(pilTemplate), 0); + + groupingModule.whitelistGroupRewardPool(address(rewardPool)); vm.label(alice, "Alice"); vm.label(bob, "Bob");