Solidity Arithmetic – Reverting Due to Arithmetic Operation Underflowed or Overflowed in Hardhat Test

ethers.jshardhatsoliditysolidity-0.8.xtesting

I'm developing a voting contract. When I try to test the "vote" function, an error is thrown, I can’t understand what’s wrong.

After this line votingContract = await voting.connect(player) of the last test
an error is thrown:

1) Voting Unit Tests
       vote
         reverts when a voter already voted:
     Error: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)
    at Voting.vote (contracts/Voting.sol:100)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at runNextTicks (node:internal/process/task_queues:64:3)
    at listOnTimeout (node:internal/timers:533:9)
    at processTimers (node:internal/timers:507:7)

Voting.sol

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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@chainlink/contracts/src/v0.8/AutomationCompatible.sol";

error Voting__NotAuthorized();
error Voting__AlreadyVoted();
error Voting__AlreadyRegistered();
error Voting__UnknownCandidate();
error Voting__WrongState();
error Voting__UpkeepNotNeeded();
error Voting__NoOneVoted();

contract Voting is Ownable, AutomationCompatibleInterface {
    enum VotingState {
        REG,
        VOTING,
        CALC
    }

    struct Voter {
        bool authorized;
        bool voted;
    }

    struct Candidate {
        uint256 votes;
    }

    mapping(address => Voter) public voters;
    Candidate[] public candidates;
    address[] private votedList;
    VotingState private state;
    //uint256 public candidatesCount;
    uint256 private registerTime;
    uint256 private votingTime;
    uint256 private lastTimeStamp;

    event VoterVoted(address indexed voter, uint256 numCandidate);
    event VoterRegistered(address indexed voter);

    constructor(uint256 _candidatesCount, uint256 _registerTime, uint256 _votingTime) {
        //candidatesCount = _candidatesCount;
        registerTime = _registerTime;
        votingTime = _votingTime;

        for (uint256 i = 0; i < _candidatesCount; i++) {
            candidates.push(Candidate({votes: 0}));
        }

        state = VotingState.REG;
        lastTimeStamp = block.timestamp;
    }

    modifier onlyState(VotingState _state) {
        if (state != _state) {
            revert Voting__WrongState();
        }
        _;
    }

    function checkUpkeep(
        bytes memory /* checkData */
    ) public view override returns (bool upkeepNeeded, bytes memory /* performData */) {
        uint256 timeDiff = block.timestamp - lastTimeStamp;
        upkeepNeeded = timeDiff > registerTime || timeDiff > votingTime;
    }

    function performUpkeep(bytes calldata /* performData */) public override {
        (bool upkeepNeeded, ) = checkUpkeep("");
        if (!upkeepNeeded) {
            revert Voting__UpkeepNotNeeded();
        }

        if (state == VotingState.REG) {
            state = VotingState.VOTING;
        } else if (state == VotingState.VOTING) {
            state = VotingState.CALC;
        }
        lastTimeStamp = block.timestamp;
    }

    function vote(uint256 _numCandidate) public onlyState(VotingState.VOTING) {
        if (voters[msg.sender].authorized == false) {
            revert Voting__NotAuthorized();
        }
        if (voters[msg.sender].voted) {
            revert Voting__AlreadyVoted();
        }
        if (_numCandidate - 1 > getCandidatesCount()) {
            revert Voting__UnknownCandidate();
        }

        candidates[_numCandidate].votes++; //uncrease count of votes
        voters[msg.sender].voted = true; //mark that this user voted
        votedList.push(msg.sender); //remember the voter
        emit VoterVoted(msg.sender, _numCandidate);
    }

    function register(address _voter) public onlyOwner onlyState(VotingState.REG) {
        if (voters[_voter].authorized) {
            revert Voting__AlreadyRegistered();
        }

        voters[_voter].authorized = true; //register an user
        voters[_voter].voted = false;
        emit VoterRegistered(_voter);
    }

    function getWinner() public view onlyState(VotingState.CALC) returns (uint256) {
        if (votedList.length <= 0) {
            revert Voting__NoOneVoted();
        }

        uint256 maxVotes = 0;
        uint256 numCandidate = 0;
        for (uint256 i = 0; i < getCandidatesCount(); i++) {
            if (candidates[i].votes > maxVotes) {
                maxVotes = candidates[i].votes;
                numCandidate = i;
            }
        }
        return numCandidate;
    }

    function getRegisterTime() public view returns (uint256) {
        return registerTime;
    }

    function getVotingTime() public view returns (uint256) {
        return votingTime;
    }

    function getState() public view returns (VotingState) {
        return state;
    }

    function getCandidatesCount() public view returns (uint256) {
        return candidates.length;
    }

    function getVotedCount() public view returns (uint256) {
        return votedList.length;
    }

    function getVoterAuthorized(address _voter) public view returns (bool) {
        return voters[_voter].authorized;
    }

    function getVoterVoted(address _voter) public view returns (bool) {
        return voters[_voter].voted;
    }
}

