xanoutas/solidity-security-findings huggingface dataset:
Users can vote proposals multiple times.
Audit: 2023-10-party-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernance.sol#L451-L453
https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernance.sol#L937-L941


# Vulnerability details

## Vulnerability Details
Each proposal state  tracks users that accepted proposal , ensuring that no user can vote more than once. This is necessary because each account is only allowed to vote once on a proposal. However, users have the option to delegate their voting power or transfer their party NFT to another account, and vote the proposal with new account using transferred voting power.

## Impact
Users have the ability to access unlimited voting power.

## Proof of Concept
1- User A possesses a voting power of 100 and gives their approval to a proposal.
2- User A either transfers their party NFT card or delegates their voting power to another address.
3- User A employs a new address to once again approve the proposal and use their voting power again.

## Tools Used
Manual Review
## Recommended Mitigation Steps
Implement a mechanism that safeguards against using voting power of a nft multiple times on same proposal.


## Assessed type

Other
The same host can pass a proposal (status ready) without the approval of other legitimate hosts
Audit: 2023-10-party-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernance.sol#L457


# Vulnerability details

## Impact

A `party` can be created using a determined number of hosts. Those hosts can vote individually to a proposal in order to pass the proposal. Additionally the proposal can not be voted multiple times by the same `host`:

```solidity
File: PartyGovernance.sol
595:     function accept(uint256 proposalId, uint256 snapIndex) public returns (uint256 totalVotes) {
...
...
626:         // Cannot vote twice.
627:         if (info.hasVoted[msg.sender]) {
628:             revert AlreadyVotedError(msg.sender);
629:         }
630:         // Mark the caller as having voted.
631:         info.hasVoted[msg.sender] = true;
...
```

