IPToken

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/IPToken.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import { IControlIPTs } from "./IControlIPTs.sol";
import { IIPToken, Metadata } from "./IIPToken.sol";
import { MustControlIpnft, Tokenizer } from "./Tokenizer.sol";
import { IPTokenUtils } from "./libraries/IPTokenUtils.sol";
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import { ERC20BurnableUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol";

error TokenCapped();

/**
 * @title IPToken 1.3
 * @author molecule.xyz
 * @notice this is a template contract that's cloned by the Tokenizer
 * @notice the owner of this contract is always the Tokenizer contract which enforces IPNFT holdership rules.
 *         The owner can increase the token supply as long as it's not explicitly capped.
 * @dev formerly known as "molecules"
 */
contract IPToken is IIPToken, ERC20Upgradeable, ERC20BurnableUpgradeable, OwnableUpgradeable {
    event Capped(uint256 atSupply);

    /// @notice the amount of tokens that ever have been issued (not necessarily == supply)
    uint256 public totalIssued;

    /// @notice when true, no one can ever mint tokens again.
    bool public capped;

    Metadata internal _metadata;

    function initialize(uint256 ipnftId, string calldata name_, string calldata symbol_, address originalOwner, string memory agreementCid)
        external
        initializer
    {
        __Ownable_init();
        __ERC20_init(name_, symbol_);
        _metadata = Metadata({ ipnftId: ipnftId, originalOwner: originalOwner, agreementCid: agreementCid });
    }

    constructor() {
        _disableInitializers();
    }

    modifier onlyTokenizerOrIPNFTController() {
        if (_msgSender() != owner() && _msgSender() != IControlIPTs(owner()).controllerOf(_metadata.ipnftId)) {
            revert MustControlIpnft();
        }
        _;
    }

    function metadata() external view override returns (Metadata memory) {
        return _metadata;
    }

    // Override ERC20 functions to resolve diamond inheritance
    function totalSupply() public view override returns (uint256) {
        return ERC20Upgradeable.totalSupply();
    }

    function balanceOf(address account) public view override returns (uint256) {
        return ERC20Upgradeable.balanceOf(account);
    }

    function transfer(address to, uint256 amount) public override returns (bool) {
        return ERC20Upgradeable.transfer(to, amount);
    }

    function allowance(address owner, address spender) public view override returns (uint256) {
        return ERC20Upgradeable.allowance(owner, spender);
    }

    function approve(address spender, uint256 amount) public override returns (bool) {
        return ERC20Upgradeable.approve(spender, amount);
    }

    function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
        return ERC20Upgradeable.transferFrom(from, to, amount);
    }

    function name() public view override returns (string memory) {
        return ERC20Upgradeable.name();
    }

    function symbol() public view override returns (string memory) {
        return ERC20Upgradeable.symbol();
    }

    function decimals() public view override returns (uint8) {
        return ERC20Upgradeable.decimals();
    }

    /**
     * @notice the supply of IP Tokens is controlled by the tokenizer contract.
     * @param receiver address
     * @param amount uint256
     */
    function issue(address receiver, uint256 amount) external override onlyTokenizerOrIPNFTController {
        if (capped) {
            revert TokenCapped();
        }
        totalIssued += amount;
        _mint(receiver, amount);
    }

    /**
     * @notice mark this token as capped. After calling this, no new tokens can be `issue`d
     */
    function cap() external override onlyTokenizerOrIPNFTController {
        capped = true;
        emit Capped(totalIssued);
    }

    /**
     * @notice contract metadata, compatible to ERC1155
     * @return string base64 encoded data url
     */
    function uri() external view override returns (string memory) {
        return IPTokenUtils.generateURI(_metadata, address(this), totalIssued);
    }
}
"
    },
    "src/IControlIPTs.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

/**
 * @title IControlIPTs 1.3
 * @author molecule.xyz
 * @notice must be implemented by contracts that should control IPTs
 */
interface IControlIPTs {
    function controllerOf(uint256 ipnftId) external view returns (address);
}
"
    },
    "src/IIPToken.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8;

/// @title IP Token Metadata Structure
/// @notice Metadata associated with an IP Token, linking it to its originating IPNFT
struct Metadata {
    /// @notice The ID of the IPNFT that this IP token is derived from
    uint256 ipnftId;
    /// @notice The original owner of the IPNFT at the time of token creation
    address originalOwner;
    /// @notice IPFS CID of the agreement governing this IP token
    string agreementCid;
}

/// @title IP Token Interface
/// @notice Interface for IP tokens that represent fractionalized intellectual property rights
/// @dev IP tokens are created from IPNFTs and represent transferable shares of IP ownership
interface IIPToken {
    /// @notice Returns the total amount of tokens that have ever been issued
    /// @dev This may differ from current supply due to potential burning mechanisms
    /// @return The total number of tokens issued since contract deployment
    function totalIssued() external view returns (uint256);

    /// @notice Returns the metadata associated with this IP token
    /// @return The metadata struct containing IPNFT ID, original owner, and agreement CID
    function metadata() external view returns (Metadata memory);

    /// @notice Issues new tokens to a specified address
    /// @param to The address to receive the newly issued tokens
    /// @param amount The number of tokens to issue
    function issue(address to, uint256 amount) external;

    /// @notice Returns or sets the maximum supply cap for this token
    /// @dev Implementation may vary - could be a getter or setter depending on context
    function cap() external;

    /// @notice Returns the URI for token metadata (typically IPFS)
    /// @return The URI string pointing to token metadata
    function uri() external view returns (string memory);
}
"
    },
    "src/Tokenizer.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import { IIPToken, Metadata as TokenMetadata } from "./IIPToken.sol";
