Skip to content

Commit

Permalink
Implement Permissionless IP Registration (#9)
Browse files Browse the repository at this point in the history
* implementing permissionless IP registration

* add chainid into name
  • Loading branch information
kingster-will authored Mar 21, 2024
1 parent bf49b64 commit ee44018
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 1 deletion.
25 changes: 25 additions & 0 deletions contracts/interfaces/registries/IIPAssetRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ interface IIPAssetRegistry is IIPAccountRegistry {
bytes metadata
);

// TODO: replace the IPRegistered event with IPRegisteredPermissionless
/// @notice Emits when an IP is officially registered into the protocol.
/// @param ipId The canonical identifier for the IP.
/// @param chainId The chain identifier of where the IP resides.
/// @param tokenContract The token contract address of the IP NFT.
/// @param tokenId The token identifier of the IP.
/// @param name The name of the IP.
/// @param uri The URI of the IP.
/// @param registrationDate The date and time the IP was registered.
event IPRegisteredPermissionless(
address ipId,
uint256 indexed chainId,
address indexed tokenContract,
uint256 indexed tokenId,
string name,
string uri,
uint256 registrationDate
);

/// @notice Emits when an IP resolver is bound to an IP.
/// @param ipId The canonical identifier of the specified IP.
/// @param resolver The address of the new resolver bound to the IP.
Expand Down Expand Up @@ -70,6 +89,12 @@ interface IIPAssetRegistry is IIPAccountRegistry {
/// @param approved Whether or not to approve that operator for registration.
function setApprovalForAll(address operator, bool approved) external;

/// @notice Registers an NFT as an IP asset.
/// @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);

/// @notice Registers an NFT as IP, creating a corresponding IP record.
/// @param chainId The chain identifier of where the NFT resides.
/// @param tokenContract The address of the NFT.
Expand Down
9 changes: 9 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ library Errors {
/// @notice The metadata provider is not valid.
error IPAssetRegistry__InvalidMetadataProvider();

/// @notice The NFT token contract is not valid ERC721 contract.
error IPAssetRegistry__UnsupportedIERC721(address contractAddress);

/// @notice The NFT token contract does not support ERC721Metadata.
error IPAssetRegistry__UnsupportedIERC721Metadata(address contractAddress);

/// @notice The NFT token id does not exist or invalid.
error IPAssetRegistry__InvalidToken(address contractAddress, uint256 tokenId);

////////////////////////////////////////////////////////////////////////////
// IPResolver ///
////////////////////////////////////////////////////////////////////////////
Expand Down
53 changes: 52 additions & 1 deletion contracts/registries/IPAssetRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
pragma solidity 0.8.23;

import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";

import { IIPAccount } from "../interfaces/IIPAccount.sol";
import { IIPAssetRegistry } from "../interfaces/registries/IIPAssetRegistry.sol";
Expand All @@ -16,6 +18,7 @@ import { IModuleRegistry } from "../interfaces/registries/IModuleRegistry.sol";
import { ILicensingModule } from "../interfaces/modules/licensing/ILicensingModule.sol";
import { IIPAssetRegistry } from "../interfaces/registries/IIPAssetRegistry.sol";
import { Governable } from "../governance/Governable.sol";
import { IPAccountStorageOps } from "../lib/IPAccountStorageOps.sol";

/// @title IP Asset Registry
/// @notice This contract acts as the source of truth for all IP registered in
Expand All @@ -27,6 +30,10 @@ import { Governable } from "../governance/Governable.sol";
/// IMPORTANT: The IP account address, besides being used for protocol
/// auth, is also the canonical IP identifier for the IP NFT.
contract IPAssetRegistry is IIPAssetRegistry, IPAccountRegistry, Governable {
using ERC165Checker for address;
using Strings for *;
using IPAccountStorageOps for IIPAccount;

/// @notice The canonical module registry used by the protocol.
IModuleRegistry public immutable MODULE_REGISTRY;

Expand Down Expand Up @@ -70,6 +77,49 @@ contract IPAssetRegistry is IIPAssetRegistry, IPAccountRegistry, Governable {
_metadataProvider = IMetadataProviderMigratable(newMetadataProvider);
}

/// @notice Registers an NFT as an IP asset.
/// @dev The IP required metadata name and URI are derived from the NFT's metadata.
/// @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);
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);
uint256 registrationDate = block.timestamp;
ipAccount.setString("NAME", name);
ipAccount.setString("URI", uri);
ipAccount.setUint256("REGISTRATION_DATE", registrationDate);

totalSupply++;

emit IPRegisteredPermissionless(id, block.chainid, tokenContract, tokenId, name, uri, registrationDate);
}

/// @notice Registers an NFT as IP, creating a corresponding IP record.
/// @param chainId The chain identifier of where the NFT resides.
/// @param tokenContract The address of the NFT.
Expand Down Expand Up @@ -146,7 +196,8 @@ contract IPAssetRegistry is IIPAssetRegistry, IPAccountRegistry, Governable {
/// @param id The canonical identifier for the IP.
/// @return isRegistered Whether the IP was registered into the protocol.
function isRegistered(address id) external view returns (bool) {
return _records[id].resolver != address(0);
// TODO: also check the ipAccount has "Name" metadata after clean up permissioned functions.
return _records[id].resolver != address(0) || id.code.length != 0;
}

/// @notice Gets the resolver bound to an IP based on its ID.
Expand Down
6 changes: 6 additions & 0 deletions contracts/utils/ShortStringOps.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
pragma solidity 0.8.23;

import { ShortString, ShortStrings } from "@openzeppelin/contracts/utils/ShortStrings.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";

/// @notice Library for working with Openzeppelin's ShortString data types.
library ShortStringOps {
using ShortStrings for *;
using Strings for *;

/// @dev Compares whether two ShortStrings are equal.
function equal(ShortString a, ShortString b) internal pure returns (bool) {
Expand Down Expand Up @@ -44,4 +46,8 @@ library ShortStringOps {
function bytes32ToString(bytes32 b) internal pure returns (string memory) {
return ShortString.wrap(b).toString();
}

function toShortString(uint256 value) internal pure returns (ShortString) {
return value.toString().toShortString();
}
}
43 changes: 43 additions & 0 deletions test/foundry/mocks/token/MockERC721WithoutMetadata.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: BUSDL-1.1
pragma solidity 0.8.23;

import { IERC721, IERC165 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";

contract MockERC721WithoutMetadata is IERC721 {
mapping(uint256 => address) private _owners;

function mint(address to, uint256 tokenId) external {
_owners[tokenId] = to;
}

function balanceOf(address owner) external view returns (uint256 balance) {
revert("MockERC721WithoutMetadata: not implemented");
}
function ownerOf(uint256 tokenId) external view returns (address owner) {
return _owners[tokenId];
}
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external {
revert("MockERC721WithoutMetadata: not implemented");
}
function safeTransferFrom(address from, address to, uint256 tokenId) external {
revert("MockERC721WithoutMetadata: not implemented");
}
function transferFrom(address from, address to, uint256 tokenId) external {
revert("MockERC721WithoutMetadata: not implemented");
}
function approve(address to, uint256 tokenId) external {
revert("MockERC721WithoutMetadata: not implemented");
}
function setApprovalForAll(address operator, bool approved) external {
revert("MockERC721WithoutMetadata: not implemented");
}
function getApproved(uint256 tokenId) external view returns (address operator) {
revert("MockERC721WithoutMetadata: not implemented");
}
function isApprovedForAll(address owner, address operator) external view returns (bool) {
revert("MockERC721WithoutMetadata: not implemented");
}
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC721).interfaceId;
}
}
105 changes: 105 additions & 0 deletions test/foundry/registries/IPAssetRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@ import { IIPAssetRegistry } from "contracts/interfaces/registries/IIPAssetRegist
import { IPAccountChecker } from "contracts/lib/registries/IPAccountChecker.sol";
import { IP } from "contracts/lib/IP.sol";
import { IPAssetRegistry } from "contracts/registries/IPAssetRegistry.sol";
import { IIPAccountRegistry } from "contracts/interfaces/registries/IIPAccountRegistry.sol";
import { Errors } from "contracts/lib/Errors.sol";
import { IIPAccount } from "contracts/interfaces/IIPAccount.sol";
import { IPAccountStorageOps } from "contracts/lib/IPAccountStorageOps.sol";
import { ShortStrings } from "@openzeppelin/contracts/utils/ShortStrings.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { MockERC721WithoutMetadata } from "test/foundry/mocks/token/MockERC721WithoutMetadata.sol";

import { BaseTest } from "../utils/BaseTest.t.sol";

/// @title IP Asset Registry Testing Contract
/// @notice Contract for testing core IP registration.
contract IPAssetRegistryTest is BaseTest {
using IPAccountStorageOps for IIPAccount;
using ShortStrings for *;
using Strings for *;
// Default IP record attributes.
string public constant IP_NAME = "IPAsset";
string public constant IP_DESCRIPTION = "IPs all the way down.";
Expand Down Expand Up @@ -63,6 +72,98 @@ contract IPAssetRegistryTest is BaseTest {
registry.register(block.chainid, tokenAddress, tokenId, resolver, true, metadata);
}

/// @notice Tests registration of IP permissionlessly.
function test_IPAssetRegistry_RegisterPermissionless() public {
uint256 totalSupply = registry.totalSupply();

assertTrue(!registry.isRegistered(ipId));
assertTrue(!IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId));
string memory name = string.concat(block.chainid.toString(), ": Ape #99");
vm.expectEmit(true, true, true, true);
emit IIPAssetRegistry.IPRegisteredPermissionless(
ipId,
block.chainid,
tokenAddress,
tokenId,
name,
"https://storyprotocol.xyz/erc721/99",
block.timestamp
);
vm.prank(alice);
registry.register(tokenAddress, tokenId);

assertEq(totalSupply + 1, registry.totalSupply());
assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId));
assertEq(IIPAccount(payable(ipId)).getString(address(registry), "NAME"), name);
assertEq(IIPAccount(payable(ipId)).getString(address(registry), "URI"), "https://storyprotocol.xyz/erc721/99");
assertEq(IIPAccount(payable(ipId)).getUint256(address(registry), "REGISTRATION_DATE"), block.timestamp);
}

/// @notice Tests registration of IP permissionlessly for IPAccount already created.
function test_IPAssetRegistry_RegisterPermissionless_IPAccountAlreadyExist() public {
uint256 totalSupply = registry.totalSupply();

IIPAccountRegistry(registry).registerIpAccount(block.chainid, tokenAddress, tokenId);
string memory name = string.concat(block.chainid.toString(), ": Ape #99");
vm.expectEmit(true, true, true, true);
emit IIPAssetRegistry.IPRegisteredPermissionless(
ipId,
block.chainid,
tokenAddress,
tokenId,
name,
"https://storyprotocol.xyz/erc721/99",
block.timestamp
);
vm.prank(alice);
registry.register(tokenAddress, tokenId);

assertEq(totalSupply + 1, registry.totalSupply());
assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId));
assertEq(IIPAccount(payable(ipId)).getString(address(registry), "NAME"), name);
assertEq(IIPAccount(payable(ipId)).getString(address(registry), "URI"), "https://storyprotocol.xyz/erc721/99");
assertEq(IIPAccount(payable(ipId)).getUint256(address(registry), "REGISTRATION_DATE"), block.timestamp);
}

