Skip to content

Commit

Permalink
Add Cross-Chain IP Registration Functionality (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
kingster-will authored Apr 14, 2024
1 parent a997c50 commit d9e60b3
Show file tree
Hide file tree
Showing 13 changed files with 124 additions and 63 deletions.
3 changes: 2 additions & 1 deletion contracts/interfaces/registries/IIPAssetRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
62 changes: 39 additions & 23 deletions contracts/registries/IPAssetRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,45 +57,27 @@ 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);
ipAccount.setUint256("REGISTRATION_DATE", registrationDate);

_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.
Expand Down Expand Up @@ -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 {}
Expand Down
2 changes: 1 addition & 1 deletion test/foundry/integration/BaseIntegration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
2 changes: 1 addition & 1 deletion test/foundry/modules/dispute/ArbitrationPolicySP.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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"));

Expand Down
4 changes: 2 additions & 2 deletions test/foundry/modules/dispute/DisputeModule.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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, "");

Expand Down
8 changes: 4 additions & 4 deletions test/foundry/modules/licensing/LicensingModule.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
8 changes: 4 additions & 4 deletions test/foundry/modules/licensing/PILicenseTemplate.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
8 changes: 4 additions & 4 deletions test/foundry/modules/metadata/CoreMetadataModule.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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"));
Expand Down
2 changes: 1 addition & 1 deletion test/foundry/modules/metadata/CoreMetadataViewModule.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
2 changes: 1 addition & 1 deletion test/foundry/modules/royalty/RoyaltyModule.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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"));

Expand Down
68 changes: 56 additions & 12 deletions test/foundry/registries/IPAssetRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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));
Expand All @@ -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));
Expand All @@ -109,26 +109,26 @@ 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();
erc721WithoutMetadata.mint(alice, 1);
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.
Expand All @@ -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 {
Expand All @@ -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
);
}

Expand Down
8 changes: 4 additions & 4 deletions test/foundry/registries/LicenseRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit d9e60b3

Please sign in to comment.