Voting.test.js

const { assert, expect } = require("chai")
const { network, deployments, ethers, getNamedAccounts } = require("hardhat")
const { developmentChains, networkConfig } = require("../../helper-hardhat-config")

!developmentChains.includes(network.name)
    ? describe.skip
    : describe("Voting Unit Tests", function () {
          let accounts, voting, votingContract, intervalRegister, intervalVoting, player, deployer //deployer
          beforeEach(async () => {
              accounts = await ethers.getSigners()
              deployer = accounts[0]
              player = accounts[1] //deployer accounts[0]
              await deployments.fixture(["voting"]) //deploy before test
              voting = await ethers.getContract("Voting")
              intervalRegister = await voting.getRegisterTime()
              intervalVoting = await voting.getVotingTime() //60
          })

          describe("constructor", function () {
              it("initializes the voting correctly", async function () {
                  const votingState = (await voting.getState()).toString()
                  assert.equal(votingState, "0")
                  const candidatesCount = (await voting.getCandidatesCount()).toString()
                  const expectedCandidatesCount =
                      networkConfig[network.config.chainId]["candidatesCount"]
                  assert.equal(candidatesCount, expectedCandidatesCount)
                  //60
                  const expectedintervalRegister =
                      networkConfig[network.config.chainId]["intervalRegister"]
                  assert.equal(intervalRegister.toString(), expectedintervalRegister)

                  const expectedintervalVoting =
                      networkConfig[network.config.chainId]["intervalRegister"]
                  assert.equal(intervalVoting.toString(), expectedintervalVoting)
              })
          })

          describe("register", function () {
              it("can register a voter", async function () {
                  //votingContract = await voting.connect(deployer)
                  await voting.register(player.address)
                  //const txReceipt = await txResponse.wait(1)
                  const registered = await voting.getVoterAuthorized(player.address)
                  assert.equal(registered, true)
                  const voted = await voting.getVoterVoted(player.address)
                  assert.equal(voted, false)
              })
              it("emits the event", async function () {
                  await expect(voting.register(player.address)).to.emit(voting, "VoterRegistered")
              })

              it("reverts when the state isn't correct", async function () {
                  await network.provider.send("evm_increaseTime", [intervalRegister.toNumber() + 1])
                  await network.provider.request({ method: "evm_mine", params: [] })
                  await voting.performUpkeep([])
                  await expect(voting.register(player.address)).to.be.revertedWith(
                      "Voting__WrongState"
                  )
              })

              it("only owner can register voters", async function () {
                  //вызвать функцию регистрации и попробовать передать адресс
                  votingContract = await voting.connect(player) //привязка игрока к контракту
                  const person = accounts[0].address
                  await expect(votingContract.register(person)).to.be.revertedWith(
                      "Ownable: caller is not the owner"
                  )
              })

              it("user can be registered only once", async function () {
                  await voting.register(player.address)
                  await expect(voting.register(player.address)).to.be.revertedWith(
                      "Voting__AlreadyRegistered"
                  )
              })
          })

          describe("vote", function () {
              //   beforeEach(async () => {
              //       await voting.register(player.address)
              //       await network.provider.send("evm_increaseTime", [intervalRegister.toNumber() + 2])
              //       await network.provider.request({ method: "evm_mine", params: [] })
              //       await voting.performUpkeep([])
              //   })

              it("reverts when the state isn't correct", async function () {
                  await voting.register(player.address)
                  votingContract = await voting.connect(player)
                  await expect(votingContract.vote(0)).to.be.revertedWith("Voting__WrongState")
              })

              it("reverts when a voter doesn't authorized", async function () {
                  //await voting.register(player.address)
                  await network.provider.send("evm_increaseTime", [intervalRegister.toNumber() + 1])
                  await network.provider.request({ method: "evm_mine", params: [] })
                  await voting.performUpkeep([])
                  votingContract = await voting.connect(player)
                  await expect(votingContract.vote(0)).to.be.revertedWith("Voting__NotAuthorized")
              })

              it("reverts when a voter already voted", async function () {
                  await voting.register(player.address)
                  await network.provider.send("evm_increaseTime", [intervalRegister.toNumber() + 1])
                  await network.provider.request({ method: "evm_mine", params: [] })
                  await voting.performUpkeep([])
                  votingContract = await voting.connect(player)
                  await votingContract.vote(0)
                  await expect(votingContract.vote(0)).to.be.revertedWith("Voting__AlreadyVoted")
              })
          })
      })

Best Answer

In the failing test you call

await votingContract.vote(0)

So the contract will revert here

    if (_numCandidate - 1 > getCandidatesCount()) {
        revert Voting__UnknownCandidate();
    }

because in the call vote(uint256 _numCandidate) _numCandidate is 0, and 0 - 1 is overflow for uint256.

Related Topic