/// @notice Tests registration of the same IP twice.
function test_IPAssetRegistry_revert_RegisterPermissionlessTwice() public {
assertTrue(!registry.isRegistered(ipId));
assertTrue(!IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId));

vm.prank(alice);
registry.register(tokenAddress, tokenId);

vm.expectRevert(Errors.IPAssetRegistry__AlreadyRegistered.selector);
vm.prank(alice);
registry.register(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);

// 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);
}

/// @notice Tests registration of IP with non-exist NFT.
function test_IPAssetRegistry_revert_InvalidNFTToken() public {
MockERC721WithoutMetadata erc721WithoutMetadata = new MockERC721WithoutMetadata();
erc721WithoutMetadata.mint(alice, 1);
// non exist token id
vm.expectRevert(
abi.encodeWithSelector(Errors.IPAssetRegistry__InvalidToken.selector, erc721WithoutMetadata, 999)
);
registry.register(address(erc721WithoutMetadata), 999);
}

/// @notice Tests registration of IP assets without licenses.
function test_IPAssetRegistry_Register() public {
uint256 totalSupply = registry.totalSupply();
Expand Down Expand Up @@ -224,4 +325,8 @@ contract IPAssetRegistryTest is BaseTest {
})
);
}

function _toBytes32(address a) internal pure returns (bytes32) {
return bytes32(uint256(uint160(a)));
}
}

0 comments on commit ee44018

Please sign in to comment.