HIGH severity: Static `GovUserKeeper::_nftInfo.totalPowerInTokens` used in quorum denominator can incorrectly make it impossible to reach quorum. **Description:** Consider the following factors:

1) `GovPoolVote::_quorumReached()` [uses](https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/libs/gov/gov-pool/GovPoolVote.sol#L337) `GovUserKeeper::getTotalVoteWeight()` as the denominator for...

Vulnerable code:
it("audit static GovUserKeeper::_nftInfo.totalPowerInTokens in quorum denominator can incorrectly make it impossible to reach quorum", async () => {
        // time when nft power calculation starts
        let powerNftCalcStartTime = (await getCurrentBlockTime()) + 200;

        // required so we can call .toFixed() on BN returned outputs
        ERC721Power.numberFormat = "BigNumber";

        // ERC721Power.totalPower should be zero as no nfts yet created
        assert.equal((await nftPower.totalPower()).toFixed(), "0");

        // so proposal doesn't need to go to validators
        await changeInternalSettings(false);

        // set nftPower as the voting nft
        // need to comment out check preventing updating existing
        // nft address in GovUserKeeper::setERC721Address()
        await impersonate(govPool.address);
        await userKeeper.setERC721Address(nftPower.address, wei("190000000000000000000"), 1, { from: govPool.address });

        // create a new VOTER account and mint them the only power nft
        let VOTER = await accounts(10);
        await nftPower.safeMint(VOTER, 1);

        // switch to using a new ERC20 token for voting; lets us
        // control exactly who has what voting power without worrying about
        // what previous setups have done
        // requires commenting out require statement in GovUserKeeper::setERC20Address()
        let newVotingToken = await ERC20Mock.new("NEWV", "NEWV", 18);
        await impersonate(govPool.address);
        await userKeeper.setERC20Address(newVotingToken.address, { from: govPool.address });

        // mint VOTER some tokens that when combined with their NFT are enough
        // to reach quorum
        let voterTokens = wei("190000000000000000000");
        await newVotingToken.mint(VOTER, voterTokens);
        await newVotingToken.approve(userKeeper.address, voterTokens, { from: VOTER });
        await nftPower.approve(userKeeper.address, "1", { from: VOTER });

        // VOTER deposits their tokens & nft to have voting power
        await govPool.deposit(VOTER, voterTokens, [1], { from: VOTER });

        // advance to the approximate time when nft power calculation starts
        await setTime(powerNftCalcStartTime);

        // verify nft power after power calculation has started
        let nftTotalPowerBefore = "900000000000000000000000000";
        assert.equal((await nftPower.totalPower()).toFixed(), nftTotalPowerBefore);

        // create a proposal which takes a snapshot of the current nft power
        let proposal1Id = 2;

        await govPool.createProposal(
          "example.com",
          [[govPool.address, 0, getBytesGovVote(3, wei("100"), [], true)]],
          [[govPool.address, 0, getBytesGovVote(3, wei("100"), [], false)]]
        );

        // vote on first proposal
        await govPool.vote(proposal1Id, true, voterTokens, [1], { from: VOTER });

        // advance time to allow proposal state change
        await setTime((await getCurrentBlockTime()) + 10);

        // verify that proposal has reached quorum;
        // VOTER's tokens & nft was enough to reach quorum
        assert.equal(await govPool.getProposalState(proposal1Id), ProposalState.SucceededFor);

        // advance time; since VOTER's nft doesn't have collateral deposited
        // its power will decrement to zero
        await setTime((await getCurrentBlockTime()) + 10000);

        // call ERC721::recalculateNftPower() for the nft, this will update
        // ERC721Power.totalPower with the actual current total power
        await nftPower.recalculateNftPower("1");

        // verify that the true totalPower has decremented to zero as the nft
        // lost all its power since it didn't have collateral deposited
        assert.equal((await nftPower.totalPower()).toFixed(), "0");

        // create 2nd proposal which takes a snapshot of the current nft power
        let proposal2Id = 3;

        await govPool.createProposal(
          "example.com",
          [[govPool.address, 0, getBytesGovVote(3, wei("100"), [], true)]],
          [[govPool.address, 0, getBytesGovVote(3, wei("100"), [], false)]]
        );

        // vote on second proposal
        await govPool.vote(proposal2Id, true, voterTokens, [1], { from: VOTER });

        // advance time to allow proposal state change
        await setTime((await getCurrentBlockTime()) + 10);

        // verify that proposal has not reached quorum;
        // even though VOTER owns 100% of the supply of the ERC20 voting token,
        // it is now impossible to reach quorum since the power of VOTER's
        // ERC20 tokens is being incorrectly diluted through the quorum calculation
        // denominator assuming the nfts still have voting power.
        //
        // this is incorrect as the nft has lost all power. The root cause
        // is GovUserKeeper::_nftInfo.totalPowerInTokens which is static
        // but used in the denominator when calculating whether
        // quorum is reached
        assert.equal(await govPool.getProposalState(proposal2Id), ProposalState.Voting);
      });

it("audit actual power nft voting power doesn't match total nft voting power", async () => {
          let powerNftCalcStartTime = (await getCurrentBlockTime()) + 200;

          // required so we can call .toFixed() on BN returned outputs
          ERC721RawPower.numberFormat = "BigNumber";

          // ERC721RawPower::totalPower should be zero as no nfts yet created
          assert.equal((await nftPower.totalPower()).toFixed(), "0");

          // set nftPower as the voting nft
          // need to comment out check preventing updating existing
          // nft address in GovUserKeeper::setERC721Address()
          await impersonate(govPool.address);
          await userKeeper.setERC721Address(nftPower.address, wei("33000"), 33, { from: govPool.address });

          // create new MASTER & SLAVE accounts
          let MASTER = await accounts(10);
          let SLAVE  = await accounts(11);

          // mint MASTER 1 power nft
          let masterNftId = 1;
          await nftPower.mint(MASTER, masterNftId, "");

          // advance to the approximate time when nft power calculation starts
          await setTime(powerNftCalcStartTime);

          // verify MASTER's nft has current power > 0
          let masterNftCurrentPowerStart = (await nftPower.getNftPower(masterNftId)).toFixed();
          assert.equal(masterNftCurrentPowerStart, "894960000000000000000000000");
          // verify MASTER's nft has minumum power = 0
          let masterNftMinPowerStart = (await nftPower.getNftMinPower(masterNftId)).toFixed();
          assert.equal(masterNftMinPowerStart, "0");

          // MASTER deposits their nft then delegates it to SLAVE, another address they control
          await nftPower.approve(userKeeper.address, masterNftId, { from: MASTER });
          await govPool.deposit("0", [masterNftId], { from: MASTER });
          await govPool.delegate(SLAVE, "0", [masterNftId], { from: MASTER });

          // delegation triggers power recalculation on master's nft. Delegation caches
          // the minimum possible voting power of master's nft 0 and uses that for
          // slaves delegated voting power. But recalculation uses the current power
          // of Master's NFT > 0 to update the contract's total power, and this value
          // is used in the denominator of the quorum calculation
          assert.equal((await nftPower.totalPower()).toFixed(), "894690000000000000000000000");

          // mint THIRD some voting tokens & deposit them
          let thirdTokens = wei("1000");
          await token.mint(THIRD, thirdTokens);
          await token.approve(userKeeper.address, thirdTokens, { from: THIRD });
          await govPool.deposit(thirdTokens, [], { from: THIRD });

          // create a proposal
          let proposalId = 1;
          await govPool.createProposal("",
            [[govPool.address, 0, getBytesDelegateTreasury(THIRD, wei("1"), [])]], [], { from: THIRD });

          // MASTER uses their SLAVE account to vote on the proposal; this reverts
          // as delegation saved the minimum possible voting power of MASTER's nft 0
          // and uses 0 as the voting power
          await truffleAssert.reverts(
            govPool.vote(proposalId, true, 0, [], { from: SLAVE }),
            "Gov: low voting power"
          );

          // MASTER has the one & only power nft
          // It has current power   = 894690000000000000000000000
          // nft.Power.totalPower() = 894690000000000000000000000
          // This value will be used in the denominator of the quorum calculation
          // But in practice its actual voting power is 0 since the minumum
          // possible voting power is used for voting power in delegation, causing
          // the quorum denominator to be over-inflated
        });

it("audit verified: nft totalPower > 0 when all nfts lost power incorrectly makes it impossible to reach quorum", async () => {
          // required so we can call .toFixed() on BN returned outputs
          ERC721RawPower.numberFormat = "BigNumber";

          // time when nft power calculation starts
          let powerNftCalcStartTime = (await getCurrentBlockTime()) + 200;

          // create a new nft power token with max power same as voting token's
          // total supply; since we only mint 1 nft this keeps PoC simple
          let voterTokens = wei("190000000000000000000");

          let newNftPower = await ERC721RawPower.new();
          await newNftPower.__ERC721RawPower_init(
            "NFTPowerMock",
            "NFTPM",
            powerNftCalcStartTime,
            token.address,
            toPercent("0.01"),
            voterTokens,
            "540"
          );

          // ERC721Power.totalPower should be zero as no nfts yet created
          assert.equal((await newNftPower.totalPower()).toFixed(), "0");

          // so proposal doesn't need to go to validators
          await changeInternalSettings(false);

          // set newNftPower as the voting nft
          // need to comment out check preventing updating existing
          // nft address in GovUserKeeper::setERC721Address()
          await impersonate(govPool.address);
          // individualPower & supply params not used for power nfts
          await userKeeper.setERC721Address(newNftPower.address, "0", 0, { from: govPool.address });

          // create a new VOTER account and mint them the only power nft
          let VOTER = await accounts(10);
          let voterNftId = 1;
          await newNftPower.mint(VOTER, voterNftId, "");

          // switch to using a new ERC20 token for voting; lets us
          // control exactly who has what voting power without worrying about
          // what previous setups have done
          // requires commenting out require statement in GovUserKeeper::setERC20Address()
          let newVotingToken = await ERC20Mock.new("NEWV", "NEWV", 18);
          await impersonate(govPool.address);
          await userKeeper.setERC20Address(newVotingToken.address, { from: govPool.address });

          // mint VOTER some tokens that when combined with their NFT are enough
          // to reach quorum
          await newVotingToken.mint(VOTER, voterTokens);
          await newVotingToken.approve(userKeeper.address, voterTokens, { from: VOTER });
          await newNftPower.approve(userKeeper.address, voterNftId, { from: VOTER });

          // VOTER deposits their tokens & nft to have voting power
          await govPool.deposit(voterTokens, [voterNftId], { from: VOTER });

          // advance to the approximate time when nft power calculation starts
          await setTime(powerNftCalcStartTime);

          // verify nft power after power calculation has started
          assert.equal((await newNftPower.totalPower()).toFixed(), voterTokens);

          // create a proposal
          let proposal1Id = 2;

          await govPool.createProposal(
            "example.com",
            [[govPool.address, 0, getBytesGovVote(3, wei("100"), [], true)]],
            [[govPool.address, 0, getBytesGovVote(3, wei("100"), [], false)]]
          ,{from : VOTER});

          // vote on first proposal
          await govPool.vote(proposal1Id, true, voterTokens, [voterNftId], { from: VOTER });

          // advance time to allow proposal state change
          await setTime((await getCurrentBlockTime()) + 10);

          // verify that proposal has reached quorum;
          // VOTER's tokens & nft was enough to reach quorum'
          // since VOTER owns all the voting erc20s & power nfts
          //
          // fails here; proposal still in Voting state?
          assert.equal(await govPool.getProposalState(proposal1Id), ProposalState.SucceededFor);

          // advance time; since VOTER's nft doesn't have collateral deposited
          // its power will decrement to zero
          await setTime((await getCurrentBlockTime()) + 10000);

          // create 2nd proposal
          let proposal2Id = 3;

          await govPool.createProposal(
            "example.com",
            [[govPool.address, 0, getBytesGovVote(3, wei("100"), [], true)]],
            [[govPool.address, 0, getBytesGovVote(3, wei("100"), [], false)]]
          ,{from : VOTER});

          // vote on second proposal
          await govPool.vote(proposal2Id, true, voterTokens, [voterNftId], { from: VOTER });

          // advance time to allow proposal state change
          await setTime((await getCurrentBlockTime()) + 10);

          // this used to fail as the proposal would fail to reach quorum
          // but now it works
          assert.equal(await govPool.getProposalState(proposal2Id), ProposalState.SucceededFor);
        });