Initial commit

This commit is contained in:
thecookingsenpai 2023-12-25 13:27:18 +01:00
commit 01d002758f
3 changed files with 522 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

456
ERC721E.sol Normal file
View File

@ -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
}

66
README.md Normal file
View File

@ -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