Created
May 26, 2023 15:31
-
-
Save aesthezel/dfb0e2b416872fd16e1f701e6be06674 to your computer and use it in GitHub Desktop.
Entity DNA storage
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity >=0.8.0 <0.9.0; | |
import "@openzeppelin/contracts/utils/Address.sol"; | |
import "@openzeppelin/contracts/utils/Strings.sol"; | |
import "@openzeppelin/contracts/access/Ownable.sol"; | |
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; | |
contract EntityDNA is Ownable | |
{ | |
using Address for address; | |
using Strings for uint256; | |
struct Entity | |
{ | |
uint256 id; | |
uint256 baseId; | |
bool isBase; | |
bool isGenesis; | |
uint[] genesisTraits; | |
uint seed; | |
uint dna; | |
address owner; | |
bool isBurned; | |
} | |
mapping(uint256 => Entity) private entities; | |
mapping(uint256 => address) private originalOwner; | |
mapping(uint256 => address) private _tokenApprovals; | |
mapping(address => mapping(address => bool)) private _operatorApprovals; | |
mapping(address => uint256) private _balances; | |
// ERC721Enumerable | |
mapping(address => mapping(uint256 => uint256)) private _ownedTokens; | |
mapping(uint256 => uint256) private _ownedTokensIndex; | |
uint256[] private _allTokens; | |
mapping(uint256 => uint256) private _allTokensIndex; | |
uint256 public totalEntities; | |
uint[] private _bitsInitial; | |
uint[] private _bitsCapacity; | |
string private _name; | |
string private _symbol; | |
string private _tokenURIServer; | |
event Transfer(address indexed _from, address indexed _to, uint256 indexed _id); | |
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _id); | |
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); | |
constructor(string memory name, string memory symbol, string memory serverURI, uint[] memory bits) | |
{ | |
_name = name; | |
_symbol = symbol; | |
_tokenURIServer = serverURI; | |
require(bits.length == 10, "The bits length doesn't equal to eleven parameters"); | |
_bitsInitial = bits; | |
for(uint i = 0; i < bits.length; i++) | |
{ | |
_bitsCapacity.push(log2(bits[i])); | |
} | |
} | |
function transferFrom (address _from, address _to, uint256 _tokenId) external payable | |
{ | |
require(_isApprovedOrOwner(_msgSender(), _tokenId), "ERC721: transfer caller is not owner nor approved"); | |
_transfer(_from, _to, _tokenId); | |
} | |
function mintEntities(address to, uint quantity) public onlyOwner() | |
{ | |
entities[totalEntities].id = totalEntities; | |
entities[totalEntities].seed = block.timestamp + block.difficulty * 1; | |
entities[totalEntities].baseId = totalEntities; | |
entities[totalEntities].isBase = true; | |
entities[totalEntities].isGenesis = false; | |
entities[totalEntities].owner = to; | |
entities[totalEntities].dna = randomEntity(block.timestamp + block.difficulty * 1, totalEntities * _bitsCapacity.length); | |
originalOwner[totalEntities] = to; | |
_balances[to] += quantity; | |
totalEntities += quantity; | |
} | |
function mintSaleEntities(address to, uint quantity, uint baseFamily, uint familyInitialLimit, uint familyFinalLimit, uint baseRarity) public onlyOwner() | |
{ | |
entities[totalEntities].id = totalEntities; | |
entities[totalEntities].seed = block.timestamp + block.difficulty * 1; | |
entities[totalEntities].baseId = totalEntities; | |
entities[totalEntities].isBase = true; | |
entities[totalEntities].isGenesis = true; | |
entities[totalEntities].owner = to; | |
entities[totalEntities].dna = randomLimitedEntity(block.timestamp + block.difficulty * 1, | |
totalEntities * _bitsCapacity.length, | |
baseFamily, | |
familyInitialLimit, | |
familyFinalLimit, | |
baseRarity); | |
// Genesis Traits | |
entities[totalEntities].genesisTraits.push(baseFamily); | |
entities[totalEntities].genesisTraits.push(familyInitialLimit); | |
entities[totalEntities].genesisTraits.push(familyFinalLimit); | |
entities[totalEntities].genesisTraits.push(baseRarity); | |
originalOwner[totalEntities] = to; | |
_balances[to] += quantity; | |
totalEntities += quantity; | |
} | |
function getEntityDNA(uint id) public view returns (uint dna) | |
{ | |
Entity memory entity = getVirtualEntity(id); | |
uint seed = entity.seed; | |
uint seedExtra = (id - entity.baseId) * _bitsCapacity.length; | |
return entity.isGenesis ? | |
randomLimitedEntity( | |
seed, | |
seedExtra, | |
entity.genesisTraits[0], | |
entity.genesisTraits[1], | |
entity.genesisTraits[2], | |
entity.genesisTraits[3]) : | |
randomEntity(seed, seedExtra); | |
} | |
function getVirtualEntity(uint id) public view returns (Entity memory) | |
{ | |
require(id < totalEntities, "Entity does not exist!"); | |
if(entities[id].seed != 0) return entities[id]; | |
// If the Entity doesnt generated yet | |
Entity storage baseNFT = entities[0]; | |
for(uint i = id; i >= 0; i--) | |
{ | |
if(entities[i].isBase) | |
{ | |
baseNFT = entities[i]; | |
break; | |
} | |
} | |
uint seed = baseNFT.seed; | |
uint seedExtra = (id - baseNFT.id) * _bitsCapacity.length; | |
Entity memory entity; | |
entity.id = id; | |
entity.baseId = baseNFT.id; | |
entity.dna = baseNFT.isGenesis ? | |
randomLimitedEntity(seed, | |
seedExtra, | |
baseNFT.genesisTraits[0], | |
baseNFT.genesisTraits[1], | |
baseNFT.genesisTraits[2], | |
baseNFT.genesisTraits[3]) : | |
randomEntity(seed, seedExtra); | |
entity.owner = originalOwner[baseNFT.id]; | |
entity.seed = baseNFT.seed; | |
entity.isBase = false; | |
entity.isGenesis = baseNFT.isGenesis; | |
entity.genesisTraits = baseNFT.genesisTraits; | |
return entity; | |
} | |
function randomEntity(uint seed, uint startingSeed) private view returns (uint) | |
{ | |
uint cromo = 0; | |
uint bitPosition = 0; | |
uint lastFamily = 0; | |
uint randomValue = 0; | |
for(uint i = 0; i < _bitsCapacity.length; i++) | |
{ | |
randomValue = random(_bitsInitial[i], seed, startingSeed + i); | |
if(i % 2 == 0) | |
{ | |
lastFamily = random(_bitsInitial[i], seed, startingSeed + i); | |
} | |
if(i % 2 != 0) | |
{ | |
if(randomValue > 4 && lastFamily < 12) | |
{ | |
randomValue = 4 - random(4, seed, startingSeed % i); | |
} | |
else if (lastFamily >= 12 && randomValue < 5) | |
{ | |
randomValue = 9 - random(4, seed, startingSeed % i); | |
} | |
} | |
cromo = cromo | (randomValue << bitPosition); | |
bitPosition += _bitsCapacity[i]; | |
} | |
return cromo; | |
} | |
// Custom random properties selected by DNA bites | |
function randomLimitedEntity(uint seed, uint startingSeed, uint baseFamily, uint familyInitialLimit, uint familyFinalLimit, uint baseRarity) private view returns (uint) | |
{ | |
uint cromo = 0; | |
uint bitPosition = 0; | |
uint randomValue = 0; | |
for(uint i = 0; i < _bitsCapacity.length; i++) | |
{ | |
if(i % 2 == 0 && i != 0) | |
{ | |
randomValue = familyInitialLimit + (familyFinalLimit - random(familyFinalLimit, seed, startingSeed + i)); | |
} | |
else if(i == 0) | |
{ | |
randomValue = baseFamily; | |
} | |
if(i % 2 != 0) | |
{ | |
randomValue = baseRarity; | |
} | |
cromo = cromo | (randomValue << bitPosition); | |
bitPosition += _bitsCapacity[i]; | |
} | |
return cromo; | |
} | |
function random(uint256 max, uint256 seed, uint seedExtra) private pure returns (uint256) | |
{ | |
return uint256( | |
keccak256( | |
abi.encodePacked( | |
seed, | |
seedExtra | |
) | |
) | |
) % max; | |
} | |
// Helper function get a base 2 - Economic way gas < 700; | |
function log2(uint x) private pure returns (uint y) | |
{ | |
assembly | |
{ | |
let arg := x | |
x := sub(x,1) | |
x := or(x, div(x, 0x02)) | |
x := or(x, div(x, 0x04)) | |
x := or(x, div(x, 0x10)) | |
x := or(x, div(x, 0x100)) | |
x := or(x, div(x, 0x10000)) | |
x := or(x, div(x, 0x100000000)) | |
x := or(x, div(x, 0x10000000000000000)) | |
x := or(x, div(x, 0x100000000000000000000000000000000)) | |
x := add(x, 1) | |
let m := mload(0x40) | |
mstore(m, 0xf8f9cbfae6cc78fbefe7cdc3a1793dfcf4f0e8bbd8cec470b6a28a7a5a3e1efd) | |
mstore(add(m,0x20), 0xf5ecf1b3e9debc68e1d9cfabc5997135bfb7a7a3938b7b606b5b4b3f2f1f0ffe) | |
mstore(add(m,0x40), 0xf6e4ed9ff2d6b458eadcdf97bd91692de2d4da8fd2d0ac50c6ae9a8272523616) | |
mstore(add(m,0x60), 0xc8c0b887b0a8a4489c948c7f847c6125746c645c544c444038302820181008ff) | |
mstore(add(m,0x80), 0xf7cae577eec2a03cf3bad76fb589591debb2dd67e0aa9834bea6925f6a4a2e0e) | |
mstore(add(m,0xa0), 0xe39ed557db96902cd38ed14fad815115c786af479b7e83247363534337271707) | |
mstore(add(m,0xc0), 0xc976c13bb96e881cb166a933a55e490d9d56952b8d4e801485467d2362422606) | |
mstore(add(m,0xe0), 0x753a6d1b65325d0c552a4d1345224105391a310b29122104190a110309020100) | |
mstore(0x40, add(m, 0x100)) | |
let magic := 0x818283848586878898a8b8c8d8e8f929395969799a9b9d9e9faaeb6bedeeff | |
let shift := 0x100000000000000000000000000000000000000000000000000000000000000 | |
let a := div(mul(x, magic), shift) | |
y := div(mload(add(m,sub(255,a))), shift) | |
y := add(y, mul(256, gt(arg, 0x8000000000000000000000000000000000000000000000000000000000000000))) | |
} | |
} | |
// ERC721 | |
function balanceOf(address _owner) public view returns (uint256) | |
{ | |
require(_owner != address(0), "ERC721: balance query for the zero address"); | |
return _balances[_owner]; | |
} | |
function ownerOf(uint256 _tokenId) public view returns (address) | |
{ | |
Entity memory entity = getVirtualEntity(_tokenId); | |
require(entity.owner != address(0), "ERC721: owner query for nonexistent token"); | |
return entity.owner; | |
} | |
function safeTransferFrom(address _from, address _to, uint256 _tokenId) public payable | |
{ | |
safeTransferFrom(_from, _to, _tokenId, ""); | |
} | |
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) public payable | |
{ | |
require(_isApprovedOrOwner(_msgSender(), _tokenId), "ERC721: transfer caller is not owner nor approved"); | |
_safeTransfer(_from, _to, _tokenId, data); | |
} | |
function _safeTransfer(address from, address to, uint256 tokenId, bytes memory _data) internal virtual | |
{ | |
_transfer(from, to, tokenId); | |
require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer"); | |
} | |
function _transfer(address from, address to, uint256 id) internal virtual | |
{ | |
Entity memory entity = getVirtualEntity(id); | |
require(entity.owner == from, "ERC721: transfer from incorrect owner"); | |
require(to != address(0), "ERC721: transfer to the zero address"); | |
address fromQuestion = from.isContract() ? address(0) : from; | |
_beforeTokenTransfer(fromQuestion, to, id); | |
// Clear approvals from the previous owner | |
_approve(address(0), id); | |
_balances[from] -= 1; | |
_balances[to] += 1; | |
entities[id].owner = to; | |
if(entities[id].seed == 0) | |
{ | |
uint seed = entity.seed; | |
uint seedExtra = (id - entity.baseId) * _bitsCapacity.length; | |
entities[id].id = id; | |
entities[id].baseId = entity.baseId; | |
entities[id].seed = entity.seed; | |
entities[id].dna = entity.isGenesis ? | |
randomLimitedEntity(seed, | |
seedExtra, | |
entity.genesisTraits[0], | |
entity.genesisTraits[1], | |
entity.genesisTraits[2], | |
entity.genesisTraits[3]) : | |
randomEntity(seed, seedExtra); | |
entities[id].isBase = false; | |
entities[id].isGenesis = entity.isGenesis; | |
entities[id].genesisTraits = entity.genesisTraits; | |
} | |
emit Transfer(from, to, id); | |
_afterTokenTransfer(from, to, id); | |
} | |
function _isApprovedOrOwner(address spender, uint256 id) internal view virtual returns (bool) | |
{ | |
require(_exists(id), "ERC721: operator query for nonexistent token"); | |
address owner = ownerOf(id); | |
return (spender == owner || isApprovedForAll(owner, spender) || getApproved(id) == spender); | |
} | |
function _exists(uint256 id) internal view virtual returns (bool) | |
{ | |
Entity memory entity = getVirtualEntity(id); | |
return entity.owner != address(0); | |
} | |
function getApproved(uint256 id) public view virtual returns (address) | |
{ | |
require(_exists(id), "ERC721: approved query for nonexistent token"); | |
return _tokenApprovals[id]; | |
} | |
function approve(address _approved, uint256 _tokenId) external payable | |
{ | |
_approve(_approved, _tokenId); | |
} | |
function _approve(address to, uint256 id) internal virtual | |
{ | |
_tokenApprovals[id] = to; | |
emit Approval(ownerOf(id), to, id); | |
} | |
function setApprovalForAll(address _operator, bool _approved) public virtual | |
{ | |
_setApprovalForAll(_msgSender(), _operator, _approved); | |
} | |
function _setApprovalForAll(address owner, address operator, bool approved) internal virtual | |
{ | |
require(owner != operator, "ERC721: approve to caller"); | |
_operatorApprovals[owner][operator] = approved; | |
emit ApprovalForAll(owner, operator, approved); | |
} | |
function isApprovedForAll(address owner, address operator) public view returns (bool) | |
{ | |
return _operatorApprovals[owner][operator]; | |
} | |
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) private returns (bool) | |
{ | |
if (to.isContract()) { | |
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) { | |
return retval == IERC721Receiver.onERC721Received.selector; | |
} catch (bytes memory reason) { | |
if (reason.length == 0) { | |
revert("ERC721: transfer to non ERC721Receiver implementer"); | |
} else { | |
assembly { | |
revert(add(32, reason), mload(reason)) | |
} | |
} | |
} | |
} else { | |
return true; | |
} | |
} | |
function tokenURI(uint256 id) public view returns (string memory) | |
{ | |
require(_exists(id), "ERC721: request URI for nonexistent token"); | |
string memory generatedTokenURI = string(abi.encodePacked(_tokenURIServer, Strings.toString(id))); | |
return generatedTokenURI; | |
} | |
// END ERC721 | |
// ERC721Enumerable | |
function tokenOfOwnerByIndex(address owner, uint256 index) public view returns (uint256) { | |
require(index < balanceOf(owner), "ERC721Enumerable: owner index out of bounds"); | |
return _ownedTokens[owner][index]; | |
} | |
function totalSupply() public view returns (uint256) { | |
return totalEntities; | |
} | |
function tokenByIndex(uint256 index) public view returns (uint256) { | |
require(index < totalSupply(), "ERC721Enumerable: global index out of bounds"); | |
return _allTokens[index]; | |
} | |
function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal | |
{ | |
if (from == address(0)) { | |
_addTokenToAllTokensEnumeration(tokenId); | |
} else if (from != to) { | |
_removeTokenFromOwnerEnumeration(from, tokenId); | |
} | |
if (to == address(0)) { | |
_removeTokenFromAllTokensEnumeration(tokenId); | |
} else if (to != from) { | |
_addTokenToOwnerEnumeration(to, tokenId); | |
} | |
} | |
function _afterTokenTransfer(address from, address to, uint256 tokenId) internal {} | |
function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private { | |
uint256 length = balanceOf(to); | |
_ownedTokens[to][length] = tokenId; | |
_ownedTokensIndex[tokenId] = length; | |
} | |
function _addTokenToAllTokensEnumeration(uint256 tokenId) private { | |
_allTokensIndex[tokenId] = _allTokens.length; | |
_allTokens.push(tokenId); | |
} | |
function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private { | |
// To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and | |
// then delete the last slot (swap and pop). | |
uint256 lastTokenIndex = balanceOf(from) - 1; | |
uint256 tokenIndex = _ownedTokensIndex[tokenId]; | |
// When the token to delete is the last token, the swap operation is unnecessary | |
if (tokenIndex != lastTokenIndex) { | |
uint256 lastTokenId = _ownedTokens[from][lastTokenIndex]; | |
_ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token | |
_ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index | |
} | |
// This also deletes the contents at the last position of the array | |
delete _ownedTokensIndex[tokenId]; | |
delete _ownedTokens[from][lastTokenIndex]; | |
} | |
function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private { | |
// To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and | |
// then delete the last slot (swap and pop). | |
uint256 lastTokenIndex = _allTokens.length - 1; | |
uint256 tokenIndex = _allTokensIndex[tokenId]; | |
// When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so | |
// rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding | |
// an 'if' statement (like in _removeTokenFromOwnerEnumeration) | |
uint256 lastTokenId = _allTokens[lastTokenIndex]; | |
_allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token | |
_allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index | |
// This also deletes the contents at the last position of the array | |
delete _allTokensIndex[tokenId]; | |
_allTokens.pop(); | |
} | |
// END ERC721Enumerable | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment