From 7e61975bab592855f5189f1e053bc91212d5d75d Mon Sep 17 00:00:00 2001 From: Sebastian Liu Date: Sat, 14 Dec 2024 22:50:00 -0800 Subject: [PATCH 1/6] feat: tokenizer module --- .../modules/tokenizer/IOwnableERC20.sol | 36 ++++++ .../modules/tokenizer/ITokenizerModule.sol | 30 +++++ contracts/lib/Errors.sol | 25 ++++ contracts/modules/tokenizer/OwnableERC20.sol | 47 ++++++++ .../modules/tokenizer/OwnerableERC20.sol | 13 --- .../modules/tokenizer/TokenizerModule.sol | 107 +++++++++++++++++- 6 files changed, 240 insertions(+), 18 deletions(-) create mode 100644 contracts/interfaces/modules/tokenizer/IOwnableERC20.sol create mode 100644 contracts/interfaces/modules/tokenizer/ITokenizerModule.sol create mode 100644 contracts/modules/tokenizer/OwnableERC20.sol delete mode 100644 contracts/modules/tokenizer/OwnerableERC20.sol diff --git a/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol b/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol new file mode 100644 index 0000000..da86324 --- /dev/null +++ b/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title Ownable ERC20 Interface +/// @notice Interface for the Ownable ERC20 token +interface IOwnableERC20 is IERC20 { + /// @notice Struct for the initialization data + /// @param name The name of the token + /// @param symbol The symbol of the token + /// @param cap The cap of the token + /// @param initialOwner The initial owner of the token + struct InitData { + string name; + string symbol; + uint256 cap; + address initialOwner; + } + + /// @notice Initializes the token + /// @param initData The initialization data + function initialize(bytes memory initData) external; + + /// @notice Mints tokens + /// @param to The address to mint tokens to + /// @param amount The amount of tokens to mint + function mint(address to, uint256 amount) external; + + /// @notice Returns the upgradable beacon + function upgradableBeacon() external view returns (address); + + /// @notice Returns whether the contract supports an interface + /// @param interfaceId The interface ID + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/contracts/interfaces/modules/tokenizer/ITokenizerModule.sol b/contracts/interfaces/modules/tokenizer/ITokenizerModule.sol new file mode 100644 index 0000000..078fa09 --- /dev/null +++ b/contracts/interfaces/modules/tokenizer/ITokenizerModule.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { IModule } from "@storyprotocol/core/interfaces/modules/base/IModule.sol"; + +/// @title Tokenizer Module Interface +/// @notice Interface for the Tokenizer Module +interface ITokenizerModule is IModule { + /// @notice Event emitted when a token template is whitelisted + /// @param tokenTemplate The address of the token template + /// @param allowed The whitelisting status + event TokenTemplateWhitelisted(address tokenTemplate, bool allowed); + + /// @notice Event emitted when an IP is tokenized + /// @param ipId The address of the IP + /// @param token The address of the token + event IPTokenized(address ipId, address token); + + /// @notice Whitelists a token template + /// @param tokenTemplate The address of the token template + /// @param allowed The whitelisting status + function whitelistTokenTemplate(address tokenTemplate, bool allowed) external; + + /// @notice Tokenizes an IP + /// @param ipId The address of the IP + /// @param tokenTemplate The address of the token template + /// @param initData The initialization data for the token + /// @return token The address of the newly created token + function tokenize(address ipId, address tokenTemplate, bytes calldata initData) external returns (address token); +} diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 16eb419..c0b5314 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -92,4 +92,29 @@ library Errors { /// @param tokenId The ID of the original NFT that was first minted with this metadata hash. /// @param nftMetadataHash The hash of the NFT metadata that caused the duplication error. error SPGNFT__DuplicatedNFTMetadataHash(address spgNftContract, uint256 tokenId, bytes32 nftMetadataHash); + + //////////////////////////////////////////////////////////////////////////// + // TokenizerModule // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Zero dispute module provided. + error TokenizerModule__ZeroDisputeModule(); + + /// @notice Zero token template provided. + error TokenizerModule__ZeroTokenTemplate(); + + /// @notice Token template is not supported. + error TokenizerModule__UnsupportedERC20(address tokenTemplate); + + /// @notice IP is disputed. + error TokenizerModule__DisputedIpId(address ipId); + + /// @notice Token template is not whitelisted. + error TokenizerModule__TokenTemplateNotWhitelisted(address tokenTemplate); + + /// @notice IP is not registered. + error TokenizerModule__IpNotRegistered(address ipId); + + /// @notice IP is expired. + error TokenizerModule__IpExpired(address ipId); } diff --git a/contracts/modules/tokenizer/OwnableERC20.sol b/contracts/modules/tokenizer/OwnableERC20.sol new file mode 100644 index 0000000..c9b23bd --- /dev/null +++ b/contracts/modules/tokenizer/OwnableERC20.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +// solhint-disable-next-line max-line-length +import { ERC20CappedUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20CappedUpgradeable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import { IOwnableERC20 } from "../../interfaces/modules/tokenizer/IOwnableERC20.sol"; + +/// @title OwnableERC20 +/// @notice A capped ERC20 token with an owner that can mint tokens. +contract OwnableERC20 is IOwnableERC20, ERC20CappedUpgradeable, OwnableUpgradeable { + address public immutable UPGRADABLE_BEACON; + + constructor(address _upgradableBeacon) { + UPGRADABLE_BEACON = _upgradableBeacon; + _disableInitializers(); + } + + /// @notice Initializes the token + /// @param initData The initialization data + function initialize(bytes memory initData) external virtual initializer { + InitData memory initData = abi.decode(initData, (InitData)); + + __ERC20Capped_init(initData.cap); + __ERC20_init(initData.name, initData.symbol); + __Ownable_init(initData.initialOwner); + } + + /// @notice Mints tokens to the specified address. + /// @param to The address to mint tokens to. + /// @param amount The amount of tokens to mint. + function mint(address to, uint256 amount) external virtual onlyOwner { + _mint(to, amount); + } + + /// @notice Returns the upgradable beacon + function upgradableBeacon() external view returns (address) { + return UPGRADABLE_BEACON; + } + + /// @notice Returns whether the contract supports an interface + /// @param interfaceId The interface ID + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return interfaceId == type(IOwnableERC20).interfaceId; + } +} diff --git a/contracts/modules/tokenizer/OwnerableERC20.sol b/contracts/modules/tokenizer/OwnerableERC20.sol deleted file mode 100644 index df21a30..0000000 --- a/contracts/modules/tokenizer/OwnerableERC20.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.26; - -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -contract OwnerableERC20 is ERC20 { - constructor() ERC20("MockERC20", "MERC20") {} - - // can only mint by owner - function mint(address to, uint256 amount) external { - _mint(to, amount); - } -} diff --git a/contracts/modules/tokenizer/TokenizerModule.sol b/contracts/modules/tokenizer/TokenizerModule.sol index 74eb33e..90d48de 100644 --- a/contracts/modules/tokenizer/TokenizerModule.sol +++ b/contracts/modules/tokenizer/TokenizerModule.sol @@ -1,38 +1,135 @@ -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { BaseModule } from "@storyprotocol/core/modules/BaseModule.sol"; import { AccessControlled } from "@storyprotocol/core/access/AccessControlled.sol"; +import { IIPAccount } from "@storyprotocol/core/interfaces/IIPAccount.sol"; +import { IPAccountStorageOps } from "@storyprotocol/core/lib/IPAccountStorageOps.sol"; import { IDisputeModule } from "@storyprotocol/core/interfaces/modules/dispute/IDisputeModule.sol"; +import { ProtocolPausableUpgradeable } from "@storyprotocol/core/pause/ProtocolPausableUpgradeable.sol"; + +import { IOwnableERC20 } from "../../interfaces/modules/tokenizer/IOwnableERC20.sol"; +import { Errors } from "../../lib/Errors.sol"; +import { ITokenizerModule } from "../../interfaces/modules/tokenizer/ITokenizerModule.sol"; /// @title Tokenizer Module /// @notice Tokenizer module is the main entry point for the IPA Tokenization and Fractionalization. /// It is responsible for: /// - Tokenize an IPA /// - Whitelist ERC20 Token Templates -contract TokenizerModule is BaseModule, AccessControlled { - using ERC165Checker for address; +contract TokenizerModule is + ITokenizerModule, + BaseModule, + AccessControlled, + ProtocolPausableUpgradeable, + UUPSUpgradeable +{ using Strings for *; + using ERC165Checker for address; + using IPAccountStorageOps for IIPAccount; + + /// @dev Storage structure for the TokenizerModule + /// @param isWhitelistedTokenTemplate Mapping of token templates to their whitelisting status + /// @custom:storage-location erc7201:story-protocol-periphery.TokenizerModule + struct TokenizerModuleStorage { + mapping(address => bool) isWhitelistedTokenTemplate; + } + + /// solhint-disable-next-line max-line-length + /// keccak256(abi.encode(uint256(keccak256("story-protocol-periphery.TokenizerModule")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant TokenizerModuleStorageLocation = + 0xef271c298b3e9574aa43cf546463b750863573b31e3d16f477ffc6f522452800; + + bytes32 public constant EXPIRATION_TIME = "EXPIRATION_TIME"; /// @notice Returns the protocol-wide dispute module + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IDisputeModule public immutable DISPUTE_MODULE; + /// @custom:oz-upgrades-unsafe-allow constructor constructor( address accessController, address ipAssetRegistry, address disputeModule ) AccessControlled(accessController, ipAssetRegistry) { + if (disputeModule == address(0)) revert Errors.TokenizerModule__ZeroDisputeModule(); + DISPUTE_MODULE = IDisputeModule(disputeModule); } - function whitelistTokenTemplate(address tokenTemplate, bool allowed) external {} + /// @notice Whitelists a token template + /// @param tokenTemplate The address of the token template + /// @param allowed The whitelisting status + function whitelistTokenTemplate(address tokenTemplate, bool allowed) external restricted { + if (tokenTemplate == address(0)) revert Errors.TokenizerModule__ZeroTokenTemplate(); + if (!tokenTemplate.supportsInterface(type(IOwnableERC20).interfaceId)) + revert Errors.TokenizerModule__UnsupportedERC20(tokenTemplate); + + TokenizerModuleStorage storage $ = _getTokenizerModuleStorage(); + $.isWhitelistedTokenTemplate[tokenTemplate] = allowed; + + emit TokenTemplateWhitelisted(tokenTemplate, allowed); + } + + /// @notice Tokenizes an IP + /// @param ipId The address of the IP + /// @param tokenTemplate The address of the token template + /// @param initData The initialization data for the token + /// @return token The address of the newly created token + function tokenize( + address ipId, + address tokenTemplate, + bytes calldata initData + ) external verifyPermission(ipId) returns (address token) { + if (DISPUTE_MODULE.isIpTagged(ipId)) revert Errors.TokenizerModule__DisputedIpId(ipId); + if (!IP_ASSET_REGISTRY.isRegistered(ipId)) revert Errors.TokenizerModule__IpNotRegistered(ipId); + if (_isExpiredNow(ipId)) revert Errors.TokenizerModule__IpExpired(ipId); + + TokenizerModuleStorage storage $ = _getTokenizerModuleStorage(); + if (!$.isWhitelistedTokenTemplate[tokenTemplate]) + revert Errors.TokenizerModule__TokenTemplateNotWhitelisted(tokenTemplate); + + token = address( + new BeaconProxy( + IOwnableERC20(tokenTemplate).upgradableBeacon(), + abi.encodeWithSelector(IOwnableERC20.initialize.selector, initData) + ) + ); - function tokenize(address ipId, address tokenTemplate, bytes calldata initData) external verifyPermission(ipId) {} + emit IPTokenized(ipId, token); + } + + /// @dev Check if an IP is expired now + /// @param ipId The address of the IP + function _isExpiredNow(address ipId) internal view returns (bool) { + uint256 expireTime = _getExpireTime(ipId); + return expireTime != 0 && expireTime < block.timestamp; + } + /// @dev Get the expiration time of an IP + /// @param ipId The address of the IP + function _getExpireTime(address ipId) internal view returns (uint256) { + return IIPAccount(payable(ipId)).getUint256(EXPIRATION_TIME); + } + + /// @dev Returns the name of the module function name() external pure override returns (string memory) { return "TOKENIZER_MODULE"; } + + /// @dev Returns the storage struct of TokenizerModule. + function _getTokenizerModuleStorage() private pure returns (TokenizerModuleStorage storage $) { + assembly { + $.slot := TokenizerModuleStorageLocation + } + } + + /// @dev Hook to authorize the upgrade according to UUPSUpgradeable + /// @param newImplementation The address of the new implementation + function _authorizeUpgrade(address newImplementation) internal override restricted {} } From 8efd8a2d1a7e91d1a982da3735b9fa1cde96d4b4 Mon Sep 17 00:00:00 2001 From: Sebastian Liu Date: Sun, 15 Dec 2024 11:43:03 -0800 Subject: [PATCH 2/6] fix: inherit IERC165 & additional checks in tokenizer --- .../modules/tokenizer/IOwnableERC20.sol | 5 +--- .../modules/tokenizer/ITokenizerModule.sol | 10 +++++++ contracts/lib/Errors.sol | 10 +++++++ .../modules/tokenizer/TokenizerModule.sol | 30 +++++++++++++++++-- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol b/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol index da86324..bf43a49 100644 --- a/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol +++ b/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; /// @title Ownable ERC20 Interface /// @notice Interface for the Ownable ERC20 token @@ -29,8 +30,4 @@ interface IOwnableERC20 is IERC20 { /// @notice Returns the upgradable beacon function upgradableBeacon() external view returns (address); - - /// @notice Returns whether the contract supports an interface - /// @param interfaceId The interface ID - function supportsInterface(bytes4 interfaceId) external view returns (bool); } diff --git a/contracts/interfaces/modules/tokenizer/ITokenizerModule.sol b/contracts/interfaces/modules/tokenizer/ITokenizerModule.sol index 078fa09..0227035 100644 --- a/contracts/interfaces/modules/tokenizer/ITokenizerModule.sol +++ b/contracts/interfaces/modules/tokenizer/ITokenizerModule.sol @@ -27,4 +27,14 @@ interface ITokenizerModule is IModule { /// @param initData The initialization data for the token /// @return token The address of the newly created token function tokenize(address ipId, address tokenTemplate, bytes calldata initData) external returns (address token); + + /// @notice Returns the fractionalized token for an IP + /// @param ipId The address of the IP + /// @return token The address of the token + function getFractionalizedToken(address ipId) external view returns (address token); + + /// @notice Checks if a token template is whitelisted + /// @param tokenTemplate The address of the token template + /// @return allowed The whitelisting status (true if whitelisted, false if not) + function isWhitelistedTokenTemplate(address tokenTemplate) external view returns (bool allowed); } diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index c0b5314..8d4e24d 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -104,17 +104,27 @@ library Errors { error TokenizerModule__ZeroTokenTemplate(); /// @notice Token template is not supported. + /// @param tokenTemplate The address of the token template that is not supported error TokenizerModule__UnsupportedERC20(address tokenTemplate); /// @notice IP is disputed. + /// @param ipId The address of the disputed IP error TokenizerModule__DisputedIpId(address ipId); /// @notice Token template is not whitelisted. + /// @param tokenTemplate The address of the token template error TokenizerModule__TokenTemplateNotWhitelisted(address tokenTemplate); /// @notice IP is not registered. + /// @param ipId The address of the IP error TokenizerModule__IpNotRegistered(address ipId); /// @notice IP is expired. + /// @param ipId The address of the expired IP error TokenizerModule__IpExpired(address ipId); + + /// @notice IP is already tokenized. + /// @param ipId The address of the already tokenized IP + /// @param token The address of the fractionalized token for the IP + error TokenizerModule__IpAlreadyTokenized(address ipId, address token); } diff --git a/contracts/modules/tokenizer/TokenizerModule.sol b/contracts/modules/tokenizer/TokenizerModule.sol index 90d48de..62cbe5e 100644 --- a/contracts/modules/tokenizer/TokenizerModule.sol +++ b/contracts/modules/tokenizer/TokenizerModule.sol @@ -2,9 +2,10 @@ pragma solidity 0.8.26; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import { BaseModule } from "@storyprotocol/core/modules/BaseModule.sol"; import { AccessControlled } from "@storyprotocol/core/access/AccessControlled.sol"; @@ -27,6 +28,7 @@ contract TokenizerModule is BaseModule, AccessControlled, ProtocolPausableUpgradeable, + ReentrancyGuardUpgradeable, UUPSUpgradeable { using Strings for *; @@ -35,9 +37,11 @@ contract TokenizerModule is /// @dev Storage structure for the TokenizerModule /// @param isWhitelistedTokenTemplate Mapping of token templates to their whitelisting status + /// @param fractionalizedTokens Mapping of IP IDs to their fractionalized tokens /// @custom:storage-location erc7201:story-protocol-periphery.TokenizerModule struct TokenizerModuleStorage { mapping(address => bool) isWhitelistedTokenTemplate; + mapping(address ipId => address token) fractionalizedTokens; } /// solhint-disable-next-line max-line-length @@ -76,7 +80,7 @@ contract TokenizerModule is emit TokenTemplateWhitelisted(tokenTemplate, allowed); } - /// @notice Tokenizes an IP + /// @notice Tokenizes (fractionalizes) an IP /// @param ipId The address of the IP /// @param tokenTemplate The address of the token template /// @param initData The initialization data for the token @@ -85,12 +89,14 @@ contract TokenizerModule is address ipId, address tokenTemplate, bytes calldata initData - ) external verifyPermission(ipId) returns (address token) { + ) external verifyPermission(ipId) nonReentrant returns (address token) { if (DISPUTE_MODULE.isIpTagged(ipId)) revert Errors.TokenizerModule__DisputedIpId(ipId); if (!IP_ASSET_REGISTRY.isRegistered(ipId)) revert Errors.TokenizerModule__IpNotRegistered(ipId); if (_isExpiredNow(ipId)) revert Errors.TokenizerModule__IpExpired(ipId); TokenizerModuleStorage storage $ = _getTokenizerModuleStorage(); + address existingToken = $.fractionalizedTokens[ipId]; + if (existingToken != address(0)) revert Errors.TokenizerModule__IpAlreadyTokenized(ipId, existingToken); if (!$.isWhitelistedTokenTemplate[tokenTemplate]) revert Errors.TokenizerModule__TokenTemplateNotWhitelisted(tokenTemplate); @@ -101,9 +107,27 @@ contract TokenizerModule is ) ); + $.fractionalizedTokens[ipId] = token; + emit IPTokenized(ipId, token); } + /// @notice Returns the fractionalized token for an IP + /// @param ipId The address of the IP + /// @return token The address of the token (0 address if IP has not been tokenized) + function getFractionalizedToken(address ipId) external view returns (address token) { + TokenizerModuleStorage storage $ = _getTokenizerModuleStorage(); + return $.fractionalizedTokens[ipId]; + } + + /// @notice Checks if a token template is whitelisted + /// @param tokenTemplate The address of the token template + /// @return allowed The whitelisting status (true if whitelisted, false if not) + function isWhitelistedTokenTemplate(address tokenTemplate) external view returns (bool allowed) { + TokenizerModuleStorage storage $ = _getTokenizerModuleStorage(); + return $.isWhitelistedTokenTemplate[tokenTemplate]; + } + /// @dev Check if an IP is expired now /// @param ipId The address of the IP function _isExpiredNow(address ipId) internal view returns (bool) { From e0d486761c0a70d3cd6dc4ef4c39d44cd3b39d92 Mon Sep 17 00:00:00 2001 From: Sebastian Liu Date: Sun, 15 Dec 2024 14:53:00 -0800 Subject: [PATCH 3/6] fix: use `isExpiredNow` in license registry --- contracts/lib/Errors.sol | 2 ++ .../modules/tokenizer/TokenizerModule.sol | 25 +++++++------------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 8d4e24d..5ff2034 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -96,6 +96,8 @@ library Errors { //////////////////////////////////////////////////////////////////////////// // TokenizerModule // //////////////////////////////////////////////////////////////////////////// + /// @notice Zero license registry provided. + error TokenizerModule__ZeroLicenseRegistry(); /// @notice Zero dispute module provided. error TokenizerModule__ZeroDisputeModule(); diff --git a/contracts/modules/tokenizer/TokenizerModule.sol b/contracts/modules/tokenizer/TokenizerModule.sol index 62cbe5e..29569d0 100644 --- a/contracts/modules/tokenizer/TokenizerModule.sol +++ b/contracts/modules/tokenizer/TokenizerModule.sol @@ -8,9 +8,10 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import { BaseModule } from "@storyprotocol/core/modules/BaseModule.sol"; -import { AccessControlled } from "@storyprotocol/core/access/AccessControlled.sol"; import { IIPAccount } from "@storyprotocol/core/interfaces/IIPAccount.sol"; +import { AccessControlled } from "@storyprotocol/core/access/AccessControlled.sol"; import { IPAccountStorageOps } from "@storyprotocol/core/lib/IPAccountStorageOps.sol"; +import { ILicenseRegistry } from "@storyprotocol/core/interfaces/registries/ILicenseRegistry.sol"; import { IDisputeModule } from "@storyprotocol/core/interfaces/modules/dispute/IDisputeModule.sol"; import { ProtocolPausableUpgradeable } from "@storyprotocol/core/pause/ProtocolPausableUpgradeable.sol"; @@ -49,7 +50,9 @@ contract TokenizerModule is bytes32 private constant TokenizerModuleStorageLocation = 0xef271c298b3e9574aa43cf546463b750863573b31e3d16f477ffc6f522452800; - bytes32 public constant EXPIRATION_TIME = "EXPIRATION_TIME"; + /// @notice Returns the protocol-wide license registry + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + ILicenseRegistry public immutable LICENSE_REGISTRY; /// @notice Returns the protocol-wide dispute module /// @custom:oz-upgrades-unsafe-allow state-variable-immutable @@ -59,10 +62,13 @@ contract TokenizerModule is constructor( address accessController, address ipAssetRegistry, + address licenseRegistry, address disputeModule ) AccessControlled(accessController, ipAssetRegistry) { + if (licenseRegistry == address(0)) revert Errors.TokenizerModule__ZeroLicenseRegistry(); if (disputeModule == address(0)) revert Errors.TokenizerModule__ZeroDisputeModule(); + LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry); DISPUTE_MODULE = IDisputeModule(disputeModule); } @@ -92,7 +98,7 @@ contract TokenizerModule is ) external verifyPermission(ipId) nonReentrant returns (address token) { if (DISPUTE_MODULE.isIpTagged(ipId)) revert Errors.TokenizerModule__DisputedIpId(ipId); if (!IP_ASSET_REGISTRY.isRegistered(ipId)) revert Errors.TokenizerModule__IpNotRegistered(ipId); - if (_isExpiredNow(ipId)) revert Errors.TokenizerModule__IpExpired(ipId); + if (LICENSE_REGISTRY.isExpiredNow(ipId)) revert Errors.TokenizerModule__IpExpired(ipId); TokenizerModuleStorage storage $ = _getTokenizerModuleStorage(); address existingToken = $.fractionalizedTokens[ipId]; @@ -128,19 +134,6 @@ contract TokenizerModule is return $.isWhitelistedTokenTemplate[tokenTemplate]; } - /// @dev Check if an IP is expired now - /// @param ipId The address of the IP - function _isExpiredNow(address ipId) internal view returns (bool) { - uint256 expireTime = _getExpireTime(ipId); - return expireTime != 0 && expireTime < block.timestamp; - } - - /// @dev Get the expiration time of an IP - /// @param ipId The address of the IP - function _getExpireTime(address ipId) internal view returns (uint256) { - return IIPAccount(payable(ipId)).getUint256(EXPIRATION_TIME); - } - /// @dev Returns the name of the module function name() external pure override returns (string memory) { return "TOKENIZER_MODULE"; From e27a0d473bd31c13d51eb82263d506f4aa6bf74f Mon Sep 17 00:00:00 2001 From: Sebastian Liu Date: Sun, 15 Dec 2024 19:06:57 -0800 Subject: [PATCH 4/6] tests: add tests and final changes --- .../modules/tokenizer/IOwnableERC20.sol | 7 +- contracts/lib/Errors.sol | 16 +- contracts/modules/tokenizer/OwnableERC20.sol | 38 ++- .../modules/tokenizer/TokenizerModule.sol | 22 +- script/utils/DeployHelper.sol | 64 +++- test/modules/tokenizer/OwnableERC20.t.sol | 89 +++++ test/modules/tokenizer/TokenizerModule.t.sol | 304 ++++++++++++++++++ test/utils/BaseTest.t.sol | 13 +- 8 files changed, 528 insertions(+), 25 deletions(-) create mode 100644 test/modules/tokenizer/OwnableERC20.t.sol create mode 100644 test/modules/tokenizer/TokenizerModule.t.sol diff --git a/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol b/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol index bf43a49..0d98150 100644 --- a/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol +++ b/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol @@ -6,7 +6,7 @@ import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol /// @title Ownable ERC20 Interface /// @notice Interface for the Ownable ERC20 token -interface IOwnableERC20 is IERC20 { +interface IOwnableERC20 is IERC20, IERC165 { /// @notice Struct for the initialization data /// @param name The name of the token /// @param symbol The symbol of the token @@ -21,7 +21,7 @@ interface IOwnableERC20 is IERC20 { /// @notice Initializes the token /// @param initData The initialization data - function initialize(bytes memory initData) external; + function initialize(address ipId, bytes memory initData) external; /// @notice Mints tokens /// @param to The address to mint tokens to @@ -30,4 +30,7 @@ interface IOwnableERC20 is IERC20 { /// @notice Returns the upgradable beacon function upgradableBeacon() external view returns (address); + + /// @notice Returns the ip id to whom this fractionalized token belongs to + function ipId() external view returns (address); } diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 5ff2034..0674cd4 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -93,6 +93,13 @@ library Errors { /// @param nftMetadataHash The hash of the NFT metadata that caused the duplication error. error SPGNFT__DuplicatedNFTMetadataHash(address spgNftContract, uint256 tokenId, bytes32 nftMetadataHash); + //////////////////////////////////////////////////////////////////////////// + // OwnableERC20 // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Zero ip id provided. + error OwnableERC20__ZeroIpId(); + //////////////////////////////////////////////////////////////////////////// // TokenizerModule // //////////////////////////////////////////////////////////////////////////// @@ -105,9 +112,12 @@ library Errors { /// @notice Zero token template provided. error TokenizerModule__ZeroTokenTemplate(); + /// @notice Zero protocol access manager provided. + error TokenizerModule__ZeroProtocolAccessManager(); + /// @notice Token template is not supported. /// @param tokenTemplate The address of the token template that is not supported - error TokenizerModule__UnsupportedERC20(address tokenTemplate); + error TokenizerModule__UnsupportedOwnableERC20(address tokenTemplate); /// @notice IP is disputed. /// @param ipId The address of the disputed IP @@ -117,10 +127,6 @@ library Errors { /// @param tokenTemplate The address of the token template error TokenizerModule__TokenTemplateNotWhitelisted(address tokenTemplate); - /// @notice IP is not registered. - /// @param ipId The address of the IP - error TokenizerModule__IpNotRegistered(address ipId); - /// @notice IP is expired. /// @param ipId The address of the expired IP error TokenizerModule__IpExpired(address ipId); diff --git a/contracts/modules/tokenizer/OwnableERC20.sol b/contracts/modules/tokenizer/OwnableERC20.sol index c9b23bd..f3ca3b1 100644 --- a/contracts/modules/tokenizer/OwnableERC20.sol +++ b/contracts/modules/tokenizer/OwnableERC20.sol @@ -1,15 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; // solhint-disable-next-line max-line-length import { ERC20CappedUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20CappedUpgradeable.sol"; -import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { Errors } from "../../lib/Errors.sol"; import { IOwnableERC20 } from "../../interfaces/modules/tokenizer/IOwnableERC20.sol"; /// @title OwnableERC20 /// @notice A capped ERC20 token with an owner that can mint tokens. contract OwnableERC20 is IOwnableERC20, ERC20CappedUpgradeable, OwnableUpgradeable { + /// @dev Storage structure for the OwnableERC20 + /// @param ipId The ip id to whom this fractionalized token belongs to + /// @custom:storage-location erc7201:story-protocol-periphery.OwnableERC20 + struct OwnableERC20Storage { + address ipId; + } + + // solhint-disable-next-line max-line-length + // keccak256(abi.encode(uint256(keccak256("story-protocol-periphery.OwnableERC20")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant OwnableERC20StorageLocation = 0xc4b74d5382372ff8ada6effed0295109822b72fe030fc4cd981ca0e25adfab00; + + /// @notice The upgradable beacon of this contract address public immutable UPGRADABLE_BEACON; constructor(address _upgradableBeacon) { @@ -19,12 +33,17 @@ contract OwnableERC20 is IOwnableERC20, ERC20CappedUpgradeable, OwnableUpgradeab /// @notice Initializes the token /// @param initData The initialization data - function initialize(bytes memory initData) external virtual initializer { + function initialize(address ipId, bytes memory initData) external virtual initializer { + if (ipId == address(0)) revert Errors.OwnableERC20__ZeroIpId(); + InitData memory initData = abi.decode(initData, (InitData)); __ERC20Capped_init(initData.cap); __ERC20_init(initData.name, initData.symbol); __Ownable_init(initData.initialOwner); + + OwnableERC20Storage storage $ = _getOwnableERC20Storage(); + $.ipId = ipId; } /// @notice Mints tokens to the specified address. @@ -39,9 +58,22 @@ contract OwnableERC20 is IOwnableERC20, ERC20CappedUpgradeable, OwnableUpgradeab return UPGRADABLE_BEACON; } + /// @notice Returns the ip id to whom this fractionalized token belongs to + function ipId() external view returns (address) { + return _getOwnableERC20Storage().ipId; + } + /// @notice Returns whether the contract supports an interface /// @param interfaceId The interface ID function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { - return interfaceId == type(IOwnableERC20).interfaceId; + return interfaceId == type(IOwnableERC20).interfaceId || interfaceId == type(IERC165).interfaceId; } + + /// @dev Returns the storage struct of OwnableERC20. + function _getOwnableERC20Storage() private pure returns (OwnableERC20Storage storage $) { + assembly { + $.slot := OwnableERC20StorageLocation + } + } + } diff --git a/contracts/modules/tokenizer/TokenizerModule.sol b/contracts/modules/tokenizer/TokenizerModule.sol index 29569d0..49824df 100644 --- a/contracts/modules/tokenizer/TokenizerModule.sol +++ b/contracts/modules/tokenizer/TokenizerModule.sol @@ -70,6 +70,17 @@ contract TokenizerModule is LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry); DISPUTE_MODULE = IDisputeModule(disputeModule); + _disableInitializers(); + } + + /// @notice Initializes the TokenizerModule + /// @param protocolAccessManager The address of the protocol access manager + function initialize(address protocolAccessManager) external initializer { + if (protocolAccessManager == address(0)) revert Errors.TokenizerModule__ZeroProtocolAccessManager(); + + __ReentrancyGuard_init(); + __UUPSUpgradeable_init(); + __ProtocolPausable_init(protocolAccessManager); } /// @notice Whitelists a token template @@ -78,7 +89,7 @@ contract TokenizerModule is function whitelistTokenTemplate(address tokenTemplate, bool allowed) external restricted { if (tokenTemplate == address(0)) revert Errors.TokenizerModule__ZeroTokenTemplate(); if (!tokenTemplate.supportsInterface(type(IOwnableERC20).interfaceId)) - revert Errors.TokenizerModule__UnsupportedERC20(tokenTemplate); + revert Errors.TokenizerModule__UnsupportedOwnableERC20(tokenTemplate); TokenizerModuleStorage storage $ = _getTokenizerModuleStorage(); $.isWhitelistedTokenTemplate[tokenTemplate] = allowed; @@ -97,7 +108,6 @@ contract TokenizerModule is bytes calldata initData ) external verifyPermission(ipId) nonReentrant returns (address token) { if (DISPUTE_MODULE.isIpTagged(ipId)) revert Errors.TokenizerModule__DisputedIpId(ipId); - if (!IP_ASSET_REGISTRY.isRegistered(ipId)) revert Errors.TokenizerModule__IpNotRegistered(ipId); if (LICENSE_REGISTRY.isExpiredNow(ipId)) revert Errors.TokenizerModule__IpExpired(ipId); TokenizerModuleStorage storage $ = _getTokenizerModuleStorage(); @@ -109,7 +119,7 @@ contract TokenizerModule is token = address( new BeaconProxy( IOwnableERC20(tokenTemplate).upgradableBeacon(), - abi.encodeWithSelector(IOwnableERC20.initialize.selector, initData) + abi.encodeWithSelector(IOwnableERC20.initialize.selector, ipId, initData) ) ); @@ -122,16 +132,14 @@ contract TokenizerModule is /// @param ipId The address of the IP /// @return token The address of the token (0 address if IP has not been tokenized) function getFractionalizedToken(address ipId) external view returns (address token) { - TokenizerModuleStorage storage $ = _getTokenizerModuleStorage(); - return $.fractionalizedTokens[ipId]; + return _getTokenizerModuleStorage().fractionalizedTokens[ipId]; } /// @notice Checks if a token template is whitelisted /// @param tokenTemplate The address of the token template /// @return allowed The whitelisting status (true if whitelisted, false if not) function isWhitelistedTokenTemplate(address tokenTemplate) external view returns (bool allowed) { - TokenizerModuleStorage storage $ = _getTokenizerModuleStorage(); - return $.isWhitelistedTokenTemplate[tokenTemplate]; + return _getTokenizerModuleStorage().isWhitelistedTokenTemplate[tokenTemplate]; } /// @dev Returns the name of the module diff --git a/script/utils/DeployHelper.sol b/script/utils/DeployHelper.sol index b12e163..6f2f3bf 100644 --- a/script/utils/DeployHelper.sol +++ b/script/utils/DeployHelper.sol @@ -43,6 +43,8 @@ import { RoyaltyWorkflows } from "../../contracts/workflows/RoyaltyWorkflows.sol import { RoyaltyTokenDistributionWorkflows } from "../../contracts/workflows/RoyaltyTokenDistributionWorkflows.sol"; import { StoryBadgeNFT } from "../../contracts/story-nft/StoryBadgeNFT.sol"; import { OrgStoryNFTFactory } from "../../contracts/story-nft/OrgStoryNFTFactory.sol"; +import { OwnableERC20 } from "../../contracts/modules/tokenizer/OwnableERC20.sol"; +import { TokenizerModule } from "../../contracts/modules/tokenizer/TokenizerModule.sol"; // script import { BroadcastManager } from "./BroadcastManager.s.sol"; @@ -94,6 +96,11 @@ contract DeployHelper is address internal defaultOrgStoryNftTemplate; address internal defaultOrgStoryNftBeacon; + // Tokenizer Module + TokenizerModule internal tokenizerModule; + address internal ownableERC20Template; + address internal ownableERC20Beacon; + // DeployHelper variable bool internal writeDeploys; @@ -152,14 +159,14 @@ contract DeployHelper is deployer = mockDeployer; _deployMockCoreContracts(); _configureMockCoreContracts(); - _deployWorkflowContracts(); - _configureWorkflowContracts(); + _deployPeripheryContracts(); + _configurePeripheryContracts(); } else { // production deployment _readStoryProtocolCoreAddresses(); // StoryProtocolCoreAddressManager.s.sol _beginBroadcast(); // BroadcastManager.s.sol - _deployWorkflowContracts(); - _configureWorkflowContracts(); + _deployPeripheryContracts(); + _configurePeripheryContracts(); // Check deployment configuration. if (spgNftBeacon.owner() != address(registrationWorkflows)) @@ -293,7 +300,7 @@ contract DeployHelper is } } - function _deployWorkflowContracts() private { + function _deployPeripheryContracts() private { address impl = address(0); // Periphery workflow contracts @@ -452,12 +459,53 @@ contract DeployHelper is ) ); _postdeploy("SPGNFTBeacon", address(spgNftBeacon)); + + // Tokenizer Module + _predeploy("TokenizerModule"); + impl = address(new TokenizerModule(address(accessController), address(ipAssetRegistry), address(licenseRegistry), address(disputeModule))); + tokenizerModule = TokenizerModule( + TestProxyHelper.deployUUPSProxy( + create3Deployer, + _getSalt(type(TokenizerModule).name), + impl, + abi.encodeCall(TokenizerModule.initialize, address(protocolAccessManager)) + ) + ); + impl = address(0); + _postdeploy("TokenizerModule", address(tokenizerModule)); + + // OwnableERC20 template + _predeploy("OwnableERC20Template"); + ownableERC20Template = address(new OwnableERC20( + _getDeployedAddress("OwnableERC20Beacon") + )); + _postdeploy("OwnableERC20Template", ownableERC20Template); + + // Upgradeable Beacon for OwnableERC20Template + _predeploy("OwnableERC20Beacon"); + ownableERC20Beacon = address(UpgradeableBeacon( + create3Deployer.deploy( + _getSalt("OwnableERC20Beacon"), + abi.encodePacked(type(UpgradeableBeacon).creationCode, abi.encode(ownableERC20Template, deployer)) + ) + )); + _postdeploy("OwnableERC20Beacon", address(ownableERC20Beacon)); + + require( + UpgradeableBeacon(ownableERC20Beacon).implementation() == address(ownableERC20Template), + "DeployHelper: Invalid beacon implementation" + ); + require( + OwnableERC20(ownableERC20Template).upgradableBeacon() == address(ownableERC20Beacon), + "DeployHelper: Invalid beacon address in template" + ); } - function _configureWorkflowContracts() private { + function _configurePeripheryContracts() private { // Transfer ownership of beacon proxy to RegistrationWorkflows spgNftBeacon.transferOwnership(address(registrationWorkflows)); - + tokenizerModule.whitelistTokenTemplate(address(ownableERC20Template), true); + moduleRegistry.registerModule("TOKENIZER_MODULE", address(tokenizerModule)); // more configurations may be added here } @@ -894,6 +942,8 @@ contract DeployHelper is // add evenSplitGroupPool to whitelist of group pools groupingModule.whitelistGroupRewardPool(address(evenSplitGroupPool), true); + + disputeModule.whitelistDisputeTag("PLAGIARISM", true); } /// @dev get the salt for the contract deployment with CREATE3 diff --git a/test/modules/tokenizer/OwnableERC20.t.sol b/test/modules/tokenizer/OwnableERC20.t.sol new file mode 100644 index 0000000..65ad609 --- /dev/null +++ b/test/modules/tokenizer/OwnableERC20.t.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +// solhint-disable-next-line max-line-length +import { ERC20CappedUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20CappedUpgradeable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; + +import { IOwnableERC20 } from "../../../contracts/interfaces/modules/tokenizer/IOwnableERC20.sol"; +import { OwnableERC20 } from "../../../contracts/modules/tokenizer/OwnableERC20.sol"; + +import { BaseTest } from "../../utils/BaseTest.t.sol"; + +contract OwnableERC20Test is BaseTest { + OwnableERC20 internal testOwnableERC20; + + function setUp() public override { + super.setUp(); + + testOwnableERC20 = OwnableERC20( + address( + new BeaconProxy( + address(ownableERC20Beacon), + abi.encodeWithSelector( + IOwnableERC20.initialize.selector, + address(0x111), + abi.encode(IOwnableERC20.InitData({ + cap: 1000, + name: "Test", + symbol: "TEST", + initialOwner: u.admin + }) + ) + ) + ) + ) + ); + } + + function test_OwnableERC20_initialize() public { + assertEq(testOwnableERC20.name(), "Test"); + assertEq(testOwnableERC20.symbol(), "TEST"); + assertEq(testOwnableERC20.owner(), u.admin); + assertEq(testOwnableERC20.cap(), 1000); + assertEq(testOwnableERC20.ipId(), address(0x111)); + } + + function test_OwnableERC20_mint() public { + vm.startPrank(u.admin); + testOwnableERC20.mint(u.admin, 100); + testOwnableERC20.mint(u.alice, 100); + testOwnableERC20.mint(u.bob, 100); + vm.stopPrank(); + + assertEq(testOwnableERC20.totalSupply(), 300); + assertEq(testOwnableERC20.balanceOf(u.admin), 100); + assertEq(testOwnableERC20.balanceOf(u.alice), 100); + assertEq(testOwnableERC20.balanceOf(u.bob), 100); + } + + function test_OwnableERC20_mint_revert_NotOwner() public { + vm.startPrank(u.alice); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, u.alice)); + testOwnableERC20.mint(u.alice, 100); + vm.stopPrank(); + + vm.startPrank(u.bob); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, u.bob)); + testOwnableERC20.mint(u.bob, 100); + vm.stopPrank(); + } + + function test_OwnableERC20_revert_ERC20ExceededCap() public { + vm.startPrank(u.admin); + vm.expectRevert(abi.encodeWithSelector(ERC20CappedUpgradeable.ERC20ExceededCap.selector, 1001, 1000)); + testOwnableERC20.mint(u.admin, 1001); + vm.stopPrank(); + + vm.startPrank(u.admin); + testOwnableERC20.mint(u.alice, 500); + testOwnableERC20.mint(u.bob, 500); + vm.stopPrank(); + + vm.startPrank(u.admin); + vm.expectRevert(abi.encodeWithSelector(ERC20CappedUpgradeable.ERC20ExceededCap.selector, 2000, 1000)); + testOwnableERC20.mint(u.admin, 1000); + vm.stopPrank(); + } +} diff --git a/test/modules/tokenizer/TokenizerModule.t.sol b/test/modules/tokenizer/TokenizerModule.t.sol new file mode 100644 index 0000000..439be38 --- /dev/null +++ b/test/modules/tokenizer/TokenizerModule.t.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { Licensing } from "@storyprotocol/core/lib/Licensing.sol"; +import { Errors as CoreErrors } from "@storyprotocol/core/lib/Errors.sol"; +import { IIPAccount } from "@storyprotocol/core/interfaces/IIPAccount.sol"; +import { IPAccountStorageOps } from "@storyprotocol/core/lib/IPAccountStorageOps.sol"; +import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol"; + +import { Errors } from "../../../contracts/lib/Errors.sol"; +import { WorkflowStructs } from "../../../contracts/lib/WorkflowStructs.sol"; +import { OwnableERC20 } from "../../../contracts/modules/tokenizer/OwnableERC20.sol"; +import { IOwnableERC20 } from "../../../contracts/interfaces/modules/tokenizer/IOwnableERC20.sol"; +import { ITokenizerModule } from "../../../contracts/interfaces/modules/tokenizer/ITokenizerModule.sol"; + +import { BaseTest } from "../../utils/BaseTest.t.sol"; + +contract TokenizerModuleTest is BaseTest { + using IPAccountStorageOps for IIPAccount; + + function setUp() public override { + super.setUp(); + } + + function test_TokenizerModule_whitelistTokenTemplate() public { + address tokenTemplate1 = address(new OwnableERC20(address(ownableERC20Beacon))); + address tokenTemplate2 = address(new OwnableERC20(address(ownableERC20Beacon))); + address tokenTemplate3 = address(new OwnableERC20(address(ownableERC20Beacon))); + + vm.startPrank(u.admin); + vm.expectEmit(true, true, true, true); + emit ITokenizerModule.TokenTemplateWhitelisted(tokenTemplate1, true); + tokenizerModule.whitelistTokenTemplate(tokenTemplate1, true); + vm.expectEmit(true, true, true, true); + emit ITokenizerModule.TokenTemplateWhitelisted(tokenTemplate2, true); + tokenizerModule.whitelistTokenTemplate(tokenTemplate2, true); + vm.expectEmit(true, true, true, true); + emit ITokenizerModule.TokenTemplateWhitelisted(tokenTemplate3, true); + tokenizerModule.whitelistTokenTemplate(tokenTemplate3, true); + vm.stopPrank(); + + assertTrue(tokenizerModule.isWhitelistedTokenTemplate(tokenTemplate1)); + assertTrue(tokenizerModule.isWhitelistedTokenTemplate(tokenTemplate2)); + assertTrue(tokenizerModule.isWhitelistedTokenTemplate(tokenTemplate3)); + } + + function test_TokenizerModule_revert_whitelistTokenTemplate_UnsupportedERC20() public { + vm.startPrank(u.admin); + vm.expectRevert(abi.encodeWithSelector(Errors.TokenizerModule__UnsupportedOwnableERC20.selector, address(spgNftImpl))); + tokenizerModule.whitelistTokenTemplate(address(spgNftImpl), true); + vm.expectRevert(abi.encodeWithSelector(Errors.TokenizerModule__UnsupportedOwnableERC20.selector, address(mockToken))); + tokenizerModule.whitelistTokenTemplate(address(mockToken), true); + vm.expectRevert(abi.encodeWithSelector(Errors.TokenizerModule__UnsupportedOwnableERC20.selector, address(mockNft))); + tokenizerModule.whitelistTokenTemplate(address(mockNft), true); + vm.stopPrank(); + } + + function test_TokenizerModule_tokenize() public { + mockToken.mint(address(this), 3 * 10 ** mockToken.decimals()); + mockToken.approve(address(spgNftPublic), 3 * 10 ** mockToken.decimals()); + + (address ipId1, ) = registrationWorkflows.mintAndRegisterIp({ + spgNftContract: address(spgNftPublic), + recipient: u.alice, + ipMetadata: ipMetadataDefault, + allowDuplicates: true + }); + + (address ipId2, ) = registrationWorkflows.mintAndRegisterIp({ + spgNftContract: address(spgNftPublic), + recipient: u.bob, + ipMetadata: ipMetadataEmpty, + allowDuplicates: true + }); + + (address ipId3, ) = registrationWorkflows.mintAndRegisterIp({ + spgNftContract: address(spgNftPublic), + recipient: u.carl, + ipMetadata: ipMetadataDefault, + allowDuplicates: true + }); + + + vm.prank(u.alice); + vm.expectEmit(true, false, false, false); + emit ITokenizerModule.IPTokenized(ipId1, address(0)); + OwnableERC20 token1 = OwnableERC20(tokenizerModule.tokenize(ipId1, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ + cap: 1000, + name: "Test1", + symbol: "T1", + initialOwner: u.alice + })))); + + vm.prank(u.bob); + vm.expectEmit(true, false, false, false); + emit ITokenizerModule.IPTokenized(ipId2, address(0)); + OwnableERC20 token2 = OwnableERC20(tokenizerModule.tokenize(ipId2, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ + cap: 1000000, + name: "Test2", + symbol: "T2", + initialOwner: u.bob + })))); + + vm.prank(u.carl); + vm.expectEmit(true, false, false, false); + emit ITokenizerModule.IPTokenized(ipId3, address(0)); + OwnableERC20 token3 = OwnableERC20(tokenizerModule.tokenize(ipId3, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ + cap: 99999, + name: "Test3", + symbol: "T3", + initialOwner: u.carl + })))); + + assertEq(tokenizerModule.getFractionalizedToken(ipId1), address(token1)); + assertEq(tokenizerModule.getFractionalizedToken(ipId2), address(token2)); + assertEq(tokenizerModule.getFractionalizedToken(ipId3), address(token3)); + + assertEq(token1.name(), "Test1"); + assertEq(token1.symbol(), "T1"); + assertEq(token1.cap(), 1000); + assertEq(token1.ipId(), ipId1); + assertEq(token1.owner(), u.alice); + + assertEq(token2.name(), "Test2"); + assertEq(token2.symbol(), "T2"); + assertEq(token2.cap(), 1000000); + assertEq(token2.ipId(), ipId2); + assertEq(token2.owner(), u.bob); + + assertEq(token3.name(), "Test3"); + assertEq(token3.symbol(), "T3"); + assertEq(token3.cap(), 99999); + assertEq(token3.ipId(), ipId3); + assertEq(token3.owner(), u.carl); + } + + function test_TokenizerModule_revert_tokenize_DisputedIpId() public { + mockToken.mint(address(this), 1 * 10 ** mockToken.decimals()); + mockToken.approve(address(spgNftPublic), 1 * 10 ** mockToken.decimals()); + (address ipId, ) = registrationWorkflows.mintAndRegisterIp({ + spgNftContract: address(spgNftPublic), + recipient: u.alice, + ipMetadata: ipMetadataDefault, + allowDuplicates: true + }); + + uint256 disputeId = disputeModule.raiseDispute({ + targetIpId: ipId, + disputeEvidenceHash: bytes32(0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef), + targetTag: "PLAGIARISM", + data: "" + }); + + vm.prank(u.admin); + disputeModule.setDisputeJudgement(disputeId, true, ""); + + vm.prank(u.alice); + vm.expectRevert(abi.encodeWithSelector(Errors.TokenizerModule__DisputedIpId.selector, ipId)); + tokenizerModule.tokenize(ipId, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ + cap: 1000, + name: "Test1", + symbol: "T1", + initialOwner: u.alice + }))); + } + + function test_TokenizerModule_revert_tokenize_IpNotRegistered() public { + address ipId = ipAssetRegistry.ipId(block.chainid, address(spgNftPublic), 1); + vm.prank(u.alice); + vm.expectRevert(abi.encodeWithSelector(CoreErrors.AccessControlled__NotIpAccount.selector, ipId)); + tokenizerModule.tokenize(ipId, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ + cap: 1000, + name: "Test1", + symbol: "T1", + initialOwner: u.alice + }))); + } + + function test_TokenizerModule_revert_tokenize_callerNotIpOwner() public { + mockToken.mint(address(this), 1 * 10 ** mockToken.decimals()); + mockToken.approve(address(spgNftPublic), 1 * 10 ** mockToken.decimals()); + (address ipId, ) = registrationWorkflows.mintAndRegisterIp({ + spgNftContract: address(spgNftPublic), + recipient: u.alice, + ipMetadata: ipMetadataDefault, + allowDuplicates: true + }); + + vm.prank(u.bob); + vm.expectRevert(abi.encodeWithSelector( + CoreErrors.AccessController__PermissionDenied.selector, + ipId, + u.bob, + address(tokenizerModule), + tokenizerModule.tokenize.selector + )); + tokenizerModule.tokenize(ipId, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ + cap: 1000, + name: "Test1", + symbol: "T1", + initialOwner: u.bob + }))); + } + + function test_TokenizerModule_revert_tokenize_IpExpired() public { + WorkflowStructs.LicenseTermsData[] memory termsData = new WorkflowStructs.LicenseTermsData[](1); + termsData[0] = WorkflowStructs.LicenseTermsData({ + terms: PILTerms({ + transferable: true, + royaltyPolicy: address(royaltyPolicyLAP), + defaultMintingFee: 0, + expiration: 10 days, + commercialUse: true, + commercialAttribution: true, + commercializerChecker: address(0), + commercializerCheckerData: "", + commercialRevShare: 0, + commercialRevCeiling: 0, + derivativesAllowed: true, + derivativesAttribution: true, + derivativesApproval: false, + derivativesReciprocal: true, + derivativeRevCeiling: 0, + currency: address(mockToken), + uri: "" + }), + licensingConfig: Licensing.LicensingConfig({ + isSet: false, + mintingFee: 0, + licensingHook: address(0), + hookData: "", + commercialRevShare: 0, + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: address(0) + }) + }); + + mockToken.mint(address(this), 2 * 10 ** mockToken.decimals()); + mockToken.approve(address(spgNftPublic), 2 * 10 ** mockToken.decimals()); + (address ipId1, , uint256[] memory licenseIds) = licenseAttachmentWorkflows.mintAndRegisterIpAndAttachPILTerms({ + spgNftContract: address(spgNftPublic), + recipient: u.alice, + ipMetadata: ipMetadataDefault, + licenseTermsData: termsData, + allowDuplicates: true + }); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipId1; + (address ipId2, ) = derivativeWorkflows.mintAndRegisterIpAndMakeDerivative({ + spgNftContract: address(spgNftPublic), + derivData: WorkflowStructs.MakeDerivative({ + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseIds, + royaltyContext: "", + maxMintingFee: 0, + maxRts: 0 + }), + recipient: u.alice, + ipMetadata: ipMetadataDefault, + allowDuplicates: true + }); + + vm.warp(11 days); + vm.prank(u.alice); + vm.expectRevert(abi.encodeWithSelector(Errors.TokenizerModule__IpExpired.selector, ipId2)); + tokenizerModule.tokenize(ipId2, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ + cap: 1000, + name: "Test1", + symbol: "T1", + initialOwner: u.alice + }))); + } + + function test_TokenizerModule_revert_tokenize_IpAlreadyTokenized() public { + mockToken.mint(address(this), 1 * 10 ** mockToken.decimals()); + mockToken.approve(address(spgNftPublic), 1 * 10 ** mockToken.decimals()); + (address ipId, ) = registrationWorkflows.mintAndRegisterIp({ + spgNftContract: address(spgNftPublic), + recipient: u.alice, + ipMetadata: ipMetadataDefault, + allowDuplicates: true + }); + + vm.prank(u.alice); + address token = tokenizerModule.tokenize(ipId, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ + cap: 1000, + name: "Test1", + symbol: "T1", + initialOwner: u.alice + }))); + + vm.prank(u.alice); + vm.expectRevert(abi.encodeWithSelector(Errors.TokenizerModule__IpAlreadyTokenized.selector, ipId, token)); + tokenizerModule.tokenize(ipId, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ + cap: 1000, + name: "Test1", + symbol: "T1", + initialOwner: u.alice + }))); + } +} diff --git a/test/utils/BaseTest.t.sol b/test/utils/BaseTest.t.sol index 793ffc5..4608ab3 100644 --- a/test/utils/BaseTest.t.sol +++ b/test/utils/BaseTest.t.sol @@ -11,6 +11,7 @@ import { ICoreMetadataModule } from "@storyprotocol/core/interfaces/modules/meta import { IIPAccount } from "@storyprotocol/core/interfaces/IIPAccount.sol"; import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol"; import { MetaTx } from "@storyprotocol/core/lib/MetaTx.sol"; +import { MockArbitrationPolicy } from "@storyprotocol/test/mocks/dispute/MockArbitrationPolicy.sol"; import { MockIPGraph } from "@storyprotocol/test/mocks/MockIPGraph.sol"; // contracts @@ -120,6 +121,7 @@ contract BaseTest is Test, DeployHelper { licenseAttachmentWorkflows.setNftContractBeacon(address(spgNftBeacon)); registrationWorkflows.setNftContractBeacon(address(spgNftBeacon)); royaltyTokenDistributionWorkflows.setNftContractBeacon(address(spgNftBeacon)); + vm.stopPrank(); } @@ -168,9 +170,18 @@ contract BaseTest is Test, DeployHelper { ); vm.stopPrank(); + vm.startPrank(u.admin); // whitelist mockToken as a royalty token - vm.prank(u.admin); royaltyModule.whitelistRoyaltyToken(address(mockToken), true); + + // whitelist mockArbitrationPolicy as an arbitration policy + address mockArbitrationPolicy = address( + new MockArbitrationPolicy(address(disputeModule), address(mockToken), 0) + ); + disputeModule.whitelistArbitrationPolicy(mockArbitrationPolicy, true); + disputeModule.setBaseArbitrationPolicy(mockArbitrationPolicy); + disputeModule.setArbitrationRelayer(mockArbitrationPolicy, address(u.admin)); + vm.stopPrank(); } function _setupIPMetadata() internal { From 4a1434c452367afc21726af44acc017f566dbdac Mon Sep 17 00:00:00 2001 From: Sebastian Liu Date: Sun, 15 Dec 2024 19:07:45 -0800 Subject: [PATCH 5/6] chore: linting --- contracts/modules/tokenizer/OwnableERC20.sol | 4 +- test/modules/tokenizer/OwnableERC20.t.sol | 8 +- test/modules/tokenizer/TokenizerModule.t.sol | 132 ++++++++++--------- 3 files changed, 72 insertions(+), 72 deletions(-) diff --git a/contracts/modules/tokenizer/OwnableERC20.sol b/contracts/modules/tokenizer/OwnableERC20.sol index f3ca3b1..009c657 100644 --- a/contracts/modules/tokenizer/OwnableERC20.sol +++ b/contracts/modules/tokenizer/OwnableERC20.sol @@ -21,7 +21,8 @@ contract OwnableERC20 is IOwnableERC20, ERC20CappedUpgradeable, OwnableUpgradeab // solhint-disable-next-line max-line-length // keccak256(abi.encode(uint256(keccak256("story-protocol-periphery.OwnableERC20")) - 1)) & ~bytes32(uint256(0xff)); - bytes32 private constant OwnableERC20StorageLocation = 0xc4b74d5382372ff8ada6effed0295109822b72fe030fc4cd981ca0e25adfab00; + bytes32 private constant OwnableERC20StorageLocation = + 0xc4b74d5382372ff8ada6effed0295109822b72fe030fc4cd981ca0e25adfab00; /// @notice The upgradable beacon of this contract address public immutable UPGRADABLE_BEACON; @@ -75,5 +76,4 @@ contract OwnableERC20 is IOwnableERC20, ERC20CappedUpgradeable, OwnableUpgradeab $.slot := OwnableERC20StorageLocation } } - } diff --git a/test/modules/tokenizer/OwnableERC20.t.sol b/test/modules/tokenizer/OwnableERC20.t.sol index 65ad609..ed9a147 100644 --- a/test/modules/tokenizer/OwnableERC20.t.sol +++ b/test/modules/tokenizer/OwnableERC20.t.sol @@ -24,12 +24,8 @@ contract OwnableERC20Test is BaseTest { abi.encodeWithSelector( IOwnableERC20.initialize.selector, address(0x111), - abi.encode(IOwnableERC20.InitData({ - cap: 1000, - name: "Test", - symbol: "TEST", - initialOwner: u.admin - }) + abi.encode( + IOwnableERC20.InitData({ cap: 1000, name: "Test", symbol: "TEST", initialOwner: u.admin }) ) ) ) diff --git a/test/modules/tokenizer/TokenizerModule.t.sol b/test/modules/tokenizer/TokenizerModule.t.sol index 439be38..664448e 100644 --- a/test/modules/tokenizer/TokenizerModule.t.sol +++ b/test/modules/tokenizer/TokenizerModule.t.sol @@ -46,11 +46,17 @@ contract TokenizerModuleTest is BaseTest { function test_TokenizerModule_revert_whitelistTokenTemplate_UnsupportedERC20() public { vm.startPrank(u.admin); - vm.expectRevert(abi.encodeWithSelector(Errors.TokenizerModule__UnsupportedOwnableERC20.selector, address(spgNftImpl))); + vm.expectRevert( + abi.encodeWithSelector(Errors.TokenizerModule__UnsupportedOwnableERC20.selector, address(spgNftImpl)) + ); tokenizerModule.whitelistTokenTemplate(address(spgNftImpl), true); - vm.expectRevert(abi.encodeWithSelector(Errors.TokenizerModule__UnsupportedOwnableERC20.selector, address(mockToken))); + vm.expectRevert( + abi.encodeWithSelector(Errors.TokenizerModule__UnsupportedOwnableERC20.selector, address(mockToken)) + ); tokenizerModule.whitelistTokenTemplate(address(mockToken), true); - vm.expectRevert(abi.encodeWithSelector(Errors.TokenizerModule__UnsupportedOwnableERC20.selector, address(mockNft))); + vm.expectRevert( + abi.encodeWithSelector(Errors.TokenizerModule__UnsupportedOwnableERC20.selector, address(mockNft)) + ); tokenizerModule.whitelistTokenTemplate(address(mockNft), true); vm.stopPrank(); } @@ -80,36 +86,38 @@ contract TokenizerModuleTest is BaseTest { allowDuplicates: true }); - vm.prank(u.alice); vm.expectEmit(true, false, false, false); emit ITokenizerModule.IPTokenized(ipId1, address(0)); - OwnableERC20 token1 = OwnableERC20(tokenizerModule.tokenize(ipId1, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ - cap: 1000, - name: "Test1", - symbol: "T1", - initialOwner: u.alice - })))); + OwnableERC20 token1 = OwnableERC20( + tokenizerModule.tokenize( + ipId1, + address(ownableERC20Template), + abi.encode(IOwnableERC20.InitData({ cap: 1000, name: "Test1", symbol: "T1", initialOwner: u.alice })) + ) + ); vm.prank(u.bob); vm.expectEmit(true, false, false, false); emit ITokenizerModule.IPTokenized(ipId2, address(0)); - OwnableERC20 token2 = OwnableERC20(tokenizerModule.tokenize(ipId2, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ - cap: 1000000, - name: "Test2", - symbol: "T2", - initialOwner: u.bob - })))); + OwnableERC20 token2 = OwnableERC20( + tokenizerModule.tokenize( + ipId2, + address(ownableERC20Template), + abi.encode(IOwnableERC20.InitData({ cap: 1000000, name: "Test2", symbol: "T2", initialOwner: u.bob })) + ) + ); vm.prank(u.carl); vm.expectEmit(true, false, false, false); emit ITokenizerModule.IPTokenized(ipId3, address(0)); - OwnableERC20 token3 = OwnableERC20(tokenizerModule.tokenize(ipId3, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ - cap: 99999, - name: "Test3", - symbol: "T3", - initialOwner: u.carl - })))); + OwnableERC20 token3 = OwnableERC20( + tokenizerModule.tokenize( + ipId3, + address(ownableERC20Template), + abi.encode(IOwnableERC20.InitData({ cap: 99999, name: "Test3", symbol: "T3", initialOwner: u.carl })) + ) + ); assertEq(tokenizerModule.getFractionalizedToken(ipId1), address(token1)); assertEq(tokenizerModule.getFractionalizedToken(ipId2), address(token2)); @@ -156,24 +164,22 @@ contract TokenizerModuleTest is BaseTest { vm.prank(u.alice); vm.expectRevert(abi.encodeWithSelector(Errors.TokenizerModule__DisputedIpId.selector, ipId)); - tokenizerModule.tokenize(ipId, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ - cap: 1000, - name: "Test1", - symbol: "T1", - initialOwner: u.alice - }))); + tokenizerModule.tokenize( + ipId, + address(ownableERC20Template), + abi.encode(IOwnableERC20.InitData({ cap: 1000, name: "Test1", symbol: "T1", initialOwner: u.alice })) + ); } function test_TokenizerModule_revert_tokenize_IpNotRegistered() public { address ipId = ipAssetRegistry.ipId(block.chainid, address(spgNftPublic), 1); vm.prank(u.alice); vm.expectRevert(abi.encodeWithSelector(CoreErrors.AccessControlled__NotIpAccount.selector, ipId)); - tokenizerModule.tokenize(ipId, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ - cap: 1000, - name: "Test1", - symbol: "T1", - initialOwner: u.alice - }))); + tokenizerModule.tokenize( + ipId, + address(ownableERC20Template), + abi.encode(IOwnableERC20.InitData({ cap: 1000, name: "Test1", symbol: "T1", initialOwner: u.alice })) + ); } function test_TokenizerModule_revert_tokenize_callerNotIpOwner() public { @@ -187,19 +193,20 @@ contract TokenizerModuleTest is BaseTest { }); vm.prank(u.bob); - vm.expectRevert(abi.encodeWithSelector( - CoreErrors.AccessController__PermissionDenied.selector, + vm.expectRevert( + abi.encodeWithSelector( + CoreErrors.AccessController__PermissionDenied.selector, + ipId, + u.bob, + address(tokenizerModule), + tokenizerModule.tokenize.selector + ) + ); + tokenizerModule.tokenize( ipId, - u.bob, - address(tokenizerModule), - tokenizerModule.tokenize.selector - )); - tokenizerModule.tokenize(ipId, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ - cap: 1000, - name: "Test1", - symbol: "T1", - initialOwner: u.bob - }))); + address(ownableERC20Template), + abi.encode(IOwnableERC20.InitData({ cap: 1000, name: "Test1", symbol: "T1", initialOwner: u.bob })) + ); } function test_TokenizerModule_revert_tokenize_IpExpired() public { @@ -266,12 +273,11 @@ contract TokenizerModuleTest is BaseTest { vm.warp(11 days); vm.prank(u.alice); vm.expectRevert(abi.encodeWithSelector(Errors.TokenizerModule__IpExpired.selector, ipId2)); - tokenizerModule.tokenize(ipId2, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ - cap: 1000, - name: "Test1", - symbol: "T1", - initialOwner: u.alice - }))); + tokenizerModule.tokenize( + ipId2, + address(ownableERC20Template), + abi.encode(IOwnableERC20.InitData({ cap: 1000, name: "Test1", symbol: "T1", initialOwner: u.alice })) + ); } function test_TokenizerModule_revert_tokenize_IpAlreadyTokenized() public { @@ -285,20 +291,18 @@ contract TokenizerModuleTest is BaseTest { }); vm.prank(u.alice); - address token = tokenizerModule.tokenize(ipId, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ - cap: 1000, - name: "Test1", - symbol: "T1", - initialOwner: u.alice - }))); + address token = tokenizerModule.tokenize( + ipId, + address(ownableERC20Template), + abi.encode(IOwnableERC20.InitData({ cap: 1000, name: "Test1", symbol: "T1", initialOwner: u.alice })) + ); vm.prank(u.alice); vm.expectRevert(abi.encodeWithSelector(Errors.TokenizerModule__IpAlreadyTokenized.selector, ipId, token)); - tokenizerModule.tokenize(ipId, address(ownableERC20Template), abi.encode(IOwnableERC20.InitData({ - cap: 1000, - name: "Test1", - symbol: "T1", - initialOwner: u.alice - }))); + tokenizerModule.tokenize( + ipId, + address(ownableERC20Template), + abi.encode(IOwnableERC20.InitData({ cap: 1000, name: "Test1", symbol: "T1", initialOwner: u.alice })) + ); } } From f90eb3335d7e1c5e9138b4fd4e0b7ffe1786fc18 Mon Sep 17 00:00:00 2001 From: Sebastian Liu Date: Sun, 15 Dec 2024 21:56:25 -0800 Subject: [PATCH 6/6] fix: add BUSL license & move whitelist dispute flag --- contracts/interfaces/modules/tokenizer/IOwnableERC20.sol | 2 +- contracts/interfaces/modules/tokenizer/ITokenizerModule.sol | 2 +- contracts/modules/tokenizer/OwnableERC20.sol | 2 +- contracts/modules/tokenizer/TokenizerModule.sol | 2 +- script/utils/DeployHelper.sol | 2 -- test/modules/tokenizer/OwnableERC20.t.sol | 2 +- test/modules/tokenizer/TokenizerModule.t.sol | 5 ++++- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol b/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol index 0d98150..01a8b84 100644 --- a/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol +++ b/contracts/interfaces/modules/tokenizer/IOwnableERC20.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/interfaces/modules/tokenizer/ITokenizerModule.sol b/contracts/interfaces/modules/tokenizer/ITokenizerModule.sol index 0227035..6c0a60d 100644 --- a/contracts/interfaces/modules/tokenizer/ITokenizerModule.sol +++ b/contracts/interfaces/modules/tokenizer/ITokenizerModule.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { IModule } from "@storyprotocol/core/interfaces/modules/base/IModule.sol"; diff --git a/contracts/modules/tokenizer/OwnableERC20.sol b/contracts/modules/tokenizer/OwnableERC20.sol index 009c657..73942e4 100644 --- a/contracts/modules/tokenizer/OwnableERC20.sol +++ b/contracts/modules/tokenizer/OwnableERC20.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; diff --git a/contracts/modules/tokenizer/TokenizerModule.sol b/contracts/modules/tokenizer/TokenizerModule.sol index 49824df..58acef6 100644 --- a/contracts/modules/tokenizer/TokenizerModule.sol +++ b/contracts/modules/tokenizer/TokenizerModule.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; diff --git a/script/utils/DeployHelper.sol b/script/utils/DeployHelper.sol index 6f2f3bf..5f24170 100644 --- a/script/utils/DeployHelper.sol +++ b/script/utils/DeployHelper.sol @@ -942,8 +942,6 @@ contract DeployHelper is // add evenSplitGroupPool to whitelist of group pools groupingModule.whitelistGroupRewardPool(address(evenSplitGroupPool), true); - - disputeModule.whitelistDisputeTag("PLAGIARISM", true); } /// @dev get the salt for the contract deployment with CREATE3 diff --git a/test/modules/tokenizer/OwnableERC20.t.sol b/test/modules/tokenizer/OwnableERC20.t.sol index ed9a147..6784cb5 100644 --- a/test/modules/tokenizer/OwnableERC20.t.sol +++ b/test/modules/tokenizer/OwnableERC20.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; // solhint-disable-next-line max-line-length diff --git a/test/modules/tokenizer/TokenizerModule.t.sol b/test/modules/tokenizer/TokenizerModule.t.sol index 664448e..1e20fbe 100644 --- a/test/modules/tokenizer/TokenizerModule.t.sol +++ b/test/modules/tokenizer/TokenizerModule.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { Licensing } from "@storyprotocol/core/lib/Licensing.sol"; @@ -143,6 +143,9 @@ contract TokenizerModuleTest is BaseTest { } function test_TokenizerModule_revert_tokenize_DisputedIpId() public { + vm.prank(u.admin); + disputeModule.whitelistDisputeTag("PLAGIARISM", true); + mockToken.mint(address(this), 1 * 10 ** mockToken.decimals()); mockToken.approve(address(spgNftPublic), 1 * 10 ** mockToken.decimals()); (address ipId, ) = registrationWorkflows.mintAndRegisterIp({