Skip to content

Commit

Permalink
Adds permissionless IPA registry auth and removes dependency of regis…
Browse files Browse the repository at this point in the history
…tration module for IP registration (storyprotocol#74)

* Adds operator authorization to IPA registry and removes need of reg. module as a dependency
  • Loading branch information
leeren authored Feb 8, 2024
1 parent 8925a5d commit f97ce66
Show file tree
Hide file tree
Showing 21 changed files with 312 additions and 33 deletions.
2 changes: 1 addition & 1 deletion contracts/IPAccountImpl.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Rec
import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import { IERC6551Account } from "@erc6551/interfaces/IERC6551Account.sol";
import { IERC6551Account } from "erc6551/interfaces/IERC6551Account.sol";

import { IAccessController } from "./interfaces/IAccessController.sol";
import { IIPAccount } from "./interfaces/IIPAccount.sol";
Expand Down
2 changes: 1 addition & 1 deletion contracts/interfaces/IIPAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.23;

import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import { IERC6551Account } from "@erc6551/interfaces/IERC6551Account.sol";
import { IERC6551Account } from "erc6551/interfaces/IERC6551Account.sol";

/// @title IIPAccount
/// @dev IPAccount is a token-bound account that adopts the EIP-6551 standard.
Expand Down
14 changes: 13 additions & 1 deletion contracts/interfaces/registries/IIPAssetRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface IIPAssetRegistry is IIPAccountRegistry {
/// @param resolver The address of the resolver linked to the IP.
/// @param provider The address of the metadata provider linked to the IP.
/// @param metadata Canonical metadata that was linked to the IP.
/// TODO: Add support for optional licenseIds.
event IPRegistered(
address ipId,
uint256 indexed chainId,
Expand All @@ -30,6 +31,12 @@ interface IIPAssetRegistry is IIPAccountRegistry {
/// @param resolver The address of the new resolver bound to the IP.
event IPResolverSet(address ipId, address resolver);

/// @notice Emits when an operator is approved for IP registration for an NFT owner.
/// @param owner The address of the IP owner.
/// @param operator The address of the operator the owneris authorizing.
/// @param approved Whether or not to approve that operator for registration.
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

/// @notice Emits when metadata is set for an IP asset.
/// @param ipId The canonical identifier of the specified IP.
/// @param metadataProvider Address of the metadata provider associated with the IP.
Expand All @@ -47,7 +54,6 @@ interface IIPAssetRegistry is IIPAccountRegistry {
function setMetadataProvider(address metadataProvider) external;

/// @notice Registers an NFT as IP, creating a corresponding IP record.
/// @dev This is only callable by an authorized registration module.
/// @param chainId The chain identifier of where the IP resides.
/// @param tokenContract The address of the IP.
/// @param tokenId The token identifier of the IP.
Expand Down Expand Up @@ -76,6 +82,12 @@ interface IIPAssetRegistry is IIPAccountRegistry {
/// @return The address of the associated IP account.
function ipId(uint256 chainId, address tokenContract, uint256 tokenId) external view returns (address);

/// @notice Checks whether an operator is approved to register on behalf of an IP owner.
/// @param owner The address of the IP owner whose approval is being checked for.
/// @param operator The address of the operator the owner has approved for registration delgation.
/// @return Whether the operator is approved on behalf of the owner for registering.
function isApprovedForAll(address owner, address operator) external view returns (bool);

/// @notice Checks whether an IP was registered based on its ID.
/// @param id The canonical identifier for the IP.
/// @return Whether the IP was registered into the protocol.
Expand Down
3 changes: 3 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ library Errors {
/// @notice The IP asset has not yet been registered.
error IPAssetRegistry__NotYetRegistered();

/// @notice The IP asset registrant is not authorized.
error IPAssetRegistry__RegistrantUnauthorized();

/// @notice The specified IP resolver is not valid.
error IPAssetRegistry__ResolverInvalid();

Expand Down
5 changes: 3 additions & 2 deletions contracts/lib/modules/Module.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ string constant IP_RESOLVER_MODULE_KEY = "IP_RESOLVER_MODULE";
// String values for core protocol modules.
string constant REGISTRATION_MODULE_KEY = "REGISTRATION_MODULE";

// String values for core protocol modules.
string constant LICENSING_MODULE_KEY = "LICENSING_MODULE";

// String values for core protocol modules.
string constant DISPUTE_MODULE_KEY = "DISPUTE_MODULE";

string constant TAGGING_MODULE_KEY = "TAGGING_MODULE";

string constant ROYALTY_MODULE_KEY = "ROYALTY_MODULE";

string constant LICENSING_MODULE_KEY = "LICENSING_MODULE";
2 changes: 1 addition & 1 deletion contracts/lib/registries/IPAccountChecker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pragma solidity ^0.8.23;

import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { IERC6551Account } from "@erc6551/interfaces/IERC6551Account.sol";
import { IERC6551Account } from "erc6551/interfaces/IERC6551Account.sol";

import { IIPAccountRegistry } from "../../interfaces/registries/IIPAccountRegistry.sol";
import { IIPAccount } from "../..//interfaces/IIPAccount.sol";
Expand Down
2 changes: 1 addition & 1 deletion contracts/registries/IPAccountRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf
pragma solidity ^0.8.23;

import { IERC6551Registry } from "@erc6551/interfaces/IERC6551Registry.sol";
import { IERC6551Registry } from "erc6551/interfaces/IERC6551Registry.sol";

import { IIPAccountRegistry } from "../interfaces/registries/IIPAccountRegistry.sol";
import { Errors } from "../lib/Errors.sol";
Expand Down
114 changes: 99 additions & 15 deletions contracts/registries/IPAssetRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf
pragma solidity ^0.8.23;

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

import { IIPAccount } from "../interfaces/IIPAccount.sol";
Expand All @@ -11,6 +12,10 @@ import { IMetadataProviderMigratable } from "../interfaces/registries/metadata/I
import { MetadataProviderV1 } from "../registries/metadata/MetadataProviderV1.sol";
import { Errors } from "../lib/Errors.sol";
import { IResolver } from "../interfaces/resolvers/IResolver.sol";
import { LICENSING_MODULE_KEY } from "contracts/lib/modules/Module.sol";
import { IModuleRegistry } from "../interfaces/registries/IModuleRegistry.sol";
import { ILicensingModule } from "../interfaces/modules/licensing/ILicensingModule.sol";
import { IIPAssetRegistry } from "../interfaces/registries/IIPAssetRegistry.sol";

/// @title IP Asset Registry
/// @notice This contract acts as the source of truth for all IP registered in
Expand All @@ -26,15 +31,23 @@ contract IPAssetRegistry is IIPAssetRegistry, IPAccountRegistry {
struct Record {
// Metadata provider for Story Protocol canonicalized metadata.
IMetadataProviderMigratable metadataProvider;
// Metadata resolver for custom metadata added by the IP owner.
// TODO: Deprecate this in favor of consolidation through the provider.
address resolver;
}

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

/// @notice Tracks the total number of IP assets in existence.
uint256 public totalSupply = 0;

/// @notice Protocol governance administrator of the IP record registry.
address public owner;

/// @notice Checks whether an operator is approved to register on behalf of an IP owner.
mapping(address owner => mapping(address operator => bool)) public isApprovedForAll;

/// @dev Maps an IP, identified by its IP ID, to an IP record.
mapping(address => Record) internal _records;

Expand All @@ -53,21 +66,37 @@ contract IPAssetRegistry is IIPAssetRegistry, IPAccountRegistry {
/// @param erc6551Registry The address of the ERC6551 registry.
/// @param accessController The address of the access controller.
/// @param ipAccountImpl The address of the IP account implementation.
/// @param moduleRegistry The address of the module registry.
/// TODO: Utilize module registry for fetching different modules.
constructor(
address accessController,
address erc6551Registry,
address ipAccountImpl
address ipAccountImpl,
address moduleRegistry
) IPAccountRegistry(erc6551Registry, accessController, ipAccountImpl) {
// TODO: Migrate this to a parameterized governance owner address.
// TODO: Replace with OZ's 2StepOwnable
owner = msg.sender;
MODULE_REGISTRY = IModuleRegistry(moduleRegistry);
_metadataProvider = IMetadataProviderMigratable(new MetadataProviderV1(address(this)));
}

/// @notice Registers an NFT as an IP, creating a corresponding IP asset.
/// @notice Enables third party operators to register on behalf of an NFT owner.
/// @param operator The address of the operator the sender authorizes.
/// @param approved Whether or not to approve that operator for registration.
/// TODO: Switch to access controller for centralizing this auth mechanism.
function setApprovalForAll(address operator, bool approved) external {
isApprovedForAll[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}

/// @notice Registers an NFT as an IP asset.
/// @param chainId The chain identifier of where the NFT resides.
/// @param tokenContract The address of the NFT.
/// @param tokenId The token identifier of the NFT.
/// @param resolverAddr The address of the resolver to associate with the IP.
/// @param createAccount Whether to create an IP account when registering.
/// @param data Canonical metadata to associate with the IP.
function register(
uint256 chainId,
address tokenContract,
Expand All @@ -76,17 +105,29 @@ contract IPAssetRegistry is IIPAssetRegistry, IPAccountRegistry {
bool createAccount,
bytes calldata data
) external returns (address id) {
id = ipId(chainId, tokenContract, tokenId);
if (_records[id].resolver != address(0)) {
revert Errors.IPAssetRegistry__AlreadyRegistered();
}
id = _register(new uint256[](0), 0, chainId, tokenContract, tokenId, resolverAddr, createAccount, data);
emit IPRegistered(id, chainId, tokenContract, tokenId, resolverAddr, address(_metadataProvider), data);
}

if (id.code.length == 0 && createAccount && id != registerIpAccount(chainId, tokenContract, tokenId)) {
revert Errors.IPAssetRegistry__InvalidAccount();
}
_setResolver(id, resolverAddr);
_setMetadata(id, _metadataProvider, data);
totalSupply++;
/// @notice Registers an NFT as an IP using licenses derived from parent IP asset(s).
/// @param licenseIds The parent IP asset licenses used to derive the new IP asset.
/// @param minRoyalty The minimum royalty to enforce if applicable, else 0.
/// @param chainId The chain identifier of where the NFT resides.
/// @param tokenContract The address of the NFT.
/// @param tokenId The token identifier of the NFT.
/// @param createAccount Whether to create an IP account when registering.
/// @param data Canonical metadata to associate with the IP.
function register(
uint256[] calldata licenseIds,
uint32 minRoyalty,
uint256 chainId,
address tokenContract,
uint256 tokenId,
address resolverAddr,
bool createAccount,
bytes calldata data
) external returns (address id) {
id = _register(licenseIds, minRoyalty, chainId, tokenContract, tokenId, resolverAddr, createAccount, data);
emit IPRegistered(id, chainId, tokenContract, tokenId, resolverAddr, address(_metadataProvider), data);
}

Expand Down Expand Up @@ -150,8 +191,8 @@ contract IPAssetRegistry is IIPAssetRegistry, IPAccountRegistry {
/// @param id The canonical ID of the IP.
/// @param data Canonical metadata to associate with the IP.
function setMetadata(address id, address provider, bytes calldata data) external {
// Metadata is set on registration and immutable thereafter, with new fields
// only added during a migration to new protocol-approved metadata provider.
// Canonical metadata is set on registration and immutable thereafter, with new
// fields only added during a migration to new protocol-approved metadata provider.
if (address(_records[id].metadataProvider) != msg.sender) {
revert Errors.IPAssetRegistry__Unauthorized();
}
Expand All @@ -166,12 +207,55 @@ contract IPAssetRegistry is IIPAssetRegistry, IPAccountRegistry {
revert Errors.IPAssetRegistry__NotYetRegistered();
}
// TODO: Update authorization logic to use the access controller.
if (msg.sender != IIPAccount(payable(id)).owner()) {
address owner = IIPAccount(payable(id)).owner();
if (msg.sender != owner && !isApprovedForAll[owner][msg.sender]) {
revert Errors.IPAssetRegistry__Unauthorized();
}
_setResolver(id, resolverAddr);
}

/// @dev Registers an NFT as an IP.
/// @param licenseIds IP asset licenses used to derive the new IP asset, if any.
/// @param minRoyalty The minimum royalty to enforce if applicable, else 0.
/// @param chainId The chain identifier of where the NFT resides.
/// @param tokenContract The address of the NFT.
/// @param tokenId The token identifier of the NFT.
/// @param resolverAddr The address of the resolver to associate with the IP.
/// @param createAccount Whether to create an IP account when registering.
/// @param data Canonical metadata to associate with the IP.
function _register(
uint256[] memory licenseIds,
uint32 minRoyalty,
uint256 chainId,
address tokenContract,
uint256 tokenId,
address resolverAddr,
bool createAccount,
bytes calldata data
) internal returns (address id) {
id = ipId(chainId, tokenContract, tokenId);
if (_records[id].resolver != address(0)) {
revert Errors.IPAssetRegistry__AlreadyRegistered();
}

address owner = IERC721(tokenContract).ownerOf(tokenId);
if (msg.sender != owner && !isApprovedForAll[owner][msg.sender]) {
revert Errors.IPAssetRegistry__RegistrantUnauthorized();
}

if (id.code.length == 0 && createAccount && id != registerIpAccount(chainId, tokenContract, tokenId)) {
revert Errors.IPAssetRegistry__InvalidAccount();
}
_setResolver(id, resolverAddr);
_setMetadata(id, _metadataProvider, data);
totalSupply++;

if (licenseIds.length != 0) {
ILicensingModule licensingModule = ILicensingModule(MODULE_REGISTRY.getModule(LICENSING_MODULE_KEY));
licensingModule.linkIpToParents(licenseIds, id, minRoyalty);
}
}

/// @dev Sets the resolver for the specified IP.
/// @param id The canonical ID of the IP.
/// @param resolverAddr The address of the resolver being set.
Expand Down
16 changes: 15 additions & 1 deletion script/foundry/deployment/Main.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler {

contractKey = "IPAssetRegistry";
_predeploy(contractKey);
ipAssetRegistry = new IPAssetRegistry(address(accessController), ERC6551_REGISTRY, address(ipAccountImpl));
ipAssetRegistry = new IPAssetRegistry(address(accessController), ERC6551_REGISTRY, address(ipAccountImpl), address(moduleRegistry));
_postdeploy(contractKey, address(ipAssetRegistry));

contractKey = "MetadataProviderV1";
Expand Down Expand Up @@ -433,6 +433,20 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler {
"https://example.com/test-ip"
);

accessController.setGlobalPermission(
address(ipAssetRegistry),
address(licensingModule),
bytes4(0),
1
);

accessController.setGlobalPermission(
address(registrationModule),
address(licenseRegistry),
bytes4(0), // wildcard
1 // AccessPermission.ALLOW
);

// wildcard allow
IIPAccount(payable(ipAcct[1])).execute(
address(accessController),
Expand Down
Loading

0 comments on commit f97ce66

Please sign in to comment.