commit 01d002758f73734e812e7f2763df6a745f08b08e Author: thecookingsenpai Date: Mon Dec 25 13:27:18 2023 +0100 Initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..4834ea0 Binary files /dev/null and b/.DS_Store differ diff --git a/ERC721E.sol b/ERC721E.sol new file mode 100644 index 0000000..bb64a17 --- /dev/null +++ b/ERC721E.sol @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: CC-BY-ND-4.0 + +pragma solidity ^0.8.15; + + +contract protected { + mapping (address => bool) is_auth; + function authorized(address addy) public view returns(bool) { + return is_auth[addy]; + } + function set_authorized(address addy, bool booly) public onlyAuth { + is_auth[addy] = booly; + } + modifier onlyAuth() { + require( is_auth[msg.sender] || msg.sender==owner, "not owner"); + _; + } + address owner; + modifier onlyOwner() { + require(msg.sender==owner, "not owner"); + _; + } + bool locked; + modifier safe() { + require(!locked, "reentrant"); + locked = true; + _; + locked = false; + } + function change_owner(address new_owner) public onlyAuth { + owner = new_owner; + } + receive() external payable {} + fallback() external payable {} +} + +/** + * @title ERC721 token receiver interface + * @dev Interface for any contract that wants to support safeTransfers + * from ERC721 asset contracts. + */ +interface IERC721Receiver { + /** + * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} + * by `operator` from `from`, this function is called. + * + * It must return its Solidity selector to confirm the token transfer. + * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + * + * The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`. + */ + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external returns (bytes4); +} + +contract ModernTypes { + // ANCHOR Uint to string conversion + function UINT_TO_STRING(uint _i) internal pure returns (string memory _uintAsString) { + if (_i == 0) { + return "0"; + } + uint j = _i; + uint len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint k = len; + while (_i != 0) { + k = k-1; + uint8 temp = (48 + uint8(_i - _i / 10 * 10)); + bytes1 b1 = bytes1(temp); + bstr[k] = b1; + _i /= 10; + } + return string(bstr); + } +} + + +contract ERC721E is protected, ModernTypes { + + /* ANCHOR Common properties */ + string public name; + string public symbol; + + /* ANCHOR 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); + + /* ANCHOR Safety */ + modifier Contract { + require(msg.sender==address(this), "Only the contract can call this function"); + _; + } + + /* ANCHOR Constructor */ + constructor() { + owner = msg.sender; + is_auth[owner] = true; + } + + // SECTION Structures for on chain metadata + + struct ATTRIBUTE { + string value_type; + string value; + } + + struct ATTRIBUTES { + ATTRIBUTE[] attributes; + mapping (string => uint) attributes_by_name; + mapping (uint => string) attributes_by_index; + uint attributes_head; // 0 is reserved + } + + struct TOKEN { + string description; + string external_url; + string image; + string name; + ATTRIBUTES attributes; + } + + // !SECTION + + // SECTION Gas saving datatypes for iterable mappings and mapped arrays + mapping(uint => TOKEN) tokens; // uint => token (tokenID) + mapping(uint => address) token_owner; // uint => owner (owner of token ID) + uint public tokens_head; // last unused tokenID + mapping(address => uint[]) tokens_owned; // address => array of tokenIDs owned by address + mapping(address => mapping(uint => uint)) tokens_index_in_owner; // address => tokenId => index in array of tokens_owned + mapping(address => mapping(uint => bool)) public tokens_ownership; // holder -> tokenIDs (ownership) + // !SECTION + + // SECTION On Chain Metadata Operations + + // SECTION Manipulate token metadata + function _set_tokenMetadata(uint token, + string memory image, + string memory description, + string memory external_url, + string memory _name) + public Contract{ + tokens[token].image = image; + tokens[token].description = description; + tokens[token].external_url = external_url; + tokens[token].name = _name; + } + // !SECTION Manipulate token metadata + + // SECTION Setting attributes + function _set_tokenAttributes(uint token, string[] memory types, string[] memory values) + public Contract { + require(types.length == values.length, "Types and values must be the same length"); + uint converted_index; + for (uint i = 0; i < types.length; i++) { + // We can check if is already there + if (tokens[token].attributes.attributes_by_name[types[i]] == 0) { + // If not, we add it to the list + converted_index = tokens[token].attributes.attributes_head; + tokens[token].attributes.attributes_by_name[types[i]] = converted_index; + tokens[token].attributes.attributes_by_index[converted_index] = types[i]; + tokens[token].attributes.attributes[converted_index].value_type = types[i]; + tokens[token].attributes.attributes[converted_index].value = values[i]; + // We also update the array to reflect attributes + tokens[token].attributes.attributes[converted_index].value_type = types[i]; + tokens[token].attributes.attributes[converted_index].value = values[i]; + // And we update the head + tokens[token].attributes.attributes_head = converted_index+1; + } + else { + // If it is, we update it + converted_index = tokens[token].attributes.attributes_by_name[types[i]]; + tokens[token].attributes.attributes[converted_index].value = values[i]; + // And we update the array too + tokens[token].attributes.attributes[converted_index].value = values[i]; + } + } + } + + // !SECTION Setting attributes + + // SECTION Returns a token metadata + // NOTE You can define tokenURI as a call to this function + function get_token(uint token) + public view returns(string memory metadata_json) { + string memory token_name = tokens[token].name; + string memory token_description = tokens[token].description; + string memory token_external_url = tokens[token].external_url; + string memory token_image = tokens[token].image; + // NOTE Getting attributes scanning the array by index + string memory token_attributes = ""; + uint total_attributes = tokens[token].attributes.attributes_head; + for (uint i = 0; i < total_attributes; i++) { + token_attributes = string.concat( + token_attributes, + '{ "trait_type": "', + tokens[token].attributes.attributes[i].value_type, + '", "value": "', + tokens[token].attributes.attributes[i].value, + '" }', + ',' + ); + } + string[13] memory delimited; + delimited = [ + '{ "name": ', token_name, + ', "description": ', token_description, + ', "external_url": ', token_external_url, + ', "image": ', token_image, + ', "attributes": [', + token_attributes, + ' {"trait_type": "ID", "value": "', UINT_TO_STRING(token), '"}' // NOTE Avoid problems with commas too + ' ]' + ' }' + ]; + + // ANCHOR Concatenating properties in a json + string memory json; + for (uint i = 0; i < delimited.length; i++) { + json = string.concat(json, delimited[i]); + } + return json; + } + // !SECTION Returns a token metadata + + // !SECTION On Chain Metadata Operations + + /* ANCHOR ERC721 compliance */ + + // SECTION Variables + + mapping(uint => address) _allowances; // tokenID => address (who is allowed to spend it) + mapping(address => mapping(address => bool)) _operatorApprovals; // address => address (who is allowed to spend) + // !SECTION Variables + + // SECTION ERC165 Logic + function supportsInterface(bytes4 interfaceId) public pure returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 + interfaceId == 0x780e9d63 || // ERC165 Interface ID for ERC721Enumerable + interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata + } + // !SECTION ERC165 Logic + + // NOTE Returns the total number of circulating tokens + function totalSupply() public view returns (uint256) { + return tokens_head; + } + + // NOTE Returns the metadata-fetchable url for a given ID + function tokenURI(uint256 tokenId) public view returns (string memory) { + + } + + // SECTION ERC721Enumerable Logic + + // NOTE Returns the ID of a token by index of owned tokens + // e.g. tokenIdByIndex(0) returns the ID of the first token owned by the caller + function tokenOfOwnerByIndex(address _owner, uint256 index) public view + returns (uint256 tokenId) { + require(tokens_owned[_owner].length > 0, "No tokens owned"); + require(index < tokens_owned[_owner].length, "Index out of range"); + return tokens_owned[owner][index]; + } + + // NOTE Return a token ID if exists + function tokenByIndex(uint256 index) public view returns (uint256) { + require(index < tokens_head, "Index out of range"); + return index; + } + + // !SECTION ERC721Enumerable Logic + + // SECTION ERC721 Logic + + // NOTE Return the balance of a single address + function balanceOf(address _owner) public view returns (uint256) { + return tokens_owned[_owner].length; + } + + function ownerOf(uint256 tokenId) public view returns (address) { + return token_owner[tokenId]; + } + + // NOTE Approve spending of an ID to a single address + function approve(address to, uint256 tokenId) public returns(bool) { + address _owner = token_owner[tokenId]; + if (!isApprovedForAll(_owner, msg.sender)) { + require(_owner == msg.sender, "Only the owner can approve"); + } + require(!(to==msg.sender), "Cannot approve to yourself"); + _allowances[tokenId] = to; + emit Approval(_owner, to, tokenId); + return true; + } + + // Get approved address for a given ID + function getApproved(uint256 tokenId) public view returns (address) { + require(tokenId < tokens_head, "Token ID out of range"); + return _allowances[tokenId]; + } + + // NOTE Allows a single address to spend tokens on behalf of another address + function setApprovalForAll(address operator, bool approved) public { + require(!(operator == msg.sender), "Cannot set approval to yourself"); + _operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + // NOTE Checks if an operator can act on behalf of an owner + function isApprovedForAll(address _owner, address operator) + public view returns (bool) { + return _operatorApprovals[_owner][operator]; + } + + // NOTE Transfer a token or transfer on behalf of an address + function transferFrom( + address from, + address to, + uint256 tokenId + ) public { + // Safety checks + require(tokenId < tokens_head, "Token does not exists"); + require(ownerOf(tokenId) == from, "Only the owner can transfer"); + require(to != address(0), "Cannot transfer to the null address"); + + // Approvals check + bool isApprovedOrOwner = (msg.sender == from || + msg.sender == getApproved(tokenId) || + isApprovedForAll(from, msg.sender)); + require(isApprovedOrOwner, "Only the owner or the approved address can transfer"); + + // Delete token approvals from previous owner + _allowances[tokenId] = address(0); + + // Assign the new ownership + token_owner[tokenId] = to; + tokens_ownership[to][tokenId] = true; + // Deleting the old ownership by replacing it with the last one owned if any + tokens_ownership[from][tokenId] = false; + if(tokens_owned[from].length > 1) { + uint index_in_owner = tokens_index_in_owner[from][tokenId]; + uint last_id_owned = tokens_owned[from][tokens_owned[from].length - 1]; + tokens_index_in_owner[from][last_id_owned] = index_in_owner; + tokens_owned[from][index_in_owner] = last_id_owned; + delete tokens_owned[from][tokens_owned[from].length - 1]; + } + // Otherwise it just delete the only owned one + else { + delete tokens_index_in_owner[from][0]; + } + + emit Transfer(from, to, tokenId); + } + + // NOTE Enable transfers to be checked against unreceivable contracts + function _checkOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) private returns (bool) { + if (to.code.length == 0) return true; + + try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, _data) returns (bytes4 retval) { + return retval == IERC721Receiver(to).onERC721Received.selector; + } catch (bytes memory reason) { + require (reason.length != 0, "ERC721Receiver not found"); + + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + + // NOTE Empowers the previous method to enable safeTransfers (if anyone uses them) + function safeTransferFrom( + address from, + address to, + uint256 id + ) public { + safeTransferFrom(from, to, id, ''); + } + + // NOTE Overloaded: empowers the previous method to enable safeTransfers (if anyone uses them) + function safeTransferFrom( + address from, + address to, + uint256 id, + bytes memory data + ) public { + transferFrom(from, to, id); + + require (_checkOnERC721Received(from, to, id, data), "Transfer failed on ERC721Receiver"); + } + + // !SECTION ERC721 Logic + + // SECTION Minting logic + + // NOTE Empowers safety checks for minting (only once to save gas) + function safeMint(address to, uint256 qty) public { + safeMint(to, qty, ''); + } + + // NOTE Overloaded: empowers safety checks for minting (only once to save gas) + function safeMint( + address to, + uint256 qty, + bytes memory data + ) public { + _mint(to, qty); + + require(_checkOnERC721Received(address(0), to, tokens_head, data), "Mint failed on ERC721Receiver"); + } + + function _mint(address to, uint256 qty) internal { + require (to != address(0), "Cannot mint to the null address"); + require (qty != 0, "Cannot mint 0 tokens"); + + uint256 _currentIndex = tokens_head; // Reminder: tokens_head is the last UNUSED tokenID + + // Cannot realistically overflow, since we are using uint256 + unchecked { + for (uint256 i; i < qty - 1; i++) { + // Assign the ownership + token_owner[_currentIndex + i] = to; // +0, +1, +2, +3, ... + // Insert the current token into the owner's array + tokens_owned[to].push(_currentIndex + i); + // Set the position of the current token in the owner array + tokens_index_in_owner[to][_currentIndex + i] = tokens_owned[to].length - 1; + // Set ownership to true for further checks + tokens_ownership[to][_currentIndex + i] = true; + // Event emission + emit Transfer(address(0), to, _currentIndex + i); + } + // Plain increasing of total tokens + tokens_head += qty; + } + + } + + // !SECTION Minting Logic + +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..31ad687 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# ERC721E + +## Another low gas ERC721 implementation + +### But this time with on-chain metadata + +ERC721A was cheap on gas. +ERC721B was even cheaper and smarter. +ERC721E is cheaper, smarter and fully on-chain. + +#### Why is cheaper? + +Because minting, enumerating and many other features are implemented using efficient types and ignoring the usual practices, especially regarding enumeration of owned tokens and enumeration of token ids. + +#### Why metadata on chain? + +Because IPFS isn't that much decentralized and your NFTs could easily disappear (actually they does usually after a while) if the metadata isn't pinned or the gateway is not available. + +With on chain metadata, you can be sure that the metadata is always available. + +#### But images can't be on chain! + +Well, probably is better to store images externally (which by the way does not damage your metadata as it can be updated easily if the image is gone). + +Anyway, you could even store SVG textual data into the image url and have them on chain too. + +#### Is this compatible with Opensea? + +Yes of couse. + +#### How to use? + +Import the contract in your contract and write something to call _mint (as it is internal by default) with your own rules. + +Example: + +``` +import './ERC721E.sol' + +contract MyNFT is ERC721E { + + [YOU MIGHT WANT TO SET NAME AND SYMBOL HERE] + name = "My NFT"; + symbol = "MNFT"; + + constructor() { + [...] + } + + mint(uint qty) payable public { + [YOUR RULES] + _mint(msg.sender, qty); + } + + [YOUR OTHER STUFF] + +} +``` + +### Credits + +https://github.com/chiru-labs/ERC721A for the original inspiration + +https://github.com/beskay/ERC721B for further inspiration + +https://stackoverflow.com for the multiple inputs especially on types conversion \ No newline at end of file