ERC721E/ERC721E.sol
thecookingsenpai 01d002758f Initial commit
2023-12-25 13:27:18 +01:00

456 lines
16 KiB
Solidity

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