import { IPToken } from "./IPToken.sol";
import { WrappedIPToken } from "./WrappedIPToken.sol";
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";

import { IControlIPTs } from "./IControlIPTs.sol";
import { IPNFT } from "./IPNFT.sol";
import { IPermissioner } from "./Permissioner.sol";

error MustControlIpnft();
error AlreadyTokenized();
error ZeroAddress();
error IPTNotControlledByTokenizer();
error InvalidTokenContract();
error InvalidTokenDecimals();

/// @title Tokenizer 1.4
/// @author molecule.xyz
/// @notice tokenizes an IPNFT to an ERC20 token (called IPToken or IPT) and controls its supply.
contract Tokenizer is UUPSUpgradeable, OwnableUpgradeable, IControlIPTs {
    event TokensCreated(
        uint256 indexed moleculesId,
        uint256 indexed ipnftId,
        address indexed tokenContract,
        address emitter,
        uint256 amount,
        string agreementCid,
        string name,
        string symbol
    );

    // @dev @TODO: index these topics
    event TokenWrapped(IERC20Metadata tokenContract, IIPToken wrappedIpt);
    event IPTokenImplementationUpdated(IIPToken indexed old, IIPToken indexed _new);
    event WrappedIPTokenImplementationUpdated(WrappedIPToken indexed old, WrappedIPToken indexed _new);
    event PermissionerUpdated(IPermissioner indexed old, IPermissioner indexed _new);

    IPNFT internal ipnft;

    /// @dev a map of all IPTs. We're staying with the the initial term "synthesized" to keep the storage layout intact
    mapping(uint256 => IIPToken) public synthesized;

    /// @dev not used, needed to ensure that storage slots are still in order after 1.1 -> 1.2, use ipTokenImplementation
    /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
    address immutable tokenImplementation;

    /// @dev the permissioner checks if senders have agreed to legal requirements
    IPermissioner public permissioner;

    /// @notice the IPToken implementation this Tokenizer clones from
    IPToken public ipTokenImplementation;

    /// @notice a WrappedIPToken implementation, used for attaching existing ERC-20 contracts as metadata bearing IPTs
    WrappedIPToken public wrappedTokenImplementation;

    /**
     * @param _ipnft the IPNFT contract
     * @param _permissioner a permissioning contract that checks if callers have agreed to the tokenized token's legal agreements
     */
    function initialize(IPNFT _ipnft, IPermissioner _permissioner) external initializer {
        __UUPSUpgradeable_init();
        __Ownable_init();
        ipnft = _ipnft;
        permissioner = _permissioner;
    }

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        tokenImplementation = address(0);
        _disableInitializers();
    }

    function getIPNFTContract() public view returns (IPNFT) {
        return ipnft;
    }

    modifier onlyController(IPToken ipToken) {
        TokenMetadata memory metadata = ipToken.metadata();

        if (address(synthesized[metadata.ipnftId]) != address(ipToken)) {
            revert IPTNotControlledByTokenizer();
        }

        if (_msgSender() != controllerOf(metadata.ipnftId)) {
            revert MustControlIpnft();
        }
        _;
    }

    /**
     * @notice sets the new implementation address of the IPToken
     * @param _ipTokenImplementation address pointing to the new implementation
     */
    function setIPTokenImplementation(IPToken _ipTokenImplementation) public onlyOwner {
        /*
        could call some functions on old contract to make sure its tokenizer not another contract behind a proxy for safety
        */
        if (address(_ipTokenImplementation) == address(0)) {
            revert ZeroAddress();
        }

        emit IPTokenImplementationUpdated(ipTokenImplementation, _ipTokenImplementation);
        ipTokenImplementation = _ipTokenImplementation;
    }

    /**
     * @notice sets the new implementation address of the WrappedIPToken
     * @param _wrappedIpTokenImplementation address pointing to the new implementation
     */
    function setWrappedIPTokenImplementation(WrappedIPToken _wrappedIpTokenImplementation) public onlyOwner {
        /*
        could call some functions on old contract to make sure its tokenizer not another contract behind a proxy for safety
        */
        if (address(_wrappedIpTokenImplementation) == address(0)) {
            revert ZeroAddress();
        }

        emit WrappedIPTokenImplementationUpdated(wrappedTokenImplementation, _wrappedIpTokenImplementation);
        wrappedTokenImplementation = _wrappedIpTokenImplementation;
    }

    /**
     * @dev sets legacy IPTs on the tokenized mapping
     */
    function reinit(WrappedIPToken _wrappedIpTokenImplementation, IPToken _ipTokenImplementation) public onlyOwner reinitializer(6) {
        setIPTokenImplementation(_ipTokenImplementation);
        setWrappedIPTokenImplementation(_wrappedIpTokenImplementation);
    }

    /**
     * @notice tokenizes ipnft#id for the current asset holder.
     * @param ipnftId the token id on the underlying nft collection
     * @param tokenAmount the initially issued supply of IP tokens
     * @param tokenSymbol the ip token's ticker symbol
     * @param agreementCid a content hash that contains legal terms for IP token owners
     * @param signedAgreement the sender's signature over the signed agreemeent text (must be created on the client)
     * @return token a new created ERC20 token contract that represents the tokenized ipnft
     */
    function tokenizeIpnft(
        uint256 ipnftId,
        uint256 tokenAmount,
        string memory tokenSymbol,
        string memory agreementCid,
        bytes calldata signedAgreement
    ) external returns (IPToken token) {
        if (_msgSender() != controllerOf(ipnftId)) {
            revert MustControlIpnft();
        }
        if (address(synthesized[ipnftId]) != address(0)) {
            revert AlreadyTokenized();
        }

        // https://github.com/OpenZeppelin/workshops/tree/master/02-contracts-clone
        token = IPToken(Clones.clone(address(ipTokenImplementation)));
        string memory name = string.concat("IP Tokens of IPNFT #", Strings.toString(ipnftId));
        token.initialize(ipnftId, name, tokenSymbol, _msgSender(), agreementCid);

        synthesized[ipnftId] = token;

        //this has been called MoleculesCreated before
        emit TokensCreated(
            //upwards compatibility: signaling a unique "Molecules ID" as first parameter ("sales cycle id"). This is unused and not interpreted.
            uint256(keccak256(abi.encodePacked(ipnftId))),
            ipnftId,
            address(token),
            _msgSender(),
            tokenAmount,
            agreementCid,
            name,
            tokenSymbol
        );
        permissioner.accept(token, _msgSender(), signedAgreement);
        token.issue(_msgSender(), tokenAmount);
    }

    /**
     * @notice since 1.4 allows attaching an existing ERC20 contract as IPT
     * @param ipnftId the token id on the underlying nft collection
     * @param agreementCid a content hash that contains legal terms for IP token owners
     * @param signedAgreement the sender's signature over the signed agreemeent text (must be created on the client)
     * @param tokenContract the ERC20 token contract to wrap
     * @return IPToken a wrapped IPToken that represents the tokenized ipnft for permissioners and carries metadata
     */
    function attachIpt(uint256 ipnftId, string memory agreementCid, bytes calldata signedAgreement, IERC20Metadata tokenContract)
        external
        returns (IIPToken)
    {
        if (_msgSender() != controllerOf(ipnftId)) {
            revert MustControlIpnft();
        }
        if (address(synthesized[ipnftId]) != address(0)) {
            revert AlreadyTokenized();
        }

        // Sanity checks for token properties
        _validateTokenContract(tokenContract);

        WrappedIPToken wrappedIpt = WrappedIPToken(Clones.clone(address(wrappedTokenImplementation)));
        wrappedIpt.initialize(ipnftId, _msgSender(), agreementCid, tokenContract);
        synthesized[ipnftId] = wrappedIpt;

        emit TokensCreated(
            uint256(keccak256(abi.encodePacked(ipnftId))),
            ipnftId,
            address(tokenContract),
            _msgSender(),
            tokenContract.totalSupply(),
            agreementCid,
            tokenContract.name(),
            tokenContract.symbol()
        );
        emit TokenWrapped(tokenContract, wrappedIpt);
        permissioner.accept(wrappedIpt, _msgSender(), signedAgreement);
        return wrappedIpt;
    }

    /**
     * @notice issues more IPTs when not capped. This can be used for new owners of legacy IPTs that otherwise wouldn't be able to pass their `onlyIssuerOrOwner` gate
     * @param ipToken The ip token to control
     * @param amount the amount of tokens to issue
     * @param receiver the address that receives the tokens
     */
    function issue(IPToken ipToken, uint256 amount, address receiver) external onlyController(ipToken) {
        ipToken.issue(receiver, amount);
    }

    /**
     * @notice caps the supply of an IPT. After calling this, no new tokens can be `issue`d
     * @dev you must compute the ipt hash externally.
     * @param ipToken the IPToken to cap.
     */
    function cap(IPToken ipToken) external onlyController(ipToken) {
        ipToken.cap();
    }

    /// @dev this will be called by IPTs. Right now the controller is the IPNFT's current owner, it can be a Governor in the future.
    function controllerOf(uint256 ipnftId) public view override returns (address) {
        return ipnft.ownerOf(ipnftId);
    }

    /**
     * @notice Validates token contract properties before wrapping
     * @param tokenContract The ERC20 token contract to validate
     */
    function _validateTokenContract(IERC20Metadata tokenContract) internal view {
        // Check if contract address is valid
        if (address(tokenContract) == address(0)) {
            revert ZeroAddress();
        }

        // Check if it's a contract (has code)
        uint256 codeSize;
        assembly {
            codeSize := extcodesize(tokenContract)
        }
        if (codeSize == 0) {
            revert InvalidTokenContract();
        }

        // Validate decimals - should be reasonable (0-18)
        try tokenContract.decimals() returns (uint8 decimals) {
            if (decimals > 18) {
                revert InvalidTokenDecimals();
            }
        } catch {
            revert InvalidTokenDecimals();
        }
    }

    /// @notice upgrade authorization logic
    function _authorizeUpgrade(address /*newImplementation*/ )
        internal
        override
        onlyOwner // solhint-disable--line no-empty-blocks
    {
        //empty block
    }
}
"
    },
    "src/libraries/IPTokenUtils.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8;

