xanoutas/solidity-security-findings huggingface dataset:
Contracts are vulnerable to fee-on-transfer accounting-related issues
Audit: 2024-05-munchables-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L376-L376


# Vulnerability details

## Impact
The function(s) below transfer funds from the caller to the receiver via `transferFrom()`, but do not ensure that the actual number of tokens received is the same as the input amount to the transfer. If the token is a fee-on-transfer token, the balance after the transfer will be smaller than expected, leading to accounting issues. Even if there are checks later, related to a secondary transfer, an attacker may be able to use latent funds (e.g. mistakenly sent by another user) in order to get a free credit. One way to address this problem is to measure the balance before and after the transfer, and use the difference as the amount, rather than the stated amount.

```solidity
Path: ./src/managers/LockManager.sol

376:            token.transferFrom(_tokenOwner, address(this), _quantity);	// @audit-issue
```
[376](https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/./src/managers/LockManager.sol#L376-L376), 


#### Recommendation

To mitigate potential vulnerabilities and ensure accurate accounting with fee-on-transfer tokens, modify your contract's token transfer logic to measure the recipient's balance before and after the transfer. Use this observed difference as the actual transferred amount for any further logic or calculations. For example:
```solidity
function transferTokenAndPerformAction(address token, address from, address to, uint256 amount) public {
    uint256 balanceBefore = IERC20(token).balanceOf(to);

    // Perform the token transfer
    IERC20(token).transferFrom(from, to, amount);

    uint256 balanceAfter = IERC20(token).balanceOf(to);
    uint256 actualReceived = balanceAfter - balanceBefore;

    // Proceed with logic using actualReceived instead of the initial amount
    require(actualReceived >= minimumRequiredAmount, "Received amount is less than required");

    // Further logic here
}
```



## Assessed type

ERC20
Incompatible with fee-on-transfer tokens
Audit: 2024-05-munchables-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L380
https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L423


# Vulnerability details

## Impact
The project description states it supports fee on transfer ERC20 tokens, however, the contract does not handle this token properly, leading to constract insolvent.

## Proof of Concept
The `_lock()` function tracks the quantity of tokens users lockup. [_lock()#L380](https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L380):

```solidity
        lockedToken.quantity += _quantity;
```

When tokens are unlocked, users can withdraw the same quantity of tokens from the contract. [unlock()#L423](https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L423):

```solidity
    token.transfer(msg.sender, _quantity);
```

However, as the contract receives less tokens than the provided `_quantity` due to the transfer fee, the contract would not have sufficient tokens for withdrawal, leading to users cannot unlock their tokens.

## Tools Used
vscode

## Recommended Mitigation Steps
Consider implement the support for fee-on-transfer tokens.


## Assessed type

ERC20
Fee-on-Transfer tokens are not handled for the lock function
Audit: 2024-05-munchables-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol/#L311


# Vulnerability details

## Impact
lockedToken quantity will be inflated for fee on transfer tokens. It will not reflect the real amount.

## Proof of Concept
According to the readme, fee on transfer token behaviour is in scope. However, fee on transfer tokens are not handled in the \_lock function

```solidity
token.transferFrom(_tokenOwner, address(this), _quantity);
```

The `_lock` function expects to receive the full transfer amount (`_quantity`).

However, the \_lock function does not account for tokens that apply a fee on transfer, which will result in the contract receiving less than `_quantity` in case of fee on transfer tokens.


```solidity
lockedToken.quantity += _quantity;
````
The function records the full `_quantity` without checking if the actual received amount is less due to fees.

## Tools Used

## Recommended Mitigation Steps
Use balanceOf before and after the `transferFrom` call to get the real amount of tokens received.


## Assessed type

ERC20
Fee-on-tranfer tokens will mess up the accounting of the `LockManager` contract
Audit: 2024-05-munchables-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L373-L377


# Vulnerability details

## Impact
Loss of funds for users over time, because the situation gets worse after every lock
## Proof of Concept
The accounting in the `LockManager` contract will be messed up, since the actual funds received by the contract, are less than the `_quantity` variable that tracks them. It gets worse as time passes, because at one point the funds in the contract will be significantly less than the less than the sum of the `quantity` variables that are supposed to track them.
Fee-on-transfer issues are in the toke behaviour scope, so this should be considered a valid issue! Also had conversation with the sponsor and was told that they will add BLAST token in the future.

## Tools Used
Manual review 

## Recommended Mitigation Steps
Get the token balances before and after the transfers, then subtract them to get the actual value that got transferred to the contract, like this:
```diff
 if (_tokenContract != address(0)) {
            IERC20 token = IERC20(_tokenContract);
+            uint256 balanceBefore = token.balanceOf(address(this));
            token.transferFrom(_tokenOwner, address(this), _quantity);
+            uint256 balanceAfter = token.balanceOf(address(this));
+            _quantity = balanceAfter - balanceBefore;
        }

```


## Assessed type

ERC20
Possible approvals of disproven price proposals
Audit: 2024-05-munchables-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L177C4-L207C6#L177


# Vulnerability details

## Impact
An unwanted price change may occur since the function ```approveUSDPrice``` doesn't check for already disproven price proposals. 

## Proof of Concept
If we take a closer look at ```approveUSDPrice```:
```
    function approveUSDPrice(
        uint256 _price
    )
        external
        onlyOneOfRoles(
            [
                Role.PriceFeed_1,
                Role.PriceFeed_2,
                Role.PriceFeed_3,
                Role.PriceFeed_4,
                Role.PriceFeed_5
            ]
        )
    {
        if (usdUpdateProposal.proposer == address(0)) revert NoProposalError();
        if (usdUpdateProposal.proposer == msg.sender)
            revert ProposerCannotApproveError();
        if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId)
            revert ProposalAlreadyApprovedError();
        if (usdUpdateProposal.proposedPrice != _price)
            revert ProposalPriceNotMatchedError();

        usdUpdateProposal.approvals[msg.sender] = _usdProposalId;
        usdUpdateProposal.approvalsCount++;

        if (usdUpdateProposal.approvalsCount >= APPROVE_THRESHOLD) {
            _execUSDPriceUpdate();
        }

        emit ApprovedUSDPrice(msg.sender);
    }
```
we can see that in lines 191-197 there are if statements which ensure that:
there is an actual proposal to approve
```
if (usdUpdateProposal.proposer == address(0)) revert NoProposalError();
``` 

that the proposer isn't trying to approve his own proposal
```
if (usdUpdateProposal.proposer == msg.sender) revert ProposerCannotApproveError();
```

that the proposal isn't already approved
```
if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError();
```
and that the price being approved matches the proposed price 
```
if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError();
```
However there is no check to see if a price proposal hasn't already been disproven by ```disapproveUSDPrice``` which can lead to an unwanted price being approved and if the ```approvalsCount``` reaches ```APPROVE_THRESHOLD```, a price change will occur.

For example, a price is proposed via the function ```proposeUSDPrice```, it is then disproven by one of the price feeds, now only two price feeds need to approve the proposal for it to go through since ```APPROVE_THRESHOLD``` and ```DISAPPROVE_THRESHOLD``` can be set to 2 and 2 respectively with the function ```setUSDThresholds```.

Here are links to all the mentioned code:
```proposeUSDPrice``` - https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L142C5-L174C6

```approveUSDPrice``` - https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager
Possible double voting potentially could manipulate USD price logic 
Audit: 2024-05-munchables-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L177-L207


# Vulnerability details

## Impact
For a proposal to be executed, it must receive sufficient approvals. On the other hand, the proposal could be disapproved. Only Role.PriceFeed can approve and disapprove. In the current implementation, it seems that they are limited to not giving more than one vote, but there is a way to make double voting possible.
Role.PriceFeed can first disapprove and then approve, which can compromise the whole logic of the vote due to the possibility of giving 2 votes rather than the expected 1.  

## Proof of Concept

Lets say we have the following scenario:
APPROVE_THRESHOLD=3; 
DISAPPROVE_THRESHOLD=3;

1.A proposal is submitted and awaits approval
2.Role.PriceFeed_1 disapproves the proposal //0 approvals - 1 disapproval
3.Role.PriceFeed_2 approves the proposal // 1 approval - 1 disapproval
4.Role.PriceFeed_3 approves the proposal // 2 approvals - 1 disapproval
5.Role.PriceFeed_4 disapproves the proposal // 2 approvals - 2 disapprovals
6.Role.PriceFeed_1 approves the proposal // 3 approval - 2 disapprovals

The proposal will be executed based on Role.PriceFeed_1 double voting (2,6).


## Tools Used
Manual review

## Recommended Mitigation Steps
Consider removing the possibility for double voting. In approveUSDPrice() add:
```
if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId)
            revert ProposalAlreadyDisapprovedError();
```


## Assessed type

Other
`approveUSDPrice` don't have a check to make sure proposal has disapproval before approval.
Audit: 2024-05-munchables-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/d3db85c1c584fd226315ebfb6c4a085e44a8806a/src/managers/LockManager.sol#L177-L197


# Vulnerability details

## Impact
proposer can approve and disapprove  one proposal at the same time, which is not reasonable.

## Proof of Concept

when disapprove, we have check to make sure user is not approve this proposal before.

```solidity

    function disapproveUSDPrice(
        uint256 _price
    )
    ...
    if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId)
    revert ProposalAlreadyApprovedError();
    if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId)
    revert ProposalAlreadyDisapprovedError();

  ...
 
```
But is approve, we don't have disapprove check. We only have the approve  check,  without this check , user can approve a proposal which he has disapproved before.

```solidity
 function approveUSDPrice(
        uint256 _price
    )

    if (usdUpdateProposal.proposer == msg.sender)
    revert ProposerCannotApproveError();
@>    if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId)
    revert ProposalAlreadyApprovedError();
    if (usdUpdateProposal.proposedPrice != _price)
    revert ProposalPriceNotMatchedError();

... 


```

## Tools Used
manual 

## Recommended Mitigation Steps

add disapprove check in `approveUSDPrice` function.



## Assessed type

Access Control
PriceFeed can first `disapproveUSDPrice` and then `approveUSDPrice`
Audit: 2024-05-munchables-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L177
https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L210


# Vulnerability details

## Vulnerability details
Other priceFeeds can `approve/disapprove` the price proposed by a priceFeed and to `prevent` a priceFeed from voting `twice`, there is a check in `disapproveUSDPrice()` 
```solidity
 function disapproveUSDPrice(
        uint256 _price
    )
     ...
 
     @>   if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId)
            revert ProposalAlreadyApprovedError();
     @>  if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId)
            revert ProposalAlreadyDisapprovedError();
    ...
    }
```
In above code, there are `2 checks` which ensures priceFeed has neither approved or disapproved the price.

But the `problem` is there is only `1 check` in `approveUSDPrice()`, which only checks for priceFeed has `approved` but not for `disapprove`, allowing a priceFeed to disapprove(via disapproveUSDPrice) first and then approve(via approveUSDPrice) the price, giving an `option` to approve & disapprove simultaneously
```solidity
  function approveUSDPrice(
        uint256 _price
    )
    ...
      @>  if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId)
            revert ProposalAlreadyApprovedError();
    ...
    }
```

## Impact
PriceFeed can approve & disapprove simultaneously

## Proof of Concept
https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L177C4-L207C6
https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L210C4-L242C6

## Tools Used
Manual Review

## Recommended Mitigation Steps
Add this line in approveUSDPrice()
```diff
+ if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId)
+            revert ProposalAlreadyDisapprovedError();
```


## Assessed type

Error
Approval After Disapproval in `approveUSDPrice` Function
Audit: 2024-05-munchables-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L177


# Vulnerability details

## Impact
A price feed can approve a USD price proposal after previously disapproving it. This inconsistency can lead to manipulation and unreliable approval processes, undermining the integrity of the price update mechanism.

## Proof of Concept
In the `approveUSDPrice` function, there is no check to see if the caller has previously disapproved the same price proposal. This allows a price feed to both disapprove and then approve the same proposal:
```solidity
function approveUSDPrice(uint256 _price) external onlyOneOfRoles([...]) {
    if (usdUpdateProposal.proposer == address(0)) revert NoProposalError();
    if (usdUpdateProposal.proposer == msg.sender) revert ProposerCannotApproveError();
    if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError();
    if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError();
    
    // Missing check for previous disapproval
    usdUpdateProposal.approvals[msg.sender] = _usdProposalId;
    usdUpdateProposal.approvalsCount++;
    
    if (usdUpdateProposal.approvalsCount >= APPROVE_THRESHOLD) {
        _execUSDPriceUpdate();
    }
    
    emit ApprovedUSDPrice(msg.sender);
}
```
## Tools Used
Manual Review
## Recommended Mitigation Steps
Add a check in the `approveUSDPrice` function to ensure that the caller has not previously disapproved the same price proposal:
```solidity
function approveUSDPrice(uint256 _price) external onlyOneOfRoles([...]) {
    if (usdUpdateProposal.proposer == address(0)) revert NoProposalError();
    if (usdUpdateProposal.proposer == msg.sender) revert ProposerCannotApproveError();
    if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError();
    if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError();
    if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId) revert ProposalAlreadyDisapprovedError(); // Add this line

    usdUpdateProposal.approvals[msg.sender] = _usdProposalId;
    usdUpdateProposal.approvalsCount++;

    if (usdUpdateProposal.approvalsCount >= APPROVE_THRESHOLD) {
        _execUSDPriceUpdate();
    }

    emit ApprovedUSDPrice(msg.sender);
}
```
This ensures that a price feed cannot approve a proposal after previously disapproving it, maintaining the integrity of the approval process.


## Assessed type

Invalid Validation
PriceFeed can vote for disapproval and approval for the same proposal
Audit: 2024-05-munchables-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L194-L195


# Vulnerability details

## Impact
There is a check to prevent disapproving a proposal after the price feed has already approved it. However, you can disapprove the proposal and then approve it because `approveUSDPrice` only checks the approvals mapping. This creates an ambiguity in the vote, as you can both approve and disapprove it simultaneously. The relevant code can be found here: https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L194-L195.

## Proof of Concept

```solidity
    function approveUSDPrice(
        uint256 _price
    )
        external
        onlyOneOfRoles(
            [
                Role.PriceFeed_1,
                Role.PriceFeed_2,
                Role.PriceFeed_3,
                Role.PriceFeed_4,
                Role.PriceFeed_5
            ]
        )
    {
        if (usdUpdateProposal.proposer == address(0)) revert NoProposalError();
        if (usdUpdateProposal.proposer == msg.sender)
            revert ProposerCannotApproveError();
     @>   if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId)
            revert ProposalAlreadyApprovedError();
// NO CHECK FOR DISSAPPROVALS
        if (usdUpdateProposal.proposedPrice != _price)
            revert ProposalPriceNotMatchedError();

```

## Tools Used
Reading source code.

## Recommended Mitigation Steps
Add dissapproval check to approveUSDPrice: 
```solidity
 if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId)
            revert ProposalAlreadyDisapprovedError();
```


## Assessed type

Error
Price Feed Caller can Approve and Disapprove USD Price At The Same Time
Audit: 2024-05-munchables-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L225-L228
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L194


# Vulnerability details

## Impact
- Price Feed Caller can Approve and Disapprove USD Price At The Same Time breaking Price Feed Vote Count functionality Due to break in proper Threshold for Executing or Removing a Proposal in LockManager contract
- The number of Price Votes can go above the number of Price Feed Voters
## Proof of Concept
```solidity
   function disapproveUSDPrice(
        uint256 _price
    )
        external
        onlyOneOfRoles(
            [
                Role.PriceFeed_1,
                Role.PriceFeed_2,
                Role.PriceFeed_3,
                Role.PriceFeed_4,
                Role.PriceFeed_5
            ]
        )
    {
        if (usdUpdateProposal.proposer == address(0)) revert NoProposalError();
 >>>       if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId)
            revert ProposalAlreadyApprovedError();
 >>>       if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId)
            revert ProposalAlreadyDisapprovedError();
        if (usdUpdateProposal.proposedPrice != _price)
            revert ProposalPriceNotMatchedError();

        usdUpdateProposal.disapprovalsCount++;
        usdUpdateProposal.disapprovals[msg.sender] = _usdProposalId;

        emit DisapprovedUSDPrice(msg.sender);

        if (usdUpdateProposal.disapprovalsCount >= DISAPPROVE_THRESHOLD) {
            delete usdUpdateProposal;

            emit RemovedUSDProposal();
        }
    }
```
The function above from the LockManager contract shows how price disapproval is implemented, it can be noted from the validation from the two pointers above that if a msg.sender or caller has previously approved or disapproved a USD price proposal, the the function will revert, the summary of this validation is simply to ensure a caller does not get two votes for the same Proposal i.e 
1. Double Disapproval 
2. An Approval and a Disapproval
```solidity
 function approveUSDPrice(
        uint256 _price
    )
        external
        onlyOneOfRoles(
            [
                Role.PriceFeed_1,
                Role.PriceFeed_2,
                Role.PriceFeed_3,
                Role.PriceFeed_4,
                Role.PriceFeed_5
            ]
        )
    {
        if (usdUpdateProposal.proposer == address(0)) revert NoProposalError();
        if (usdUpdateProposal.proposer == msg.sender)
            revert ProposerCannotApproveError();
  >>>      if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId)
            revert ProposalAlreadyApprovedError();
        if (usdUpdateProposal.proposedPrice != _price)
            revert ProposalPriceNotMatchedError();

        usdUpdateProposal.approvals[msg.sender] = _usdProposalId;
        usdUpdateProposal.approvalsCount++;

        if (usdUpdateProposal.approvalsCount >= A