Is there a better way to loop over an array with undefined length?

in my contract a function loop over an array with undefined length :

function tallyVotes() external onlyOwner {
        uint _winningProposalId;
        for (uint256 p = 0; p < proposalsArray.length; p++) {
            if (proposalsArray[p].voteCount > proposalsArray[_winningProposalId].voteCount) {
                _winningProposalId = p;
            }
        }
        winningProposalID = _winningProposalId;
    }

Here it's okay the array length is under 100 but if length would have been more than 100 000 000 ( I overdo it I know) I will be out of gas ! So my question is how to handle this ? Is there a better way to loop over an array if I can't be sure I will get enough gas ?

Hey @tristan_deneuve
loops are never a good things, overall on-chain. I suggesto to do it offchain if possible

Hello Freezy,
What do you mean by 'offchain' ?

Probably you are trying to do "too much" in a single transaction.

Have you considered keeping track of the to-be winner after every vote instead? That would split the winner determination work by all the votes.

Maybe keeping an array (in case of a tie) of potential winners and updating it as needed would do it. In sum converting from tally-at-end-of-voting to a tally-as-you-vote system

NOTE: In case of a tie, your code will iterate all proposals and pick the last one as a winner. If proposal A is in the array before proposal B and both have the same votes, proposal B is considered the winner

With Off chain I mean using a script maybe in js, python or whatever you want

It's an owner-only function, so you can execute the entire computation within an off-chain script (as suggested in previous comments), and have the owner-only function do only the actual state-changing operation:

function set_winningProposalId(uint _winningProposalId) external onlyOwner {
    winningProposalID = _winningProposalId;
}

Alternatively, if you wish to keep this procedure fully trustless (i.e., in the example above, the owner can technically ignore the actual voting results), then you can implement this function to act on a restricted part of the array, meaning that the owner will need to call it several times until the the entire array is processed. You'll need to employ a couple of additional state variables indicating the current state of that process:

uint256 private currIndex;
uint256 private maxCountID;

function tallyVotes(uint256 length) external onlyOwner {
    uint bgnIndex = currIndex;
    uint endIndex = bgnIndex + length;
    uint _maxCountID = maxCountID;
    uint _maxCount = proposalsArray[_maxCountID].voteCount;

    if (endIndex < proposalsArray.length) {
        for (uint256 p = bgnIndex; p < endIndex; p++) {
            uint voteCount = proposalsArray[p].voteCount; 
            if (_maxCount < voteCount) {
                _maxCount = voteCount;
                _maxCountID = p;
            }
        }
        currIndex = endIndex;
        maxCountID = _maxCountID;
    }
    else {
        for (uint256 p = bgnIndex; p < proposalsArray.length; p++) {
            uint voteCount = proposalsArray[p].voteCount; 
            if (_maxCount < voteCount) {
                _maxCount = voteCount;
                _maxCountID = p;
            }
        }
        winningProposalID = _maxCountID;
        currIndex = 0;
        maxCountID = 0;
    }
}

Note that I have tried to minimize the amount of storage-read and storage write operations there, in order to save as much gas as possible. The equivalent approach applied on your original solution, would be to maintain a local copy of the value stored in proposalsArray[_winningProposalId].voteCount, instead of reading it from storage on every iteration.

P.S.:
In order to make this process fully trustless, I would suggest to actually remove the owner-only access restriction, and allow anyone to call this function. You'll need to employ an additional state variable indicating whether or not this function can be called (i.e., it can be called only after the vote has ended and only when it hasn't completed the process yet). When the function completes the process, it should reset that state variable, so it will not be callable again until the end of the next vote. When the next vote ends, the owner will be able to set that state variable in order to allow users once again to call the function.
This state variable will in fact also serve as an indication of whether or not the value stored in winningProposalID can be considered valid.