import { Metadata } from "../IIPToken.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";

/// @title IP Token Utilities Library
/// @notice Shared utilities for IP Token contracts
/// @dev Contains common functions used by both IPToken and WrappedIPToken
library IPTokenUtils {
    /// @notice Generates a base64-encoded data URL containing token metadata
    /// @param metadata_ The metadata struct containing IPNFT information
    /// @param tokenContract The ERC20 token contract address
    /// @param supply The token supply to include in metadata
    /// @return The complete data URL string
    function generateURI(Metadata memory metadata_, address tokenContract, uint256 supply) internal view returns (string memory) {
        string memory tokenId = Strings.toString(metadata_.ipnftId);

        string memory props = string.concat(
            '"properties": {',
            '"ipnft_id": ',
            tokenId,
            ',"agreement_content": "ipfs://',
            metadata_.agreementCid,
            '","original_owner": "',
            Strings.toHexString(metadata_.originalOwner),
            '","erc20_contract": "',
            Strings.toHexString(tokenContract),
            '","supply": "',
            Strings.toString(supply),
            '"}'
        );

        return string.concat(
            "data:application/json;base64,",
            Base64.encode(
                bytes(
                    string.concat(
                        '{"name": "IP Tokens of IPNFT #',
                        tokenId,
                        '","description": "IP Tokens, derived from IP-NFTs, are ERC-20 tokens governing IP pools.","decimals": ',
                        Strings.toString(IERC20Metadata(tokenContract).decimals()),
                        ',"external_url": "https://molecule.xyz","image": "",',
                        props,
                        "}"
                    )
                )
            )
        );
    }
}
"
    },
    "lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/ContextUpgradeable.sol";
