Skip to content

Commit

Permalink
Add Implementation to Tokenizer Module and OwnableERC20 (#144)
Browse files Browse the repository at this point in the history
* feat: tokenizer module

* fix: inherit IERC165 & additional checks in tokenizer

* fix: use `isExpiredNow` in license registry

* tests: add tests and final changes

* chore: linting

* fix: add BUSL license & move whitelist dispute flag
  • Loading branch information
sebsadface authored Dec 16, 2024
1 parent 71912fb commit df18d84
Show file tree
Hide file tree
Showing 10 changed files with 787 additions and 25 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: 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

0 comments on commit df18d84

Please sign in to comment.