Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add Implementation to Tokenizer Module and OwnableERC20 #144

Merged
merged 6 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions contracts/interfaces/modules/tokenizer/IOwnableERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: BUSL-1.1
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
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
/// @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(address ipId, 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 the ip id to whom this fractionalized token belongs to
function ipId() external view returns (address);
}
40 changes: 40 additions & 0 deletions contracts/interfaces/modules/tokenizer/ITokenizerModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: BUSL-1.1
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);

/// @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);
}
43 changes: 43 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,47 @@ 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);

////////////////////////////////////////////////////////////////////////////
// OwnableERC20 //
////////////////////////////////////////////////////////////////////////////

/// @notice Zero ip id provided.
error OwnableERC20__ZeroIpId();

////////////////////////////////////////////////////////////////////////////
// TokenizerModule //
////////////////////////////////////////////////////////////////////////////
/// @notice Zero license registry provided.
error TokenizerModule__ZeroLicenseRegistry();

/// @notice Zero dispute module provided.
error TokenizerModule__ZeroDisputeModule();

/// @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__UnsupportedOwnableERC20(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 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);
}
79 changes: 79 additions & 0 deletions contracts/modules/tokenizer/OwnableERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: BUSL-1.1
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 { 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) {
UPGRADABLE_BEACON = _upgradableBeacon;
_disableInitializers();
}

/// @notice Initializes the token
/// @param initData The initialization data
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.
/// @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 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 || interfaceId == type(IERC165).interfaceId;
}

/// @dev Returns the storage struct of OwnableERC20.
function _getOwnableERC20Storage() private pure returns (OwnableERC20Storage storage $) {
assembly {
$.slot := OwnableERC20StorageLocation
}
}
}
13 changes: 0 additions & 13 deletions contracts/modules/tokenizer/OwnerableERC20.sol

This file was deleted.

130 changes: 126 additions & 4 deletions contracts/modules/tokenizer/TokenizerModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,159 @@
pragma solidity 0.8.26;

import { Strings } from "@openzeppelin/contracts/utils/Strings.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 { 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";

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,
ReentrancyGuardUpgradeable,
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
/// @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
/// keccak256(abi.encode(uint256(keccak256("story-protocol-periphery.TokenizerModule")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant TokenizerModuleStorageLocation =
0xef271c298b3e9574aa43cf546463b750863573b31e3d16f477ffc6f522452800;

/// @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
IDisputeModule public immutable DISPUTE_MODULE;

/// @custom:oz-upgrades-unsafe-allow constructor
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);
_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
/// @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__UnsupportedOwnableERC20(tokenTemplate);

TokenizerModuleStorage storage $ = _getTokenizerModuleStorage();
$.isWhitelistedTokenTemplate[tokenTemplate] = allowed;

emit TokenTemplateWhitelisted(tokenTemplate, allowed);
}

function whitelistTokenTemplate(address tokenTemplate, bool allowed) external {}
/// @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
/// @return token The address of the newly created token
function tokenize(
address ipId,
address tokenTemplate,
bytes calldata initData
) external verifyPermission(ipId) nonReentrant returns (address token) {
if (DISPUTE_MODULE.isIpTagged(ipId)) revert Errors.TokenizerModule__DisputedIpId(ipId);
if (LICENSE_REGISTRY.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);

token = address(
new BeaconProxy(
IOwnableERC20(tokenTemplate).upgradableBeacon(),
abi.encodeWithSelector(IOwnableERC20.initialize.selector, ipId, initData)
)
);

function tokenize(address ipId, address tokenTemplate, bytes calldata initData) external verifyPermission(ipId) {}
$.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) {
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) {
return _getTokenizerModuleStorage().isWhitelistedTokenTemplate[tokenTemplate];
}

/// @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 {}
}
Loading
Loading