import "../proxy/utils/Initializable.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract OwnableUpgradeable is Initializable, ContextUpgradeable {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    function __Ownable_init() internal onlyInitializing {
        __Ownable_init_unchained();
    }

    function __Ownable_init_unchained() internal onlyInitializing {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }

    /**
     * @dev This empty reserved space is put in place to allow future versions to add new
     * variables without shifting down storage in the inheritance chain.
     * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
     */
    uint256[49] private __gap;
}
"
    },
    "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol)

pragma solidity ^0.8.0;

import "./IERC20Upgradeable.sol";
import "./extensions/IERC20MetadataUpgradeable.sol";
import "../../utils/ContextUpgradeable.sol";
import "../../proxy/utils/Initializable.sol";

/**
 * @dev Implementation of the {IERC20} interface.
 *
 * This implementation is agnostic to the way tokens are created. This means
 * that a supply mechanism has to be added in a derived contract using {_mint}.
 * For a generic mechanism see {ERC20PresetMinterPauser}.
 *
 * TIP: For a detailed writeup see our guide
 * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
 * to implement supply mechanisms].
 *
 * The default value of {decimals} is 18. To change this, you should override
 * this function so it returns a different value.
 *
 * We have followed general OpenZeppelin Contracts guidelines: functions revert
 * instead returning `false` on failure. This behavior is nonetheless
 * conventional and does not conflict with the expectations of ERC20
 * applications.
 *
 * Additionally, an {Approval} event is emitted on calls to {transferFrom}.
 * This allows applications to reconstruct the allowance for all accounts just
 * by listening to said events. Other implementations of the EIP may not emit
 * these events, as it isn't required by the specification.
 *
 * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
 * functions have been added to mitigate the well-known issues around setting
 * allowances. See {IERC20-approve}.
 */
contract ERC20Upgradeable is Initializable, ContextUpgradeable, IERC20Upgradeable, IERC20MetadataUpgradeable {
    mapping(address => uint256) private _balances;

    mapping(address => mapping(address => uint256)) private _allowances;

    uint256 private _totalSupply;

    string private _name;
    string private _symbol;

    /**
     * @dev Sets the values for {name} and {symbol}.
     *
     * All two of these values are immutable: they can only be set once during
     * construction.
     */
    function __ERC20_init(string memory name_, string memory symbol_) internal onlyInitializing {
        __ERC20_init_unchained(name_, symbol_);
    }

    function __ERC20_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing {
        _name = name_;
        _symbol = symbol_;
    }

    /**
     * @dev Returns the name of the token.
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev Returns the symbol of the token, usually a shorter version of the
     * name.
     */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev Returns the number of decimals used to get its user representation.
     * For example, if `decimals` equals `2`, a balance of `505` tokens should
     * be displayed to a user as `5.05` (`505 / 10 ** 2`).
     *
     * Tokens usually opt for a value of 18, imitating the relationship between
     * Ether and Wei. This is the default value returned by this function, unless
     * it's overridden.
     *
     * NOTE: This information is only used for _display_ purposes: it in
     * no way affects any of the arithmetic of the contract, including
     * {IERC20-balanceOf} and {IERC20-transfer}.
     */
    function decimals() public view virtual override returns (uint8) {
        return 18;
    }

    /**
     * @dev See {IERC20-totalSupply}.
     */
    function totalSupply() public view virtual override returns (uint256) {
        return _totalSupply;
    }

    /**
     * @dev See {IERC20-balanceOf}.
     */
    function balanceOf(address account) public view virtual override returns (uint256) {
        return _balances[account];
    }

    /**
     * @dev See {IERC20-transfer}.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - the caller must have a balance of at least `amount`.
     */
    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

    /**
     * @dev See {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

    /**
     * @dev See {IERC20-approve}.
     *
     * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
     * `transferFrom`. This is semantically equivalent to an infinite approval.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `amount`.
     */
    function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

    /**
     * @dev Atomically increases the allowance granted to `spender` by the caller.
     *
     * This is an alternative to {approve} that can be used as a mitigation for
     * problems described in {IERC20-approve}.
     *
     * Emits an {Approval} event indicating the updated allowance.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, allowance(owner, spender) + addedValue);
        return true;
    }

    /**
     * @dev Atomically decreases the allowance granted to `spender` by the caller.
     *
     * This is an alternative to {approve} that can be used as a mitigation for
     * problems described in {IERC20-approve}.
     *
     * Emits an {Approval} event indicating the updated allowance.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     * - `spender` must have allowance for the caller of at least
     * `subtractedValue`.
     */
    function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
        address owner = _msgSender();
        uint256 currentAllowance = allowance(owner, spender);
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
        unchecked {
            _approve(owner, spender, currentAllowance - subtractedValue);
        }

        return true;
    }

    /**
     * @dev Moves `amount` of tokens from `from` to `to`.
     *
     * This internal function is equivalent to {transfer}, and can be used to
     * e.g. implement automatic token fees, slashing mechanisms, etc.
     *
     * Emits a {Transfer} event.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     */
    function _transfer(address from, address to, uint256 amount) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
            // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
            // decrementing then incrementing.
            _balances[to] += amount;
        }

        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, amount);
    }

    /** @dev Creates `amount` tokens and assigns them to `account`, increasing
     * the total supply.
     *
     * Emits a {Transfer} event with `from` set to the zero address.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     */
    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

        _totalSupply += amount;
        unchecked {
            // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
            _balances[account] += amount;
        }
        emit Transfer(address(0), account, amount);

        _afterTokenTransfer(address(0), account, amount);
    }

    /**
     * @dev Destroys `amount` tokens from `account`, reducing the
     * total supply.
     *
     * Emits a {Transfer} event with `to` set to the zero address.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     * - `account` must have at least `amount` tokens.
     */
    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");

        _beforeTokenTransfer(account, address(0), amount);

        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        unchecked {
            _balances[account] = accountBalance - amount;
            // Overflow not possible: amount <= accountBalance <= totalSupply.
            _totalSupply -= amount;
        }

        emit Transfer(account, address(0), amount);

        _afterTokenTransfer(account, address(0), amount);
    }

    /**
     * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
     *
     * This internal function is equivalent to `approve`, and can be used to
     * e.g. set automatic allowances for certain subsystems, etc.
     *
     * Emits an {Approval} event.
     *
     * Requirements:
     *
     * - `owner` cannot be the zero address.
     * - `spender` cannot be the zero address.
     */
    function _approve(address owner, address spender, uint256 amount) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    /**
     * @dev Updates `owner` s allowance for `spender` based on spent `amount`.
     *
     * Does not update the allowance amount in case of infinite allowance.
     * Revert if not enough allowance is available.
     *
     * Might emit an {Approval} event.
     */
    function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

    /**
     * @dev Hook that is called before any transfer of tokens. This includes
     * minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * will be transferred to `to`.
     * - when `from` is zero, `amount` tokens will be minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens will be burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}

    /**
     * @dev Hook that is called after any transfer of tokens. This includes
     * minting and burning.
     *
     * Calling conditions:
     *
     * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
     * has been transferred to `to`.
     * - when `from` is zero, `amount` tokens have been minted for `to`.
     * - when `to` is zero, `amount` of ``from``'s tokens have been burned.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}

    /**
     * @dev This empty reserved space is put in place to allow future versions to add new
     * variables without shifting down storage in the inheritance chain.
     * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
     */
    uint256[45] private __gap;
}
"
    },
    "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20BurnableUpgradeable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/extensions/ERC20Burnable.sol)

pragma solidity ^0.8.0;

import "../ERC20Upgradeable.sol";
import "../../../utils/ContextUpgradeable.sol";
import "../../../proxy/utils/Initializable.sol";

/**
 * @dev Extension of {ERC20} that allows token holders to destroy both their own
 * tokens and those that they have an allowance for, in a way that can be
 * recognized off-chain (via event analysis).
 */
abstract contract ERC20BurnableUpgradeable is Initializable, ContextUpgradeable, ERC20Upgradeable {
    function __ERC20Burnable_init() internal onlyInitializing {
    }

    function __ERC20Burnable_init_unchained() internal onlyInitializing {
    }
    /**
     * @dev Destroys `amount` tokens from the caller.
     *
     * See {ERC20-_burn}.
     */
    function burn(uint256 amount) public virtual {
        _burn(_msgSender(), amount);
    }

    /**
     * @dev Destroys `amount` tokens from `account`, deducting from the caller's
     * allowance.
     *
     * See {ERC20-_burn} and {ERC20-allowance}.
     *
     * Requirements:
     *
     * - the caller must have allowance for ``accounts``'s tokens of at least
     * `amount`.
     */
    function burnFrom(address account, uint256 amount) public virtual {
        _spendAllowance(account, _msgSender(), amount);
        _burn(account, amount);
    }

    /**
     * @dev This empty reserved space is put in place to allow future versions to add new
     * variables without shifting down storage in the inheritance chain.
     * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
     */
    uint256[50] private __gap;
}
"
    },
    "src/WrappedIPToken.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import { IIPToken, Metadata } from "./IIPToken.sol";
import { IPTokenUtils } from "./libraries/IPTokenUtils.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

/**
 * @title WrappedIPToken
 * @author molecule.xyz
 * @notice A read-only wrapper that extends existing ERC20 tokens with IP metadata
 * @dev This contract provides IP metadata for an existing ERC20 token without proxying
 *      state-changing operations. It only implements IIPToken interface functions and
 *      read-only ERC20 view functions. Users must interact with the underlying token
 *      directly for transfers, approvals, and other state changes.
 */
contract WrappedIPToken is IIPToken, Initializable {
    IERC20Metadata public wrappedToken;

    Metadata internal _metadata;

    /**
     * @dev Initialize the contract with the provided parameters.
     * @param ipnftId the token id on the underlying nft collection
     * @param originalOwner the original owner of the ipnft
     * @param agreementCid a content hash that contains legal terms for IP token owners
     * @param wrappedToken_ the ERC20 token contract to wrap
     */
    function initialize(uint256 ipnftId, address originalOwner, string memory agreementCid, IERC20Metadata wrappedToken_) external initializer {
        _metadata = Metadata({ ipnftId: ipnftId, originalOwner: originalOwner, agreementCid: agreementCid });
        wrappedToken = wrappedToken_;
    }

    function metadata() external view override returns (Metadata memory) {
        return _metadata;
    }

    /**
     * @dev Returns the name of the token.
     */
    function name() public view returns (string memory) {
        return wrappedToken.name();
    }

    /**
     * @dev Returns the symbol of the token.
     */
    function symbol() public view returns (string memory) {
        return wrappedToken.symbol();
    }

    /**
     * @dev Returns the decimals places of the token.
     */
    function decimals() public view returns (uint8) {
        return wrappedToken.decimals();
    }

    function totalSupply() public view returns (uint256) {
        return wrappedToken.totalSupply();
    }

    function balanceOf(address account) public view returns (uint256) {
        return wrappedToken.balanceOf(account);
    }

    function totalIssued() public view override returns (uint256) {
        return wrappedToken.totalSupply();
    }

    function issue(address, uint256) public virtual override {
        revert("WrappedIPToken: read-only wrapper - use underlying token for minting");
    }

    function cap() public virtual override {
        revert("WrappedIPToken: read-only wrapper - use underlying token for cappping");
    }

    function uri() external view override returns (string memory) {
        return IPTokenUtils.generateURI(_metadata, address(wrappedToken), wrappedToken.totalSupply());
    }
}
"
    },
    "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/UUPSUpgradeable.sol)