The problem is that the `host` can transfer his voting power using [abdicateHost()](https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernance.sol#L457) function to a controlled address then vote again to the same proposal. The same `host` can pass a proposal without the approval of other hosts.

## Proof of Concept

I created a test where the same `host` passes the proposal (status ready) without the approval of the other host. Test steps:

1. The host `defaultGovernanceOpts.hosts[0]` accepts the proposal
2. The same host can not vote to the same proposal. Transaction will be reverted.
3. The same host transfers his voting power to `address(1337)` using the `abdicateHost` func. The new controlled `address(1337)` can vote to the same proposal.
4. The proposal now is `ready` and the malicious host does not need the approval of other legitimate hosts.

```solidity
// File: PartyGovernanceUnit.t.sol
// $ forge test --match-test "testHostCanPassAProposalWithoutTheApproveOfTheOtherHosts" -vvv
//
    function testHostCanPassAProposalWithoutTheApproveOfTheOtherHosts() external {
        (
            IERC721[] memory preciousTokens,
            uint256[] memory preciousTokenIds
        ) = _createPreciousTokens(2);
        TestablePartyGovernance gov = _createGovernance(
            false,
            100e18,
            preciousTokens,
            preciousTokenIds
        );
        address undelegatedVoter = _randomAddress();
        gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0));
        //
        // Create a one-step proposal.
        PartyGovernance.Proposal memory proposal = _createProposal(1);
        uint256 proposalId = gov.getNextProposalId();
        //
        // Skip because `accept()` will query voting power at `proposedTime - 1`
        skip(1);
        vm.prank(undelegatedVoter);
        assertEq(gov.propose(proposal, 0), proposalId);
        _assertProposalStatusEq(gov, proposalId, PartyGovernance.ProposalStatus.Passed);
        //
        // 1. The host defaultGovernanceOpts.hosts[0] accepts the proposal
        vm.startPrank(defaultGovernanceOpts.hosts[0]);
The ragequit function uses a fixed precision of 1e18 for the calculation of withdrawamounts, which leads to incompatibility with other erc20 tokens whose precision is not equal to 1e18
Audit: 2023-10-party-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernanceNFT.sol#L393


# Vulnerability details

## Impact
The ragequit function uses a fixed precision of 1e18 for the calculation of withrawamounts, but not all erc20's precision are equal to 1e18. For example, the precision of `USDC` token is only 1e6.
Suppose a user uses `USDC` token as withrawtokens, and the statement for calculating withrawamounts is as follows:

```solidity
withdrawAmounts[i] += (balance * getVotingPowerShareOf(tokenIds[j])) / 1e18;
```

It can be seen from the formula that it should have been divided by `1e6`, but now it has been divided by `1e18`. The calculation error is very large! The user will get fewer tokens than the actual one.
## Proof of Concept
The code calculated with fixed precision is in line 393 of the `ragequit` function:
https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernanceNFT.sol#L393

```solidity
withdrawAmounts[i] += (balance * getVotingPowerShareOf(tokenIds[j])) / 1e18;
```

## Tools Used
Visual Studio Code
Foundry 

## Recommended Mitigation Steps
Consider using the following optimization code:

```solidity
uint256 decimals = address(withdrawTokens[i]) == ETH_ADDRESS ? 1e18 : IERC20(withdrawTokens[i]).decimals();
withdrawAmounts[i] += (balance * getVotingPowerShareOf(tokenIds[j])) /decimals;
```



## Assessed type

Decimal
InProgress proposals in PartyGovernance can get "stuck" indefinitely if maxExecutableTime has passed.
Audit: 2023-10-party-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernance.sol#L706-L760


# Vulnerability details

## Impact
An InProgress proposal could prevent any other proposals from executing indefinitely if it gets stuck. This could essentially lock up the governance of the party.
The severity depends on the specific use case, but I would categorize this as a high severity issue for a few reasons:
• It prevents any new proposals from executing, halting governance. This is very disruptive.
• There is no good way to recover besides the cancel() function, which itself has time delays. And cancel() may not even work if the proposal is truly stuck.
• Itblocks).
So in summary:
• Impact: High. Prevents new proposals, halts governance.
• Severity: High. Very disruptive, hard to recover from.


So in summary:

Impact: High. Prevents new proposals, halts governance.
Severity: High. Very disruptive, hard to recover from.

## Proof of Concept
`ExecutionTimeExceededError` reverts if `maxExecutableTime` has passed. But for `InProgress` proposals, this check is skipped. So an `InProgress` proposal could get "stuck" indefinitely. 

Here is how it works:
• When a proposal is first executed via `execute()`, its status is set to `InProgress` and `executedTime` is set.
• Later when `execute()` is called again on the proposal, here is the relevant code:

       // Get proposal state
       ProposalState storage proposalState = _proposalStateByProposalId[proposalId]; 

       // Validate proposal hash matches
       _validateProposalHash(proposal, proposalState.hash);

       // Get proposal values 
       ProposalStateValues memory values = proposalState.values;

       // Get proposal status
       ProposalStatus status = _getProposalStatus(values);

       // Check if status is Ready or InProgress
         if (status != ProposalStatus.Ready && status != ProposalStatus.InProgress) {
         revert BadProposalStatusError(status); 
       }

       // If Ready, check maxExecutableTime
       if (status == ProposalStatus.Ready) {
         if (proposal.maxExecutableTime < block.timestamp) {
           revert ExecutionTimeExceededError(...); 
         }
       }

       // Execute proposal...

As you can see, the `maxExecutableTime` check is only done if the status is Ready. For `InProgress` proposals, this check is skipped.
So an `InProgress` proposal can be executed again even if `maxExecutableTime` has passed. This means a proposal could get "stuck" in the `InProgress` state indefinitely.
The impact is that an attacker could exploit this to DoS the contract by occupying the single `InProgress` proposal slot, preventing any other proposals from executing.


## Tools Used
Manual
## Recommended Mitigation Steps
the `maxExecutableTime chec`k should also be added for `InProgress` proposals:

       // Additional check:
       if (status == ProposalStatus.InProgress) {
         if (proposal.maxExecutabl
Missing duplicate token address when rage quiting
Audit: 2023-10-party-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernanceNFT.sol#L344
https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernanceNFT.sol#L386


# Vulnerability details

## Impact

Missing duplicate token address when rage quiting

## Proof of Concept

When rageQuit, the user can [pass in withdrawToken parameter](https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernanceNFT.sol#L344)

```solidity
function rageQuit(
	uint256[] calldata tokenIds,
	IERC20[] calldata withdrawTokens,
	uint256[] calldata minWithdrawAmounts,
	address receiver
) external {
```

however, the code does not validate if there are duplicate token address in array withdrawTokens,

the user can basically pass in two token id with different voting power but withdraw the same token [two times](https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernanceNFT.sol#L386)

```solidity
// Check token's balance.
	uint256 balance = address(withdrawTokens[i]) == ETH_ADDRESS
		? address(this).balance
		: withdrawTokens[i].balanceOf(address(this));

	// Add fair share of tokens from the party to total.
	for (uint256 j; j < tokenIds.length; ++j) {
		// Must be retrieved before burning the token.
		withdrawAmounts[i] += (balance * getVotingPowerShareOf(tokenIds[j])) / 1e18;
	}
```

basically the first a few members can choose to withdraw the most valuable token multiple times when the member that withdraw later are forced to withdraw less valuable token

## Tools Used

Manual Review

## Recommended Mitigation Steps

validate there are no duplicate element in the withdraw array









## Assessed type

Token-Transfer
PartyGovernanceNFT.sol#mint - If an authority mints Party Cards before the InitialETHCrowdfund has finished user funds will be stuck 
Audit: 2023-10-party-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernanceNFT.sol#L179-L183


# Vulnerability details

## Impact
The protocol uses authorities, which are smart contracts that offer more functionality to the party and their hosts.

Authorities can call `mint`, to mint new Party Cards to different users in combination with `increaseTotalVotingPower` to increase the total voting power of the Party correctly.

The issue here arises, if this happens before the `InitialETHCrowdfund` has finalized. There is nothing stopping an authority from calling `mint` or `increaseTotalVotingPower` before the crowdfund has finalized.

Let's first look at the `mint` function and specifically this part of the code:
```js
        if (totalVotingPower != 0 && totalVotingPower - mintedVotingPower_ < votingPower_) {
            unchecked {
                votingPower_ = totalVotingPower - mintedVotingPower_;
            }
        }
```
This check enforces a limit so to say, on the `votingPower_` so that it doesn't exceed `totalVotingPower - mintedVotingPower_`. The equation acts as sort of a cap/quorum.

Now let's see what happens if we call `increaseTotalVotingPower` and `mint`, before the crowdfund has finalized.

- The crowdfund and the party get created and initialized.
- The authority decides to mint a Card with `5e18` voting power for a user, so they call `increaseTotalVotingPower` and `mint`.
- After the two functions get executed we get the following state:
`totalVotingPower = 5e18`
`mintedVotingPower = 5e18`
- Alice decides to contribute `2e18` to the crowdfund.
- We hit this line again:
Calculating the equation we get `5e18 - 5e18 < 2e18` and Alice's voting power is set to 0 and we mint her a card with 0 voting power, but her `2e18` ETH was still sent to the crowdfund and was added as a contribution.
```js
        if (totalVotingPower != 0 && totalVotingPower - mintedVotingPower_ < votingPower_) {
            unchecked {
                votingPower_ = totalVotingPower - mintedVotingPower_;
            }
        }
```

Alice just lost `2e18` ETH and got 0 voting power in return. 

Note that every user that contributes to the crowdfund will get 0 voting power and their tokens will be stuck.
The only user that won't lose anything will be the final contributor, that makes `totalContributions >= maxTotalContributions`. The crowdfund will get finalized, calling `party.increaseTotalVotingPower` and then his token will be minted correctly and it will have voting power.

If `emergencyExecuteDisabled = true` on the Party and the Crowdfund, users funds will be stuck and even the DAO can't save them.


## Proof of Concept
Paste the following inside `test/crowdfund/InitialETHCrowdfund.t.sol` in contract `InitialETHCrowdfundTest` and run `forge test --mt test_mintingACardBeforeCrowdfundHasFinalized -vvvv`
```js
function _createCrowdfundWithAuthorities(
        CreateCrowdfundArgs memory args,
Users can lose all of their funds if they ```rageQuit``` when ```totalVotingPower``` is 0. 
Audit: 2023-10-party-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernanceNFT.sol#L344-L448


# Vulnerability details

## Impact
Users can lose all of their funds if they ```rageQuit``` when ```totalVotingPower``` is 0. 
## Proof of Concept
If user rage quits when ```totalVotingPower``` is 0 they can lose all of their funds.

- A new ```party``` is created with a starting ```totalVotingPower``` of 0
- A user calls contribute with ```msg.value = 1 ETH``` and is credited a proportionate voting power.
- After some time he decides that he wants his contribution back. (this happens while contributions are still allowed, ```totalVotingPower``` is only increased when the crowdfund is ```finalized```)
- He calls ```rageQuit``` on the party and sets ```withdrawsTokens = [ETH]```

```solidity
for (uint256 i; i < withdrawTokens.length; ++i) {
    // Check if order of tokens to transfer is valid.
    // Prevent null and duplicate transfers.
    if (prevToken >= withdrawTokens[i]) revert InvalidTokenOrderError();

    prevToken = withdrawTokens[i];

    // Check token's balance.
    uint256 balance = address(withdrawTokens[i]) == ETH_ADDRESS
        ? address(this).balance
        : withdrawTokens[i].balanceOf(address(this));

    // Add fair share of tokens from the party to total.
    for (uint256 j; j < tokenIds.length; ++j) {
        // Must be retrieved before burning the token.
        withdrawAmounts[i] += (balance * getVotingPowerShareOf(tokenIds[j])) / 1e18;
    }
}
```
- Let's look at the following line of code    
```withdrawAmounts[i] += (balance * getVotingPowerShareOf(tokenIds[j])) / 1e18;```
- The ```getVotingPowerShareOf``` function call will return ```0``` as the ```totalVotingPower``` is 0. So ```balance * 0 / 1e18``` will be set as ```withdrawAmounts[i]``` which evaluates to 0.
```solidity
function getVotingPowerShareOf(uint256 tokenId) public view returns (uint256) {
    uint256 totalVotingPower = _getSharedProposalStorage().governanceValues.totalVotingPower;
    return
        totalVotingPower == 0 ? 0 : (votingPowerByTokenId[tokenId] * 1e18) / totalVotingPower;
}
```
- As the ```amount```  is 0 the contract doesn't send any tokens/eth to the caller.
```solidity
for (uint256 i; i < withdrawTokens.length; ++i) {
    IERC20 token = withdrawTokens[i];
    uint256 amount = withdrawAmounts[i];

    // Take fee from amount.
    uint256 fee = (amount * feeBps_) / 1e4;

    if (fee > 0) {
        ...
    }

    if (amount > 0) {
        uint256 minAmount = minWithdrawAmounts[i];

        // Check amount is at least minimum.
        if (amount < minAmount) {
            revert BelowMinWithdrawAmountError(amount, minAmount);
        }

        // Transfer token from party to recipient.
        if (address(token) == ETH_ADDRESS) {
            payable(receiver).transferEth(amount);
        } else {
            token.compatTransfer(receiver, amount);
        }
    }
}
```
- Contract burns
A malicious host can skip the ```executionDelay``` by himself. 
Audit: 2023-10-party-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernance.sol#L1122-L1127
https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernance.sol#L1098-L1100
https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernance.sol#L595-L658
https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernance.sol#L457-L472


# Vulnerability details

## Impact
A malicious host can skip the ```executionDelay``` by himself. 
## Proof of Concept
- Let's say a ```party``` was initialized with 10 hosts. 
- If hosts want to skip the ```executionDelay``` this needs to hold true
```solidity
if (_hostsAccepted(pv.numHosts, pv.numHostsAccepted)) {
    return ProposalStatus.Ready;
}
```
```solidity
function _hostsAccepted(
    uint8 snapshotNumHosts,
    uint8 numHostsAccepted
) private pure returns (bool) {
    return snapshotNumHosts > 0 && snapshotNumHosts == numHostsAccepted;
}
```
- So all the hosts would need to vote on the proposal for it to be possible to skip the delay.
- But one host can increase the number of the hosts voted ```numHostsAccepted``` as high as he wants.
```solidity
function accept(uint256 proposalId, uint256 snapIndex) public returns (uint256 totalVotes) {
    ...
    // Cannot vote twice.
    if (info.hasVoted[msg.sender]) {
        revert AlreadyVotedError(msg.sender);
    }
    // Mark the caller as having voted.
    info.hasVoted[msg.sender] = true;

    // Increase the total votes that have been cast on this proposal.
    uint96 votingPower = getVotingPowerAt(msg.sender, values.proposedTime - 1, snapIndex);
    values.votes += votingPower;
    if (isHost[msg.sender]) {
        ++values.numHostsAccepted;
    }
    info.values = values;
    emit ProposalAccepted(proposalId, msg.sender, votingPower);

    // Update the proposal status if it has reached the pass threshold.
    if (
        values.passedTime == 0 &&
        _areVotesPassing(
            values.votes,
            values.totalVotingPower,
            _getSharedProposalStorage().governanceValues.passThresholdBps
        )
    ) {
        ...
    }
    return values.votes;
}

```
- As we can see in the function above the ```votingPower``` is not checked to be 0. The ```getVotingPowerAt``` can return 0 and this would let anyone vote on the proposal. Since they vote with 0 votes it is not a problem if a user is not a host. But what if the voter is a host?
- Alice is a host and an active member. She votes on a proposal increasing the  ```numHostsAccepted``` as intended. 
- She can then call ```abdicateHost``` and transfer host to another ```EOA``` that she controls and has a ```votingPower of >= 0```.
```solidity
function abdicateHost(address newPartyHost) external {
    _assertHost();
    // 0 is a special case burn address.
    if (new
Not all the tokens have 18 decimal
Audit: 2023-10-party-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-party/blob/main/contracts/party/PartyGovernanceNFT.sol#L393


# Vulnerability details

## Description
The `PartyGovernanceNFT.rageQuit()` is to burn the governance NFT of the user and withdraw a fair share of the selected ERC20 tokens and ETH from the party.
The users can specify the fungible tokens to withdraw (ERC 20)

This for loop iterates through tokens, adding their amounts to calculate the total withdrawal sum. It ensures precision and accuracy in determining the overall amount to be withdrawn.
```solidity
PartyGovernanceNFT.sol

            for (uint256 i; i < withdrawTokens.length; ++i) {
                // Check if order of tokens to transfer is valid.
                // Prevent null and duplicate transfers.
                if (prevToken >= withdrawTokens[i]) revert InvalidTokenOrderError();

                prevToken = withdrawTokens[i];

                // Check token's balance.
                uint256 balance = address(withdrawTokens[i]) == ETH_ADDRESS
                    ? address(this).balance
                    : withdrawTokens[i].balanceOf(address(this));

                // Add fair share of tokens from the party to total.
                for (uint256 j; j < tokenIds.length; ++j) {
                    // Must be retrieved before burning the token.
                    withdrawAmounts[i] += (balance * getVotingPowerShareOf(tokenIds[j])) / 1e18;
                }
```
the `PartyGovernanceNFT.getVotingPowerShareOf()` function will return the voting power share of a token. Denominated fractions of **1e18**
The return value will be multiplied by the `balance` (token's balance) and then divided by 1e18 to ensure the accuracy of 1e18.
The issue is not all tokens have 18 decimals. So The problem arises from tokens with varying decimal places. the `rageQuit()` function will end up affecting subsequent steps in the process.

 
## Impact

- Users could receive less/more than the intended amount 

## Tools Used

Manual Review - GPT

## Recommended Mitigation Steps
1- `getVotingPowerShareOf()` should return the voting power share of a token. Denominated fractions of the decimal of each token
2- 
```solidity   
PartyGovernanceNFT.sol

            for (uint256 i; i < withdrawTokens.length; ++i) {
                // Check if order of tokens to transfer is valid.
                // Prevent null and duplicate transfers.
                if (prevToken >= withdrawTokens[i]) revert InvalidTokenOrderError();

                prevToken = withdrawTokens[i];

                // Check token's balance.
                uint256 balance = address(withdrawTokens[i]) == ETH_ADDRESS
                    ? address(this).balance
                    : withdrawTokens[i].balanceOf(address(this));

                // Add fair share of tokens from the party to total.
                   uint256 decimals =  address(withdrawTokens[i]) == ETH_ADDRESS
                   ?1e18
                   :10 ** withdrawTokens[i].de