Skip to content

Evolving NFTs #443

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

Merged
merged 15 commits into from
Aug 7, 2023
Merged

Evolving NFTs #443

merged 15 commits into from
Aug 7, 2023

Conversation

nkrishang
Copy link
Contributor

@nkrishang nkrishang commented Jul 28, 2023

Summary

New contracts introduced: EvolvingNFT, SharedMetadataBatch, RulesEngine.

EvolvingNFT is an ERC721 NFT contract. It is an 'open edition' where the metadata of NFT's changes based on its owner's token ownership profile.

Unlike OpenEditionERC721 which uses SharedMetadata, this contract uses SharedMetadataBatch instead, and lets an address with minter role store multiple metadata in the contract. Using RulesEngine, the tokenURI method returns the appropriate metadata for the open edition NFT based on the token ownership score of the NFT owner.

Expected usage

The big picture

The actual distribution of open edition NFTs works exactly like in OpenEditionERC721, through the use of the Drop extension contract.

But unlike OpenEdtiionERC721, you can store multiple shared metadata in the contract. Ultimately, we want to get to the following tokenURI function:

function tokenURI(uint256 _tokenId) public view virtual override returns (string memory) {
    if (!_exists(_tokenId)) {
        revert("!ID");
    }

    // Get score
    uint256 score = getScore(ownerOf(_tokenId));

    // Get the target ID i.e. `start` of the range that the score falls into.
    bytes32[] memory ids = _sharedMetadataBatchStorage().ids.values();
    bytes32 targetId = 0;

    for (uint256 i = 0; i < ids.length; i += 1) {
        if (uint256(ids[i]) <= score) {
            targetId = ids[i];
        } else {
            break;
        }
    }
    return _getURIFromSharedMetadata(targetId, _tokenId);
}

Here, we first get a 'token ownership score' for the NFT owner and then return metadata for the NFT based on this score. Different ranges of scores result in different shared metadata to be returned for a given token ID. For example:

Score range inclusive Returned metadata
0 -> 19 metadata_1 = Charmander
20 -> 99 metadata_2 = Charmeleon
Greater than 100 metadata_3 = Charizard

Pokemon evolution over 3 levels

1. Creating rules

An authorized signer (address with minter role) creates rules; each rule has an associated score.

enum TokenType { ERC20, ERC721, ERC1155 }

struct Rule {
    address token;
    TokenType tokenType;
    uint256 tokenId
    uint256 balance;
    uint256 score;
}

The authorized signer creates rules by calling createRule. This returns a ruleId, which can be used to later call deleteRule if needed.

function createRule(Rule memory rule) external returns (uint256 ruleId);
function deleteRule(uint256 ruleId) external;

A Rule defines a criterion of token ownership. For all token types (ERC20, ERC721 and ERC1155), the token ownership criterion checks for the total balance of the token owned by a given address.

Note: this means that the tokenId field is ignored for both ERC20 and ERC721 tokens. For example:

  1. "Owns at least 20 ApeToken tokens (ERC20)"
  2. "Owns at least 3 Bored Ape Yacht Club NFTs (ERC721)"
  3. "Owns the Bored Ape Yacht Club NFT with tokenId 1 (ERC721)"

From the above, [1] and [2] are valid Rules. [3] is not a valid Rule since it is not a check of the total balance of tokens held by a given address.

2. Setting shared metadata

The OpenEditionERC721 contract has only one shared metadata at a time (all NFTs have the exact same metadata, except for their own unique token ID in the NFT name).

The EvolvingNFT contract lets an authorized signer (an address with admin role) store multiple shared metadata, each with a unique bytes32 ID. The contract gets this functionality from the SharedMetadataBatch extension contract.

An authorized signer can store shared metadata by calling the setSharedMetadata function:

function setSharedMetadata(SharedMetadataInfo calldata metadata, bytes32 id) external;
function deleteSharedMetadata(bytes32 id) external;
function getAllSharedMetadata() external view returns (SharedMetadataWithId[] memory metadata);

Metadata for a given NFT token ID is accessed using the bytes32 id assigned to shared metadata during creation:

// Used in `EvolvingNFT.tokenURI(uint256 tokenId)`
function _getURIFromSharedMetadata(bytes32 id, uint256 tokenId) internal view returns (string memory);

3. Assigning the right IDs to shared metadata

For a given NFT token ID, the tokenURI method first gets a token ownership score for the NFT's owner, and then returns shared metadata assigned to a 'target ID' as seen in the full implementation of the method in the The big picture section.

And so, the bytes32 id assigned to a shared metadata must be the above mentioned the start (inclusive) of the score range. The relevance of this is explained in the The big picture section.

@nkrishang nkrishang self-assigned this Jul 28, 2023
@nkrishang nkrishang merged commit 339cac7 into main Aug 7, 2023
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

Successfully merging this pull request may close these issues.

1 participant