pragma solidity ^0.8.0;

import "../../interfaces/draft-IERC1822Upgradeable.sol";
import "../ERC1967/ERC1967UpgradeUpgradeable.sol";
import "./Initializable.sol";

/**
 * @dev An upgradeability mechanism designed for UUPS proxies. The functions included here can perform an upgrade of an
 * {ERC1967Proxy}, when this contract is set as the implementation behind such a proxy.
 *
 * A security mechanism ensures that an upgrade does not turn off upgradeability accidentally, although this risk is
 * reinstated if the upgrade retains upgradeability but removes the security mechanism, e.g. by replacing
 * `UUPSUpgradeable` with a custom implementation of upgrades.
 *
 * The {_authorizeUpgrade} function must be overridden to include access restriction to the upgrade mechanism.
 *
 * _Available since v4.1._
 */
abstract contract UUPSUpgradeable is Initializable, IERC1822ProxiableUpgradeable, ERC1967UpgradeUpgradeable {
    function __UUPSUpgradeable_init() internal onlyInitializing {
    }

    function __UUPSUpgradeable_init_unchained() internal onlyInitializing {
    }
    /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment
    address private immutable __self = address(this);

    /**
     * @dev Check that the execution is being performed through a delegatecall call and that the execution context is
     * a proxy contract with an implementation (as defined in ERC1967) pointing to self. This should only be the case
     * for UUPS and transparent proxies that are using the current contract as their implementation. Execution of a
     * function through ERC1167 minimal proxies (clones) would not normally pass this test, but is not guaranteed to
     * fail.
     */
    modifier onlyProxy() {
        require(address(this) != __self, "Function must be called through delegatecall");
        require(_getImplementation() == __self, "Function must be called through active proxy");
        _;
    }

    /**
     * @dev Check that the execution is not being performed through a delegate call. This allows a function to be
     * callable on the implementing contract but not through proxies.
     */
    modifier notDelegated() {
        require(address(this) == __self, "UUPSUpgradeable: must not be called through delegatecall");
        _;
    }

    /**
     * @dev Implementation of the ERC1822 {proxiableUUID} function. This returns the storage slot used by the
     * implementation. It is used to validate the implementation's compatibility when performing an upgrade.
     *
     * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks
     * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this
     * function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier.
     */
    function proxiableUUID() external view virtual override notDelegated returns (bytes32) {
        return _IMPLEMENTATION_SLOT;
    }

    /**
     * @dev Upgrade the implementation of the proxy to `newImplementation`.
     *
     * Calls {_authorizeUpgrade}.
     *
     * Emits an {Upgraded} event.
     *
     * @custom:oz-upgrades-unsafe-allow-reachable delegatecall
     */
    function upgradeTo(address newImplementation) public virtual onlyProxy {
        _authorizeUpgrade(newImplementation);
        _upgradeToAndCallUUPS(newImplementation, new bytes(0), false);
    }

    /**
     * @dev Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call
     * encoded in `data`.
     *
     * Calls {_authorizeUpgrade}.
     *
     * Emits an {Upgraded} event.
     *
     * @custom:oz-upgrades-unsafe-allow-reachable delegatecall
     */
    function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy {
        _authorizeUpgrade(newImplementation);
        _upgradeToAndCallUUPS(newImplementation, data, true);
    }

    /**
     * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by
     * {upgradeTo} and {upgradeToAndCall}.
     *
     * Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}.
     *
     * ```solidity
     * function _authorizeUpgrade(address) internal override onlyOwner {}
     * ```
     */
    function _authorizeUpgrade(address newImplementation) internal virtual;

    /**
     * @dev This empty reserved space is put in place to allow future versions to add new
     * variables without shifting down storage in the inheritance chain.
     * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
     */
    uint256[50] private __gap;
}
"
    },
    "lib/openzeppelin-contracts/contracts/proxy/Clones.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (proxy/Clones.sol)

pragma solidity ^0.8.0;

/**
 * @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for
 * deploying minimal proxy contracts, also known as "clones".
 *
 * > To simply and cheaply clone contract functionality in an immutable way, this standard specifies
 * > a minimal bytecode implementation that delegates all calls to a known, fixed address.
 *
 * The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2`
 * (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the
 * deterministic method.
 *
 * _Available since v3.4._
 */
