Search code examples
blockchainethereumtokenizesolidity

Handling multiple ERC721 Tokens in the same Contract


Is it possible for the same contract to handle multiple ERC721 tokens? And if so, how?

Let’s say you’re making a game and you need to track two types of ERC721 tokens: unique Weapons and unique Vehicles.

You could have Structs for them, as follows:

struct Weapon { 
    uint256 IDNumber;
    string type; // Sword, Bow&Arrow, Gun, Rifle, etc.  
    string specialFeature; 
}


struct Vehicle { 
    uint256 IDNumber;
    string type;  // Car, Airplane, Boat, Spaceship, etc.  
    uint256 damageFactor; 
}

To track their ownership, you could double the arrays managing them - for example instead of having the standard:

// Enumerable mapping from token ids to their owners
EnumerableMap.UintToAddressMap private _tokenOwners;

You would do:

// Enumerable mapping from token ids to their owners
EnumerableMap.UintToAddressMap private _weaponTokenOwners;
EnumerableMap.UintToAddressMap private _vehicleTtokenOwners;

(This may not be the most elegant way, but it's a start.)

The real question though is: how would you handle mandatory functions that are part of the ERC721 standard, such as balanceOf() and ownerOf()?

To be specific, are you allowed to say add an additional argument to these methods’ signatures to help indicate which particular Token you’re querying about?

For example, instead of this:

function balanceOf(address owner) public view override returns (uint256) {

}

You’d add a tokenName argument to the function’s signature, as follows:

function balanceOf(address owner, string tokenName) public view override returns (uint256) {
    if(tokenName == “weapon”) {
       return ownerAndHisWeaponTokensDictionary[owner].length;
    }
    else if(tokenName == “vehicle”) {
       return ownerAndHisVehicleTokensDictionary[owner].length;
    }
}

And you’d do something similar for ownerOf()?

Is this allowable? And is this even the right approach tackling this - or is there a different way to reason about all of this and approach it differently?


Solution

  • My approach would be to define 3 separate contracts on 3 separate addresses:

    • address 0x123123 as the Weapons ERC-721 token contract
    • address 0x456456 as the Vehicles ERC-721 token contract
    • address 0x789789 as the actual game contract

    In the game contract, you can then call the NFTs contracts to get or validate values:

    function attack(uint attackerWeaponId) {
        require(weaponsContract.isOwnerOf(msg.sender, attackerWeaponId));
        // ...
    }
    

    The isOwnerOf() function takes 2 arguments, address owner and uint256 weaponId. Also, a user can probably own more weapons so that's why I'm showing the validation.

    And the weapons contract balanceOf(address) would reflect the total amount of the Weapon NFTs that the user has.

    mapping (address => Weapon[]) userOwnedWeapons;
    
    function balanceOf(address owner) external view returns (uint256) {
        return userOwnedWeapons[msg.sender].length;
    }