I'm working on a Game into Solidity and I want to be able to populate metadata into my smart contract to be able to access them directly from game-play functions.

So I have a basic ERC721 to start:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract GameEngine is ERC721Enumerable, Ownable {

  string baseURI;
  uint256 public maxSupply = 10000;

  bool isPopulated = false;
  struct Character {
    uint8 race;
    uint8 strength;
    uint8 agility;
    uint8 hp;
    uint8 xp;
  mapping(uint => Character) characters;

    string memory _initBaseURI
  ) ERC721("GameCharacters", "GC") {
    _baseURI = _initBaseURI;

  /* All basic functions for ERC721: mint, tokenURI, ...*/

Now... I want to be able to pre-fill all Character struct in the characters mapping. I can implement 2 ways:

A function to add a single one

Problem: But I'll have to call it 10.000 times (or more) !! And it'll cost way to much in fees..

function populateOne(uint8 race, uint8 strength, uint8 agility, uint8 hp, /*...*/ ) external onlyOwner {
    require(!isPopulated, "Already Populated");
    characters[characters.length] = Character(race, strength, agility, hp, 0, /*...*/ );
function setPopulated() external onlyOwner { 
    isPopulated = true;

All in one function

Problem: Not sure I'll be able to call this function with 10,20 or 50 arrays of 10k, 20k or 100k items..

// All array must be the same size (maxSupply)
function populateAll(uint8[] races, uint8[] strengths, uint8[] agilities, uint8[] hps, /*...*/ ) external onlyOwner {
    require(!isPopulated, "Already Populated");
    for (uint256 i = 0; i < type.length; i++) {
        characters[i] = Character(races[i], strengths[i], agilities[i], hps[i], 0, /*...*/ );
    isPopulated = true;

Did I miss something ? What's the solution to do it ? Thank for help, articles, codes, .. I'm taking everything :p

1 Answer 1


Your understanding of both the solutions is right. There are a few things that are needed to be considered.

Solution 1

This is simple to implement, but as you mentioned, you'll pay gas for every transaction. You can use multicall to batch multiple calls into a single transaction, but it'll be quire expensive for large number of calls.

Solution 2

This makes sense, but its not possible to give arbitrarily large data as inputs. While EIP-1985 states that calldata can have a maximum size of around 4GB, you'll hit the block gas limit much earlier.

Solution 3

You can implement solution 2 but in a batched format. Let's say, you decide the batch size to be 100, you add in the metadata for the first 100 tokens. In the next call, you add in the next 100. You can decide upon the batch size based on the gas usage for each batch. This way, you can reduce the number of calls to a minimum and yet stay below the gas limit.

  • Oky yes, I didn't think to merge both solution to a single one. I never did thing like that, I just know that request have a maximum size for input but how can I test that ? My question is, how can I decide if I'll send batch of 100 or 1000 items ? Just by testing and when if I receive an error I try with less ? And as I know they are no Solution 4 with another concept ? Everyone is using simple function to populate right ?
    – Arthur
    Jan 27, 2022 at 17:22
  • 1
    I'm not aware of any other strategies. As for the batch size, there isn't an upper limit, so you can go as high as you want, just make sure that the gas cost in below the block limit. You can use eth_estimateGas RPC call to get the gas estimate, and decide based on that.
    – pbsh
    Jan 27, 2022 at 18:23
  • Sure sure I'll not forget that.
    – Arthur
    Jan 27, 2022 at 18:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.