library Clones {
    /**
     * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
     *
     * This function uses the create opcode, which should never revert.
     */
    function clone(address implementation) internal returns (address instance) {
        /// @solidity memory-safe-assembly
        assembly {
            // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
            // of the `implementation` address with the bytecode before the address.
            mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
            // Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
            mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
            instance := create(0, 0x09, 0x37)
        }
        require(instance != address(0), "ERC1167: create failed");
    }

    /**
     * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
     *
     * This function uses the create2 opcode and a `salt` to deterministically deploy
     * the clone. Using the same `implementation` and `salt` multiple time will revert, since
     * the clones cannot be deployed twice at the same address.
     */
    function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
        /// @solidity memory-safe-assembly
        assembly {
            // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
            // of the `implementation` address with the bytecode before the address.
            mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
            // Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
            mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
            instance := create2(0, 0x09, 0x37, salt)
        }
        require(instance != address(0), "ERC1167: create2 failed");
    }

    /**
     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
     */
    function predictDeterministicAddress(
        address implementation,
        bytes32 salt,
        address deployer
    ) internal pure returns (address predicted) {
        /// @solidity memory-safe-assembly
        assembly {
            let ptr := mload(0x40)
            mstore(add(ptr, 0x38), deployer)
            mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
            mstore(add(ptr, 0x14), implementation)
            mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73)
            mstore(add(ptr, 0x58), salt)
            mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37))
            predicted := keccak256(add(ptr, 0x43), 0x55)
        }
    }

    /**
     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
     */
    function predictDeterministicAddress(
        address implementation,
        bytes32 salt
    ) internal view returns (address predicted) {
        return predictDeterministicAddress(implementation, salt, address(this));
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol)

pragma solidity ^0.8.0;

import "../IERC20.sol";

/**
 * @dev Interface for the optional metadata functions from the ERC20 standard.
 *
 * _Available since v4.1._
 */
interface IERC20Metadata is IERC20 {
    /**
     * @dev Returns the name of the token.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the symbol of the token.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the decimals places of the token.
     */
    function decimals() external view returns (uint8);
}
"
    },
    "lib/openzeppelin-contracts/contracts/utils/Strings.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol)

pragma solidity ^0.8.0;

import "./math/Math.sol";
import "./math/SignedMath.sol";

/**
 * @dev String operations.
 */
library Strings {
    bytes16 private constant _SYMBOLS = "0123456789abcdef";
    uint8 private constant _ADDRESS_LENGTH = 20;

    /**
     * @dev Converts a `uint256` to its ASCII `string` decimal representation.
     */
    function toString(uint256 value) internal pure returns (string memory) {
        unchecked {
            uint256 length = Math.log10(value) + 1;
            string memory buffer = new string(length);
            uint256 ptr;
            /// @solidity memory-safe-assembly
            assembly {
                ptr := add(buffer, add(32, length))
            }
            while (true) {
                ptr--;
                /// @solidity memory-safe-assembly
                assembly {
                    mstore8(ptr, byte(mod(value, 10), _SYMBOLS))
                }
                value /= 10;
                if (value == 0) break;
            }
            return buffer;
        }
    }

    /**
     * @dev Converts a `int256` to its ASCII `string` decimal representation.
     */
    function toString(int256 value) internal pure returns (string memory) {
        return string(abi.encodePacked(value < 0 ? "-" : "", toString(SignedMath.abs(value))));
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
     */
    function toHexString(uint256 value) internal pure returns (string memory) {
        unchecked {
            return toHexString(value, Math.log256(value) + 1);
        }
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
     */
    function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = "0";
        buffer[1] = "x";
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = _SYMBOLS[value & 0xf];
            value >>= 4;
        }
        require(value == 0, "Strings: hex length insufficient");
        return string(buffer);
    }

    /**
     * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation.
     */
    function toHexString(address addr) internal pure returns (string memory) {
        return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);
    }

    /**
     * @dev Returns true if the two strings are equal.
     */
    function equal(string memory a, string memory b) internal pure returns (bool) {
        return keccak256(bytes(a)) == keccak256(bytes(b));
    }
}
"
    },
    "src/IPNFT.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import { ERC721BurnableUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol";
