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
Submitted on: 2025-10-24 09:30:29
Comments
Log in to comment.
No comments yet.