mirror of
https://github.com/tcsenpai/ERC721E.git
synced 2025-06-02 16:50:07 +00:00
Initial commit
This commit is contained in:
commit
01d002758f
456
ERC721E.sol
Normal file
456
ERC721E.sol
Normal 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
66
README.md
Normal 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
|
Loading…
x
Reference in New Issue
Block a user