// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; /// @notice Modern, minimalist, and gas efficient ERC-721 implementation. /// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol) abstract contract ERC721 { /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ event Transfer(address indexed from, address indexed to, uint256 indexed id); event Approval(address indexed owner, address indexed spender, uint256 indexed id); event ApprovalForAll(address indexed owner, address indexed operator, bool approved); /*////////////////////////////////////////////////////////////// METADATA STORAGE/LOGIC //////////////////////////////////////////////////////////////*/ string public name; string public symbol; function tokenURI(uint256 id) public view virtual returns (string memory); /*////////////////////////////////////////////////////////////// ERC721 BALANCE/OWNER STORAGE //////////////////////////////////////////////////////////////*/ mapping(uint256 => address) internal _ownerOf; mapping(address => uint256) internal _balanceOf; function ownerOf(uint256 id) public view virtual returns (address owner) { require((owner = _ownerOf[id]) != address(0), "NOT_MINTED"); } function balanceOf(address owner) public view virtual returns (uint256) { require(owner != address(0), "ZERO_ADDRESS"); return _balanceOf[owner]; } /*////////////////////////////////////////////////////////////// ERC721 APPROVAL STORAGE //////////////////////////////////////////////////////////////*/ mapping(uint256 => address) public getApproved; mapping(address => mapping(address => bool)) public isApprovedForAll; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; } /*////////////////////////////////////////////////////////////// ERC721 LOGIC //////////////////////////////////////////////////////////////*/ function approve(address spender, uint256 id) public virtual { address owner = _ownerOf[id]; require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "NOT_AUTHORIZED"); getApproved[id] = spender; emit Approval(owner, spender, id); } function setApprovalForAll(address operator, bool approved) public virtual { isApprovedForAll[msg.sender][operator] = approved; emit ApprovalForAll(msg.sender, operator, approved); } function transferFrom( address from, address to, uint256 id ) public virtual { require(from == _ownerOf[id], "WRONG_FROM"); require(to != address(0), "INVALID_RECIPIENT"); require( msg.sender == from || isApprovedForAll[from][msg.sender] || msg.sender == getApproved[id], "NOT_AUTHORIZED" ); // Underflow of the sender's balance is impossible because we check for // ownership above and the recipient's balance can't realistically overflow. unchecked { _balanceOf[from]--; _balanceOf[to]++; } _ownerOf[id] = to; delete getApproved[id]; emit Transfer(from, to, id); } function safeTransferFrom( address from, address to, uint256 id ) public virtual { transferFrom(from, to, id); require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } function safeTransferFrom( address from, address to, uint256 id, bytes calldata data ) public virtual { transferFrom(from, to, id); require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } /*////////////////////////////////////////////////////////////// ERC165 LOGIC //////////////////////////////////////////////////////////////*/ function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { return interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata } /*////////////////////////////////////////////////////////////// INTERNAL MINT/BURN LOGIC //////////////////////////////////////////////////////////////*/ function _mint(address to, uint256 id) internal virtual { require(to != address(0), "INVALID_RECIPIENT"); require(_ownerOf[id] == address(0), "ALREADY_MINTED"); // Counter overflow is incredibly unrealistic. unchecked { _balanceOf[to]++; } _ownerOf[id] = to; emit Transfer(address(0), to, id); } function _burn(uint256 id) internal virtual { address owner = _ownerOf[id]; require(owner != address(0), "NOT_MINTED"); // Ownership check above ensures no underflow. unchecked { _balanceOf[owner]--; } delete _ownerOf[id]; delete getApproved[id]; emit Transfer(owner, address(0), id); } /*////////////////////////////////////////////////////////////// INTERNAL SAFE MINT LOGIC //////////////////////////////////////////////////////////////*/ function _safeMint(address to, uint256 id) internal virtual { _mint(to, id); require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } function _safeMint( address to, uint256 id, bytes memory data ) internal virtual { _mint(to, id); require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } } /// @notice A generic interface for a contract which properly accepts ERC721 tokens. /// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol) abstract contract ERC721TokenReceiver { function onERC721Received( address, address, uint256, bytes calldata ) external virtual returns (bytes4) { return ERC721TokenReceiver.onERC721Received.selector; } } // OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol) // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } /** * @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 Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the deployer as the initial owner. */ constructor() { _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 anymore. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby removing 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); } } // Interface for Molecule Protocol Smart Contract interface IMoleculeController { // Human readable name of the controller // string public _controllerName; // // selected logic combinations // uint32[] private _selected; // Gated: gated by logic // Blocked: always return `false` // Bypassed: always return `true` enum Status { Gated, Blocked, Bypassed } // // list id => atomic logic contract address // mapping(uint32 => address) private _logicContract; // // list id => allow- (true) or block- (false) list // mapping(uint32 => bool) private _isAllowList; // // list id => list name // mapping(uint32 => string) private _name; // // list id => logic modifier: add negation if true or false for as-is // mapping(uint32 => bool) private _reverseLogic; // NOT used, always false // event emitted when the controller name is changed event ControllerNameUpdated(string name); // event emitted when a new list is added event LogicAdded( uint32 indexed id, address indexed logicContract, bool isAllowList, string name, bool reverseLogic ); // event emitted when a list is removed event LogicRemoved(uint32 indexed id); // event emitted when a new logic combination is selected event Selected(uint32[] ids); // event emitted when status changed event StatusChanged(Status status); // Use default logic combination function check(address toCheck) external view returns (bool); // Use custom logic combination, passed in as an array of list ids function check( uint32[] memory ids, address toCheck ) external view returns (bool); // Get the current selected logic combination function selected() external view returns (uint32[] memory); // Get the current status of the contract function status() external view returns (Status); // Owner only functions // Control the status of the contract function setStatus(Status status) external; // Preselect logic combinations function select(uint32[] memory ids) external; // Add a new logic function addLogic( uint32 id, address logicContract, string memory name, bool reverseLogic ) external; // Remove a logic function removeLogic(uint32 id) external; // Add new logics in batch function addLogicBatch( uint32[] memory ids, address[] memory logicContracts, string[] memory names, bool[] memory reverseLogics ) external; // Remove logics in batch function removeLogicBatch(uint32[] memory ids) external; } contract ERC721m is ERC721, Ownable { enum MoleculeType { Approve, Burn, Mint, Transfer } // Custom error definitions error AccountNotAllowedToMint(address to); error AccountNotAllowedToBurn(address burner); error SenderNotAllowedToTransfer(address sender); error OwnerNotAllowedToApprove(address owner); error SpenderNotAllowedToReceive(address spender); // Molecule addresses address public _moleculeApprove; address public _moleculeBurn; address public _moleculeMint; address public _moleculeTransfer; event MoleculeUpdated(address molecule, MoleculeType mtype); constructor( string memory _name, string memory _symbol ) ERC721(_name, _symbol) {} function mint(address to, uint256 tokenId) external { if (_moleculeMint != address(0)) { if (!IMoleculeController(_moleculeMint).check(to)) { revert AccountNotAllowedToMint(to); } } _safeMint(to, tokenId); } function burn(uint256 tokenId) external { address tokenOwner = ownerOf(tokenId); if (_moleculeBurn != address(0)) { if (!IMoleculeController(_moleculeBurn).check(tokenOwner)) { revert AccountNotAllowedToBurn(tokenOwner); } } _burn(tokenId); } // Transfer function: not internal in solmate contract function transferFrom( address from, address to, uint256 tokenId ) public virtual override { if (_moleculeTransfer != address(0)) { if (!IMoleculeController(_moleculeTransfer).check(from)) { revert SenderNotAllowedToTransfer(from); } if (!IMoleculeController(_moleculeTransfer).check(to)) { revert SpenderNotAllowedToReceive(to); } } super.transferFrom(from, to, tokenId); } // @internal functions: applying gating at the internal function level, will apply to both transferFrom and safeTransferFrom // Approve function function approve(address to, uint256 tokenId) public virtual override { if (_moleculeApprove != address(0)) { address tokenOwner = ownerOf(tokenId); if (!IMoleculeController(_moleculeApprove).check(tokenOwner)) { revert OwnerNotAllowedToApprove(tokenOwner); } if (!IMoleculeController(_moleculeApprove).check(to)) { revert SpenderNotAllowedToReceive(to); } } super.approve(to, tokenId); } // Owner only functions // Molecule ERC721 token function updateMolecule( address molecule, MoleculeType mtype ) external onlyOwner { // allows 0x0 address to be set to remove molecule access control if (mtype == MoleculeType.Approve) { _moleculeApprove = molecule; } else if (mtype == MoleculeType.Burn) { _moleculeBurn = molecule; } else if (mtype == MoleculeType.Mint) { _moleculeMint = molecule; } else if (mtype == MoleculeType.Transfer) { _moleculeTransfer = molecule; } emit MoleculeUpdated(molecule, mtype); } function tokenURI( uint256 id ) public view virtual override returns (string memory) {} }