diff --git a/contracts/interfaces/registries/IIPAssetRegistry.sol b/contracts/interfaces/registries/IIPAssetRegistry.sol index d6463ffa1..dc2facd61 100644 --- a/contracts/interfaces/registries/IIPAssetRegistry.sol +++ b/contracts/interfaces/registries/IIPAssetRegistry.sol @@ -28,10 +28,11 @@ interface IIPAssetRegistry is IIPAccountRegistry { function totalSupply() external view returns (uint256); /// @notice Registers an NFT as an IP asset. + /// @param chainid The chain identifier of where the IP NFT resides. /// @param tokenContract The address of the NFT. /// @param tokenId The token identifier of the NFT. /// @return id The address of the newly registered IP. - function register(address tokenContract, uint256 tokenId) external returns (address id); + function register(uint256 chainid, address tokenContract, uint256 tokenId) external returns (address id); /// @notice Gets the canonical IP identifier associated with an IP NFT. /// @dev This is equivalent to the address of its bound IP account. diff --git a/contracts/registries/IPAssetRegistry.sol b/contracts/registries/IPAssetRegistry.sol index f69836dd9..94b1e89ae 100644 --- a/contracts/registries/IPAssetRegistry.sol +++ b/contracts/registries/IPAssetRegistry.sol @@ -57,37 +57,19 @@ contract IPAssetRegistry is IIPAssetRegistry, IPAccountRegistry, AccessManagedUp /// @notice Registers an NFT as an IP asset. /// @dev The IP required metadata name and URI are derived from the NFT's metadata. + /// @param chainid The chain identifier of where the IP NFT resides. /// @param tokenContract The address of the NFT. /// @param tokenId The token identifier of the NFT. /// @return id The address of the newly registered IP. - function register(address tokenContract, uint256 tokenId) external returns (address id) { - if (!tokenContract.supportsInterface(type(IERC721).interfaceId)) { - revert Errors.IPAssetRegistry__UnsupportedIERC721(tokenContract); - } - - if (IERC721(tokenContract).ownerOf(tokenId) == address(0)) { - revert Errors.IPAssetRegistry__InvalidToken(tokenContract, tokenId); - } - - if (!tokenContract.supportsInterface(type(IERC721Metadata).interfaceId)) { - revert Errors.IPAssetRegistry__UnsupportedIERC721Metadata(tokenContract); - } - - id = registerIpAccount(block.chainid, tokenContract, tokenId); + function register(uint256 chainid, address tokenContract, uint256 tokenId) external returns (address id) { + id = registerIpAccount(chainid, tokenContract, tokenId); IIPAccount ipAccount = IIPAccount(payable(id)); if (bytes(ipAccount.getString("NAME")).length != 0) { revert Errors.IPAssetRegistry__AlreadyRegistered(); } - string memory name = string.concat( - block.chainid.toString(), - ": ", - IERC721Metadata(tokenContract).name(), - " #", - tokenId.toString() - ); - string memory uri = IERC721Metadata(tokenContract).tokenURI(tokenId); + (string memory name, string memory uri) = _getNameAndUri(chainid, tokenContract, tokenId); uint256 registrationDate = block.timestamp; ipAccount.setString("NAME", name); ipAccount.setString("URI", uri); @@ -95,7 +77,7 @@ contract IPAssetRegistry is IIPAssetRegistry, IPAccountRegistry, AccessManagedUp _getIPAssetRegistryStorage().totalSupply++; - emit IPRegistered(id, block.chainid, tokenContract, tokenId, name, uri, registrationDate); + emit IPRegistered(id, chainid, tokenContract, tokenId, name, uri, registrationDate); } /// @notice Gets the canonical IP identifier associated with an IP NFT. @@ -125,6 +107,40 @@ contract IPAssetRegistry is IIPAssetRegistry, IPAccountRegistry, AccessManagedUp return _getIPAssetRegistryStorage().totalSupply; } + /// @dev Retrieves the name and URI of from IP NFT. + function _getNameAndUri( + uint256 chainid, + address tokenContract, + uint256 tokenId + ) internal view returns (string memory name, string memory uri) { + if (chainid != block.chainid) { + name = string.concat(chainid.toString(), ": ", tokenContract.toHexString(), " #", tokenId.toString()); + uri = ""; + return (name, uri); + } + // Handle NFT on the same chain + if (!tokenContract.supportsInterface(type(IERC721).interfaceId)) { + revert Errors.IPAssetRegistry__UnsupportedIERC721(tokenContract); + } + + if (IERC721(tokenContract).ownerOf(tokenId) == address(0)) { + revert Errors.IPAssetRegistry__InvalidToken(tokenContract, tokenId); + } + + if (!tokenContract.supportsInterface(type(IERC721Metadata).interfaceId)) { + revert Errors.IPAssetRegistry__UnsupportedIERC721Metadata(tokenContract); + } + + name = string.concat( + block.chainid.toString(), + ": ", + IERC721Metadata(tokenContract).name(), + " #", + tokenId.toString() + ); + uri = IERC721Metadata(tokenContract).tokenURI(tokenId); + } + /// @dev Hook to authorize the upgrade according to UUPSUpgradeable /// @param newImplementation The address of the new implementation function _authorizeUpgrade(address newImplementation) internal override restricted {} diff --git a/test/foundry/integration/BaseIntegration.t.sol b/test/foundry/integration/BaseIntegration.t.sol index 52e309870..74b9c0ea5 100644 --- a/test/foundry/integration/BaseIntegration.t.sol +++ b/test/foundry/integration/BaseIntegration.t.sol @@ -72,7 +72,7 @@ contract BaseIntegration is BaseTest { }); vm.startPrank(owner); - return ipAssetRegistry.register(nft, tokenId); + return ipAssetRegistry.register(block.chainid, nft, tokenId); } function registerIpAccount(MockERC721 nft, uint256 tokenId, address caller) internal returns (address) { diff --git a/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol b/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol index 5c7610909..72efac65b 100644 --- a/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol +++ b/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol @@ -237,11 +237,11 @@ contract e2e is Test { uint256 tokenId6 = mockNft.mint(dave); uint256 tokenId7 = mockNft.mint(eve); - ipId1 = ipAssetRegistry.register(address(mockNft), tokenId1); - ipId2 = ipAssetRegistry.register(address(mockNft), tokenId2); - ipId3 = ipAssetRegistry.register(address(mockNft), tokenId3); - ipId6 = ipAssetRegistry.register(address(mockNft), tokenId6); - ipId7 = ipAssetRegistry.register(address(mockNft), tokenId7); + ipId1 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId1); + ipId2 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId2); + ipId3 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId3); + ipId6 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId6); + ipId7 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId7); // register license terms uint256 lcId1 = piLicenseTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); diff --git a/test/foundry/modules/dispute/ArbitrationPolicySP.t.sol b/test/foundry/modules/dispute/ArbitrationPolicySP.t.sol index 62957f02f..25af61b37 100644 --- a/test/foundry/modules/dispute/ArbitrationPolicySP.t.sol +++ b/test/foundry/modules/dispute/ArbitrationPolicySP.t.sol @@ -48,7 +48,7 @@ contract TestArbitrationPolicySP is BaseTest { vm.label(expectedAddr, "IPAccount0"); vm.startPrank(u.admin); - ipAddr = ipAssetRegistry.register(address(mockNFT), 0); + ipAddr = ipAssetRegistry.register(block.chainid, address(mockNFT), 0); licensingModule.attachLicenseTerms(ipAddr, address(pilTemplate), getSelectedPILicenseTermsId("cheap_flexible")); diff --git a/test/foundry/modules/dispute/DisputeModule.t.sol b/test/foundry/modules/dispute/DisputeModule.t.sol index ac964c60f..7302654cb 100644 --- a/test/foundry/modules/dispute/DisputeModule.t.sol +++ b/test/foundry/modules/dispute/DisputeModule.t.sol @@ -84,7 +84,7 @@ contract DisputeModuleTest is BaseTest { ); vm.startPrank(u.alice); - ipAddr = ipAssetRegistry.register(address(mockNFT), 0); + ipAddr = ipAssetRegistry.register(block.chainid, address(mockNFT), 0); licensingModule.attachLicenseTerms(ipAddr, address(pilTemplate), getSelectedPILicenseTermsId("cheap_flexible")); // Bob mints 1 license of policy "pil-commercial-remix" from IPAccount1 and registers the derivative IP for @@ -105,7 +105,7 @@ contract DisputeModuleTest is BaseTest { royaltyContext: "" }); // first license minted - ipAddr2 = ipAssetRegistry.register(address(mockNFT), 1); + ipAddr2 = ipAssetRegistry.register(block.chainid, address(mockNFT), 1); licensingModule.registerDerivativeWithLicenseTokens(ipAddr2, licenseIds, ""); diff --git a/test/foundry/modules/licensing/LicensingModule.t.sol b/test/foundry/modules/licensing/LicensingModule.t.sol index e60f8ee87..59d2a4b93 100644 --- a/test/foundry/modules/licensing/LicensingModule.t.sol +++ b/test/foundry/modules/licensing/LicensingModule.t.sol @@ -48,10 +48,10 @@ contract LicensingModuleTest is BaseTest { mockNft.mintId(ipOwner3, tokenId3); mockNft.mintId(ipOwner5, tokenId5); - ipId1 = ipAssetRegistry.register(address(mockNft), tokenId1); - ipId2 = ipAssetRegistry.register(address(mockNft), tokenId2); - ipId3 = ipAssetRegistry.register(address(mockNft), tokenId3); - ipId5 = ipAssetRegistry.register(address(mockNft), tokenId5); + ipId1 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId1); + ipId2 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId2); + ipId3 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId3); + ipId5 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId5); vm.label(ipId1, "IPAccount1"); vm.label(ipId2, "IPAccount2"); diff --git a/test/foundry/modules/licensing/PILicenseTemplate.t.sol b/test/foundry/modules/licensing/PILicenseTemplate.t.sol index 529650ea0..ff83ecaf6 100644 --- a/test/foundry/modules/licensing/PILicenseTemplate.t.sol +++ b/test/foundry/modules/licensing/PILicenseTemplate.t.sol @@ -43,10 +43,10 @@ contract PILicenseTemplateTest is BaseTest { mockNft.mintId(ipOwner3, tokenId3); mockNft.mintId(ipOwner5, tokenId5); - ipId1 = ipAssetRegistry.register(address(mockNft), tokenId1); - ipId2 = ipAssetRegistry.register(address(mockNft), tokenId2); - ipId3 = ipAssetRegistry.register(address(mockNft), tokenId3); - ipId5 = ipAssetRegistry.register(address(mockNft), tokenId5); + ipId1 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId1); + ipId2 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId2); + ipId3 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId3); + ipId5 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId5); vm.label(ipId1, "IPAccount1"); vm.label(ipId2, "IPAccount2"); diff --git a/test/foundry/modules/metadata/CoreMetadataModule.t.sol b/test/foundry/modules/metadata/CoreMetadataModule.t.sol index 97bc4d9d9..3178a51ff 100644 --- a/test/foundry/modules/metadata/CoreMetadataModule.t.sol +++ b/test/foundry/modules/metadata/CoreMetadataModule.t.sol @@ -17,7 +17,7 @@ contract CoreMetadataModuleTest is BaseTest { mockNFT.mintId(alice, 1); - ipAccount = IIPAccount(payable(ipAssetRegistry.register(address(mockNFT), 1))); + ipAccount = IIPAccount(payable(ipAssetRegistry.register(block.chainid, address(mockNFT), 1))); vm.label(address(ipAccount), "IPAccount1"); } @@ -39,7 +39,7 @@ contract CoreMetadataModuleTest is BaseTest { assertEq(ipAccount.getBytes32(address(coreMetadataModule), "NFT_METADATA_HASH"), bytes32("0x1234")); mockNFT.mintId(alice, 2); - IIPAccount ipAccount2 = IIPAccount(payable(ipAssetRegistry.register(address(mockNFT), 2))); + IIPAccount ipAccount2 = IIPAccount(payable(ipAssetRegistry.register(block.chainid, address(mockNFT), 2))); vm.label(address(ipAccount2), "IPAccount2"); vm.prank(alice); @@ -97,7 +97,7 @@ contract CoreMetadataModuleTest is BaseTest { assertEq(ipAccount.getBytes32(address(coreMetadataModule), "METADATA_HASH"), bytes32("0x1234")); mockNFT.mintId(alice, 2); - IIPAccount ipAccount2 = IIPAccount(payable(ipAssetRegistry.register(address(mockNFT), 2))); + IIPAccount ipAccount2 = IIPAccount(payable(ipAssetRegistry.register(block.chainid, address(mockNFT), 2))); vm.label(address(ipAccount2), "IPAccount2"); vm.prank(alice); @@ -212,7 +212,7 @@ contract CoreMetadataModuleTest is BaseTest { coreMetadataModule.updateNftTokenURI(address(ipAccount), bytes32("0x1234")); mockNFT.mintId(alice, 2); - IIPAccount ipAccount2 = IIPAccount(payable(ipAssetRegistry.register(address(mockNFT), 2))); + IIPAccount ipAccount2 = IIPAccount(payable(ipAssetRegistry.register(block.chainid, address(mockNFT), 2))); vm.label(address(ipAccount2), "IPAccount2"); coreMetadataModule.setMetadataURI(address(ipAccount2), "My MetadataURI2", bytes32("0x5678")); diff --git a/test/foundry/modules/metadata/CoreMetadataViewModule.t.sol b/test/foundry/modules/metadata/CoreMetadataViewModule.t.sol index ddf421697..494ac6fd5 100644 --- a/test/foundry/modules/metadata/CoreMetadataViewModule.t.sol +++ b/test/foundry/modules/metadata/CoreMetadataViewModule.t.sol @@ -20,7 +20,7 @@ contract CoreMetadataViewModuleTest is BaseTest { mockNFT.mintId(alice, 99); - ipAccount = IIPAccount(payable(ipAssetRegistry.register(address(mockNFT), 99))); + ipAccount = IIPAccount(payable(ipAssetRegistry.register(block.chainid, address(mockNFT), 99))); vm.label(address(ipAccount), "IPAccount1"); } diff --git a/test/foundry/modules/royalty/RoyaltyModule.t.sol b/test/foundry/modules/royalty/RoyaltyModule.t.sol index b25ef85d5..bf170fc7c 100644 --- a/test/foundry/modules/royalty/RoyaltyModule.t.sol +++ b/test/foundry/modules/royalty/RoyaltyModule.t.sol @@ -77,7 +77,7 @@ contract TestRoyaltyModule is BaseTest { vm.label(expectedAddr, "IPAccount0"); vm.startPrank(u.alice); - ipAddr = ipAssetRegistry.register(address(mockNFT), 0); + ipAddr = ipAssetRegistry.register(block.chainid, address(mockNFT), 0); licensingModule.attachLicenseTerms(ipAddr, address(pilTemplate), getSelectedPILicenseTermsId("cheap_flexible")); diff --git a/test/foundry/registries/IPAssetRegistry.t.sol b/test/foundry/registries/IPAssetRegistry.t.sol index 13acf12c7..8a2ba8aaa 100644 --- a/test/foundry/registries/IPAssetRegistry.t.sol +++ b/test/foundry/registries/IPAssetRegistry.t.sol @@ -42,12 +42,12 @@ contract IPAssetRegistryTest is BaseTest { tokenId = mockNFT.mintId(alice, 99); assertEq(ipAccountRegistry.getIPAccountImpl(), address(ipAccountImpl)); - ipId = _getIPAccount(tokenId); + ipId = _getIPAccount(block.chainid, tokenId); } /// @notice Tests retrieval of IP canonical IDs. function test_IPAssetRegistry_IpId() public { - assertEq(registry.ipId(block.chainid, tokenAddress, tokenId), _getIPAccount(tokenId)); + assertEq(registry.ipId(block.chainid, tokenAddress, tokenId), _getIPAccount(block.chainid, tokenId)); } /// @notice Tests registration of IP permissionlessly. @@ -68,7 +68,7 @@ contract IPAssetRegistryTest is BaseTest { block.timestamp ); vm.prank(alice); - registry.register(tokenAddress, tokenId); + registry.register(block.chainid, tokenAddress, tokenId); assertEq(totalSupply + 1, registry.totalSupply()); assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); @@ -94,7 +94,7 @@ contract IPAssetRegistryTest is BaseTest { block.timestamp ); vm.prank(alice); - registry.register(tokenAddress, tokenId); + registry.register(block.chainid, tokenAddress, tokenId); assertEq(totalSupply + 1, registry.totalSupply()); assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); @@ -109,18 +109,18 @@ contract IPAssetRegistryTest is BaseTest { assertTrue(!IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); vm.prank(alice); - registry.register(tokenAddress, tokenId); + registry.register(block.chainid, tokenAddress, tokenId); vm.expectRevert(Errors.IPAssetRegistry__AlreadyRegistered.selector); vm.prank(alice); - registry.register(tokenAddress, tokenId); + registry.register(block.chainid, tokenAddress, tokenId); } /// @notice Tests registration of IP with non ERC721 token. function test_IPAssetRegistry_revert_InvalidTokenContract() public { // not an ERC721 contract vm.expectRevert(abi.encodeWithSelector(Errors.IPAssetRegistry__UnsupportedIERC721.selector, address(0x12345))); - registry.register(address(0x12345), 1); + registry.register(block.chainid, address(0x12345), 1); // not implemented ERC721Metadata contract MockERC721WithoutMetadata erc721WithoutMetadata = new MockERC721WithoutMetadata(); @@ -128,7 +128,7 @@ contract IPAssetRegistryTest is BaseTest { vm.expectRevert( abi.encodeWithSelector(Errors.IPAssetRegistry__UnsupportedIERC721Metadata.selector, erc721WithoutMetadata) ); - registry.register(address(erc721WithoutMetadata), 1); + registry.register(block.chainid, address(erc721WithoutMetadata), 1); } /// @notice Tests registration of IP with non-exist NFT. @@ -139,7 +139,7 @@ contract IPAssetRegistryTest is BaseTest { vm.expectRevert( abi.encodeWithSelector(Errors.IPAssetRegistry__InvalidToken.selector, erc721WithoutMetadata, 999) ); - registry.register(address(erc721WithoutMetadata), 999); + registry.register(block.chainid, address(erc721WithoutMetadata), 999); } function test_IPAssetRegistry_not_registered() public { @@ -150,15 +150,59 @@ contract IPAssetRegistryTest is BaseTest { assertTrue(!registry.isRegistered(ipAssetRegistry.registerIpAccount(block.chainid, address(mockNFT), 1000))); } + /// @notice Tests registration of IP NFT from other chain. + function test_IPAssetRegistry_RegisterPermissionless_CrossChain() public { + uint256 totalSupply = registry.totalSupply(); + tokenAddress = address(0x12345); + tokenId = 1; + uint256 chainid = 55555555; + + ipId = _getIPAccount(chainid, tokenId); + + assertTrue(!registry.isRegistered(ipId)); + assertTrue(!IPAccountChecker.isRegistered(ipAccountRegistry, chainid, tokenAddress, tokenId)); + string memory name = string.concat( + chainid.toString(), + ": ", + tokenAddress.toHexString(), + " #", + tokenId.toString() + ); + vm.expectEmit(); + emit IIPAssetRegistry.IPRegistered(ipId, chainid, tokenAddress, tokenId, name, "", block.timestamp); + address registeredIpId = registry.register(chainid, tokenAddress, tokenId); + + assertEq(totalSupply + 1, registry.totalSupply()); + assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, chainid, tokenAddress, tokenId)); + assertEq(IIPAccount(payable(ipId)).getString(address(registry), "NAME"), name); + assertEq(IIPAccount(payable(ipId)).getUint256(address(registry), "REGISTRATION_DATE"), block.timestamp); + } + + /// @notice Tests registration of the same IP twice from cross chain. + function test_IPAssetRegistry_revert_RegisterPermissionlessTwice_CrossChain() public { + tokenAddress = address(0x12345); + tokenId = 1; + uint256 chainid = 55555555; + + ipId = _getIPAccount(chainid, tokenId); + assertTrue(!registry.isRegistered(ipId)); + assertTrue(!IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); + + registry.register(chainid, tokenAddress, tokenId); + + vm.expectRevert(Errors.IPAssetRegistry__AlreadyRegistered.selector); + registry.register(chainid, tokenAddress, tokenId); + } + /// @notice Helper function for generating an account address. - function _getIPAccount(uint256 contractId) internal view returns (address) { + function _getIPAccount(uint256 chainid, uint256 tokenId) internal view returns (address) { return erc6551Registry.account( address(ipAccountImpl), ipAccountRegistry.IP_ACCOUNT_SALT(), - block.chainid, + chainid, tokenAddress, - contractId + tokenId ); } diff --git a/test/foundry/registries/LicenseRegistry.t.sol b/test/foundry/registries/LicenseRegistry.t.sol index bde77637d..ce55ae9cd 100644 --- a/test/foundry/registries/LicenseRegistry.t.sol +++ b/test/foundry/registries/LicenseRegistry.t.sol @@ -49,10 +49,10 @@ contract LicenseRegistryTest is BaseTest { mockNft.mintId(ipOwner3, tokenId3); mockNft.mintId(ipOwner5, tokenId5); - ipId1 = ipAssetRegistry.register(address(mockNft), tokenId1); - ipId2 = ipAssetRegistry.register(address(mockNft), tokenId2); - ipId3 = ipAssetRegistry.register(address(mockNft), tokenId3); - ipId5 = ipAssetRegistry.register(address(mockNft), tokenId5); + ipId1 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId1); + ipId2 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId2); + ipId3 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId3); + ipId5 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId5); vm.label(ipId1, "IPAccount1"); vm.label(ipId2, "IPAccount2");