2

does anyone know how I can fill an array with numbers 1 - 10,000 efficiently? For example, in our constructor we have the following but it doesn't work due to gas costs; too many items to push to a dynamic array. If I do, let's say 100, it works fine.

    constructor() ERC721("Contract", "C") {
        for (uint i = 1; i < 10001; i++) {
            tokens.push(i);
        }
    }

Any ideas?

For context, in my smart contract we want to randomly choose an id to mint to avoid sequential minting, so we use a Chainlink VRF to get a random number less than the tokens array length, choose an id randomly from the array, then remove the last index to shrink the array using roughly this code:

    uint[] tokens;
    uint randomNumber = RandomNumberContract.randomResult();
    function handleGetTokenId(uint _tokenOffset) public onlyOwner returns(uint) {
        require(tokens.length > 0, "No more tokens left");

        // random number based on number of tokens left
        uint randomIndex = uint(keccak256(abi.encode(randomNumber, _tokenOffset))) % tokens.length;

        // random id at index in array
        uint randomId = tokens[randomIndex];

        require(!_exists(randomId), "token already minted");

        // at position randomIndex, replace with last id
        tokens[randomIndex] = tokens[tokens.length - 1];
        // remove last index; shortens length and ensures no double minting
        tokens.pop();

        return randomId;
    }

The issue is, we have 10,000 ids to choose from and we cannot efficiently fill the dynamic array at initialization. Is there a better way to randomly choose an id to mint, or to keep track of what's been minted or not? Also we tried using a fixed size array which is cheaper with gas, but couldn't find a solution to effectively choose a random number within the range of 0 to numberOfTokensMinted because we do not have the arrays length.

5
  • 1
    You can try the modulo operator (%) to take any number and take it to your desired range. If you are already getting a random number or something like that, then you can do something like: randomNumber % 10000 and that will return the remainder of that division, producing a number in between 0 and 10000. Aug 11, 2022 at 14:25
  • 1
    Thank you @JeremyThen. In that case, how can we tell if that randomNumber (assuming it is an id) is already minted? Lets say its 500. What if 500 is already minted, how could we get the next not minted id?
    – binkie
    Aug 11, 2022 at 14:29
  • 1
    If you are receiving those ids to check if they are minted or not, you can use a mapping(uint => bool) tokenMint to then save if it has been minted or not: if(tokenMint[id]) {...}. You can also remove it delete tokenMint[id];, add a new id tokenMint[id] = true;, etc. Or maybe you need an Iterable mapping? solidity-by-example.org/app/iterable-mapping Aug 11, 2022 at 14:45
  • 1
    Great thank you for your help @JeremyThen. I will think on this some more, an iterable mapping looks promising.
    – binkie
    Aug 11, 2022 at 14:47
  • 1
    Take a look at the approach I describe below approach solution. Aug 11, 2022 at 15:37

1 Answer 1

1

Filling an array in this way is not efficient. Tool many elements being added at once, depleting all the gas. Adding one element at a time after construction is not feasible either, since there are too many of them.

For your use case, since you only want some sort of a list of ids from 1 to 10000, you can use some interesting tricks with the modulo operator % and a single uint variable.

You can create a uint public tokensCount = 10_000 state variable and use a random number of the block.timestamp alone or mixed with a random number or any other number, to generate a number to be used as an id in your desired range and decrease the tokensCount by one so next time you get a number in between a new range.

Then, you can save in a mapping if this id was already used or not.

Take a look at my example below.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

contract ModuloRanging {

    uint constant public tokensCount = 10_000;
    uint public tokensProcessed = 0;
    uint public lastId;
    mapping(uint => bool) public mited;

    function handleGetTokenId() public returns(uint) {
        require(tokensProcessed >= tokensCount, "No more tokens left");

        // For more "randomness", mix the block.timestamp with any other "random" number before applying the modulo operator
        // or simply use a random number, or the keccak256 of the random number and the _tokenOffset
        // uint id = (block.timestamp + randomNumber) % tokensCount;
        uint id = (block.timestamp + tokensProcessed) % tokensCount;

        require(!mited[id], "Token already minted");

        tokensProcessed++;

        lastId = id;
        return id;
    }

}

You can try it out and see the value of lastId in remix.

This approach is extremely efficient compared to using arrays and mappings.

2
  • 1
    The problem with your code is that it becomes increasingly hard to mint a new NFT as more IDs are already minted. I would just increment tokenProcessed and generate another mapping for random traits. Aug 11, 2022 at 20:58
  • 1
    Thank you for your response. We have actually implemented a Chainlink VRF for our random id already. Once I get some time I will explore using a mapping for knowing if it's minted or not. Thanks again!
    – binkie
    Aug 12, 2022 at 13:14

Your Answer

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

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