Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Permissionless IP Registration #9

Merged
merged 4 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
jdubpark marked this conversation as resolved.
Show resolved Hide resolved
/// @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(
jdubpark marked this conversation as resolved.
Show resolved Hide resolved
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) {
LeoHChen marked this conversation as resolved.
Show resolved Hide resolved
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);
jdubpark marked this conversation as resolved.
Show resolved Hide resolved
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)));
}
}
Loading