xanoutas/solidity-security-findings huggingface dataset:
If the confirmation wallet rejects the proposal, the managedwallet will never be able to upgrade
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/ManagedWallet.sol#L49


# Vulnerability details

# Bug Description

The Salty protocol's `ManagedWallet` contract, designed for managing 2 wallet addresses, has a critical flaw in its address update process. This process involves a three-step procedure where the main wallet proposes new main and confirmation wallets, the confirmation wallet confirms or rejects the proposal, and there's a 30-day timelock before the change takes effect.

A significant issue arises because the `proposedMainWallet` and `proposedConfirmationWallet` are not reset after a rejection. The `receive()` function, responsible for handling the confirmation or rejection, does not reset these addresses if a proposal is rejected.

```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
	else
		activeTimelock = type(uint256).max; // effectively never
	}
```

Additionally, the `proposeWallets` function is designed to reject any new proposals if a non-zero `proposedMainWallet` already exists.

```solidity
// Make sure we're not overwriting a previous proposal (as only the confirmationWallet can reject proposals)

require( proposedMainWallet == address(0), "Cannot overwrite non-zero proposed mainWallet." );
```

This combination of behaviors leads to a deadlock in the contract where, once a proposal is rejected, no further proposals can be made.

# Impact

This bug effectively freezes the `ManagedWallet` contract, preventing any further updates to the main and confirmation wallets if a proposal is ever rejected.

# Proof of Concept

The issue can be demonstrated as follows:

- A main wallet proposes new main and confirmation wallets.
- The confirmation wallet rejects the proposal.
- Subsequent attempts to propose new wallets fail due to the non-reset `proposedMainWallet`.

```solidity
function testRejectFreezesFunctionality() public {
	address nonZeroProposedMainWallet = address(0x5555);
	address nonZeroProposedConfirmationWallet = address(0x6666);
	ManagedWallet managedWallet = new ManagedWallet(alice, address(0x2222));

	// Simulate that the mainWallet has already proposed wallets
	vm.prank(alice);
	managedWallet.proposeWallets(nonZeroProposedMainWallet, nonZeroProposedConfirmationWallet);

	//Now the confirmation wallet rejects the proposal
	vm.startPrank(address(this));
	vm.deal(address(this), 0.04 ether);
	(bool success,) = address(managedWallet).call{value: 0.04 ether}("");
	vm.stopPrank();

	// Try proposing new wallets again, which will not be possible
	address newProposedMainWallet = address(0x7777);
	address newProposedConfirmationWallet = addre
tokens transfer fees.
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/pools/Pools.sol#L283


# Vulnerability details

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

The contract assumes that tokens do not have transfer fees. If a token with transfer fees is used, it could disrupt the balance calculations.
## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.


## Tools Used

## Recommended Mitigation Steps
we should keep in mind tokens with transfer fees.


## Assessed type

Context
Token unwhitelisting does not update rewards earned by the token's Pools
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/pools/PoolsConfig.sol#L63-L74
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/rewards/RewardsEmitter.sol#L57-L77
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/StakingRewards.sol#L182-L206


# Vulnerability details

## Summary
When a token is unwhitelisted, it's Pools are also unwhitelisted. These Pools may have accrued awards between the last time `performUpkeep()` was called and it's subsequent unwhitelisting. However, once a Pool is unwhitelisted, it is no longer eligible to receive Liquidity Rewards meaning users who provided liquidity to these pools will lose any rewards accrued from Arbitrage Profits.

## Vulnerability Details
There may be arbitrage profits sitting in the Pools contract; a portion of which was generated by the Pool being unwhitelisted. There is no logic in `PoolsConfig::unwhitelistPool()` to account for those profits before unwhitelisting. 

```
	function unwhitelistPool( IPools pools, IERC20 tokenA, IERC20 tokenB ) external onlyOwner
		{
		bytes32 poolID = PoolUtils._poolID(tokenA,tokenB);

		_whitelist.remove(poolID);
		delete underlyingPoolTokens[poolID];

		// Make sure that the cached arbitrage indicies in PoolStats are updated
		pools.updateArbitrageIndicies();

		emit PoolUnwhitelisted(address(tokenA), address(tokenB));
		}
