diff --git a/contracts/FranchiseRegistry.sol b/contracts/FranchiseRegistry.sol index d2232571..7b747075 100644 --- a/contracts/FranchiseRegistry.sol +++ b/contracts/FranchiseRegistry.sol @@ -40,7 +40,7 @@ contract FranchiseRegistry is constructor(address _factory) { _disableInitializers(); - if (_factory == address(0)) revert ZeroAddress("factory"); + if (_factory == address(0)) revert ZeroAddress(); FACTORY = IPAssetRegistryFactory(_factory); } @@ -66,7 +66,7 @@ contract FranchiseRegistry is string calldata description ) external returns (uint256, address) { FranchiseStorage storage $ = _getFranchiseStorage(); - address ipAssetRegistry = FACTORY.createFranchiseBlocks( + address ipAssetRegistry = FACTORY.createFranchiseIPAssets( ++$.franchiseIds, name, symbol, diff --git a/contracts/IPAsset.sol b/contracts/IPAsset.sol index c7698019..efbffc01 100644 --- a/contracts/IPAsset.sol +++ b/contracts/IPAsset.sol @@ -12,4 +12,4 @@ enum IPAsset { ITEM } - \ No newline at end of file +uint8 constant EXTERNAL_ASSET = type(uint8).max; \ No newline at end of file diff --git a/contracts/access-control/AccessControlSingleton.sol b/contracts/access-control/AccessControlSingleton.sol index dfed319b..b8e050e5 100644 --- a/contracts/access-control/AccessControlSingleton.sol +++ b/contracts/access-control/AccessControlSingleton.sol @@ -20,7 +20,7 @@ contract AccessControlSingleton is AccessControlUpgradeable, UUPSUpgradeable, Mu * @param _admin address to be the PROTOCOL_ADMIN_ROLE. */ function initialize(address _admin) external initializer { - if (_admin == address(0)) revert ZeroAddress("_admin"); + if (_admin == address(0)) revert ZeroAddress(); __AccessControl_init(); __UUPSUpgradeable_init(); _grantRole(PROTOCOL_ADMIN_ROLE, _admin); diff --git a/contracts/access-control/AccessControlledUpgradeable.sol b/contracts/access-control/AccessControlledUpgradeable.sol index 5f06c25d..a4b5810e 100644 --- a/contracts/access-control/AccessControlledUpgradeable.sol +++ b/contracts/access-control/AccessControlledUpgradeable.sol @@ -73,4 +73,9 @@ abstract contract AccessControlledUpgradeable is UUPSUpgradeable { emit AccessControlUpdated(accessControl); } + function getAccessControl() public view returns (address) { + AccessControlledStorage storage $ = _getAccessControlledUpgradeable(); + return address($.accessControl); + } + } \ No newline at end of file diff --git a/contracts/access-control/ProtocolRoles.sol b/contracts/access-control/ProtocolRoles.sol index 6320a031..bf9b2aec 100644 --- a/contracts/access-control/ProtocolRoles.sol +++ b/contracts/access-control/ProtocolRoles.sol @@ -4,4 +4,5 @@ pragma solidity ^0.8.13; bytes32 constant PROTOCOL_ADMIN_ROLE = bytes32(0); bytes32 constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); -bytes32 constant DAM_APPROVER = keccak256("DAM_APPROVER"); \ No newline at end of file +bytes32 constant RELATIONSHIP_MANAGER_ROLE = keccak256("RELATIONSHIP_MANAGER_ROLE"); +bytes32 constant RELATIONSHIP_DISPUTER_ROLE = keccak256("RELATIONSHIP_DISPUTER_ROLE"); diff --git a/contracts/errors/General.sol b/contracts/errors/General.sol index 347fc036..e1205aad 100644 --- a/contracts/errors/General.sol +++ b/contracts/errors/General.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.13; -error ZeroAddress(string name); -error ZeroAmount(string name); +error ZeroAddress(); +error ZeroAmount(); error UnsupportedInterface(string name); error Unauthorized(); error NonExistentID(uint256 id); \ No newline at end of file diff --git a/contracts/ip-assets/IIPAssetRegistry.sol b/contracts/ip-assets/IIPAssetRegistry.sol index 2c1ee05c..c7dc93d7 100644 --- a/contracts/ip-assets/IIPAssetRegistry.sol +++ b/contracts/ip-assets/IIPAssetRegistry.sol @@ -13,4 +13,6 @@ interface IIPAssetRegistry is IERC721Upgradeable, IIPAssetData, IGroupDAM - { } + { + function franchiseId() external view returns (uint256); + } diff --git a/contracts/ip-assets/IPAssetRegistry.sol b/contracts/ip-assets/IPAssetRegistry.sol index 864a48ef..05921624 100644 --- a/contracts/ip-assets/IPAssetRegistry.sol +++ b/contracts/ip-assets/IPAssetRegistry.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; //import "forge-std/console.sol"; import { IIPAssetRegistry } from "./IIPAssetRegistry.sol"; import { LibIPAssetId } from "./LibIPAssetId.sol"; -import { Unauthorized, ZeroAddress } from "../errors/General.sol"; +import { Unauthorized, ZeroAmount } from "../errors/General.sol"; import { IPAssetData } from "./data-access-modules/storage/IPAssetData.sol"; import { IPAsset } from "contracts/IPAsset.sol"; import { GroupDAM } from "./data-access-modules/group/GroupDAM.sol"; @@ -44,7 +44,7 @@ contract IPAssetRegistry is ) public initializer { __ERC721_init(_name, _symbol); __Multicall_init(); - if (_franchiseId == 0) revert ZeroAddress("franchiseId"); + if (_franchiseId == 0) revert ZeroAmount(); IPAssetRegistryStorage storage $ = _getIPAssetRegistryStorage(); $.franchiseId = _franchiseId; $.description = _description; diff --git a/contracts/ip-assets/IPAssetRegistryFactory.sol b/contracts/ip-assets/IPAssetRegistryFactory.sol index 6d39f983..aed2b198 100644 --- a/contracts/ip-assets/IPAssetRegistryFactory.sol +++ b/contracts/ip-assets/IPAssetRegistryFactory.sol @@ -25,7 +25,7 @@ contract IPAssetRegistryFactory is Ownable { BEACON = new UpgradeableBeacon(address(new IPAssetRegistry())); } - function createFranchiseBlocks( + function createFranchiseIPAssets( uint256 franchiseId, string calldata name, string calldata symbol, diff --git a/contracts/modules/relationships/IRelationshipModule.sol b/contracts/modules/relationships/IRelationshipModule.sol new file mode 100644 index 00000000..bd580241 --- /dev/null +++ b/contracts/modules/relationships/IRelationshipModule.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +import { IRelationshipProcessor } from "./RelationshipProcessors/IRelationshipProcessor.sol"; +import { IPAsset } from "contracts/IPAsset.sol"; + + +interface IRelationshipModule { + + event RelationSet( + address sourceContract, + uint256 sourceId, + address destContract, + uint256 destId, + bytes32 indexed relationshipId, + uint256 endTime + ); + event RelationUnset( + address sourceContract, + uint256 sourceId, + address destContract, + uint256 destId, + bytes32 indexed relationshipId + ); + event RelationPendingProcessor( + address sourceContract, + uint256 sourceId, + address destContract, + uint256 destId, + bytes32 indexed relationshipId + ); + + event RelationshipConfigSet( + bytes32 indexed relationshipId, + uint256 sourceIPAssetTypeMask, + uint256 destIPAssetTypeMask, + bool onlySameFranchise, + address processor, + uint256 maxTTL, + uint256 minTTL + ); + + event RelationshipConfigUnset(bytes32 indexed relationshipId); + + error NonExistingRelationship(); + error IntentAlreadyRegistered(); + error UnsupportedRelationshipSrc(); + error UnsupportedRelationshipDst(); + error CannotRelateToOtherFranchise(); + error InvalidTTL(); + error InvalidEndTimestamp(); + + struct TimeConfig { + uint112 maxTTL; + uint112 minTTL; + bool renewable; + } + + struct RelationshipConfig { + uint256 sourceIPAssetTypeMask; + uint256 destIPAssetTypeMask; + bool onlySameFranchise; + IRelationshipProcessor processor; + address disputer; + TimeConfig timeConfig; + } + + struct SetRelationshipConfigParams { + IPAsset[] sourceIPAssets; + bool allowedExternalSource; + IPAsset[] destIPAssets; + bool allowedExternalDest; + bool onlySameFranchise; + address processor; + address disputer; + TimeConfig timeConfig; + } + + struct RelationshipParams { + address sourceContract; + uint256 sourceId; + address destContract; + uint256 destId; + bytes32 relationshipId; + uint256 ttl; + } + + function relate(RelationshipParams calldata params, bytes calldata data) external; + function unrelate(RelationshipParams calldata params) external; + function areTheyRelated(RelationshipParams calldata params) external view returns (bool); + function isRelationshipExpired(RelationshipParams calldata params) external view returns (bool); + function setRelationshipConfig(bytes32 relationshipId, SetRelationshipConfigParams calldata params) external; + function unsetRelationshipConfig(bytes32 relationshipId) external; + function relationshipConfig(bytes32 relationshipId) external view returns (RelationshipConfig memory); +} \ No newline at end of file diff --git a/contracts/modules/relationships/ProtocolRelationshipModule.sol b/contracts/modules/relationships/ProtocolRelationshipModule.sol new file mode 100644 index 00000000..77b5188c --- /dev/null +++ b/contracts/modules/relationships/ProtocolRelationshipModule.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +import { RelationshipModuleBase } from "./RelationshipModuleBase.sol"; +import { UPGRADER_ROLE, RELATIONSHIP_MANAGER_ROLE, RELATIONSHIP_DISPUTER_ROLE } from "contracts/access-control/ProtocolRoles.sol"; + +/** + * @title ProtocolRelationshipModule + * @dev Implementation of RelationshipModuleBase that allows relationship configs that will be used protocol wide. + * The meaning and parameters of the relationships are to be defined in Story Protocol Improvement Proposals. + * Example: https://github.com/storyprotocol/protocol-contracts/issues/33 + * The relationship configs are set by the RELATIONSHIP_MANAGER_ROLE. + * Upgrades are done by the UPGRADER_ROLE. + */ +contract ProtocolRelationshipModule is RelationshipModuleBase { + + constructor(address _franchiseRegistry) RelationshipModuleBase(_franchiseRegistry) {} + + function initialize(address accessControl) public initializer { + __RelationshipModuleBase_init(accessControl); + } + + /********* Setting Relationships *********/ + function setRelationshipConfig(bytes32 relationshipId, SetRelationshipConfigParams calldata params) external onlyRole(RELATIONSHIP_MANAGER_ROLE) { + _setRelationshipConfig(relationshipId, params); + } + + function unsetRelationshipConfig(bytes32 relationshipId) external onlyRole(RELATIONSHIP_MANAGER_ROLE) { + _unsetRelationshipConfig(relationshipId); + } + + + function _authorizeUpgrade( + address newImplementation + ) internal virtual override onlyRole(UPGRADER_ROLE) {} + +} \ No newline at end of file diff --git a/contracts/modules/relationships/RelationshipModuleBase.sol b/contracts/modules/relationships/RelationshipModuleBase.sol new file mode 100644 index 00000000..3c5daa64 --- /dev/null +++ b/contracts/modules/relationships/RelationshipModuleBase.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { ERC165CheckerUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165CheckerUpgradeable.sol"; +import { Multicall } from "@openzeppelin/contracts/utils/Multicall.sol"; +import { AccessControlledUpgradeable } from "contracts/access-control/AccessControlledUpgradeable.sol"; +import { ZeroAddress, UnsupportedInterface, Unauthorized } from "contracts/errors/General.sol"; +import { FranchiseRegistry } from "contracts/FranchiseRegistry.sol"; +import { IIPAssetRegistry } from "contracts/ip-assets/IIPAssetRegistry.sol"; +import { RelationshipTypeChecker } from "./RelationshipTypeChecker.sol"; +import { IRelationshipModule } from "./IRelationshipModule.sol"; +import { IRelationshipProcessor } from "./RelationshipProcessors/IRelationshipProcessor.sol"; + + +/** + * @title RelationshipModuleBase + * @author Raul Martinez + * @notice The relationship module is responsible for managing relationships between IP assets, and/or between them and external ERC721 contracts. + * Relationships are defined by a relationship ID, which is a bytes32 value that represents a relationship type, for example (APPEARS_IN, CONTINUES_STORY, etc). + * The meaning of each relationship may have different side effects in Story Protocol, which could other modules could react on, and even legal implications if + * especified by the IPAsset licenses. + * To be able to relate two elements, a RelationshipConfig must be set for the relationship ID, which defines the following: + * - The IPAsset types that can be related as source and destination. + * - The processor that will be called when a relationship is set, which can be used to perform additional checks or actions (checking ownership, asking for fees...). + * - The disputer, which is the address that can unset a relationship. + * - The time config, which defines the maximum and minimum TTL (time to live) for the relationship, and if it can be renewed. (maxTTL = 0 means no expiration) + * - If the relationship can only be set between IPAssets of the same franchise, or it could link to IPAssets of other franchises. + * + * It's up to subclasses to define which addresses can set relationship configs. + */ +abstract contract RelationshipModuleBase is IRelationshipModule, AccessControlledUpgradeable, RelationshipTypeChecker, Multicall { + using ERC165CheckerUpgradeable for address; + + /// @custom:storage-location erc7201:story-protocol.relationship-module.storage + struct RelationshipModuleStorage { + mapping(bytes32 => bool) relationships; + mapping(bytes32 => uint256) relationshipExpirations; + mapping(bytes32 => RelationshipConfig) relConfigs; + } + + // keccak256(bytes.concat(bytes32(uint256(keccak256("story-protocol.relationship-module.storage")) - 1))) + bytes32 private constant _STORAGE_LOCATION = 0xd16687d5cf786234491b4cc484b2a64f24855aadee9b1b73824db1ed2840fd0b; + FranchiseRegistry public immutable FRANCHISE_REGISTRY; + + /** + * reverts if the TTL is not well configured for the relationship. + * @param params the relationship params + */ + modifier onlyValidTTL(RelationshipParams calldata params) { + RelationshipConfig storage relConfig = _getRelationshipModuleStorage().relConfigs[params.relationshipId]; + if (relConfig.timeConfig.maxTTL != 0 && params.ttl != 0) { + if (params.ttl > relConfig.timeConfig.maxTTL || params.ttl < relConfig.timeConfig.minTTL) revert InvalidEndTimestamp(); + } + _; + } + + constructor(address _franchiseRegistry) { + if (_franchiseRegistry == address(0)) revert ZeroAddress(); + FRANCHISE_REGISTRY = FranchiseRegistry(_franchiseRegistry); + _disableInitializers(); + } + + function __RelationshipModuleBase_init(address accessControl) public initializer { + __AccessControlledUpgradeable_init(accessControl); + } + + function _getRelationshipModuleStorage() + private + pure + returns (RelationshipModuleStorage storage $) + { + assembly { + $.slot := _STORAGE_LOCATION + } + } + + /** + * @notice Relates two IPAssets or an IPAsset and an external ERC721 contract. + * To not revert, the params must be valid according to the relationship config, and the processor must not revert. + * Processor can be used to perform additional checks or actions (checking ownership, asking for fees...). + * Processors returning false imply that the relationship is pending (multi step process), and the relationship will not be set yet. + * @param params the relationship params + * @param data optional data that will be passed to the processor + */ + function relate(RelationshipParams calldata params, bytes calldata data) external onlyValidTTL(params) { + RelationshipModuleStorage storage $ = _getRelationshipModuleStorage(); + RelationshipConfig storage relConfig = $.relConfigs[params.relationshipId]; + _verifyRelationshipParams(params, relConfig); + + if (!relConfig.processor.processRelationship(params, data, msg.sender)) { + emit RelationPendingProcessor(params.sourceContract, params.sourceId, params.destContract, params.destId, params.relationshipId); + } else { + bytes32 relKey = _getRelationshipKey(params); + $.relationships[relKey] = true; + uint256 endTime = _updateEndTime(relKey, relConfig.timeConfig, params.ttl); + emit RelationSet(params.sourceContract, params.sourceId, params.destContract, params.destId, params.relationshipId, endTime); + } + } + + /** + * @notice Updates the end time of a relationship, if TimeConfig allows it. + * @param relKey the relationship key, given by _getRelationshipKey(params) + * @param timeConfig the relationship time config + * @param ttl the new ttl + * @return the new end time + */ + function _updateEndTime(bytes32 relKey, TimeConfig memory timeConfig, uint256 ttl) private returns (uint256) { + RelationshipModuleStorage storage $ = _getRelationshipModuleStorage(); + if (timeConfig.maxTTL != 0) { + uint256 endTime = $.relationshipExpirations[relKey]; + if (endTime == 0 || timeConfig.renewable) { + endTime = block.timestamp + ttl; + $.relationshipExpirations[relKey] = endTime; + return endTime; + } + } + return 0; + } + + /** + * @notice Unrelates two IPAssets or an IPAsset and an external ERC721 contract. + * Only callable by the disputer of the relationship, as defined in the relationship config. + * @param params the relationship params + */ + function unrelate(RelationshipParams calldata params) external { + RelationshipModuleStorage storage $ = _getRelationshipModuleStorage(); + if ($.relConfigs[params.relationshipId].disputer != msg.sender) revert Unauthorized(); + bytes32 key = _getRelationshipKey(params); + if (!$.relationships[key]) revert NonExistingRelationship(); + delete $.relationships[key]; + emit RelationUnset(params.sourceContract, params.sourceId, params.destContract, params.destId, params.relationshipId); + } + + /** + * @notice Checks if two IPAssets or an IPAsset and an external ERC721 contract are related. + * @param params the relationship params + * @return true if they are related and the relationship has not expired, false otherwise + */ + function areTheyRelated(RelationshipParams calldata params) external view returns (bool) { + RelationshipModuleStorage storage $ = _getRelationshipModuleStorage(); + return $.relationships[_getRelationshipKey(params)] && !isRelationshipExpired(params); + } + + /** + * @notice Checks if a relationship has expired. + * @param params the relationship params + * @return true if the relationship has expired, false if not expired or if it has no expiration + */ + function isRelationshipExpired(RelationshipParams calldata params) public view returns (bool) { + RelationshipModuleStorage storage $ = _getRelationshipModuleStorage(); + uint256 endTime = $.relationshipExpirations[_getRelationshipKey(params)]; + return endTime != 0 && endTime < block.timestamp; + } + + /** + * @notice validates the relationship params according to the relationship config. + * @param params the relationship params + * @param relConfig the relationship config + */ + function _verifyRelationshipParams(RelationshipParams calldata params, RelationshipConfig memory relConfig) private view { + if (relConfig.sourceIPAssetTypeMask == 0) revert NonExistingRelationship(); + (bool sourceResult, bool sourceIsAssetRegistry) = _checkRelationshipNode(params.sourceContract, params.sourceId, relConfig.sourceIPAssetTypeMask); + if (!sourceResult) revert UnsupportedRelationshipSrc(); + (bool destResult, bool destIsAssetRegistry) = _checkRelationshipNode(params.destContract, params.destId, relConfig.destIPAssetTypeMask); + if (!destResult) revert UnsupportedRelationshipDst(); + if(sourceIsAssetRegistry && destIsAssetRegistry && params.sourceContract != params.destContract && relConfig.onlySameFranchise) revert CannotRelateToOtherFranchise(); + } + + /** + * @notice checks if an address is a valid SP IPAssetRegistry. + * @param ipAssetRegistry the address to check + * @return true if it's a valid SP IPAssetRegistry, false otherwise + */ + function _isAssetRegistry(address ipAssetRegistry) internal virtual override view returns(bool) { + try IIPAssetRegistry(ipAssetRegistry).franchiseId() returns (uint256 franchiseId) { + return FRANCHISE_REGISTRY.ipAssetRegistryForId(franchiseId) == ipAssetRegistry; + } catch { + return false; + } + } + + /// calculates the relationship key by keccak256 hashing srcContract, srcId, dstContract, dstId and relationshipId + function _getRelationshipKey(RelationshipParams calldata params) internal pure returns (bytes32) { + return keccak256( + abi.encode( + params.sourceContract, + params.sourceId, + params.destContract, + params.destId, + params.relationshipId + ) + ); + } + + /********* Setting Relationships *********/ + + /** + * @notice Sets a relationship config for a relationship ID. + * @param relationshipId the relationship ID + * @param params the relationship config params + */ + function _setRelationshipConfig(bytes32 relationshipId, SetRelationshipConfigParams calldata params) internal { + RelationshipConfig memory relConfig = _convertRelParams(params); + RelationshipModuleStorage storage $ = _getRelationshipModuleStorage(); + $.relConfigs[relationshipId] = relConfig; + emit RelationshipConfigSet( + relationshipId, + relConfig.sourceIPAssetTypeMask, + relConfig.destIPAssetTypeMask, + relConfig.onlySameFranchise, + params.processor, + relConfig.timeConfig.maxTTL, + relConfig.timeConfig.minTTL + ); + } + + /** + * @notice Unsets a relationship config for a relationship ID, reverts if it doesn't exist. + * @param relationshipId the relationship ID + */ + function _unsetRelationshipConfig(bytes32 relationshipId) internal { + RelationshipModuleStorage storage $ = _getRelationshipModuleStorage(); + if ( + $.relConfigs[relationshipId].sourceIPAssetTypeMask == 0 + ) revert NonExistingRelationship(); + delete $.relConfigs[relationshipId]; + emit RelationshipConfigUnset(relationshipId); + } + + /** + * @notice Converts the SetRelationshipConfigParams to a RelationshipConfig after validating them. + * @dev reverts if + * - the processor doesn't support IRelationshipProcessor interface + * - the TTL is not well configured. + * - the disputer is the zero address + * + * @param params the SetRelationshipConfigParams + * @return the RelationshipConfig + */ + function _convertRelParams(SetRelationshipConfigParams calldata params) private view returns(RelationshipConfig memory) { + if (!params.processor.supportsInterface(type(IRelationshipProcessor).interfaceId)) revert UnsupportedInterface("IRelationshipProcessor"); + if (params.timeConfig.maxTTL < params.timeConfig.minTTL) revert InvalidTTL(); + if (params.disputer == address(0)) revert ZeroAddress(); + return RelationshipConfig( + _convertToMask(params.sourceIPAssets, params.allowedExternalSource), + _convertToMask(params.destIPAssets, params.allowedExternalDest), + params.onlySameFranchise, + IRelationshipProcessor(params.processor), + params.disputer, + params.timeConfig + ); + } + + /// returns a RelationshipConfig for the given relationshipId, or an empty one if it doesn't exist + function relationshipConfig(bytes32 relationshipId) external view returns (RelationshipConfig memory) { + RelationshipModuleStorage storage $ = _getRelationshipModuleStorage(); + return $.relConfigs[relationshipId]; + } + +} \ No newline at end of file diff --git a/contracts/modules/relationships/RelationshipProcessors/BaseRelationshipProcessor.sol b/contracts/modules/relationships/RelationshipProcessors/BaseRelationshipProcessor.sol new file mode 100644 index 00000000..94f09e48 --- /dev/null +++ b/contracts/modules/relationships/RelationshipProcessors/BaseRelationshipProcessor.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +import { IRelationshipProcessor } from "./IRelationshipProcessor.sol"; +import { ZeroAddress } from "contracts/errors/General.sol"; +import { IRelationshipModule } from "../IRelationshipModule.sol"; +import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +/** + * @title BaseRelationshipProcessor + * @dev Base contract for relationship processors. + * Relationship processors are used to process relationships between IP Assets before they are set. + * They are set per relationship config in a IRelationshipModule + * This base contracts implements ERC165 and checks if the caller is the relationship module. + * All relationship processors must inherit from this contract. + */ +abstract contract BaseRelationshipProcessor is IRelationshipProcessor, ERC165 { + + address internal immutable _RELATIONSHIP_MODULE; + error OnlyRelationshipModule(); + + constructor(address _relationshipModule) { + if(_relationshipModule == address(0)) revert ZeroAddress(); + _RELATIONSHIP_MODULE = _relationshipModule; + } + + /** + * @inheritdoc IRelationshipProcessor + * @dev Checks if the caller is the relationship module and calls implementation. + */ + function processRelationship(IRelationshipModule.RelationshipParams memory params, bytes calldata data, address caller) external override returns(bool) { + if(msg.sender != _RELATIONSHIP_MODULE) revert OnlyRelationshipModule(); + return _processRelationship(params, data, caller); + } + + + function _processRelationship(IRelationshipModule.RelationshipParams memory params, bytes calldata data, address caller) internal virtual returns(bool); + + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC165) returns (bool) { + return super.supportsInterface(interfaceId) || interfaceId == type(IRelationshipProcessor).interfaceId; + } + +} \ No newline at end of file diff --git a/contracts/modules/relationships/RelationshipProcessors/DstOwnerRelationshipProcessor.sol b/contracts/modules/relationships/RelationshipProcessors/DstOwnerRelationshipProcessor.sol new file mode 100644 index 00000000..a1b3a3f1 --- /dev/null +++ b/contracts/modules/relationships/RelationshipProcessors/DstOwnerRelationshipProcessor.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +import { BaseRelationshipProcessor } from "./BaseRelationshipProcessor.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { Unauthorized } from "contracts/errors/General.sol"; +import { IRelationshipModule } from "../IRelationshipModule.sol"; + +/** + * @title DstOwnerRelationshipProcessor + * @dev Relationship processor that checks if the caller (relationship setter) is the owner of the destination IP Asset. + */ +contract DstRelationshipProcessor is BaseRelationshipProcessor { + + constructor(address relationshipModule) BaseRelationshipProcessor(relationshipModule) {} + + /** + * Returns true if the caller is the owner of the destination IP Asset, reverts otherwise. + */ + function _processRelationship(IRelationshipModule.RelationshipParams memory params, bytes calldata, address caller) internal view virtual override returns(bool) { + if (IERC721(params.destContract).ownerOf(params.destId) != caller) { + revert Unauthorized(); + } + return true; + } + +} \ No newline at end of file diff --git a/contracts/modules/relationships/RelationshipProcessors/IRelationshipProcessor.sol b/contracts/modules/relationships/RelationshipProcessors/IRelationshipProcessor.sol new file mode 100644 index 00000000..81375384 --- /dev/null +++ b/contracts/modules/relationships/RelationshipProcessors/IRelationshipProcessor.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +import { IRelationshipModule } from "../IRelationshipModule.sol"; + +/** + * @title IRelationshipProcessor + * @dev Interface for relationship processors. + * Relationship processors are used to process relationships between IP Assets before they are set. + * They are set per relationship config in a IRelationshipModule + */ +interface IRelationshipProcessor { + + /** + * @dev Processes a relationship between two IP Assets before it is set. This can be use for validity checks, actions, etc. It must: + * - revert if the relationship is invalid + * - return true if the relationship is valid and the relationship should be set immediately in the relationship module. + * - return false if the relationship is valid but there is need for further processing. + * In this case, the relationship module will emit a RelationPendingProcessor event. + * This can be leveraged for multi-step relationship setting, e.g. for a relationship that requires approval from the destination IP Asset owner. + */ + function processRelationship(IRelationshipModule.RelationshipParams memory params, bytes calldata data, address caller) external returns(bool); +} \ No newline at end of file diff --git a/contracts/modules/relationships/RelationshipProcessors/PermissionlessRelationshipProcessor.sol b/contracts/modules/relationships/RelationshipProcessors/PermissionlessRelationshipProcessor.sol new file mode 100644 index 00000000..337c2c51 --- /dev/null +++ b/contracts/modules/relationships/RelationshipProcessors/PermissionlessRelationshipProcessor.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +import { BaseRelationshipProcessor } from "./BaseRelationshipProcessor.sol"; +import { IRelationshipModule } from "../IRelationshipModule.sol"; + +/** + * @title PermissionlessRelationshipProcessor + * @dev Relationship processor that always returns true. + */ +contract PermissionlessRelationshipProcessor is BaseRelationshipProcessor { + + constructor(address relationshipModule) BaseRelationshipProcessor(relationshipModule) {} + + /** + * Returns true. + */ + function _processRelationship(IRelationshipModule.RelationshipParams memory, bytes calldata, address) internal virtual override returns(bool) { + return true; + } +} \ No newline at end of file diff --git a/contracts/modules/relationships/RelationshipProcessors/SrcDstOwnerRelationshipProcessor.sol b/contracts/modules/relationships/RelationshipProcessors/SrcDstOwnerRelationshipProcessor.sol new file mode 100644 index 00000000..7a3efd1a --- /dev/null +++ b/contracts/modules/relationships/RelationshipProcessors/SrcDstOwnerRelationshipProcessor.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +import { BaseRelationshipProcessor } from "./BaseRelationshipProcessor.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { Unauthorized } from "contracts/errors/General.sol"; +import { IRelationshipModule } from "../IRelationshipModule.sol"; + +/** + * @title SrcDstOwnerRelationshipProcessor + * @dev Relationship processor that checks if the caller (relationship setter) is the owner of the source and destination IP Assets. + */ +contract SrcDstRelationshipProcessor is BaseRelationshipProcessor { + + constructor(address relationshipModule) BaseRelationshipProcessor(relationshipModule) {} + + /** + * Returns true if the caller is the owner of the source and destination IP Assets, reverts otherwise. + */ + function _processRelationship(IRelationshipModule.RelationshipParams memory params, bytes calldata, address caller) internal view virtual override returns(bool) { + if ( + IERC721(params.sourceContract).ownerOf(params.sourceId) != caller || + IERC721(params.destContract).ownerOf(params.destId) != caller) { + revert Unauthorized(); + } + return true; + } + +} \ No newline at end of file diff --git a/contracts/modules/relationships/RelationshipProcessors/SrcOwnerRelationshipProcessor.sol b/contracts/modules/relationships/RelationshipProcessors/SrcOwnerRelationshipProcessor.sol new file mode 100644 index 00000000..1f38b66d --- /dev/null +++ b/contracts/modules/relationships/RelationshipProcessors/SrcOwnerRelationshipProcessor.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +import { BaseRelationshipProcessor } from "./BaseRelationshipProcessor.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { Unauthorized } from "contracts/errors/General.sol"; +import { IRelationshipModule } from "../IRelationshipModule.sol"; + +/** + * @title SrcOwnerRelationshipProcessor + * @dev Relationship processor that checks if the caller (relationship setter) is the owner of the source IP Asset. + */ +contract SrcRelationshipProcessor is BaseRelationshipProcessor { + + constructor(address relationshipModule) BaseRelationshipProcessor(relationshipModule) {} + + /** + * Returns true if the caller is the owner of the source IP Asset, reverts otherwise. + */ + function _processRelationship(IRelationshipModule.RelationshipParams memory params, bytes calldata, address caller) internal view virtual override returns(bool) { + if (IERC721(params.sourceContract).ownerOf(params.sourceId) != caller) { + revert Unauthorized(); + } + return true; + } + +} \ No newline at end of file diff --git a/contracts/modules/relationships/RelationshipTypeChecker.sol b/contracts/modules/relationships/RelationshipTypeChecker.sol new file mode 100644 index 00000000..7fb9c63c --- /dev/null +++ b/contracts/modules/relationships/RelationshipTypeChecker.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +import { IPAsset, EXTERNAL_ASSET } from "contracts/IPAsset.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { LibIPAssetId } from "contracts/ip-assets/LibIPAssetId.sol"; +import { IIPAssetRegistry } from "contracts/ip-assets/IIPAssetRegistry.sol"; + +/** + * @title RelationshipTypeChecker + * @dev Gives tools to check if the "endpoints" of a relationship are valid, according to the allowed asset types set in the relationship config. + */ +abstract contract RelationshipTypeChecker { + + error InvalidIPAssetArray(); + + /** + * @dev Checks if the source or destination type of a relationship is allowed by the relationship config. + * @param collection The address of the collection of the relationship endpoint + * @param id The id of the relationship endpoint + * @param assetTypeMask The asset type mask of the relationship config, which contains the allowed asset types and the external asset flag + * @return result Whether the relationship endpoint is valid + * @return isAssetRegistry Whether the relationship endpoint is a Story Protocol IP Asset Registry + */ + function _checkRelationshipNode(address collection, uint256 id, uint256 assetTypeMask) internal view returns (bool result, bool isAssetRegistry) { + if (IERC721(collection).ownerOf(id) == address(0)) return (false, false); + isAssetRegistry = _isAssetRegistry(collection); + if (isAssetRegistry) { + result = _supportsIPAssetType(assetTypeMask, uint8(LibIPAssetId._ipAssetTypeFor(id))); + } else { + result = _supportsIPAssetType(assetTypeMask, EXTERNAL_ASSET); + } + return (result, isAssetRegistry); + } + + /// must return true if the address is a Story Protocol IP Asset Registry + function _isAssetRegistry(address ipAssetRegistry) internal virtual view returns(bool); + + /** + * @dev converts an array of IPAssets types and the allows external flag to a mask, by setting the bits corresponding + * to the uint8 equivalent of the IPAsset types to 1. + * @param ipAssets The array of IPAsset types + * @param allowsExternal Whether the relationship config allows external (non SP ERC721) assets + * @return mask The mask representing the IPAsset types and the allows external flag + */ + function _convertToMask(IPAsset[] calldata ipAssets, bool allowsExternal) internal pure returns (uint256) { + if (ipAssets.length == 0) revert InvalidIPAssetArray(); + uint256 mask = 0; + for (uint256 i = 0; i < ipAssets.length;) { + if (ipAssets[i] == IPAsset.UNDEFINED) revert InvalidIPAssetArray(); + mask |= 1 << (uint256(ipAssets[i]) & 0xff); + unchecked { + i++; + } + } + if (allowsExternal) { + mask |= uint256(EXTERNAL_ASSET) << 248; + } + return mask; + } + + /// returns true if the asset type is supported by the mask, false otherwise + function _supportsIPAssetType(uint256 mask, uint8 assetType) internal pure returns (bool) { + return mask & (1 << (uint256(assetType) & 0xff)) != 0; + } + +} \ No newline at end of file diff --git a/deployment-31337.json b/deployment-31337.json new file mode 100644 index 00000000..8aaa72c3 --- /dev/null +++ b/deployment-31337.json @@ -0,0 +1,14 @@ +{ + "AccessControlSingleton-Impl": "0xc4B957Cd61beB9b9afD76204b30683EDAaaB51Ec", + "AccessControlSingleton-Proxy": "0xFa2f6c96C30e7F652C9BD6fA4f1EF1D47a88C18f", + "DstOwnerRelationshipProcessor": "0x571F4b96Abc69429e1F112232BDe599160360b6B", + "FranchiseRegistry-Impl": "0x75e7d780d4Fd26f1787192d8939a9f69D723E79a", + "FranchiseRegistry-Proxy": "0xb84132FbB7a1f19987CE8536c9885392B932b05a", + "IPAssetRegistryFactory": "0x6cdBd1b486b8FBD4140e8cd6daAED05bE13eD914", + "PermissionlessRelationshipProcessor": "0x1705a2D33C95E22F627486d1151c034a851c14e0", + "ProtocolRelationshipModule-Impl": "0xE7c415001162206eF27d7cA29cc871f3c9eE6cf4", + "ProtocolRelationshipModule-Proxy": "0xC8751DBe333604f45b98f96125BAd88bedC5a021", + "SrcDstOwnerRelationshipProcessor": "0xdBBC352fF1aefB16be2a5982508fDE2070B20828", + "SrcOwnerRelationshipProcessor": "0x16Af8E10E1FcDdA9dF1935839bdED3Cf8C1b8A44", + "contracts": 9 +} \ No newline at end of file diff --git a/test/foundry/FranchiseRegistry.t.sol b/test/foundry/FranchiseRegistry.t.sol index dd3ce1e2..ba635c89 100644 --- a/test/foundry/FranchiseRegistry.t.sol +++ b/test/foundry/FranchiseRegistry.t.sol @@ -25,8 +25,14 @@ contract FranchiseRegistryTest is Test, ProxyHelper { function setUp() public { factory = new IPAssetRegistryFactory(); - vm.prank(admin); - acs = new AccessControlSingleton(); + acs = AccessControlSingleton( + _deployUUPSProxy( + address(new AccessControlSingleton()), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), admin + ) + ) + ); address accessControl = address(acs); FranchiseRegistry impl = new FranchiseRegistry(address(factory)); @@ -50,7 +56,7 @@ contract FranchiseRegistryTest is Test, ProxyHelper { vm.startPrank(franchiseOwner); vm.expectCall(address(factory), abi.encodeCall( - factory.createFranchiseBlocks, + factory.createFranchiseIPAssets, ( 1, "name", diff --git a/test/foundry/IPAssetsRegistry.t.sol b/test/foundry/IPAssetsRegistry.t.sol new file mode 100644 index 00000000..99d82da6 --- /dev/null +++ b/test/foundry/IPAssetsRegistry.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: BUSDL-1.1 +pragma solidity ^0.8.13; + +import { IPAssetRegistry } from "../../contracts/ip-assets/IPAssetRegistry.sol"; +import { IPAssetRegistryFactory } from "../../contracts/ip-assets/IPAssetRegistryFactory.sol"; +import { IPAsset } from "../../contracts/IPAsset.sol"; +import { LibIPAssetId } from "../../contracts/ip-assets/LibIPAssetId.sol"; +import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import { IERC1967 } from "@openzeppelin/contracts/interfaces/IERC1967.sol"; + +import "forge-std/Test.sol"; + +contract IPAssetRegistryTest is Test { + using stdStorage for StdStorage; + + event CollectionCreated(address indexed collection, string name, string indexed symbol); + event CollectionsUpgraded(address indexed newImplementation, string version); + event BeaconUpgraded(address indexed beacon); + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + error IdOverBounds(); + error InvalidBlockType(); + + IPAssetRegistryFactory public factory; + IPAssetRegistry public ipAssetRegistry; + address owner = address(this); + address mintee = address(1); + address mintee2 = address(2); + + uint256 private constant _ID_RANGE = 10**12; + uint256 private constant _FIRST_ID_STORY = 1; + uint256 private constant _FIRST_ID_CHARACTER = _ID_RANGE + _FIRST_ID_STORY; + uint256 private constant _FIRST_ID_ART = _ID_RANGE + _FIRST_ID_CHARACTER; + uint256 private constant _FIRST_ID_GROUP = _ID_RANGE + _FIRST_ID_ART; + uint256 private constant _FIRST_ID_LOCATION = _ID_RANGE + _FIRST_ID_GROUP; + uint256 private constant _LAST_ID = _ID_RANGE + _FIRST_ID_LOCATION; + + function setUp() public { + factory = new IPAssetRegistryFactory(); + ipAssetRegistry = IPAssetRegistry(factory.createFranchiseIPAssets(1, "name", "symbol", "description")); + } + + function test_setUp() public { + assertEq(ipAssetRegistry.name(), "name"); + assertEq(ipAssetRegistry.symbol(), "symbol"); + assertEq(ipAssetRegistry.description(), "description"); + assertEq(ipAssetRegistry.version(), "0.1.0"); + } + + function test_mintIdAssignment() public { + uint8 firstIPAssetType = uint8(IPAsset.STORY); + uint8 lastIPAssetTypeId = uint8(IPAsset.ITEM); + for(uint8 i = firstIPAssetType; i < lastIPAssetTypeId; i++) { + IPAsset sb = IPAsset(i); + uint256 zero = LibIPAssetId._zeroId(sb); + assertEq(ipAssetRegistry.currentIdFor(sb), zero, "starts with zero"); + vm.prank(mintee); + uint256 blockId1 = ipAssetRegistry.createIPAsset(sb, "name", "description", "mediaUrl"); + assertEq(blockId1, zero + 1, "returned blockId is incremented by one"); + assertEq(ipAssetRegistry.currentIdFor(sb), zero + 1, "mint increments currentIdFor by one"); + vm.prank(mintee); + uint256 blockId2 = ipAssetRegistry.createIPAsset(sb, "name2", "description2", "mediaUrl2"); + assertEq(blockId2, zero + 2, "returned blockId is incremented by one again"); + assertEq(ipAssetRegistry.currentIdFor(sb), zero + 2, "2 mint increments currentIdFor by one again"); + } + + } + + function test_mintStoryOwnership() public { + uint8 firstIPAssetType = uint8(IPAsset.STORY); + uint8 lastIPAssetTypeId = uint8(IPAsset.ITEM); + for(uint8 i = firstIPAssetType; i < lastIPAssetTypeId; i++) { + IPAsset sb = IPAsset(i); + uint256 loopBalance = ipAssetRegistry.balanceOf(mintee); + assertEq(loopBalance, (i - 1) * 2, "balance is zero for block type"); + vm.prank(mintee); + uint256 blockId1 = ipAssetRegistry.createIPAsset(sb, "name", "description", "mediaUrl"); + assertEq(ipAssetRegistry.balanceOf(mintee), loopBalance + 1, "balance is incremented by one"); + assertEq(ipAssetRegistry.ownerOf(blockId1), mintee); + vm.prank(mintee); + uint256 blockId2 = ipAssetRegistry.createIPAsset(sb, "name", "description", "mediaUrl"); + assertEq(ipAssetRegistry.balanceOf(mintee), loopBalance + 2, "balance is incremented by one again"); + assertEq(ipAssetRegistry.ownerOf(blockId2), mintee); + } + } + + function test_revertMintUnknownIPAsset() public { + vm.startPrank(mintee); + vm.expectRevert(InvalidBlockType.selector); + ipAssetRegistry.createIPAsset(IPAsset.UNDEFINED, "name", "description", "mediaUrl"); + } + + function test_IPAssetCreationData() public { + vm.prank(mintee); + uint256 blockId = ipAssetRegistry.createIPAsset(IPAsset.STORY, "name", "description", "mediaUrl"); + IPAssetRegistry.IPAssetData memory data = ipAssetRegistry.readIPAsset(blockId); + assertEq(uint8(data.blockType), uint8(IPAsset.STORY)); + assertEq(data.name, "name"); + assertEq(data.description, "description"); + assertEq(data.mediaUrl, "mediaUrl"); + } + + function test_emptyIPAssetRead() public { + IPAssetRegistry.IPAssetData memory data = ipAssetRegistry.readIPAsset(12312313); + assertEq(uint8(data.blockType), uint8(IPAsset.UNDEFINED)); + assertEq(data.name, ""); + assertEq(data.description, ""); + assertEq(data.mediaUrl, ""); + } + + function test_tokenUriReturnsMediaURL() public { + vm.prank(mintee); + uint256 blockId = ipAssetRegistry.createIPAsset(IPAsset.STORY, "name", "description", "https://mediaUrl.xyz"); + assertEq(ipAssetRegistry.tokenURI(blockId), "https://mediaUrl.xyz"); + } + +} \ No newline at end of file diff --git a/test/foundry/StoryBlocksRegistryFactory.t.sol b/test/foundry/IPAssetsRegistryFactory.t.sol similarity index 96% rename from test/foundry/StoryBlocksRegistryFactory.t.sol rename to test/foundry/IPAssetsRegistryFactory.t.sol index 2c671bd9..2df7de07 100644 --- a/test/foundry/StoryBlocksRegistryFactory.t.sol +++ b/test/foundry/IPAssetsRegistryFactory.t.sol @@ -38,7 +38,7 @@ contract IPAssetRegistryFactoryTest is Test { // TODO: figure why this is not matching correctly, the event is emitted according to traces // vm.expectEmit(); // emit BeaconUpgraded(address(0x123)); - address collection = factory.createFranchiseBlocks(1, "name", "symbol", "description"); + address collection = factory.createFranchiseIPAssets(1, "name", "symbol", "description"); assertTrue(collection != address(0)); assertEq(IPAssetRegistry(collection).name(), "name"); assertEq(IPAssetRegistry(collection).symbol(), "symbol"); diff --git a/test/foundry/StoryBlocksRegistry.t.sol b/test/foundry/StoryBlocksRegistry.t.sol index e2e098cb..99d82da6 100644 --- a/test/foundry/StoryBlocksRegistry.t.sol +++ b/test/foundry/StoryBlocksRegistry.t.sol @@ -37,7 +37,7 @@ contract IPAssetRegistryTest is Test { function setUp() public { factory = new IPAssetRegistryFactory(); - ipAssetRegistry = IPAssetRegistry(factory.createFranchiseBlocks(1, "name", "symbol", "description")); + ipAssetRegistry = IPAssetRegistry(factory.createFranchiseIPAssets(1, "name", "symbol", "description")); } function test_setUp() public { diff --git a/test/foundry/relationships/ProtocolRelationshipModule.Config.t.sol b/test/foundry/relationships/ProtocolRelationshipModule.Config.t.sol new file mode 100644 index 00000000..ee2a539c --- /dev/null +++ b/test/foundry/relationships/ProtocolRelationshipModule.Config.t.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: BUSDL-1.1 +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import '../utils/ProxyHelper.sol'; +import "contracts/FranchiseRegistry.sol"; +import "contracts/access-control/AccessControlSingleton.sol"; +import "contracts/access-control/ProtocolRoles.sol"; +import "contracts/ip-assets/IPAssetRegistryFactory.sol"; +import "contracts/modules/relationships/ProtocolRelationshipModule.sol"; +import "contracts/IPAsset.sol"; +import "contracts/errors/General.sol"; +import "contracts/modules/relationships/RelationshipProcessors/PermissionlessRelationshipProcessor.sol"; + +contract ProtocolRelationshipModuleSetupRelationshipsTest is Test, ProxyHelper { + + IPAssetRegistryFactory public factory; + IPAssetRegistry public ipAssetRegistry; + FranchiseRegistry public register; + ProtocolRelationshipModule public relationshipModule; + AccessControlSingleton acs; + PermissionlessRelationshipProcessor public RelationshipProcessor; + + address admin = address(123); + address relationshipManager = address(234); + address franchiseOwner = address(456); + + bytes32 relationship = keccak256("RELATIONSHIP"); + + function setUp() public { + factory = new IPAssetRegistryFactory(); + acs = AccessControlSingleton( + _deployUUPSProxy( + address(new AccessControlSingleton()), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), admin + ) + ) + ); + vm.prank(admin); + acs.grantRole(RELATIONSHIP_MANAGER_ROLE, relationshipManager); + + address accessControl = address(acs); + + FranchiseRegistry impl = new FranchiseRegistry(address(factory)); + register = FranchiseRegistry( + _deployUUPSProxy( + address(impl), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), accessControl + ) + ) + ); + vm.startPrank(franchiseOwner); + (uint256 id, address ipAssets) = register.registerFranchise("name", "symbol", "description"); + ipAssetRegistry = IPAssetRegistry(ipAssets); + vm.stopPrank(); + relationshipModule = ProtocolRelationshipModule( + _deployUUPSProxy( + address(new ProtocolRelationshipModule(address(register))), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), address(acs) + ) + ) + ); + RelationshipProcessor = new PermissionlessRelationshipProcessor(address(relationshipModule)); + } + + function test_setProtocolLevelRelationship() public { + IPAsset[] memory sourceIPAssets = new IPAsset[](1); + sourceIPAssets[0] = IPAsset.STORY; + IPAsset[] memory destIPAssets = new IPAsset[](2); + destIPAssets[0] = IPAsset.CHARACTER; + destIPAssets[1] = IPAsset.ART; + + IRelationshipModule.SetRelationshipConfigParams memory params = IRelationshipModule.SetRelationshipConfigParams({ + sourceIPAssets: sourceIPAssets, + allowedExternalSource: false, + destIPAssets: destIPAssets, + allowedExternalDest: true, + onlySameFranchise: true, + processor: address(RelationshipProcessor), + disputer: address(this), + timeConfig: IRelationshipModule.TimeConfig({ + minTTL: 0, + maxTTL: 0, + renewable: false + }) + }); + vm.prank(relationshipManager); + relationshipModule.setRelationshipConfig(relationship, params); + + IRelationshipModule.RelationshipConfig memory config = relationshipModule.relationshipConfig(relationship); + assertEq(config.sourceIPAssetTypeMask, 1 << (uint256(IPAsset.STORY) & 0xff)); + assertEq(config.destIPAssetTypeMask, 1 << (uint256(IPAsset.CHARACTER) & 0xff) | 1 << (uint256(IPAsset.ART) & 0xff) | (uint256(EXTERNAL_ASSET) << 248)); + assertTrue(config.onlySameFranchise); + // TODO: test for event + + } + + function test_revert_IfSettingProtocolLevelRelationshipUnauthorized() public { + IPAsset[] memory sourceIPAssets = new IPAsset[](1); + sourceIPAssets[0] = IPAsset.STORY; + IPAsset[] memory destIPAssets = new IPAsset[](2); + destIPAssets[0] = IPAsset.CHARACTER; + destIPAssets[1] = IPAsset.ART; + + IRelationshipModule.SetRelationshipConfigParams memory params = IRelationshipModule.SetRelationshipConfigParams({ + sourceIPAssets: sourceIPAssets, + allowedExternalSource: false, + destIPAssets: destIPAssets, + allowedExternalDest: true, + onlySameFranchise: true, + processor: address(RelationshipProcessor), + disputer: address(this), + timeConfig: IRelationshipModule.TimeConfig({ + minTTL: 0, + maxTTL: 0, + renewable: false + }) + }); + vm.expectRevert(); + vm.prank(franchiseOwner); + relationshipModule.setRelationshipConfig(relationship, params); + } + +} + +contract ProtocolRelationshipModuleUnsetRelationshipsTest is Test, ProxyHelper { + + IPAssetRegistryFactory public factory; + IPAssetRegistry public ipAssetRegistry; + FranchiseRegistry public register; + ProtocolRelationshipModule public relationshipModule; + AccessControlSingleton acs; + PermissionlessRelationshipProcessor public RelationshipProcessor; + + address admin = address(123); + address relationshipManager = address(234); + address franchiseOwner = address(456); + + bytes32 relationship = keccak256("PROTOCOL_Relationship"); + + function setUp() public { + factory = new IPAssetRegistryFactory(); + acs = AccessControlSingleton( + _deployUUPSProxy( + address(new AccessControlSingleton()), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), admin + ) + ) + ); + vm.prank(admin); + acs.grantRole(RELATIONSHIP_MANAGER_ROLE, relationshipManager); + address accessControl = address(acs); + + FranchiseRegistry impl = new FranchiseRegistry(address(factory)); + register = FranchiseRegistry( + _deployUUPSProxy( + address(impl), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), accessControl + ) + ) + ); + vm.startPrank(franchiseOwner); + (uint256 id, address ipAssets) = register.registerFranchise("name", "symbol", "description"); + ipAssetRegistry = IPAssetRegistry(ipAssets); + vm.stopPrank(); + relationshipModule = ProtocolRelationshipModule( + _deployUUPSProxy( + address(new ProtocolRelationshipModule(address(register))), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), address(acs) + ) + ) + ); + RelationshipProcessor = new PermissionlessRelationshipProcessor(address(relationshipModule)); + IPAsset[] memory sourceIPAssets = new IPAsset[](1); + sourceIPAssets[0] = IPAsset.STORY; + IPAsset[] memory destIPAssets = new IPAsset[](1); + destIPAssets[0] = IPAsset.CHARACTER; + IRelationshipModule.SetRelationshipConfigParams memory params = IRelationshipModule.SetRelationshipConfigParams({ + sourceIPAssets: sourceIPAssets, + allowedExternalSource: false, + destIPAssets: destIPAssets, + allowedExternalDest: true, + onlySameFranchise: true, + processor: address(RelationshipProcessor), + disputer: address(this), + timeConfig: IRelationshipModule.TimeConfig({ + minTTL: 0, + maxTTL: 0, + renewable: false + }) + }); + vm.prank(relationshipManager); + relationshipModule.setRelationshipConfig(relationship, params); + + } + + function test_unsetRelationshipConfig() public { + vm.prank(relationshipManager); + relationshipModule.unsetRelationshipConfig(relationship); + + IRelationshipModule.RelationshipConfig memory config = relationshipModule.relationshipConfig(relationship); + assertEq(config.sourceIPAssetTypeMask, 0); + assertEq(config.destIPAssetTypeMask, 0); + assertFalse(config.onlySameFranchise); + // TODO: test for event + } + + function test_revert_unsetRelationshipConfigNotAuthorized() public { + vm.expectRevert(); + vm.prank(franchiseOwner); + relationshipModule.unsetRelationshipConfig(relationship); + } + +} diff --git a/test/foundry/relationships/RelationShipTypeChecker.t.sol b/test/foundry/relationships/RelationShipTypeChecker.t.sol new file mode 100644 index 00000000..70a3b8d1 --- /dev/null +++ b/test/foundry/relationships/RelationShipTypeChecker.t.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: BUSDL-1.1 +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import { IPAssetRegistryFactory } from "contracts/ip-assets/IPAssetRegistryFactory.sol"; +import { RelationshipTypeChecker } from "contracts/modules/relationships/RelationshipTypeChecker.sol"; +import { IPAsset, EXTERNAL_ASSET } from "contracts/IPAsset.sol"; +import { FranchiseRegistry } from "contracts/FranchiseRegistry.sol"; +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import { LibIPAssetId } from "contracts/ip-assets/LibIPAssetId.sol"; + +contract RelationshipTypeCheckerHarness is RelationshipTypeChecker { + + bool private _returnIsAssetRegistry; + + function setIsAssetRegistry(bool value) external { + _returnIsAssetRegistry = value; + } + + function _isAssetRegistry(address ipAssetRegistry) internal virtual override view returns(bool) { + return _returnIsAssetRegistry; + } + + function checkRelationshipNode(address collection, uint256 id, uint256 assetTypeMask) view external returns (bool result, bool isAssetRegistry) { + return _checkRelationshipNode(collection, id, assetTypeMask); + } + + function convertToMask(IPAsset[] calldata ipAssets, bool allowsExternal) pure external returns (uint256) { + return _convertToMask(ipAssets, allowsExternal); + } + + function supportsIPAssetType(uint256 mask, uint8 assetType) pure external returns (bool) { + return _supportsIPAssetType(mask, assetType); + } +} + +contract MockERC721 is ERC721 { + constructor(string memory name, string memory symbol) ERC721(name, symbol) {} + + function mint(address to, uint256 tokenId) external { + _mint(to, tokenId); + } +} + + +contract RelationshipTypeCheckerConvertToMaskTest is Test { + + RelationshipTypeCheckerHarness public checker; + + error InvalidIPAssetArray(); + + function setUp() public { + checker = new RelationshipTypeCheckerHarness(); + } + + function test_convertToMaskWithoutExternal() public { + for (uint8 i = 1; i <= uint8(IPAsset.ITEM); i++) { + IPAsset[] memory ipAssets = new IPAsset[](i); + uint256 resultMask; + for (uint8 j = 1; j <= i; j++) { + ipAssets[j-1] = IPAsset(j); + resultMask |= 1 << (uint256(IPAsset(j)) & 0xff); + } + uint256 mask = checker.convertToMask(ipAssets, false); + assertEq(mask, resultMask); + } + } + + function test_convertToMaskWithExternal() public { + for (uint8 i = 1; i <= uint8(IPAsset.ITEM); i++) { + IPAsset[] memory ipAssets = new IPAsset[](i); + uint256 resultMask; + for (uint8 j = 1; j <= i; j++) { + ipAssets[j-1] = IPAsset(j); + resultMask |= 1 << (uint256(IPAsset(j)) & 0xff); + } + resultMask |= uint256(EXTERNAL_ASSET) << 248; + uint256 mask = checker.convertToMask(ipAssets, true); + assertEq(mask, resultMask); + } + } + + function test_revert_convertToMaskWithExternal_ifEmptyArray() public { + IPAsset[] memory ipAssets = new IPAsset[](0); + vm.expectRevert(InvalidIPAssetArray.selector); + checker.convertToMask(ipAssets, false); + } + + function test_revert_convertToMaskWithExterna_ifZeroRow() public { + IPAsset[] memory ipAssets = new IPAsset[](1); + ipAssets[0] = IPAsset(0); + vm.expectRevert(InvalidIPAssetArray.selector); + checker.convertToMask(ipAssets, false); + } + +} + +contract RelationshipTypeCheckerSupportsAssetTypeTest is Test { + + RelationshipTypeCheckerHarness public checker; + + error InvalidIPAssetArray(); + + function setUp() public { + checker = new RelationshipTypeCheckerHarness(); + } + + function test_supportsIPAssetType_true() public { + uint256 mask = 0; + for (uint8 i = 1; i <= uint8(IPAsset.ITEM); i++) { + mask |= 1 << (uint256(IPAsset(i)) & 0xff); + } + mask |= uint256(EXTERNAL_ASSET) << 248; + for (uint8 i = 1; i <= uint8(IPAsset.ITEM); i++) { + assertTrue(checker.supportsIPAssetType(mask, i)); + } + assertTrue(checker.supportsIPAssetType(mask, type(uint8).max)); + } + + function test_supportIPAssetType_false() public { + uint256 zeroMask; + for (uint8 i = 1; i <= uint8(IPAsset.ITEM); i++) { + assertFalse(checker.supportsIPAssetType(zeroMask, i)); + } + assertFalse(checker.supportsIPAssetType(zeroMask, type(uint8).max)); + } + +} + +contract RelationshipTypeCheckerNodesTest is Test { + + RelationshipTypeCheckerHarness public checker; + MockERC721 public collection; + address public owner = address(0x1); + + error InvalidIPAssetArray(); + + function setUp() public { + checker = new RelationshipTypeCheckerHarness(); + collection = new MockERC721("Test", "TEST"); + } + + function test_checkRelationshipNode_ipAsset_true() public { + uint256 tokenId = LibIPAssetId._zeroId(IPAsset(1)) + 1; + console.log(tokenId); + collection.mint(owner, tokenId); + checker.setIsAssetRegistry(true); + uint256 mask = 1 << (uint256(IPAsset(1)) & 0xff); + (bool result, bool isAssetRegistry) = checker.checkRelationshipNode(address(collection), tokenId, mask); + assertTrue(result); + assertTrue(isAssetRegistry); + } + + function test_checkRelationshipNode_ipAsset_false() public { + uint256 tokenId = LibIPAssetId._zeroId(IPAsset(1)) + 1; + collection.mint(owner, tokenId); + checker.setIsAssetRegistry(true); + uint256 mask = 1 << (uint256(IPAsset(2)) & 0xff); + (bool result, bool isAssetRegistry) = checker.checkRelationshipNode(address(collection), tokenId, mask); + assertFalse(result); + assertTrue(isAssetRegistry); + } + + function test_checkRelationshipNode_external_true() public { + uint256 tokenId = LibIPAssetId._zeroId(IPAsset(1)) + 1; + collection.mint(owner, tokenId); + checker.setIsAssetRegistry(false); + uint256 mask = 1 << (uint256(EXTERNAL_ASSET) & 0xff); + (bool result, bool isAssetRegistry) = checker.checkRelationshipNode(address(collection), tokenId, mask); + assertTrue(result); + assertFalse(isAssetRegistry); + } + + function test_revert_nonExistingToken() public { + vm.expectRevert("ERC721: invalid token ID"); + checker.checkRelationshipNode(address(collection), 1, uint256(type(uint8).max)); + } + + function test_revert_notERC721() public { + vm.expectRevert(); + checker.checkRelationshipNode(owner, 1, uint256(type(uint8).max)); + } + + +} \ No newline at end of file diff --git a/test/foundry/relationships/RelationshipModule.Config.t.sol b/test/foundry/relationships/RelationshipModule.Config.t.sol new file mode 100644 index 00000000..87e84221 --- /dev/null +++ b/test/foundry/relationships/RelationshipModule.Config.t.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: BUSDL-1.1 +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import '../utils/ProxyHelper.sol'; +import "contracts/FranchiseRegistry.sol"; +import "contracts/access-control/AccessControlSingleton.sol"; +import "contracts/access-control/ProtocolRoles.sol"; +import "contracts/ip-assets/IPAssetRegistryFactory.sol"; +import "./RelationshipModuleHarness.sol"; +import "contracts/IPAsset.sol"; +import "contracts/errors/General.sol"; +import "contracts/modules/relationships/RelationshipProcessors/PermissionlessRelationshipProcessor.sol"; + +contract RelationshipModuleSetupRelationshipsTest is Test, ProxyHelper { + + IPAssetRegistryFactory public factory; + IPAssetRegistry public ipAssetRegistry; + FranchiseRegistry public register; + RelationshipModuleHarness public relationshipModule; + AccessControlSingleton acs; + PermissionlessRelationshipProcessor public RelationshipProcessor; + + address admin = address(123); + address relationshipManager = address(234); + address franchiseOwner = address(456); + + bytes32 relationship = keccak256("RELATIONSHIP"); + + function setUp() public { + factory = new IPAssetRegistryFactory(); + acs = AccessControlSingleton( + _deployUUPSProxy( + address(new AccessControlSingleton()), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), admin + ) + ) + ); + vm.prank(admin); + address accessControl = address(acs); + + FranchiseRegistry impl = new FranchiseRegistry(address(factory)); + register = FranchiseRegistry( + _deployUUPSProxy( + address(impl), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), accessControl + ) + ) + ); + vm.startPrank(franchiseOwner); + (uint256 id, address ipAssets) = register.registerFranchise("name", "symbol", "description"); + ipAssetRegistry = IPAssetRegistry(ipAssets); + vm.stopPrank(); + relationshipModule = RelationshipModuleHarness( + _deployUUPSProxy( + address(new RelationshipModuleHarness(address(register))), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), address(acs) + ) + ) + ); + RelationshipProcessor = new PermissionlessRelationshipProcessor(address(relationshipModule)); + } + + function test_setProtocolLevelRelationship() public { + IPAsset[] memory sourceIPAssets = new IPAsset[](1); + sourceIPAssets[0] = IPAsset.STORY; + IPAsset[] memory destIPAssets = new IPAsset[](2); + destIPAssets[0] = IPAsset.CHARACTER; + destIPAssets[1] = IPAsset.ART; + + IRelationshipModule.SetRelationshipConfigParams memory params = IRelationshipModule.SetRelationshipConfigParams({ + sourceIPAssets: sourceIPAssets, + allowedExternalSource: false, + destIPAssets: destIPAssets, + allowedExternalDest: true, + onlySameFranchise: true, + processor: address(RelationshipProcessor), + disputer: address(this), + timeConfig: IRelationshipModule.TimeConfig({ + minTTL: 0, + maxTTL: 0, + renewable: false + }) + }); + vm.prank(relationshipManager); + relationshipModule.setRelationshipConfig(relationship, params); + + IRelationshipModule.RelationshipConfig memory config = relationshipModule.relationshipConfig(relationship); + assertEq(config.sourceIPAssetTypeMask, 1 << (uint256(IPAsset.STORY) & 0xff)); + assertEq(config.destIPAssetTypeMask, 1 << (uint256(IPAsset.CHARACTER) & 0xff) | 1 << (uint256(IPAsset.ART) & 0xff) | (uint256(EXTERNAL_ASSET) << 248)); + assertTrue(config.onlySameFranchise); + // TODO: test for event + + } + + function test_revert_IfMasksNotConfigured() public { + IPAsset[] memory sourceIPAssets = new IPAsset[](1); + sourceIPAssets[0] = IPAsset.UNDEFINED; + IPAsset[] memory destIPAssets = new IPAsset[](2); + + IRelationshipModule.SetRelationshipConfigParams memory params = IRelationshipModule.SetRelationshipConfigParams({ + sourceIPAssets: sourceIPAssets, + allowedExternalSource: false, + destIPAssets: destIPAssets, + allowedExternalDest: true, + onlySameFranchise: true, + processor: address(RelationshipProcessor), + disputer: address(this), + timeConfig: IRelationshipModule.TimeConfig({ + minTTL: 0, + maxTTL: 0, + renewable: false + }) + }); + vm.startPrank(relationshipManager); + vm.expectRevert(); + relationshipModule.setRelationshipConfig(relationship, params); + } + +} + +contract RelationshipModuleUnsetRelationshipsTest is Test, ProxyHelper { + + IPAssetRegistryFactory public factory; + IPAssetRegistry public ipAssetRegistry; + FranchiseRegistry public register; + RelationshipModuleHarness public relationshipModule; + AccessControlSingleton acs; + PermissionlessRelationshipProcessor public RelationshipProcessor; + + address admin = address(123); + address relationshipManager = address(234); + address franchiseOwner = address(456); + + bytes32 relationship = keccak256("PROTOCOL_Relationship"); + + function setUp() public { + factory = new IPAssetRegistryFactory(); + acs = AccessControlSingleton( + _deployUUPSProxy( + address(new AccessControlSingleton()), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), admin + ) + ) + ); + vm.prank(admin); + + address accessControl = address(acs); + + FranchiseRegistry impl = new FranchiseRegistry(address(factory)); + register = FranchiseRegistry( + _deployUUPSProxy( + address(impl), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), accessControl + ) + ) + ); + vm.startPrank(franchiseOwner); + (uint256 id, address ipAssets) = register.registerFranchise("name", "symbol", "description"); + ipAssetRegistry = IPAssetRegistry(ipAssets); + vm.stopPrank(); + relationshipModule = RelationshipModuleHarness( + _deployUUPSProxy( + address(new RelationshipModuleHarness(address(register))), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), address(acs) + ) + ) + ); + RelationshipProcessor = new PermissionlessRelationshipProcessor(address(relationshipModule)); + IPAsset[] memory sourceIPAssets = new IPAsset[](1); + sourceIPAssets[0] = IPAsset.STORY; + IPAsset[] memory destIPAssets = new IPAsset[](1); + destIPAssets[0] = IPAsset.CHARACTER; + IRelationshipModule.SetRelationshipConfigParams memory params = IRelationshipModule.SetRelationshipConfigParams({ + sourceIPAssets: sourceIPAssets, + allowedExternalSource: false, + destIPAssets: destIPAssets, + allowedExternalDest: true, + onlySameFranchise: true, + processor: address(RelationshipProcessor), + disputer: address(this), + timeConfig: IRelationshipModule.TimeConfig({ + minTTL: 0, + maxTTL: 0, + renewable: false + }) + }); + vm.prank(relationshipManager); + relationshipModule.setRelationshipConfig(relationship, params); + + } + + function test_unsetRelationshipConfig() public { + vm.prank(relationshipManager); + relationshipModule.unsetRelationshipConfig(relationship); + + IRelationshipModule.RelationshipConfig memory config = relationshipModule.relationshipConfig(relationship); + assertEq(config.sourceIPAssetTypeMask, 0); + assertEq(config.destIPAssetTypeMask, 0); + assertFalse(config.onlySameFranchise); + // TODO: test for event + } + + function test_revert_unsetRelationshipConfigNonExistingRelationship() public { + vm.prank(relationshipManager); + vm.expectRevert(IRelationshipModule.NonExistingRelationship.selector); + relationshipModule.unsetRelationshipConfig(keccak256("UNDEFINED_Relationship")); + } + +} diff --git a/test/foundry/relationships/RelationshipModule.Relating.t.sol b/test/foundry/relationships/RelationshipModule.Relating.t.sol new file mode 100644 index 00000000..f66ef694 --- /dev/null +++ b/test/foundry/relationships/RelationshipModule.Relating.t.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: BUSDL-1.1 +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import '../utils/ProxyHelper.sol'; +import "contracts/FranchiseRegistry.sol"; +import "contracts/access-control/AccessControlSingleton.sol"; +import "contracts/access-control/ProtocolRoles.sol"; +import "contracts/ip-assets/IPAssetRegistryFactory.sol"; +import "./RelationshipModuleHarness.sol"; +import "contracts/IPAsset.sol"; +import "contracts/errors/General.sol"; +import "contracts/modules/relationships/RelationshipProcessors/PermissionlessRelationshipProcessor.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract MockExternalAsset is ERC721 { + constructor() ERC721("MockExternalAsset", "MEA") {} + + function mint(address to, uint256 tokenId) public { + _mint(to, tokenId); + } +} + +contract RelationshipModuleRelationshipTest is Test, ProxyHelper { + + IPAssetRegistryFactory public factory; + IPAssetRegistry public ipAssetRegistry; + FranchiseRegistry public register; + RelationshipModuleHarness public relationshipModule; + AccessControlSingleton acs; + PermissionlessRelationshipProcessor public processor; + + address admin = address(123); + address relationshipManager = address(234); + address franchiseOwner = address(456); + address ipAssetOwner = address(567); + + bytes32 relationship = keccak256("RELATIONSHIP"); + + mapping(uint8 => uint256) public ipAssetIds; + + MockExternalAsset public externalAsset; + + + function setUp() public { + factory = new IPAssetRegistryFactory(); + acs = AccessControlSingleton( + _deployUUPSProxy( + address(new AccessControlSingleton()), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), admin + ) + ) + ); + vm.prank(admin); + + FranchiseRegistry impl = new FranchiseRegistry(address(factory)); + register = FranchiseRegistry( + _deployUUPSProxy( + address(impl), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), address(acs) + ) + ) + ); + vm.prank(franchiseOwner); + (uint256 id, address ipAssets) = register.registerFranchise("name", "symbol", "description"); + ipAssetRegistry = IPAssetRegistry(ipAssets); + + relationshipModule = RelationshipModuleHarness( + _deployUUPSProxy( + address(new RelationshipModuleHarness(address(register))), + abi.encodeWithSelector( + bytes4(keccak256(bytes("initialize(address)"))), address(acs) + ) + ) + ); + IPAsset[] memory sourceIPAssets = new IPAsset[](1); + sourceIPAssets[0] = IPAsset.STORY; + IPAsset[] memory destIPAssets = new IPAsset[](2); + destIPAssets[0] = IPAsset.CHARACTER; + destIPAssets[1] = IPAsset.ART; + + processor = new PermissionlessRelationshipProcessor(address(relationshipModule)); + IRelationshipModule.SetRelationshipConfigParams memory params = IRelationshipModule.SetRelationshipConfigParams({ + sourceIPAssets: sourceIPAssets, + allowedExternalSource: false, + destIPAssets: destIPAssets, + allowedExternalDest: true, + onlySameFranchise: true, + processor: address(processor), + disputer: address(this), + timeConfig: IRelationshipModule.TimeConfig(0, 0, false) + }); + vm.prank(relationshipManager); + relationshipModule.setRelationshipConfig(relationship, params); + vm.startPrank(ipAssetOwner); + + ipAssetIds[uint8(IPAsset.STORY)] = ipAssetRegistry.createIPAsset(IPAsset.STORY, "name", "description", "mediaUrl"); + ipAssetIds[uint8(IPAsset.CHARACTER)] = ipAssetRegistry.createIPAsset(IPAsset.CHARACTER, "name", "description", "mediaUrl"); + ipAssetIds[uint8(IPAsset.ART)] = ipAssetRegistry.createIPAsset(IPAsset.ART, "name", "description", "mediaUrl"); + + externalAsset = new MockExternalAsset(); + ipAssetIds[EXTERNAL_ASSET] = 333; + externalAsset.mint(ipAssetOwner, 333); + vm.stopPrank(); + } + + function test_relate() public { + relationshipModule.relate( + IRelationshipModule.RelationshipParams( + address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.STORY)], address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.CHARACTER)], relationship, 0 + ), + "" + ); + assertTrue( + relationshipModule.areTheyRelated( + IRelationshipModule.RelationshipParams( + address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.STORY)], address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.CHARACTER)], relationship, 0 + ) + ) + ); + + relationshipModule.relate( + IRelationshipModule.RelationshipParams( + address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.STORY)], address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.ART)], relationship, 0 + ), + "" + ); + assertTrue( + relationshipModule.areTheyRelated( + IRelationshipModule.RelationshipParams( + address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.STORY)], address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.ART)], relationship, 0 + ) + ) + ); + + relationshipModule.relate( + IRelationshipModule.RelationshipParams( + address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.STORY)], address(externalAsset), ipAssetIds[EXTERNAL_ASSET], relationship, 0 + ), + "" + ); + assertTrue( + relationshipModule.areTheyRelated( + IRelationshipModule.RelationshipParams( + address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.STORY)], address(externalAsset), ipAssetIds[EXTERNAL_ASSET], relationship, 0 + ) + ) + ); + // TODO check for event + + } + + function test_not_related() public { + assertFalse( + relationshipModule.areTheyRelated( + IRelationshipModule.RelationshipParams(address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.STORY)], address(1), 2, relationship, 0) + ) + ); + assertFalse( + relationshipModule.areTheyRelated( + IRelationshipModule.RelationshipParams( + address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.STORY)], address(externalAsset), ipAssetIds[EXTERNAL_ASSET], keccak256("WRONG"), 0 + ) + ) + ); + } + + function test_revert_unknown_relationship() public { + vm.expectRevert(IRelationshipModule.NonExistingRelationship.selector); + relationshipModule.relate( + IRelationshipModule.RelationshipParams( + address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.STORY)], address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.CHARACTER)], keccak256("WRONG"), 0 + ), + "" + ); + } + + function test_revert_relationshipsNotSameFranchise() public { + vm.prank(franchiseOwner); + (uint256 id, address otherIPAssets) = register.registerFranchise("name2", "symbol2", "description2"); + IPAssetRegistry otherIPAssetRegistry = IPAssetRegistry(otherIPAssets); + vm.prank(ipAssetOwner); + uint256 otherId = otherIPAssetRegistry.createIPAsset(IPAsset.CHARACTER, "name", "description", "mediaUrl"); + vm.expectRevert(IRelationshipModule.CannotRelateToOtherFranchise.selector); + relationshipModule.relate( + IRelationshipModule.RelationshipParams( + address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.STORY)], otherIPAssets, otherId, relationship, 0 + ), + "" + ); + } + + function test_revert_relateUnsupportedSource() public { + vm.prank(ipAssetOwner); + uint256 wrongId = ipAssetRegistry.createIPAsset(IPAsset.GROUP, "name", "description", "mediaUrl"); + vm.expectRevert(IRelationshipModule.UnsupportedRelationshipSrc.selector); + relationshipModule.relate( + IRelationshipModule.RelationshipParams( + address(ipAssetRegistry), wrongId, address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.CHARACTER)], relationship, 0 + ), + "" + ); + } + + function test_revert_relateUnsupportedDestination() public { + vm.prank(ipAssetOwner); + uint256 wrongId = ipAssetRegistry.createIPAsset(IPAsset.GROUP, "name", "description", "mediaUrl"); + vm.expectRevert(IRelationshipModule.UnsupportedRelationshipDst.selector); + relationshipModule.relate( + IRelationshipModule.RelationshipParams( + address(ipAssetRegistry), ipAssetIds[uint8(IPAsset.STORY)], address(ipAssetRegistry), wrongId, relationship, 0 + ), + "" + ); + } + + +} diff --git a/test/foundry/relationships/RelationshipModuleHarness.sol b/test/foundry/relationships/RelationshipModuleHarness.sol new file mode 100644 index 00000000..e9c1ec41 --- /dev/null +++ b/test/foundry/relationships/RelationshipModuleHarness.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +import { RelationshipModuleBase } from "contracts/modules/relationships/RelationshipModuleBase.sol"; + +contract RelationshipModuleHarness is RelationshipModuleBase { + + constructor(address _franchiseRegistry) RelationshipModuleBase(_franchiseRegistry) {} + + function initialize(address accessControl) public initializer { + __RelationshipModuleBase_init(accessControl); + } + + function setRelationshipConfig(bytes32 relationshipId, SetRelationshipConfigParams calldata params) external { + _setRelationshipConfig(relationshipId, params); + } + + function unsetRelationshipConfig(bytes32 relationshipId) external { + _unsetRelationshipConfig(relationshipId); + } + + function _authorizeUpgrade( + address newImplementation + ) internal virtual override {} + +} \ No newline at end of file