Skip to content

Instantly share code, notes, and snippets.

@ponyjackal
Created September 14, 2022 03:19
Show Gist options
  • Save ponyjackal/6d6e0d4a880114a31d8b995818cf9965 to your computer and use it in GitHub Desktop.
Save ponyjackal/6d6e0d4a880114a31d8b995818cf9965 to your computer and use it in GitHub Desktop.
Non-dilutive NFT metadata
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import { IMimeticMetadata } from "./IMimeticMetadata.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
error MintExceedsMaxSupply();
error MintCostMismatch();
error MintNotEnabled();
error GenerationAlreadyLoaded();
error GenerationNotDifferent();
error GenerationNotEnabled();
error GenerationNotDowngradable();
error GenerationNotToggleable();
error GenerationCostMismatch();
error TokenNonExistent();
error TokenNotRevealed();
error TokenRevealed();
error TokenOwnerMismatch();
error WithdrawFailed();
/**
* @title Non-Dilutive 721
* @author nftchance
* @notice This token was created to serve as a proof for a conversational point. Non-dilutive 721
* tokens can exist. Teams can easily build around this concept. Teams can additionally
* still monetize the going ons and hard work of their team. However, that does not need to
* come at the cost of their holders. As it stands every token drop following the
* initial is a holder mining experience in which every single holders is impacted by the
* lower market concentration of liquidty and attention.
* @notice If you plan on yoinking this code. Please message me. Curiosity breeds progress. I am
* here to help if you need or want it. I do not want a cut; I do not want paid. I want a
* market of * honest and holder thoughtful devs. This is a very very weird 721
* implementation and comes with many nuances. I'd love to discuss.
* @notice Doodles drop of the Spaceships by wrapping into a new token is 100% dilutive.
* @dev The extendable 'Generations' wrap the token metadata within the content to remove the need
* of dropping another token into the collection. By doing this, that does not inherently
* mean the metadata is mutable beyond the extent that the token holder can change the
* active metadata. The underlying generations still much exist and can be configured in a
* way that allows accessing them again if desired. However, there does also exist the
* ability to have truly immutable layers that cannot be removed. (If following this
* implementation it is vitally noted that object permanence must be achieved from day one.
* A project CANNOT implement this on a mutable URL that is massive holder-trust betrayal.)
*/
contract MimeticMetadata is
IMimeticMetadata
,Ownable
{
using Strings for uint256;
uint256 public MAX_SUPPLY;
string public baseUnrevealedURI;
mapping(uint256 => Generation) public generations;
mapping(uint256 => uint256) tokenToGeneration;
mapping(bytes32 => uint256) tokenGenerationToFunded;
/**
* @notice Generates a psuedo-random number that is to be used for the
* metadata offset. In production, this realistically should be an
* implementation with VRF (Chainlink). It is incredibly easy to setup
* and use, additionally with this structure there is no reason it needs
* to be expensive.
* @dev A focus of psuedo-random number quality has not been a focus. In order for
* for the modulus to even return a fair chance for all #s it must be a
* power of 2.
* @param _layerId the generation the offset is used for.
*/
function _getOffset(
uint256 _layerId
)
internal
view
returns (
uint256
)
{
return uint256(
keccak256(
abi.encodePacked(
msg.sender
,_layerId
,block.number
,block.difficulty
)
)
) % MAX_SUPPLY + 1;
}
/**
* @notice Allows for generation-level reveal. That means that just because the assets
* in Generation Zero have been revealed, Generation One is not revealed. The
* reveal mechanisms of them are entirely separate. Precisely like a normal
* ERC721 token.
* @notice Cannot be reverted once a token has been revealed. No mutable metadata!
* @dev With this implementation it is vital that you implement and utilize an offset.
* This is not something that you can skip because you don't want to work
* with Chainlink or another VRF method. Even if not VRF, you must implement
* at least a generally fair offset mechanism. Holders for the most part
* do not know how Solidity works. That does not mean you take advantage of that.
* @param _layerId the generation that is being revealed
* @param _topTokenId the highest token id to be revealed
*/
function setRevealed(
uint256 _layerId
,uint256 _topTokenId
)
override
public
virtual
onlyOwner
{
Generation storage generation = generations[_layerId];
// Make sure the generation has been loaded and enabled
if(!generation.loaded || !generation.enabled) revert GenerationNotEnabled();
// Make sure that the amount of tokens revealed is not being lowered
if(_topTokenId < generation.top) revert TokenRevealed();
// Make sure that we create the offset the first time a generation is revealed
if(generation.offset == 0) {
generation.offset = _getOffset(_layerId);
}
// Finally set the top token of the generation
generation.top = _topTokenId;
}
/**
* @notice Allows users to calculate the metadata id that is associated with this token
* at all times on any of the layers. This is not code that validates the input
* as it operates like an internal function but has been exposed to holders
* for UX purposes.
* @param _offset how far the ids have been shifted
* @param _tokenId the token we are getting the generational data for
*/
function getGenerationToken(
uint256 _offset
,uint256 _tokenId
)
override
public
virtual
view
returns (
uint256 generationTokenId
)
{
generationTokenId = _tokenId + _offset - 1;
if (generationTokenId > MAX_SUPPLY) generationTokenId - MAX_SUPPLY - 1;
}
/**
* @notice Function that controls which metadata the token is currently utilizing.
* By default every token is using layer zero which is loaded during the time
* of contract deployment. Cannot be removed, is immutable, holders can always
* revert back. However, if at any time they choose to "wrap" their token then
* it is automatically reflected here.
* @notice Errors out if the token has not yet been revealed within this collection.
* @param _tokenId the token we are getting the URI for
* @return _tokenURI The internet accessible URI of the token
*/
function _tokenURI(
uint256 _tokenId
)
internal
virtual
view
returns (
string memory
)
{
// Make sure that the token has been minted
uint256 activeGenerationLayer = tokenToGeneration[_tokenId];
// Make sure that the token has been revealed
Generation memory activeGeneration = generations[activeGenerationLayer];
/**
* @dev Returns a non-token specific URI that is to be used for unrevealed tokens. This is
* not a case where every generation has it's own unrevealed URI. All generations
* utilize the same one so that "evolution in progress" is consistent across the collection.
*/
if(_tokenId > activeGeneration.top) return baseUnrevealedURI;
// Make sure the baseTokenId is within the bounds of MAX_SUPPLY and fix if not
// Apply the generational offset to the tokens metadata
uint256 generationTokenId = getGenerationToken(
activeGeneration.offset
,_tokenId
);
return string(
abi.encodePacked(
activeGeneration.baseURI
,generationTokenId.toString()
)
);
}
/**
* @notice Allows the project owner to establish a new generation. Generations are enabled by
* default. With this we initialize the generation to be loaded.
* @dev _name is passed as a param, if this is not needed; remove it. Don't be superfluous.
* @dev only accessed by owner of contract
* @param _layerId the z-depth of the metadata being loaded
* @param _enabled a generation can be connected before a token can utilize it
* @param _locked can this layer be disabled by the project owner
* @param _sticky can this layer be removed by the holder
* @param _cost the focus cost
* @param _evolutionClosure if set to zero, disabled. If not set to zero is the last timestamp
* at which someone can focus this generation.
* @param _baseURI the internet URI the metadata is stored on
*/
function loadGeneration(
uint256 _layerId
,bool _enabled
,bool _locked
,bool _sticky
,uint256 _cost
,uint256 _evolutionClosure
,string memory _baseURI
)
override
public
virtual
onlyOwner
{
Generation storage generation = generations[_layerId];
// Make sure that we are not overwriting an existing layer.
if(generation.loaded) revert GenerationAlreadyLoaded();
generations[_layerId] = Generation({
loaded: true
,enabled: _enabled
,locked: _locked
,sticky: _sticky
,cost: _cost
,evolutionClosure: _evolutionClosure
,baseURI: _baseURI
,offset: 0
,top: 0
});
}
/**
* @notice Used to toggle the state of a generation. Disable generations cannot be focused by
* token holders.
*/
function toggleGeneration(
uint256 _layerId
)
override
public
virtual
onlyOwner
{
Generation memory generation = generations[_layerId];
// Make sure that the token isn't locked (immutable but overlapping keywords is spicy)
if(generation.enabled && generation.locked) revert GenerationNotToggleable();
generations[_layerId].enabled = !generation.enabled;
}
/**
* @notice Allows any user to see the layer that a token currently has enabled.
*/
function _getTokenGeneration(
uint256 _tokenId
)
internal
virtual
view
returns(
uint256
)
{
return tokenToGeneration[_tokenId];
}
/**
* @notice Internal view function to clean up focusGeneration(). Pretty useless but the
* function was getting out of control.
*/
function _generationEnabled(Generation memory generation)
internal
view
returns (
bool
)
{
if(!generation.enabled) return false;
if(generation.evolutionClosure != 0) return block.timestamp < generation.evolutionClosure;
return true;
}
/**
* @notice Function that allows token holders to focus a generation and wear their skin.
* This is not in control of the project maintainers once the layer has been
* initialized.
* @dev This function is utilized when building supporting functions around the concept of
* extendable metadata. For example, if Doodles were to drop their spaceships, it would
* be loaded and then enabled by the holder through this function on a front-end.
* @param _layerId the layer that this generation belongs on. The bottom is zero.
* @param _tokenId the token that we are updating the metadata for
*/
function _focusGeneration(
uint256 _layerId
,uint256 _tokenId
)
internal
virtual
{
uint256 activeGenerationLayer = tokenToGeneration[_tokenId];
if(activeGenerationLayer == _layerId) revert GenerationNotDifferent();
// Make sure that the generation has been enabled
Generation memory generation = generations[_layerId];
if(!_generationEnabled(generation)) revert GenerationNotEnabled();
// Make sure a user can't take off a sticky generation
Generation memory activeGeneration = generations[activeGenerationLayer];
if(activeGeneration.sticky && _layerId < activeGenerationLayer) revert GenerationNotDowngradable();
// Make sure they've supplied the right amount of money to unlock access
bytes32 tokenIdGeneration = keccak256(abi.encodePacked(_tokenId, _layerId));
if(msg.value + tokenGenerationToFunded[tokenIdGeneration] != generation.cost) revert GenerationCostMismatch();
tokenGenerationToFunded[tokenIdGeneration] = msg.value;
// Finally evolve to the generation
tokenToGeneration[_tokenId] = _layerId;
emit GenerationChange(
_layerId
,_tokenId
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment