Skip to content

Commit

Permalink
feat: tokenizer module
Browse files Browse the repository at this point in the history
  • Loading branch information
sebsadface committed Dec 15, 2024
1 parent 71912fb commit 7e61975
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 18 deletions.
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: 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);
}
30 changes: 30 additions & 0 deletions contracts/interfaces/modules/tokenizer/ITokenizerModule.sol
Original file line number Diff line number Diff line change
@@ -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);
}
25 changes: 25 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
47 changes: 47 additions & 0 deletions contracts/modules/tokenizer/OwnableERC20.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
13 changes: 0 additions & 13 deletions contracts/modules/tokenizer/OwnerableERC20.sol

This file was deleted.

107 changes: 102 additions & 5 deletions contracts/modules/tokenizer/TokenizerModule.sol
Original file line number Diff line number Diff line change
@@ -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 {}
}

0 comments on commit 7e61975

Please sign in to comment.