```

After Pools have been unwhitelisted there is a check in `RewardsEmitter::addSALTRewards()` preventing any additional rewards being added to `liquidityRewardsEmitter`; so the rewards will remain in the `saltRewards` contract.
There is a subsequent check in `stakingRewards::addSALTRewards()` which prevents additional rewards being added so the protocol cannot manually add rewards to rectify the issue.

```
	function addSALTRewards( AddedReward[] calldata addedRewards ) external nonReentrant
		{
		uint256 sum = 0;
		for( uint256 i = 0; i < addedRewards.length; i++ )
			{
			AddedReward memory addedReward = addedRewards[i];
>>>>		require( poolsConfig.isWhitelisted( addedReward.poolID ), "Invalid pool" );

			uint256 amountToAdd = addedReward.amountToAdd;
			if ( amountToAdd != 0 )
				{
				// Update pendingRewards so the SALT can be distributed later
				pendingRewards[ addedReward.poolID ] += amountToAdd;
				sum += amountToAdd;
				}
			}

		// Transfer the SALT from the sender for all of the specified rewards
		if ( sum > 0 )
			salt.safeTransferFrom( msg.sender, address(this), sum );
		}
```

## POC
Add the test function below to `DAO.t.sol` and run.
The test shows Alice is able to claim due rewards from `stakingRewards` which had already accrued for an unwhitelisted Pool. It then shows that any new attempt to add rewards via `liquidityRewardsEmitter::addSALTRewards` or `stakingRewards::addSALTRewards` will revert.


    ```
	function testClaimAllRewards() public {

	    vm.startPran
Precision loss in Airdrop contract
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/launch/Airdrop.sol#L64


# Vulnerability details

## Impact
A small amount of `xSALT` tokens will be stuck in the `Airdrop` contract due to precision loss. The amount of tokens that should be sent to users is equal to `saltBalance / numberAuthorized()`. When claiming is allowed, every user can call the `claimAirdrop` function to receive their reward.

Unfortunately, due to precision loss, a small amount of tokens will be stuck in the contract. In a scenario where the total number of users is, for example, `2707`, the amount that should be sent to users will be `1847063169560398965644` = `(5 * MILLION_ETHER / 2707)` and the total sent amount will be `4999999999999999999998308`. The result of `5 * MILLION_ETHER - 4999999999999999999998308 `will remain in the `Airdrop` contract forever.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Multiply the `saltBalance` before division by `numberAuthorized()` to avoid precision loss.


## Assessed type

Other
Unstaking xSALT does not remove cast votes meaning user can double vote
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/Staking.sol#L60-L79
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/Staking.sol#L101-L126


# Vulnerability details

## Summary
When a user unstakes `xSALT` any votes they have cast on open ballots are not removed meaninf they can vote on the same ballot with their recovered `SALT` by staking again.

## Vulnerability Details
In `Staking::unstake()` there is no logic to remove user's vites which they have cast on open ballots. This means that when they `recoverSALT()` they can simply transfer to another address which has access and the other user can vote on the same ballot.

This could be manipulaed in a number of different ways but the protocol's unstaking configuration would need to be taken into account by an attacker.
An attacker needs to be willing to take a loss in returned SALT in order to get a vote over the line which earned them a large advantage.
In it's default configuration the minimum unstaking term is 2 weeks, which will return `minUnstakePercent` of 20% of a user's `xSALT` in `SALT`. To use the exploit an attacker would need to recover their `SALT` before the end of the vote. 
A vote to send SALT to themselves could net the user enough SALT to cover the losses they incur by unstaking early.

The exploit likelihood increases as `minUnstakePercent` is increased towards it's 50% limit and `minUnstakeWeeks()` is reduced. 

There could also arise a situation where quorum is not reached on an early ballot, which the sits open, forgotten about. A user could unstake over the normal time period and use 100% of their voting power to vote again on the ballot and get it across the line.



```
function unstake( uint256 amountUnstaked, uint256 numWeeks ) external nonReentrant returns (uint256 unstakeID)
		{
		require( userShareForPool(msg.sender, PoolUtils.STAKED_SALT) >= amountUnstaked, "Cannot unstake more than the amount staked" );

		uint256 claimableSALT = calculateUnstake( amountUnstaked, numWeeks );
		uint256 completionTime = block.timestamp + numWeeks * ( 1 weeks );

		unstakeID = nextUnstakeID++;
		Unstake memory u = Unstake( UnstakeState.PENDING, msg.sender, amountUnstaked, claimableSALT, completionTime, unstakeID );

		_unstakesByID[unstakeID] = u;
		_userUnstakeIDs[msg.sender].push( unstakeID );

		// Decrease the user's staking share so that they will receive less future SALT rewards
		// This call will send any pending SALT rewards to msg.sender as well.
		// Note: _decreaseUserShare checks to make sure that the user has the specified staking share balance.
		_decreaseUserShare( msg.sender, PoolUtils.STAKED_SALT, amountUnstaked, false );

		emit UnstakeInitiated(msg.sender, unstakeID, amountUnstaked, claimableSALT, numWeeks);
		}
```

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

    ```
	function test_DoubleVote() public {

		vm.startPrank(alice
Confirmation proposals can be front run and 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#L240-L246
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/DAO.sol#L219-L228
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/DAO.sol#L155-L215


# Vulnerability details

## Summary
The ballotName check in `Proposals::_possiblyCreateProposal()` can be exploited to DOS `proposeSetContractAddress()` & `proposeWebsiteUpdate()` proposals.

## Vulnerability Details
Where a ballot, requiring a confirmation vote, is created with a genuine ballotName such as `setContract:pricefeed1`; a malicious user can create another ballot with ballotName `setContract:pricefeed1_confirm`.

```
	function proposeSetContractAddress( string calldata contractName, address newAddress, string calldata description ) external nonReentrant returns (uint256 ballotID)
		{
        // Some Code //

>>>		string memory ballotName = string.concat("setContract:", contractName );

        // Some Code //
		}

```

This will prevent the initial ballot from being finalized, even if it has gathered enough votes, because a confirmation proposal cannot be created via `_finalizeApprovalBallot`. Confirmation proposals are created with `_confirm` appended to the end of the initial ballotName; i.e. `setContract:pricefeed1` + `_confirm`; where it will hit the ballotName check:

```
require( openBallotsByName[ballotName] == 0, "Cannot create a proposal similar to a ballot that is still open" );
```

Because there is no way to cancel a proposal; this also blocks any future attempts to update the address for `pricefeed1`.
In the same way, address updates to `pricefeed2`, `pricefeed3` & `accessManager`  can be prevented.

Proposals for updating the website can be blocked in a very similar way by creating an identical proposal using the same `newWebsiteURL` and appending `_confirm` which will create a proposal `setURL:newWebsiteURL_confirm` and block the automatic confirmation proposal creation.

```
	function proposeWebsiteUpdate( string calldata newWebsiteURL, string calldata description ) external nonReentrant returns (uint256 ballotID)
		{
        // Some Code //

		string memory ballotName = string.concat("setURL:", newWebsiteURL );

        // Some Code //
		}
```


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

    ```
	function test_DOS_SetContract_Proposal() public
		{
        vm.startPrank(alice);

        staking.stakeSALT( 10000000 ether );
		uint256 ballotID = 1;
        proposals.proposeSetContractAddress( "priceFeed1", address(0x1231236 ), "description" );
		assertEq(proposals.ballotForID(ballotID).ballotName, "setContract:priceFeed1");

		assertEq(proposals.ballotForID(ballotID).ballotIsLive, true, "Ballot 1 not created correctly");

		proposals.castVote(ballotID, Vote.YES);

		vm.stopPrank();

		// front-run the finalization and create a proposal wh
LACK OF SLIPPAGE PROTECTION IN THE SWAP OPERATION PERFORMED IN THE `Liquidity._dualZapInLiquidity` FUNCTION
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

The `Liquidity._dualZapInLiquidity` function is used to deposit an arbitrary amount of one or both tokens into the pool and receive liquidity corresponding to the value of both of them. 

In this function the `excessive tokens` are swapped such that the liquidity is added to the pool in accordance with the pool reserve ratio. In order to `swap the excessive tokens` (Eg: swapAmountA to tokenB) the `pools.depositSwapWithdraw` function is called as shown below:

			zapAmountB += pools.depositSwapWithdraw( tokenA, tokenB, swapAmountA, 0, block.timestamp );

But the issue here is that there is no check on `slippage` and `deadline` is too strict. Since the slippage parameter is set to `0` in the function call as shwon above the swap transaction could be executed at a bad price point if the pool is manipulated. This will return less amount of `tokenB after the swap`. As a result the `zapAmountB` will be less than expected hence it will not complement the pool reserve ratio when the liquidity is added to the pool.

Furthermore the deadline is set to `block.timestamp` and this would make it difficult to execute the `_dualZapInLiquidity` transaction when the blockchain is congested. Since the `_dualZapInLiquidity` function is expected to be called frequently and the network can be congested this would result in frequent transaction reverts which could lead to bad user experience.

## Proof of Concept

```solidity
			zapAmountB += pools.depositSwapWithdraw( tokenA, tokenB, swapAmountA, 0, block.timestamp );
```

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

## Tools Used
Manual Review and VSCode

## Recommended Mitigation Steps

Hence it is recommended to set a minimum slippage value in the `pools.depositSwapWithdraw` function call so that bad swap operations can be omitted and set the `deadline` parameter slightly loosely such that the `_dualZapInLiquidity` transaction can be executed even during high network congestion without revert.


## Assessed type

Other
Division Leading to Loss of Precision
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/launch/Airdrop.sol#L64


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
Division Leading to Loss of Precision: When calculating saltAmountForEachUser, any remainder from the division is not accounted for. This could lead to a loss of precision and potentially leave some SALT tokens undistributed.

## 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 saltBalance = salt.balanceOf(address(this));
	@>	saltAmountForEachUser = saltBalance / numberAuthorized();

		// Have the Airdrop approve max so that that xSALT (staked SALT) can later be transferred to airdrop recipients.
		salt.approve( address(staking), saltBalance );

## Tools Used
VS Code
## Recommended Mitigation Steps
currently, I cannot find the exact location where to send the remaining salt.



## Assessed type

Other
ManagedWallet: Rejecting a proposal to change the wallet addresses prohibits future proposals
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/ManagedWallet.sol#L68


# Vulnerability details

A proposal to change the main and confirmation wallet addresses of a ManagedWallet can only be made if the current value of `proposedMainWallet` is `address(0)`, i.e. no pending proposal exists (https://github.com/code-423n4/2024-01-salty/blob/main/src/ManagedWallet.sol#L49).

A proposal is accepted or rejected by sending Ether to the ManagedWallet from the current confirmation wallet. If the Ether amount sent is less than 0.05 Ether, the proposal is rejected. However, rejecting the proposal does not reset `proposedMainWallet` to `address(0)`, so no future proposals can be made.

## Impact

After a proposal to change the main and confirmation wallet addresses of a ManagedWallet has been rejected, no further proposals to change the wallet can be made, except if the proposal is accepted and then executed (which is still possible after rejecting it).

## Proof of Concept

https://github.com/code-423n4/2024-01-salty/blob/main/src/ManagedWallet.sol#L41-L55:

Proposing new addresses via `proposeWallets()` is only possible if `proposedMainWallet == address(0)` and this sets `proposedMainWallet`:

```solidity
// Make a request to change the main and confirmation wallets.
function proposeWallets( address _proposedMainWallet, address _proposedConfirmationWallet ) external
    {
    [...]

    // Make sure we're not overwriting a previous proposal (as only the confirmationWallet can reject proposals)
    require( proposedMainWallet == address(0), "Cannot overwrite non-zero proposed mainWallet." );

    proposedMainWallet = _proposedMainWallet;

    [...]

    }
```

https://github.com/code-423n4/2024-01-salty/blob/main/src/ManagedWallet.sol#L58-L69:

Rejecting the proposal sets the `activeTimelock` to `type(uint256).max` so `changeWallets()` cannot be called to execute the change, but doesn't reset `proposedMainWallet`, prohibiting future proposals:

```solidity
// The confirmation wallet confirms or rejects wallet proposals by sending a specific amount of ETH to this contract
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
    else
        activeTimelock = type(uint256).max; // effectively never
    }
```

## Recommended Mitigation Steps

Set `proposedMainWallet` to `address(0)` in the `else` branch in `receive()`.


## Assessed type

Invalid Validation
Using the collateral assets' oracle price at 100% of its value to mint USDS without a fee can be used for arbitrage
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/stable/CollateralAndLiquidity.sol#L95-L111
https://github.com/code-423n4/2024-01-salty/blob/main/src/stable/CollateralAndLiquidity.sol#L268-L287


# Vulnerability details

## Impact

Allowing the users to mint and borrow USDS using the collateral assets, at 100% of its value based on the oracle prices without a fee can easily be exploited by the arbitragers.

## Proof of Concept

The Oracle price can not be trusted as the real-time price. Even if 3 different price feeds are used in `PriceAggregator`, the Deviation Threshold feature in Chainlink and the fact that Uniswap already gives a time-weighted *average price* will be enough to differentiate it from real-time pricing.

For example, the BTC/USD and ETH/USD Chainlink price feeds on mainnet have a Deviation threshold of 0.5%, meaning that the price will only be updated once the price movement exceeds 0.5% within the heartbeat period.

Say if the previous price point for WETH is 1000 USD, the price will only be updated once the price goes up to more than 1005 USD or down to less than 995 USD.

Below are the code snippets that show USDS being minted based on the oracle prices of the collateral assets;

```solidity
function borrowUSDS( uint256 amountBorrowed ) external nonReentrant
	{
	require( exchangeConfig.walletHasAccess(msg.sender), "Sender does not have exchange access" );
	require( userShareForPool( msg.sender, collateralPoolID ) > 0, "User does not have any collateral" );
	require( amountBorrowed <= maxBorrowableUSDS(msg.sender), "Excessive amountBorrowed" );

	// Increase the borrowed amount for the user
	usdsBorrowedByUsers[msg.sender] += amountBorrowed;

	// Remember that the user has borrowed USDS (so they can later be checked for sufficient collateralization ratios and liquidated if necessary)
	_walletsWithBorrowedUSDS.add(msg.sender);

	// Mint USDS and send it to the user
	usds.mintTo( msg.sender, amountBorrowed );

	emit BorrowedUSDS(msg.sender, amountBorrowed);
	}
```
```solidity
function maxBorrowableUSDS( address wallet ) public view returns (uint256)
	{
	// If the user doesn't have any collateral, then they can't borrow any USDS
	if ( userShareForPool( wallet, collateralPoolID ) == 0 )
		return 0;

	// The user's current collateral value will determine the maximum amount that can be borrowed
	uint256 userCollateralValue  = userCollateralValueInUSD( wallet );

	if ( userCollateralValue < stableConfig.minimumCollateralValueForBorrowing() )
		return 0;

	uint256 maxBorrowableAmount = ( userCollateralValue * 100 ) / stableConfig.initialCollateralRatioPercent();

	// Already borrowing more than the max?
	if ( usdsBorrowedByUsers[wallet] >= maxBorrowableAmount )
		return 0;

	return maxBorrowableAmount - usdsBorrowedByUsers[wallet];
   	}
```
## Tools Used

Manual Review

## Recommended Mitigation Steps

Require the collateral to be locked for a certain period of time before we can borrow USDS with the collate