Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customizing contracts - Mint Reveal #10

Open
AddressXception opened this issue Apr 20, 2022 · 0 comments
Open

Customizing contracts - Mint Reveal #10

AddressXception opened this issue Apr 20, 2022 · 0 comments

Comments

@AddressXception
Copy link
Member

As a user i would like to receive an unrevealed token immediately so that i have possession of my token but also have confidence that I have a fair shot at receiving a rare token. As a user i would like my unrevealed token to reveal its artwork at some point in the future and for the developer to prove to me that it was distributed fairly so that i can verify I received the right token.

The concept of a mint reveal is an interesting one. We are basically telling our users that we are going to mutate the state of the product that they purchased, which is very assuredly against everything immutable ledgers are supposed to stand for. But, as with everything there are exceptions to every rule. This pattern arose out of expedience as a cheap alternative to guarantee fairness when pulling rares.

Generative NFT token mints are a lot like opening a pack of cards, or a variable-reward lottery and users expect that all else being equal, they will have a shot at buying in for price X and have a chance at receiving something valued at X+Y.

Of course in practice, this puts a lot of trust in the developer. In fact, it is quite common for a developer to launch a project using a mint reveal pattern and then immediately "rug" the project. In fact, some of them do it on purpose.

Consider the alternative that we do not do a mint reveal and instead we simply publish the artwork and metadata for the tokens to the blockchain before the sale starts. Any user can go and check which token is at index 0, at index, 1, at index 500 and so forth. We can see which ones might be valuable and which ones might not be. We can also determine beforehand which tokens we might get when we submit transactions, or we can bribe a miner to mine our transactions in a particular order to ensure we get the tokens we want. That's no fun.

In any case, the reason for this is because it is extremely difficult to define randomness in a smart contract cheaply. This article describes why it is so easy to predict random numbers in ethereum smart contracts and this article describes Verifiable Random Functions as a solution. There are some other options out there but all of it is either extremely expensive to use or is technically complex for our use case.

So now that we understand why we need to use a mint reveal scheme, we can talk about how to do it.

The most simple implementation is to define two separate states, unrevealed and revealed. If the contract is unrevealed, then all calls to tokenURI return the same image (usually just a placeholder). If the state is revealed, then the actual image for a given token id is returned from the tokenURI call.

First, you will need to override the OZ tokenURI function. Then you will need to modify it internally to handle the state changes. Here's an example that demonstrates a few new solidity features that we'll go over:

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), 'Token does not exist');

        string memory revealedBaseURI = _revealedBaseURI;
        return
            bytes(revealedBaseURI).length > 0
                ? string(abi.encodePacked(revealedBaseURI, tokenId.toString()))
                : _tokenBaseURI;
    }

So what's going on here? First we use arequire clause to error out if the caller is searching for a token that doesnt exist. Next we grab a handle to a global state variable, but this could also be the result of a function call. Notice we have a memory declaration. This tells the compiler that we want to read the variable into memory. Storage access on the blockchain is extremely expensive, so we save a little gas by reading the variable from storage only once and using the memory version in our if statement.

next, we have a short-hand if statement to check if the revealedBaseURI has been set (is not null), by casting it to bytes and checking it's length.

if the revealed base has been set, then we return a representation of the URI that resolves to the token. If it has not, we return the base.

There are a few things to go over in this section. First, what is the actual content of these variables? _revealedBaseURI and _tokenBaseURI are both IPFS hashes. We will go over IPFS more in a later section but for now:

  • _tokenBaseURI - an ipfs CID route in the form of ipfs://QmcZyZppFdQ5MPDEtNnAa8ri3QSHtkqYCRMYv5o1dQcjZm' that points to a metadata file containing the pre-revealed data
  • _revealedBaseURI - an ipfs CID route in the same form that points to a folder containing metadata files named after their tokenid (e.g. /folder/0, /folder/1, /folder/500, etc.)

Why is this important?

Storing the hash of the folder containing the appropriately named token files allows us to take up a single storage slot for each type baseURI so that we do not have to store a mapping of token id's to their IFS file hashes. It keeps our code more simple, but also each storage of a 32 byte IPFS costs about $7 at current prices, so storing one for each of our 1k tokens would cost $7,000.

Second, and most importantly we are taking advantage of the abi.encodePacked function to concatenate the two strings together.

I'm sure you will be shocked to hear this, but strings are fickle in solidity. Here are a couple of articles that go into greater detail, but basically it works like this: Strings are just a convenience declaration for a variable-length byte array that contains characters (similar to other C-based languages). There is a lot of nuance to how they work and most of the time using abi.encodePacked is how you solve your problem.

So, picking up where we left off, we'll need to implement a couple more state variables in our contract and add a setter function so we can update the _revealedBaseURI. Don't forget to mark it onlyOwner!

And of course, let's write some tests!

How can you configure the contract to test the state pre-reveal and post-reveal?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant