xanoutas/solidity-security-findings huggingface dataset:
Persistent Contract Call revert prevents finalizing a ballot
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/DAO.sol#L180
https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/DAO.sol#L219
https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/DAO.sol#L219


# Vulnerability details

## Description

The `DAO._executeApproval` function does not handle an external contract call error:

```solidity
else if (ballot.ballotType == BallotType.CALL_CONTRACT) {
    // @audit-issue unhandled revert
    ICalledContract(ballot.address1).callFromDAO(ballot.number1);

    emit ContractCalled(ballot.address1, ballot.number1);
}
```

Given an approved `CALL_CONTRACT` ballot that can be finalized, the ballot won't be marked as finalized if the external contract call (from above) reverts.

```solidity
function _finalizeApprovalBallot(uint256 ballotID) internal {
    if (proposals.ballotIsApproved(ballotID)) {
        Ballot memory ballot = proposals.ballotForID(ballotID);
        _executeApproval(ballot);
    }

    // @audit-issue the line below won't be executed if `_executeApproval` reverts
    proposals.markBallotAsFinalized(ballotID);
}
```

## Impact

A permanent revert leaves the ballot unfinalized, and the user that posted it with an active proposal (in the `_userHasActiveProposal` mapping); a state that prevents the user account from creating a new proposal. At this point the user has two options to sort out the situation:

A. Unstake and transfer its SALT into a new account.
B. Convince the other users to reach quorum on NO and finalize the ballot without calling the external contract.

## Proof Of Concept

New contract to be created in `/src/dao/tests`:

```solidity
// SPDX-License-Identifier: BUSL 1.1
pragma solidity =0.8.22;

import "../interfaces/ICalledContract.sol";

contract TestCallReceiverFaulty is ICalledContract {
    uint256 public value;

    function callFromDAO(uint256 n) external {
        value = n;
        revert("callFromDAO() unexpectedly reverted");
    }
}
```

Add the following test in `DAO.t.sol`:

```solidity
function testCallContractApproveReverted() public {
    // Arrange
    vm.startPrank(alice);
    staking.stakeSALT(1000000 ether);

    TestCallReceiverFaulty testReceiver = new TestCallReceiverFaulty();

    uint256 ballotID = proposals.proposeCallContract(
        address(testReceiver),
        123,
        "description"
    );
    assertEq(
        proposals.ballotForID(ballotID).ballotIsLive,
        true,
        "Ballot not correctly created"
    );

    proposals.castVote(ballotID, Vote.YES);
    vm.warp(block.timestamp + 11 days);

    vm.expectRevert("callFromDAO() unexpectedly reverted");

    // Act
    dao.finalizeBallot(ballotID);

    // Assert
    assertEq(
        proposals.ballotForID(ballotID).ballotIsLive,
        true,
        "Ballot not correctly finalized"
    );
    assertTrue(
        testReceiver.value() != 123,
        "Receiver shouldn't receive the call"
    );
    assertEq(
        proposals.userHasActivePropos
Lack of voting power checkpoint per account and proposal can be used to decide proposals
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L385
https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L259
https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L342
https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L317


# Vulnerability details

## Description

The DAO Governance voting system implements the following measures to prevent an attack via a sudden increase of the user's voting power (e.g. borrowing voting tokens with a flash-loan):

- Voting power is obtained by staking SALT for xSALT (on a 1:1 basis). Therefore, casting a vote on an open ballot requires first to stake SALT.
- Unstaking SALT is penalized according to these parameters:
  - `minUnstakeWeeks`: The minimum number of weeks for an unstake request at which point `minUnstakePercent` of the original staked SALT is reclaimable. Default is 2 days, range is from 1 to 12 days.
  - `maxUnstakeWeeks`: The maximum number of weeks for an unstake request at which point 100% of the original staked SALT is reclaimable. Default is 52 days, range is from 20 to 108 days.
  - `minUnstakePercent`: The percentage of the original staked SALT that is reclaimable when unstaking the minimum number of weeks.
    Default is 20%, range is from 10% to 50%.
  - `modificationCooldown`: The minimum time between increasing and decreasing user shares. Default is 1 hour, range is from 15 minutes to 6 hours.
- Holding xSALT is incentivised with SALT rewards (proportionally to the percent of the total xSALT hold).

Also, the proposals to be voted (aka `Ballot`) have these relevant properties:

- `ballotIsLive`: Whether the ballot accepts votes or has ended.
- `ballotMinimumEndTime`: The earliest timestamp at which a ballot can end. **Can be open longer if the quorum has not yet been reached for instance or if no EOA has finalized the ballot (after reaching quorum)**. Default is 10 days, range is from 3 to 14 days.

Finally, a ballot can finalize when (from [DAO.canFinalizeBallot](https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L385:L400)):

- The ballot is live (from `Ballot.ballotIsLive`).
- The ballot minimum duration has passed (from `Ballot.ballotMinimumEndTime`).
- **The total votes casted for the ballot is greater than the required quorum for the ballot type, which is at least the 0.5% of the SALT total supply (500k SALT) or the current total SALT staked in the protocol multiplied by a ballot-type-dependant factor**.

```solidity
// Checks that ballot is live, and minimumEndTime and quorum have both been reached.
function canFinalizeBallot(uint256 ballotID) external view returns (bool) {
    Ballot memory ballot = ballots[ballotID];
    if (!ballot.ballotIsLive) return false;

    // Check that the minimum duration has passed
    if (block.timestamp < ballot.ballotMinimumEndTime) return false;

    // Check that the required quorum has been reached
    if
Confirmation wallet can bypass 30-day timelock completely or postpone confirmed wallet change infinitely
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/ManagedWallet.sol#L65-L66


# Vulnerability details

## Impact
Confirmation wallet can bypass 30-day timelock completely or postpone approved wallet change infinitely

## Proof of Concept
In ManagedWallet.sol, confirmation wallet can confirm a proposal to change wallet addresses by sending greater than 0.05 ether to the contract. 

However, the current implementation is flawed and allows two unsafe behaviors against intended design:
(1) 30-day timelock bypass:
There should be 30-day timelock after a proposal. But this can be bypassed by the current confirmation wallet send greater than 0.05 ether to the contract at the very beginning ( e.g. after contract deployment)

This way `activeTimelock` will be set to a known timestamp well before a proposal is made. Then when the mainwallet makes a proposal through `proposeWallets()`. The new proposedMainWallet can call `changeWallets()` instantly due to `block.timestamp` already passed a historic `activeTimelock`.

```solidity
    receive() external payable
    	{
    	require( msg.sender == confirmationWallet, "Invalid sender" );

		// Confirm if .05 or more ether is sent and otherwise reject.
		// Done this way in case custodial wallets are used as the confirmationWallet - which sometimes won't allow for smart contract calls.
    	if ( msg.value >= .05 ether )
|>    		activeTimelock = block.timestamp + TIMELOCK_DURATION; // establish the timelock
```
(https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/ManagedWallet.sol#L65-L66)
```solidity
	function changeWallets() external
		{
		// proposedMainWallet calls the function - to make sure it is a valid address.
		require( msg.sender == proposedMainWallet, "Invalid sender" );
                //@audit : this require will pass without 30day timelock due to activeTimelock is a historic timestamp
|>		require( block.timestamp >= activeTimelock, "Timelock not yet completed" );
```
(https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/ManagedWallet.sol#L77)
(2) infinite postpone of a confirmed proposal:
Confirmation wallet can also postpone a confirmed proposal infinitely by sending 0.05 ether to the contract before the first 30-day timelock is passed, which effectively reset `activeTimelock` to a new future timestamp. 30-day timelock can be extended to whenever.

## Tools Used

Manual

## Recommended Mitigation Steps
 In `receive()`- `if ( msg.value >= .05 ether )` body, add checks to ensure `proposedMainWallet != address(0) && activeTimelock == type(uint256).max`


## Assessed type

Invalid Validation
Slippage and Front-Running
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/staking/Liquidity.sol#L83


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
Slippage and Front-Running: The contract does not seem to handle slippage or front-running protection explicitly. Users specify minLiquidityReceived, minReclaimedA, and minReclaimedB to mitigate slippage, but there's no mention of price oracles or other mechanisms to prevent front-running.
## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

function _depositLiquidityAndIncreaseShare( IERC20 tokenA, IERC20 tokenB, uint256 maxAmountA, uint256 maxAmountB, uint256 minLiquidityReceived, bool useZapping ) internal returns (uint256 addedAmountA, uint256 addedAmountB, uint256 addedLiquidity)
		{
		require( exchangeConfig.walletHasAccess(msg.sender), "Sender does not have exchange access" );

		// Transfer the specified maximum amount of tokens from the user
		tokenA.safeTransferFrom(msg.sender, address(this), maxAmountA );
		tokenB.safeTransferFrom(msg.sender, address(this), maxAmountB );

		// Balance the token amounts by swapping one to the other before adding the liquidity?
		if ( useZapping )
			(maxAmountA, maxAmountB) = _dualZapInLiquidity(tokenA, tokenB, maxAmountA, maxAmountB );

		// Approve the liquidity to add
		tokenA.approve( address(pools), maxAmountA );
		tokenB.approve( address(pools), maxAmountB );

		// Deposit the specified liquidity into the Pools contract
		// The added liquidity will be owned by this contract. (external call to Pools contract)
		bytes32 poolID = PoolUtils._poolID( tokenA, tokenB );
		(addedAmountA, addedAmountB, addedLiquidity) = pools.addLiquidity( tokenA, tokenB, maxAmountA, maxAmountB, minLiquidityReceived, totalShares[poolID]);

		// Increase the user's liquidity share by the amount of addedLiquidity.
		// Cooldown is specified to prevent reward hunting (ie - quickly depositing and withdrawing large amounts of liquidity to snipe rewards as they arrive)
		// _increaseUserShare confirms the pool as whitelisted as well.
		_increaseUserShare( msg.sender, poolID, addedLiquidity, true );

## Tools Used

## Recommended Mitigation Steps

use price oracles for front-running.


## Assessed type

Context
DOS creation of proposal
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/Proposals.sol#L103


# Vulnerability details

## Impact
The creation of proposals can be front-run with another proposal bearing the same name, causing the previous transaction to fail.

## Proof of Concept
``` solidity 
// Make sure that a proposal of the same name is not already open for the ballot
require( openBallotsByName[ballotName] == 0, "Cannot create a proposal similar to a ballot that is still open" );
require( openBallotsByName[ string.concat(ballotName, "_confirm")] == 0, "Cannot create a proposal for a ballot with a secondary confirmation" );
```

## Tools Used
Manual review.

## Recommended Mitigation Steps
Each proposal should have a nonce attached to the name to differentiate them even with the same name.


## Assessed type

DoS
There is No Refund Mechanism for Excess Ether Sent for Confirming or Rejecting Wallet Proposals in `receive()` function in `ManagedWallet` contract
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/ManagedWallet.sol#L59-L69


# Vulnerability details

The `receive()` function is intended to accept Ether to confirm or reject wallet proposals. If 0.05 Ether or more is sent, it triggers a confirmation and starts a timelock. However, any Ether sent in excess of the 0.05 Ether threshold is not refunded to the sender.

## Impact
The confirmation wallet may send more than the required 0.05 Ether for confirmation, either by mistake or due to a lack of precision. The excess Ether is not returned, potentially resulting in an unintended loss of funds.

## Proof of Concept
See the below code for confirming or rejecting wallet proposals by sending 0.05 ETH. But the below receiver function is not implemented any refund mechanism for excess transfer.

```solidity
File: src/ManagedWallet.sol

59:    receive() external payable
60:        {
61:        require( msg.sender == confirmationWallet, "Invalid sender" );
62:
63:                // Confirm if .05 or more ether is sent and otherwise reject.
64:                // Done this way in case custodial wallets are used as the confirmationWallet - which sometimes won't allow for smart contract calls.
65:        if ( msg.value >= .05 ether )
66:                activeTimelock = block.timestamp + TIMELOCK_DURATION; // establish the timelock
67:        else
68:                        activeTimelock = type(uint256).max; // effectively never
69:        }
```
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/ManagedWallet.sol#L59C1-L69C10

### Steps to Reproduce
1. Invoke `proposeWallets` from the `mainWallet` with new wallet addresses.
2. Send an amount greater than 0.05 Ether to the contract from the `confirmationWallet`.
3. Notice that the surplus Ether is retained by the contract without a refund.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Introduce a refund mechanism within the `receive()` function to return any surplus Ether above the 0.05 Ether confirmation requirement.

### Suggested Fix

```diff
    receive() external payable
    	{
    	require( msg.sender == confirmationWallet, "Invalid sender" );

		// Confirm if .05 or more ether is sent and otherwise reject.
		// Done this way in case custodial wallets are used as the confirmationWallet - which sometimes won't allow for smart contract calls.
-   	if ( msg.value >= .05 ether )
-    		activeTimelock = block.timestamp + TIMELOCK_DURATION; // establish the timelock
+       if ( msg.value >= .05 ether ){
+               payable(msg.sender).call{value: msg.value - 0.05 ether}("");
+       }
    	else
			activeTimelock = type(uint256).max; // effectively never
        }
```


## Assessed type

ETH-Transfer
WBTC Price becomes problematic if WBTC depegs
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/price_feed/CoreChainlinkFeed.sol#L21


# Vulnerability details

## Impact
 Reserve ratios and pool constraints may break if the WBTC bridge becomes compromised, and WBTC undergoes depegging

## Proof of Concept

The chainlink BTC/USD oracle is utilized to price WBTC [https://github.com/code-423n4/2024-01-salty/blob/main/src/price_feed/CoreChainlinkFeed.sol#L21] .
 WBTC, being a bridged asset, could depeg if the bridge is compromised or fails, causing it to no longer be equivalent to BTC. This scenario would result in all swap functions (essentially the entire protocol) being active against an asset that is now effectively worthless.The protocol continues to value WBTC via BTC/USD, leading to several potential malicious actions, especially since many invariants depend on the WBTC/ETH pool


As the protocol operates smoothly with at least two functioning price feeds,the `CoreUniswapFeed` UniV3 TWAP safeguards against such asset depegging but other two `CoreSaltyFeed` and `CoreChainlinkFeed` will have no effect and hence protocol will keep functioning even in such case
## Tools Used

Manual

## Recommended Mitigation Steps

Conduct a thorough analysis and implement a robust solution for determining the price of WBTC.


## Assessed type

Oracle
Rounding Errors
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/price_feed/PriceAggregator.sol#L139


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
Rounding Errors: The contract calculates the average price as the sum of two prices divided by two. This can introduce rounding errors, especially if the prices are odd numbers.

## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

uint256 averagePrice = ( priceA + priceB ) / 2;

## Tools Used

## Recommended Mitigation Steps
use some other method for odd numbers.


## Assessed type

Context
Centralization Risk
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/price_feed/PriceAggregator.sol#L37


# Vulnerability details

## Impact
Detailed description of the impact of this finding.

Centralization Risk: The contract relies on the owner to manage price feeds and settings, which introduces a central point of failure. If the owner's account is compromised, the attacker could manipulate the price feeds or settings.

## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
@>> function setInitialFeeds( IPriceFeed _priceFeed1, IPriceFeed _priceFeed2, IPriceFeed _priceFeed3 ) public onlyOwner
		{
		require( address(priceFeed1) == address(0), "setInitialFeeds() can only be called once" );

		priceFeed1 = _priceFeed1;
		priceFeed2 = _priceFeed2;
		priceFeed3 = _priceFeed3;
		}

@ >>function setPriceFeed( uint256 priceFeedNum, IPriceFeed newPriceFeed ) public onlyOwner
		{
@>> function changeMaximumPriceFeedPercentDifferenceTimes1000(bool increase) public onlyOwner
		{


## Tools Used
VS code
## Recommended Mitigation Steps
use multsig 


## Assessed type

Access Control
Creation of token whitelisting proposals can be DOS'd
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/Proposals.sol#L162-L177
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/Proposals.sol#L81-L118


# Vulnerability details

## Summary
The creation of token whitelisting proposals isn limited to 5 proposals; after which it is DOS'd until proposals are voted on and finalized.

## Vulnerability Details
In `proposeTokenWhitelisting()` when a proposal is created to whitelist a new token; the `ballotId` is added to `_openBallotsForTokenWhitelisting`.
There is then a check to ensure that the length of `_openBallotsForTokenWhitelisting` does not exceed `daoConfig.maxPendingTokensForWhitelisting()`.

Where `maxPendingTokensForWhitelisting` has been reached, new proposals will not be created for whitelisting tokens. This could happen by accident or by malicious actions on the part of users manipulating the system.

By default `maxPendingTokensForWhitelisting` is set to 5 but it could be decreased via vote to 3 which would make the issue even more common place.

```
	function proposeTokenWhitelisting( IERC20 token, string calldata tokenIconURL, string calldata description ) external nonReentrant returns (uint256 _ballotID)
		{

         // SOME CODE

>>>      require( _openBallotsForTokenWhitelisting.length() < daoConfig.maxPendingTokensForWhitelisting(), "The maximum number of token whitelisting proposals are already pending" );

         // SOME CODE

	 uint256 ballotID = _possiblyCreateProposal( ballotName, BallotType.WHITELIST_TOKEN, address(token), 0, tokenIconURL, description );
		_openBallotsForTokenWhitelisting.add( ballotID );

		return ballotID;
		}
```

## POC
Add the test function below to `DAO.t.sol` and run:

    ```
    function test_DOS_TokenWhitelisting() public
		{

		// declare names; need 4 more to create proposals
		address charlie = address( 0x3333 );
		address deployer = address( 0x73107dA86708c2DAd0D91388fB057EeE3E2581aF );
		address paul = address( 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 );
		address jim = address( 0x123456789 );

		// transfer salt to all 6 for staking and voting
		vm.startPrank(address(daoVestingWallet));
		salt.transfer(alice, 200000 ether);		
		salt.transfer(bob, 200000 ether);
		salt.transfer(charlie, 200000 ether);
		salt.transfer(deployer, 200000 ether);
		salt.transfer(paul, 200000 ether);
		salt.transfer(jim, 200000 ether);
		salt.transfer(address(dao), 1000000 ether);
		vm.stopPrank();

		// create 6 tokens
		IERC20 test = new TestERC20( "TEST", 18 );
		IERC20 test1 = new TestERC20( "TEST1", 18 );
		IERC20 test2 = new TestERC20( "TEST2", 18 );
		IERC20 test3 = new TestERC20( "TEST3", 18 );
		IERC20 test4 = new TestERC20( "TEST4", 18 );
		IERC20 test5 = new TestERC20( "TEST5", 18 );

		// Each user stake SALT and try to create token whitelisting proposal
		vm.startPrank(alice);
			staking.stakeSALT(100000 ether);
			proposals.proposeTokenWhite