diff --git a/contracts/interfaces/modules/IRegistrationModule.sol b/contracts/interfaces/modules/IRegistrationModule.sol index 024d7f97e..dfc2abdf9 100644 --- a/contracts/interfaces/modules/IRegistrationModule.sol +++ b/contracts/interfaces/modules/IRegistrationModule.sol @@ -4,12 +4,12 @@ pragma solidity ^0.8.23; interface IRegistrationModule { event RootIPRegistered(address indexed caller, address indexed ipId, uint256 indexed policyId); - event DerivativeIPRegistered(address indexed caller, address indexed ipId, uint256 licenseId); + event DerivativeIPRegistered(address indexed caller, address indexed ipId, uint256[] licenseIds); function registerRootIp(uint256 policyId, address tokenContract, uint256 tokenId) external returns (address); function registerDerivativeIp( - uint256 licenseId, + uint256[] calldata licenseIds, address tokenContract, uint256 tokenId, string memory ipName, diff --git a/contracts/interfaces/registries/ILicenseRegistry.sol b/contracts/interfaces/registries/ILicenseRegistry.sol index 56324f1b8..20478024f 100644 --- a/contracts/interfaces/registries/ILicenseRegistry.sol +++ b/contracts/interfaces/registries/ILicenseRegistry.sol @@ -63,8 +63,8 @@ interface ILicenseRegistry { /// @notice Emitted when an IP is linked to its parent by burning a license /// @param caller The address that called the function /// @param ipId The id of the IP - /// @param parentIpId The id of the parent IP - event IpIdLinkedToParent(address indexed caller, address indexed ipId, address indexed parentIpId); + /// @param parentIpIds The ids of the parent IP + event IpIdLinkedToParents(address indexed caller, address indexed ipId, address[] indexed parentIpIds); /// @notice Registers a policy framework manager into the contract, so it can add policy data for /// licenses. @@ -95,11 +95,12 @@ interface ILicenseRegistry { address receiver ) external returns (uint256 licenseId); - /// @notice Links an IP to its parent IP, burning the license NFT and the policy allows it - /// @param licenseId The id of the license to burn - /// @param childIpId The id of the child IP + /// @notice Links an IP to the licensors (parent IP IDs) listed in the License NFTs, if their policies allow it, + /// burning the NFTs in the proccess. The caller must be the owner of the NFTs and the IP owner. + /// @param licenseIds The id of the licenses to burn + /// @param childIpId The id of the child IP to be linked /// @param holder The address that holds the license - function linkIpToParent(uint256 licenseId, address childIpId, address holder) external; + function linkIpToParents(uint256[] calldata licenseIds, address childIpId, address holder) external; /// /// Getters diff --git a/contracts/modules/RegistrationModule.sol b/contracts/modules/RegistrationModule.sol index 86bdcc961..74af5e5b7 100644 --- a/contracts/modules/RegistrationModule.sol +++ b/contracts/modules/RegistrationModule.sol @@ -67,8 +67,8 @@ contract RegistrationModule is BaseModule, IRegistrationModule { return ipId; } - /// @notice Registers an IP derivative into the protocol. - /// @param licenseId The license to incorporate for the new IP. + /// @notice Registers IP derivatives into the protocol. + /// @param licenseIds The licenses to incorporate for the new IP. /// @param tokenContract The address of the NFT bound to the derivative IP. /// @param tokenId The token id of the NFT bound to the derivative IP. /// @param ipName The name assigned to the new IP. @@ -77,7 +77,7 @@ contract RegistrationModule is BaseModule, IRegistrationModule { /// TODO: Replace all metadata with a generic bytes parameter type, and do /// encoding on the periphery contract level instead. function registerDerivativeIp( - uint256 licenseId, + uint256[] calldata licenseIds, address tokenContract, uint256 tokenId, string memory ipName, @@ -115,10 +115,9 @@ contract RegistrationModule is BaseModule, IRegistrationModule { // metadataProvider.setMetadata(ipId, metadata); // Perform core IP derivative licensing - the license must be owned by the caller. - // TODO: return resulting policy index - LICENSE_REGISTRY.linkIpToParent(licenseId, ipId, msg.sender); + LICENSE_REGISTRY.linkIpToParents(licenseIds, ipId, msg.sender); - emit DerivativeIPRegistered(msg.sender, ipId, licenseId); + emit DerivativeIPRegistered(msg.sender, ipId, licenseIds); } /// @notice Gets the protocol-wide module identifier for this module. diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index 18ea21f44..79fcd82bf 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -58,14 +58,6 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { /// This tracks the number of licenses registered in the protocol, it will not decrease when a license is burnt. uint256 private _totalLicenses; - modifier onlyLicensee(uint256 licenseId, address holder) { - // Should ERC1155 operator count? IMO is a security risk. Better use ACL - if (balanceOf(holder, licenseId) == 0) { - revert Errors.LicenseRegistry__NotLicensee(); - } - _; - } - constructor(address accessController, address ipAccountRegistry) ERC1155("") { ACCESS_CONTROLLER = IAccessController(accessController); IP_ACCOUNT_REGISTRY = IIPAccountRegistry(ipAccountRegistry); @@ -106,7 +98,7 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { if (!isPolicyDefined(polId)) { revert Errors.LicenseRegistry__PolicyNotFound(); } - return _addPolictyIdToIp(ipId, polId, false); + return _addPolicyIdToIp(ipId, polId, false); } /// @notice Registers a policy into the contract. MUST be called by a registered @@ -191,58 +183,61 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { return licenseId; } - /// Relates an IP ID with its parents (licensors), by burning the License NFT the holder owns - /// Licensing parameters related to linking IPAs must be verified in order to succeed, reverts otherwise. - /// The child IP ID will have the policy that the license represent added to it's own, if it's compatible with - /// existing child policies. - /// The child IP ID will be linked to the parent (if it wasn't before). - /// @param licenseId license NFT to be burned - /// @param childIpId that will receive the policy defined by licenseId - /// @param holder of the license NFT - function linkIpToParent( - uint256 licenseId, + /// @notice Links an IP to the licensors (parent IP IDs) listed in the License NFTs, if their policies allow it, + /// burning the NFTs in the proccess. The caller must be the owner of the NFTs and the IP owner. + /// @param licenseIds The id of the licenses to burn + /// @param childIpId The id of the child IP to be linked + /// @param holder The address that holds the license + function linkIpToParents( + uint256[] calldata licenseIds, address childIpId, address holder - ) external onlyLicensee(licenseId, holder) { + ) external { // check the caller is owner or authorized by the childIp if ( msg.sender != childIpId && - !ACCESS_CONTROLLER.checkPermission(childIpId, msg.sender, address(this), this.linkIpToParent.selector) + !ACCESS_CONTROLLER.checkPermission(childIpId, msg.sender, address(this), this.linkIpToParents.selector) ) { revert Errors.LicenseRegistry__UnauthorizedAccess(); } - // TODO: check if childIpId exists and is owned by holder - Licensing.License memory licenseData = _licenses[licenseId]; - address parentIpId = licenseData.licensorIpId; - if (parentIpId == childIpId) { - revert Errors.LicenseRegistry__ParentIdEqualThanChild(); - } - // TODO: check licensor exist - // TODO: check licensor not part of a branch tagged by disputer - - // Verify linking params - Licensing.Policy memory pol = policy(licenseData.policyId); - if (ERC165Checker.supportsInterface(pol.policyFramework, type(ILinkParamVerifier).interfaceId)) { - if ( - !ILinkParamVerifier(pol.policyFramework).verifyLink( - licenseId, - msg.sender, - childIpId, - parentIpId, - pol.data - ) - ) { - revert Errors.LicenseRegistry__LinkParentParamFailed(); + uint256 licenses = licenseIds.length; + address[] memory licensors = new address[](licenses); + uint256[] memory values = new uint256[](licenses); + for (uint256 i = 0; i < licenses; i++) { + uint256 licenseId = licenseIds[i]; + if (balanceOf(holder, licenseId) == 0) { + revert Errors.LicenseRegistry__NotLicensee(); + } + Licensing.License memory licenseData = _licenses[licenseId]; + licensors[i] = licenseData.licensorIpId; + // TODO: check licensor not part of a branch tagged by disputer + if (licensors[i] == childIpId) { + revert Errors.LicenseRegistry__ParentIdEqualThanChild(); } + // Verify linking params + Licensing.Policy memory pol = policy(licenseData.policyId); + if (ERC165Checker.supportsInterface(pol.policyFramework, type(ILinkParamVerifier).interfaceId)) { + if ( + !ILinkParamVerifier(pol.policyFramework).verifyLink( + licenseId, + msg.sender, + childIpId, + licensors[i], + pol.data + ) + ) { + revert Errors.LicenseRegistry__LinkParentParamFailed(); + } + } + // Add policy to kid + _addPolicyIdToIp(childIpId, licenseData.policyId, true); + // Set parent + _ipIdParents[childIpId].add(licensors[i]); + values[i] = 1; } - // Add policy to kid - _addPolictyIdToIp(childIpId, licenseData.policyId, true); - // Set parent - _ipIdParents[childIpId].add(parentIpId); - emit IpIdLinkedToParent(msg.sender, childIpId, parentIpId); - - // Burn license - _burn(holder, licenseId, 1); + emit IpIdLinkedToParents(msg.sender, childIpId, licensors); + // Burn licenses + _burnBatch(holder, licenseIds, values); } /// @notice True if the framework address is registered in LicenseRegistry @@ -373,7 +368,7 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { super._update(from, to, ids, values); } - function _verifyRegisteredFramework(address policyFramework) internal view returns (address) { + function _verifyRegisteredFramework(address policyFramework) internal view { if (!_registeredFrameworkManagers[policyFramework]) { revert Errors.LicenseRegistry__FrameworkNotFound(); } @@ -385,7 +380,7 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { /// @param policyId id of the policy data /// @param inheritedPolicy true if set in linkIpToParent, false otherwise /// @return index of the policy added to the set - function _addPolictyIdToIp(address ipId, uint256 policyId, bool inheritedPolicy) internal returns (uint256 index) { + function _addPolicyIdToIp(address ipId, uint256 policyId, bool inheritedPolicy) internal returns (uint256 index) { EnumerableSet.UintSet storage _pols = _policiesPerIpId[ipId]; if (!_pols.add(policyId)) { revert Errors.LicenseRegistry__PolicyAlreadySetForIpId(); @@ -431,7 +426,7 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { return (id, true); } - function _verifyPolicy(Licensing.Policy memory pol) private view { + function _verifyPolicy(Licensing.Policy memory pol) private pure { if (pol.policyFramework == address(0)) { revert Errors.LicenseRegistry__PolicyNotFound(); } diff --git a/script/foundry/deployment/Main.s.sol b/script/foundry/deployment/Main.s.sol index 613ca8c89..45e69170a 100644 --- a/script/foundry/deployment/Main.s.sol +++ b/script/foundry/deployment/Main.s.sol @@ -340,9 +340,11 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { 2, deployer ); + uint256[] memory licenseIds = new uint256[](1); + licenseIds[0] = licenseId1; registrationModule.registerDerivativeIp( - licenseId1, + licenseIds, address(mockNft), nftIds[3], "best derivative ip", @@ -353,8 +355,8 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { // /*/////////////////////////////////////////////////////////////// // LINK IPACCOUNTS TO PARENTS USING LICENSES // ////////////////////////////////////////////////////////////////*/ - - licenseRegistry.linkIpToParent(licenseId1, getIpId(mockNft, nftIds[4]), deployer); + + licenseRegistry.linkIpToParents(licenseIds, getIpId(mockNft, nftIds[4]), deployer); } function getIpId(MockERC721 mnft, uint256 tokenId) public view returns (address ipId) { diff --git a/test/foundry/integration/BaseIntegration.sol b/test/foundry/integration/BaseIntegration.sol index fc8523e0f..22609f39b 100644 --- a/test/foundry/integration/BaseIntegration.sol +++ b/test/foundry/integration/BaseIntegration.sol @@ -174,7 +174,7 @@ contract BaseIntegration is Test { accessController.setGlobalPermission( address(registrationModule), address(licenseRegistry), - bytes4(licenseRegistry.linkIpToParent.selector), + bytes4(licenseRegistry.linkIpToParents.selector), 1 // AccessPermission.ALLOW ); } @@ -252,8 +252,8 @@ contract BaseIntegration is Test { return registerIpAccount(address(nft), tokenId, caller); } - function registerDerivativeIp( - uint256 licenseId, + function registerDerivativeIps( + uint256[] calldata licenseIds, address nft, uint256 tokenId, string memory ipName, @@ -271,10 +271,15 @@ contract BaseIntegration is Test { ); vm.label(expectedAddr, string(abi.encodePacked("IPAccount", Strings.toString(tokenId)))); - - uint256 policyId = licenseRegistry.policyIdForLicense(licenseId); - address parentIpId = licenseRegistry.licensorIpId(licenseId); - uint256 newPolicyIndex = licenseRegistry.totalPoliciesForIp(expectedAddr); + + uint256[] memory policyIds = new uint256[](licenseIds.length); + address[] memory parentIpIds = new address[](licenseIds.length); + uint256[] memory newPolicyIndexes = new uint256[](licenseIds.length); + for (uint256 i = 0; i < licenseIds.length; i++) { + policyIds[i] = licenseRegistry.policyIdForLicense(licenseIds[i]); + parentIpIds[i] = licenseRegistry.licensorIpId(licenseIds[i]); + newPolicyIndexes[i] = licenseRegistry.totalPoliciesForIp(expectedAddr); + } vm.expectEmit(); emit IERC6551Registry.ERC6551AccountCreated({ @@ -320,84 +325,116 @@ contract BaseIntegration is Test { }); // Note that below events are emitted in function that's called by the registration module. - + for (uint256 i = 0; i < licenseIds.length; i++) { + vm.expectEmit(); + emit ILicenseRegistry.PolicyAddedToIpId({ + caller: address(registrationModule), + ipId: expectedAddr, + policyId: policyIds[i], + index: newPolicyIndexes[i], + inheritedPolicy: true + }); + } vm.expectEmit(); - emit ILicenseRegistry.PolicyAddedToIpId({ + emit ILicenseRegistry.IpIdLinkedToParents({ caller: address(registrationModule), ipId: expectedAddr, - policyId: policyId, - index: newPolicyIndex, - inheritedPolicy: true + parentIpIds: parentIpIds }); - vm.expectEmit(); - emit ILicenseRegistry.IpIdLinkedToParent({ - caller: address(registrationModule), - ipId: expectedAddr, - parentIpId: parentIpId - }); - - vm.expectEmit(); - emit IERC1155.TransferSingle({ - operator: address(registrationModule), - from: caller, - to: address(0), // burn addr - id: licenseId, - value: 1 - }); + if (licenseIds.length == 1) { + vm.expectEmit(); + emit IERC1155.TransferSingle({ + operator: address(registrationModule), + from: caller, + to: address(0), // burn addr + id: licenseIds[0], + value: 1 + }); + } else { + vm.expectEmit(); + emit IERC1155.TransferBatch({ + operator: address(registrationModule), + from: caller, + to: address(0), // burn addr + ids: licenseIds, + values: new uint256[](licenseIds.length) + }); + } vm.expectEmit(); - emit IRegistrationModule.DerivativeIPRegistered({ caller: caller, ipId: expectedAddr, licenseId: licenseId }); + emit IRegistrationModule.DerivativeIPRegistered({ caller: caller, ipId: expectedAddr, licenseIds: licenseIds }); vm.startPrank(caller); - registrationModule.registerDerivativeIp(licenseId, nft, tokenId, ipName, contentHash, externalUrl); + registrationModule.registerDerivativeIp(licenseIds, nft, tokenId, ipName, contentHash, externalUrl); return expectedAddr; } - function linkIpToParent(uint256 licenseId, address ipId, address caller) internal { - uint256 policyId = licenseRegistry.policyIdForLicense(licenseId); - address parentIpId = licenseRegistry.licensorIpId(licenseId); - uint256 newPolicyIndex = licenseRegistry.totalPoliciesForIp(ipId); - - uint256 prevLicenseAmount = licenseRegistry.balanceOf(caller, licenseId); + function linkIpToParents(uint256[] calldata licenseIds, address ipId, address caller) internal { + uint256[] memory policyIds = new uint256[](licenseIds.length); + address[] memory parentIpIds = new address[](licenseIds.length); + uint256[] memory newPolicyIndexes = new uint256[](licenseIds.length); + uint256[] memory prevLicenseAmounts = new uint256[](licenseIds.length); + uint256[] memory values = new uint256[](licenseIds.length); + + for (uint256 i = 0; i < licenseIds.length; i++) { + policyIds[i] = licenseRegistry.policyIdForLicense(licenseIds[i]); + parentIpIds[i] = licenseRegistry.licensorIpId(licenseIds[i]); + newPolicyIndexes[i] = licenseRegistry.totalPoliciesForIp(ipId); + prevLicenseAmounts[i] = licenseRegistry.balanceOf(caller, licenseIds[i]); + values[i] = 1; + vm.expectEmit(); + emit ILicenseRegistry.PolicyAddedToIpId({ + caller: caller, + ipId: ipId, + policyId: policyIds[i], + index: newPolicyIndexes[i], + inheritedPolicy: true + }); + } vm.expectEmit(); - emit ILicenseRegistry.PolicyAddedToIpId({ - caller: caller, - ipId: ipId, - policyId: policyId, - index: newPolicyIndex, - inheritedPolicy: true - }); - - vm.expectEmit(); - emit ILicenseRegistry.IpIdLinkedToParent({ caller: caller, ipId: ipId, parentIpId: parentIpId }); - - vm.expectEmit(); - emit IERC1155.TransferSingle({ - operator: caller, - from: caller, - to: address(0), // burn addr - id: licenseId, - value: 1 - }); + emit ILicenseRegistry.IpIdLinkedToParents({ caller: caller, ipId: ipId, parentIpIds: parentIpIds }); + + if (licenseIds.length == 1) { + vm.expectEmit(); + emit IERC1155.TransferSingle({ + operator: caller, + from: caller, + to: address(0), // burn addr + id: licenseIds[0], + value: 1 + }); + } else { + vm.expectEmit(); + emit IERC1155.TransferBatch({ + operator: caller, + from: caller, + to: address(0), // burn addr + ids: licenseIds, + values: values + }); + } vm.startPrank(caller); - licenseRegistry.linkIpToParent(licenseId, ipId, caller); - - assertEq(licenseRegistry.balanceOf(caller, licenseId), prevLicenseAmount - 1, "license not burnt on linking"); - assertTrue(licenseRegistry.isParent(parentIpId, ipId), "parent IP account is not parent"); - assertEq( - keccak256( - abi.encode( - licenseRegistry.policyForIpAtIndex( - parentIpId, - licenseRegistry.indexOfPolicyForIp(parentIpId, policyId) + licenseRegistry.linkIpToParents(licenseIds, ipId, caller); + + for (uint256 i = 0; i < licenseIds.length; i++) { + assertEq(licenseRegistry.balanceOf(caller, licenseIds[i]), prevLicenseAmounts[i] - 1, "license not burnt on linking"); + assertTrue(licenseRegistry.isParent(parentIpIds[i], ipId), "parent IP account is not parent"); + assertEq( + keccak256( + abi.encode( + licenseRegistry.policyForIpAtIndex( + parentIpIds[i], + licenseRegistry.indexOfPolicyForIp(parentIpIds[i], policyIds[i]) + ) ) - ) - ), - keccak256(abi.encode(licenseRegistry.policyForIpAtIndex(ipId, newPolicyIndex))), - "policy not the same in parent to child" - ); + ), + keccak256(abi.encode(licenseRegistry.policyForIpAtIndex(ipId, newPolicyIndexes[i]))), + "policy not the same in parent to child" + ); + } } + } diff --git a/test/foundry/modules/licensing/UMLPolicyFramework.t.sol b/test/foundry/modules/licensing/UMLPolicyFramework.t.sol index 46e0d6c0e..885e426f8 100644 --- a/test/foundry/modules/licensing/UMLPolicyFramework.t.sol +++ b/test/foundry/modules/licensing/UMLPolicyFramework.t.sol @@ -243,9 +243,11 @@ contract UMLPolicyFrameworkTest is Test { umlFramework.setApproval(licenseId, ipId2, false); assertFalse(umlFramework.isDerivativeApproved(licenseId, ipId2)); + uint256[] memory licenseIds = new uint256[](1); + licenseIds[0] = licenseId; vm.expectRevert(Errors.LicenseRegistry__LinkParentParamFailed.selector); vm.prank(ipOwner); - registry.linkIpToParent(licenseId, ipId2, licenseHolder); + registry.linkIpToParents(licenseIds, ipId2, licenseHolder); } function test_UMLPolicyFrameworkManager_derivatives_withApproval_linkApprovedIpId() public { @@ -274,9 +276,11 @@ contract UMLPolicyFrameworkTest is Test { vm.prank(ipOwner); umlFramework.setApproval(licenseId, ipId2, true); assertTrue(umlFramework.isDerivativeApproved(licenseId, ipId2)); + uint256[] memory licenseIds = new uint256[](1); + licenseIds[0] = licenseId; vm.prank(ipOwner); - registry.linkIpToParent(licenseId, ipId2, licenseHolder); + registry.linkIpToParents(licenseIds, ipId2, licenseHolder); assertTrue(registry.isParent(ipId1, ipId2)); } diff --git a/test/foundry/registries/LicenseRegistry.t.sol b/test/foundry/registries/LicenseRegistry.t.sol index e85622190..df89acfdb 100644 --- a/test/foundry/registries/LicenseRegistry.t.sol +++ b/test/foundry/registries/LicenseRegistry.t.sol @@ -99,7 +99,7 @@ contract LicenseRegistryTest is Test { function test_LicenseRegistry_registerPolicy_revert_policyAlreadyAdded() public { registry.registerPolicyFrameworkManager(address(module1)); vm.startPrank(address(module1)); - uint256 polId = registry.registerPolicy(_createPolicy()); + registry.registerPolicy(_createPolicy()); vm.expectRevert(Errors.LicenseRegistry__PolicyAlreadyAdded.selector); registry.registerPolicy(_createPolicy()); vm.stopPrank(); @@ -208,11 +208,12 @@ contract LicenseRegistryTest is Test { return licenseId; } - function test_LicenseRegistry_linkIpToParent() public { - // TODO: something cleaner than this + function test_LicenseRegistry_linkIpToParents_single_parent() public { uint256 licenseId = test_LicenseRegistry_mintLicense(); + uint256[] memory licenseIds = new uint256[](1); + licenseIds[0] = licenseId; vm.prank(ipOwner); - registry.linkIpToParent(licenseId, ipId2, licenseHolder); + registry.linkIpToParents(licenseIds, ipId2, licenseHolder); assertEq(registry.balanceOf(licenseHolder, licenseId), 1, "not burnt"); assertEq(registry.isParent(ipId1, ipId2), true, "not parent"); assertEq(