import { ERC721URIStorageUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import { CountersUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { IAuthorizeMints, SignedMintAuthorization } from "./IAuthorizeMints.sol";
import { IReservable } from "./IReservable.sol";

/*
 ______ _______         __    __ ________ ________
|      \       \       |  \  |  \        \        \
 \▓▓▓▓▓▓ ▓▓▓▓▓▓▓\      | ▓▓\ | ▓▓ ▓▓▓▓▓▓▓▓\▓▓▓▓▓▓▓▓
  | ▓▓ | ▓▓__/ ▓▓______| ▓▓▓\| ▓▓ ▓▓__      | ▓▓
  | ▓▓ | ▓▓    ▓▓      \ ▓▓▓▓\ ▓▓ ▓▓  \     | ▓▓
  | ▓▓ | ▓▓▓▓▓▓▓ \▓▓▓▓▓▓ ▓▓\▓▓ ▓▓ ▓▓▓▓▓     | ▓▓
 _| ▓▓_| ▓▓            | ▓▓ \▓▓▓▓ ▓▓        | ▓▓
|   ▓▓ \ ▓▓            | ▓▓  \▓▓▓ ▓▓        | ▓▓
 \▓▓▓▓▓▓\▓▓             \▓▓   \▓▓\▓▓         \▓▓
*/

/// @title IPNFT V2.5.1
/// @author molecule.to
/// @notice IP-NFTs capture intellectual property to be traded and synthesized
contract IPNFT is ERC721URIStorageUpgradeable, ERC721BurnableUpgradeable, IReservable, UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeable {
    using CountersUpgradeable for CountersUpgradeable.Counter;

    CountersUpgradeable.Counter private _reservationCounter;

    /// @notice by reserving a mint an user captures a new token id
    mapping(uint256 => address) public reservations;

    /// @notice an authorizer that checks whether a mint operation is allowed
    IAuthorizeMints mintAuthorizer;

    mapping(uint256 => mapping(address => uint256)) internal readAllowances;

    uint256 constant SYMBOLIC_MINT_FEE = 0.001 ether;

    /// @notice an IPNFT's base symbol, to be determined by the minter / owner, e.g. BIO-00001
    mapping(uint256 => string) public symbol;

    /// @dev the highest possible reservation id
    uint256 private constant MAX_RESERVATION_ID = type(uint128).max; 

    event Reserved(address indexed reserver, uint256 indexed reservationId);
    event IPNFTMinted(address indexed owner, uint256 indexed tokenId, string tokenURI, string symbol);
    event ReadAccessGranted(uint256 indexed tokenId, address indexed reader, uint256 until);
    event AuthorizerUpdated(address authorizer);

    error NotOwningReservation(uint256 id);
    error ToZeroAddress();
    error Unauthorized();
    error InsufficientBalance();
    error BadDuration();
    error MintingFeeTooLow();

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    /// @notice Contract initialization logic
    function initialize() external initializer {
        __UUPSUpgradeable_init();
        __Ownable_init();
        __Pausable_init();
        __ERC721_init("IPNFT", "IPNFT");
        _reservationCounter.increment(); //start at 1.
    }

    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    function setAuthorizer(IAuthorizeMints authorizer_) external onlyOwner {
        mintAuthorizer = authorizer_;
        emit AuthorizerUpdated(address(authorizer_));
    }

    /// @notice https://docs.opensea.io/docs/contract-level-metadata
    function contractURI() public pure returns (string memory) {
        return "https://mint.molecule.to/contract-metadata/ipnft.json";
    }

    /**
     * @notice reserves a new token id. Checks that the caller is authorized, according to the current implementation of IAuthorizeMints.
     * @return reservationId a new reservation id
     */
    function reserve() external whenNotPaused returns (uint256 reservationId) {
        if (!mintAuthorizer.authorizeReservation(_msgSender())) {
            revert Unauthorized();
        }
        reservationId = _reservationCounter.current();
        _reservationCounter.increment();
        reservations[reservationId] = _msgSender();
        emit Reserved(_msgSender(), reservationId);
    }

    /**
     * @notice mints an IPNFT with `tokenURI` as source of metadata.
     * Minting the IPNFT can happen either with a reservation id or poi hash (Proof of Idea).
     * if the tokenId is a reservationId then it invalidates the reservation.
     * @notice We are charging a nominal fee to symbolically represent the transfer of ownership rights, for a price of .001 ETH (<$2USD at current prices). This helps ensure the protocol is affordable to almost all projects, but discourages frivolous IP-NFT minting.
     *
     * @param to the recipient of the NFT
     * @param reservationId the reserved token id that has been reserved with `reserve()` / or the poi hash
     * @param _tokenURI a location that resolves to a valid IP-NFT metadata structure
     * @param _symbol a symbol that represents the IPNFT's derivatives. Can be changed by the owner
     * @param authorization a bytes encoded parameter that's handed to the current authorizer
     * @return the `tokenId`
     */
    function mintReservation(address to, uint256 reservationId, string calldata _tokenURI, string calldata _symbol, bytes calldata authorization)
        external
        payable
        override
        whenNotPaused
        returns (uint256)
    {
        bool _isPoi = isPoi(reservationId);
        if (!_isPoi && reservations[reservationId] != _msgSender()) {
            revert NotOwningReservation(reservationId);
        }

        if (msg.value < SYMBOLIC_MINT_FEE) {
            revert MintingFeeTooLow();
        }

        if (!mintAuthorizer.authorizeMint(_msgSender(), to, abi.encode(SignedMintAuthorization(reservationId, _tokenURI, authorization)))) {
            revert Unauthorized();
        }
        if (!_isPoi) {
            delete reservations[reservationId];
        }

        symbol[reservationId] = _symbol;
        mintAuthorizer.redeem(authorization);

        _mint(to, reservationId);
        _setTokenURI(reservationId, _tokenURI);
        emit IPNFTMinted(to, reservationId, _tokenURI, _symbol);
        return reservationId;
    }

    /**
     * @notice grants time limited "read" access to gated resources
     * @param reader the address that should be able to access gated content
     * @param tokenId token id
     * @param until the timestamp when read access expires (unsafe but good enough for this use case)
     */
    function grantReadAccess(address reader, uint256 tokenId, uint256 until) external {
        if (ownerOf(tokenId) != _msgSender()) {
            revert InsufficientBalance();
        }

        if (block.timestamp >= until) {
            revert BadDuration();
        }

        readAllowances[tokenId][reader] = until;
        emit ReadAccessGranted(tokenId, reader, until);
    }

    function amendMetadata(uint256 tokenId, string calldata _newTokenURI, bytes calldata authorization) external {
        if (ownerOf(tokenId) != _msgSender()) {
            revert Unauthorized();
        }

        if (!mintAuthorizer.authorizeMint(_msgSender(), _msgSender(), abi.encode(SignedMintAuthorization(tokenId, _newTokenURI, authorization)))) {
            revert Unauthorized();
        }

        _setTokenURI(tokenId, _newTokenURI);
    }

    /**
     * @notice check whether `reader` currently is able to access gated content behind `tokenId`
     * @param reader the address in question
     * @param tokenId the ipnft id
     * @return bool current read allowance
     */
    function canRead(address reader, uint256 tokenId) external view returns (bool) {
        return (ownerOf(tokenId) == reader || readAllowances[tokenId][reader] > block.timestamp);
    }

    /// @notice in case someone sends Eth to this contract, this function gets it out again
    function withdrawAll() public whenNot

Tags:
ERC20, ERC721, ERC165, Multisig, Burnable, Pausable, Non-Fungible, Yield, Voting, Upgradeable, Multi-Signature, Factory|addr:0xd79fe2c4879b3a3d732df11294329a60cff3a0a9|verified:true|block:23633871|tx:0x69a3f85cf5d6765c0c3ec1e7043489e5c5b35d31642de56f0d5bda7ca92aa088|first_check:1761291026

Submitted on: 2025-10-24 09:30:29

Comments

Log in to comment.

No comments yet.