xanoutas/solidity-security-findings huggingface dataset:
Upgraded Q -> 2 from #907 [1706966375748]
Audit: 2024-01-salty-findings
Severity: MEDIUM

Judge has assessed an item in Issue #907 as 2 risk. The relevant finding follows:

 [L-02] In ManagedWallet.sol, the activeTimelock can be set even before proposeWallets is called, so the activeTimelock can be bypassed.
The confirmation wallet can send 0.05 ether even before proposeWallets() function is called to skip the TIMELOCK_DURATION.

    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 )
                //@audit - Skip this part by depositing 0.05 ether before calling `proposeWallets()`
                activeTimelock = block.timestamp + TIMELOCK_DURATION; // establish the timelock
        else
                        activeTimelock = type(uint256).max; // effectively never
        }
Make sure the activeTimelock is only set once the proposeWallet() function is called.

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/ManagedWallet.sol#L59-L69
Upgraded Q -> 2 from #913 [1706966292449]
Audit: 2024-01-salty-findings
Severity: MEDIUM

Judge has assessed an item in Issue #913 as 2 risk. The relevant finding follows:

 [L-01] Reserves Above DUST Incorrectly Enforced
Pools Line 270

Issue Description:

In the Salty protocol’s _adjustReservesForSwap() function, the intention, as per the accompanying comment (“// Make sure that the reserves after swap are above DUST”), is to ensure that the reserves always remain above a certain threshold defined by the DUST constant. However, the current implementation of this check uses >= (greater than or equal to), which leads to an off-by-one error. The actual requirement, as intended, is for the reserves to be strictly greater than DUST.

Current implementation:

require( (reserve0 >= PoolUtils.DUST) && (reserve1 >= PoolUtils.DUST), "Insufficient reserves after swap");
This condition allows the reserves to be exactly equal to DUST, which contradicts the intended logic of keeping them above DUST.

Recommended Mitigation Steps

To align the implementation with the intended behavior, the require statement should be adjusted to enforce that reserves are strictly greater than DUST. This change will ensure that the reserves are maintained above the minimal threshold, as originally planned.

Proposed change:

require( (reserve0 > PoolUtils.DUST) && (reserve1 > PoolUtils.DUST), "Insufficient reserves after swap");
By modifying the condition to > (greater than), the function will correctly enforce that the reserves are maintained above the DUST level, thus adhering to the protocol’s intended safety and operational standards.
Upgraded Q -> 2 from #978 [1706965914473]
Audit: 2024-01-salty-findings
Severity: MEDIUM

Judge has assessed an item in Issue #978 as 2 risk. The relevant finding follows:

 2, approve() in _depositLiquidityAndIncreaseShare() does not reset to 0 after transfer, future approve can be reverted
Link to code
https://github.com/code-423n4/2024-01-salty/blob/main/src/staking/Liquidity.sol#L83-#L117

Vulnerability details
To make Pools contract able to use token in the contract, approve() is used:

	tokenA.approve( address(pools), maxAmountA );
	tokenB.approve( address(pools), maxAmountB );
But it might not use all of them, so the unused part will be refunded to the user:

	if ( addedAmountA < maxAmountA )
		tokenA.safeTransfer( msg.sender, maxAmountA - addedAmountA );

	if ( addedAmountB < maxAmountB )
		tokenB.safeTransfer( msg.sender, maxAmountB - addedAmountB );
Some tokens, like USDT, will revert new approve if allowance is not zero, it will lead to function revert.

Mitigration steps
Approve to zero after calling pools.addLiquidity() function
Breaking of protocol AMM invariant
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/pools/Pools.sol#L168-L188


# Vulnerability details

## Impact
When implementing an AMM, the token reserves are usually kept in a balanced ratio to avoid issues like price manipulation etc     

## Proof of Concept
When implementing an AMM, the token reserves are usually kept in a balanced ratio to avoid issues. We can see that in the removeliquidity() the reserves are checked after liquidity is removed to maintain that the reserves of each token are still above the Dust amount set by the protocol
```solidity
                // Make sure that removing liquidity doesn't drive either of the reserves below DUST.
		// This is to ensure that ratios remain relatively constant even after a maximum withdrawal.
   require((reserves.reserve0 >= PoolUtils.DUST) && (reserves.reserve0 >= PoolUtils.DUST), "Insufficient reserves after liquidity removal");
```
But this check made above is wrong as it validates just the reserve for a particular reserve.
A malicious depositor or liquidity provider can be the first to deposit liquidity in a new deployed pool and supply lets say tokenA and tokenB and then after other LPs provides a certain amount of liquidity they withdraw all of their tokenB and the supposed amount of TokenA. This will cause a huge imbalance of tokens in the pool and can be used to manipulate price as the pool will be serving wrong prices due to this imbalance.
In a situation like with the protocol that intend on using pool reserves as a source of price feed it can be generating wrong prices. The attacker can go ahead and do this for multiple other pools, thereby rrendering the pool reserves prices unusable. If should the chainlink price feeds have a downtime or an issue like i discussed in another finding then source for prices will be bricked   
## Tools Used
Manual Review
## Recommended Mitigation Steps
	require((reserves.reserve0 >= PoolUtils.DUST) && (reserves.reserve1 >= PoolUtils.DUST), "Insufficient reserves after liquidity removal");



## Assessed type

Other
liquidateUse cannot happen.
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
If there is a sharp change in collateral price and it goes than than 95 percent and 
loss in liquidateUse is more than 5 percentage than no one wants to liquidate the asset.
## 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 liquidateUser( address wallet ) external nonReentrant
		{
		require( wallet != msg.sender, "Cannot liquidate self" );

		// First, make sure that the user's collateral ratio is below the required level
		require( canUserBeLiquidated(wallet), "User cannot be liquidated" );

		uint256 userCollateralAmount = userShareForPool( wallet, collateralPoolID );

		// Withdraw the liquidated collateral from the liquidity pool.
		// The liquidity is owned by this contract so when it is withdrawn it will be reclaimed by this contract.
		(uint256 reclaimedWBTC, uint256 reclaimedWETH) = pools.removeLiquidity(wbtc, weth, userCollateralAmount, 0, 0, totalShares[collateralPoolID] );

		// Decrease the user's share of collateral as it has been liquidated and they no longer have it.
		_decreaseUserShare( wallet, collateralPoolID, userCollateralAmount, true );

	@>>	// The caller receives a default 5% of the value of the liquidated collateral.
		uint256 rewardPercent = stableConfig.rewardPercentForCallingLiquidation();

		uint256 rewardedWBTC = (reclaimedWBTC * rewardPercent) / 100;
		uint256 rewardedWETH = (reclaimedWETH * rewardPercent) / 100;

		// Make sure the value of the rewardAmount is not excessive
		uint256 rewardValue = underlyingTokenValueInUSD( rewardedWBTC, rewardedWETH ); // in 18 decimals
		uint256 maxRewardValue = stableConfig.maxRewardValueForCallingLiquidation(); // 18 decimals
		if ( rewardValue > maxRewardValue )
			{
			rewardedWBTC = (rewardedWBTC * maxRewardValue) / rewardValue;
			rewardedWETH = (rewardedWETH * maxRewardValue) / rewardValue;
			}

		// Reward the caller
		wbtc.safeTransfer( msg.sender, rewardedWBTC );
		weth.safeTransfer( msg.sender, rewardedWETH );

		// Send the remaining WBTC and WETH to the Liquidizer contract so that the tokens can be converted to USDS and burned (on Liquidizer.performUpkeep)
		wbtc.safeTransfer( address(liquidizer), reclaimedWBTC - rewardedWBTC );
		weth.safeTransfer( address(liquidizer), reclaimedWETH - rewardedWETH );

		// Have the Liquidizer contract remember the amount of USDS that will need to be burned.
		uint256 originallyBorrowedUSDS = usdsBorrowedByUsers[wallet];
		liquidizer.incrementBurnableUSDS(originallyBorrowedUSDS);

		// Clear the borrowedUSDS for the user who was liquidated so that they can simply keep the USDS they previously borrowed.
		usdsBorrowedByUsers[wallet] = 0;
		_walletsWithBorrowedUSDS.remove(wallet);

		emit Liquidation(msg.sender, wallet,
Voting power is not recalculated on ballot finalizing, which makes possible to win with a unfair amount of votes
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L259-L293
https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/DAO.sol#L278-L291
https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L385-L400
https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/DAO.sol#L219-L228
https://github.com/code-423n4/2024-01-salty/blob/main/src/dao/Proposals.sol#L130-L152


# Vulnerability details

## Impact
In the function `Proposals::castVote` is used to vote for a proposal made to the DAO. This functions uses the amount staked in the SALT pool to calculate the voting power of the user. 

However, the vote is not recalculated in none of the functions used during the finalization of a proposal, this are the functions:

* `DAO::finalizeBallot()`
* `DAO::_finalizeApprovalBallot()`
* `Proposal::canFinalizeBallot()`
* `Proposal::markBallotAsFinalized()`

As impact, this enables a user to win a proposal vote with a unfair amount of voting power, as shown in the Proof of Concept.

## Proof of Concept

Here is the scenario showing that if a user has 33.34% voting power, they can win any Yes or No proposal.

- A proposal is created by Alice
- Alice stakes Salt.
- Alice votes
- Alice untakes Salt
- Alice wait two weeks to get her tokens.
- Alice transfers her Salts to another account of hers (bob).
- Alice uses the other account to stake Salt
- The other account votes on the proposal, doubling the vote power since it used the same tokens on Alice's account.

Save the test in a file called POC.t.sol and run the test with 
`NETWORK="sep" forge test -vv --mt testVotingPOC --rpc-url "YOUR_RPC_URL"`

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

import "../../dev/Deployment.sol";
import "./ExcessiveSupplyToken.sol";

contract TestProposals is Deployment
{
	// User wallets for testing
    address public constant alice = address(0x1111);
    address public constant bob = address(0x2222);
    address public constant charlie = address(0x3333);

	constructor()
	{
		grantAccessAlice();
		grantAccessBob();
		grantAccessCharlie();
		grantAccessDeployer();
		grantAccessDefault();

		vm.prank(address(initialDistribution));
		salt.transfer(DEPLOYER, 100000000 ether);

		vm.startPrank( DEPLOYER );
		salt.transfer( alice, 10000000 ether );
		salt.transfer( DEPLOYER, 90000000 ether );
		vm.stopPrank();

		// Mint some USDS to the DEPLOYER and alice
		vm.startPrank( address(collateralAndLiquidity) );
		usds.mintTo( DEPLOYER, 2000000 ether );
		usds.mintTo( alice, 1000000 ether );
		vm.stopPrank();

		// Allow time for proposals
		vm.warp( block.timestamp + 45 days );
	}

    function setUp() public
    {
    	vm.startPrank( DEPLOYER );
    	usds.approve( address(proposals), type(uint256).max );
    	salt.approve( address(staking), type(uint256).max );
    	vm.stopPrank();

    	vm.startPrank( alice );
    	usds.approve( address(proposals), type(uint256).max );
    	salt.approve( addre
ChainlinkAdapterOracle uses the ChainlinkFeedRegistry to obtain the price of the requested tokens.
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#L2


# Vulnerability details

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

Chainlink aggregators have a built in circuit breaker if the price of an asset goes outside of a predetermined price band. The result is that if an asset experiences a huge drop in value (i.e. LUNA crash) the price of the oracle will continue to return the minPrice instead of the actual price of the asset. This would allow user to continue borrowing with the asset but at the wrong price. This is exactly what happened to Venus on BSC when LUNA imploded.

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


try chainlinkFeed.latestRoundData()
		returns (
			uint80, // _roundID
			int256 _price,
			uint256, // _startedAt
			uint256 _answerTimestamp,
			uint80 // _answeredInRound
		)
			{
			// Make sure that the Chainlink price update has occurred within its 60 minute heartbeat
			// https://docs.chain.link/data-feeds#check-the-timestamp-of-the-latest-answer
			uint256 answerDelay = block.timestamp - _answerTimestamp;

			if ( answerDelay <= MAX_ANSWER_DELAY )
				price = _price;
			else
				price = 0;
			}
		catch (bytes memory) // Catching any failure
			{
			// In case of failure, price will remain 0
			}

		if ( price < 0 )
			return 0;

		// Convert the 8 decimals from the Chainlink price to 18 decimals
		return uint256(price) * 10**10;
		}


ChainlinkFeedRegistry#latestRoundData pulls the associated aggregator and requests round data from it. ChainlinkAggregators have minPrice and maxPrice circuit breakers built into them. This means that if the price of the asset drops below the minPrice, the protocol will continue to value the token at minPrice instead of it's actual value. This will allow users to take out huge amounts of bad debt and bankrupt the protocol.

Example: TokenA has a minPrice of $1. The price of TokenA drops to $0.10. The aggregator still returns $1 allowing the user to borrow against TokenA as if it is $1 which is 10x it's actual value.

Note: Chainlink oracles are used a just one piece of the OracleAggregator system and it is assumed that using a combination of other oracles, a scenario like this can be avoided. However this is not the case because the other oracles also have their flaws that can still allow this to be exploited. As an example if the chainlink oracle is being used with a UniswapV3Oracle which uses a long TWAP then this will be exploitable when the TWAP is near the minPrice on the way down. In a scenario like that it wouldn't matter what the third oracle was because it would be bypassed with the two matching oracles prices. If secondary oracles like Band are used a malicious user could DDOS relayers to prevent update pricing. Once the price becomes stale the chainlink oracle would be the only oracle left and i
Liquidation logic is not robust enough to ensure proper collateralization of USDS, which can lead to depeg
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/stable/Liquidizer.sol#L132-L152
https://github.com/code-423n4/2024-01-salty/blob/main/src/pools/Pools.sol#L409-L419


# Vulnerability details

## Impact

The liquidation logic is generally flawed and is not robust enough to ensure that USDS does not depeg. This occurs for multiple reasons, the first being the default liquidation threshold of 110%, which is too low & during times of high congestion its very possible this drops lower than 100% by the time `CollateralAndLiquidity:liquidateUser` lands on chain. Also, unless the position being liquidated is > 10_000 USD (based on `stableConfig.maxRewardValueForCallingLiquidation()`), the liquidator will get exactly 5% of the collateral as a liquidation fee. This means the value of the collateral even if this was called exactly when the user is liquidated is 110-5.5=104.5%.

Additionally, the `liquidateUser` function also doesn't actually liquidate the WBTC & WETH collateral, but instead sends it to the Liquidizer contract. Here it will sit (losing additional value during a market dump) until `step1` of `Upkeep:performUpkeep` is done. Since there's no central upkeeper, this assumes that there's enough arbitrage rewards to pay a user to pay the gas to trigger `Upkeep:performUpkeep` (in `step2`) - if this is not the case, again the collateral will be sitting and losing additional value during a market dump. 

Then, the logic in `Liquidizer:performUpkeep` will only swap WBTC/WETH into USDS up to 1% of the outstanding reserves in the the WBTC-USDS & WETH-USDS pools. Although this might make sense to lessen the impact of slippage, this amount is independent of how much USDS is actually required / how much collateral was received which should be swapped into USDS. This means that its possible not enough WBTC & WETH collateral is swapped, and so the remaining again sits in the Liquidizer contract losing value (during a market dump). Also relevant is that when this occurs, DAO POL will be removed (when its not strictly necessary that it is), meaning there is also a loss of funds.

## Proof of Concept

Let's walk through the liquidation logic to see where some of these issues lay. A user is first liquidated when their collateralization ratio is below 110%. This is done with a call to [`CollateralAndLiquidity:liquidateUser`](https://github.com/code-423n4/2024-01-salty/blob/main/src/stable/CollateralAndLiquidity.sol#L140-L188), which has the following logic:
```solidity
...
// The caller receives a default 5% of the value of the liquidated collateral.
uint256 rewardPercent = stableConfig.rewardPercentForCallingLiquidation();

@> uint256 rewardedWBTC = (reclaimedWBTC * rewardPercent) / 100;
@> uint256 rewardedWETH = (reclaimedWETH * rewardPercent) / 100;
...
// Reward the caller
wbtc.safeTransfer( msg.sender, rewardedWBTC );
weth.safeTransfer( msg.sender, rewardedWETH );

// Send the remaining WBTC and WETH to the Liquidizer contract so tha
Checks for Zero Balance
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
Checks for Zero Balance: The burnTokensInContract function does not check if the balance is zero before attempting to burn, which would result in unnecessary gas usaAccessge if there are no tokens to burn.
## 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 burnTokensInContract() external
    	{
    	uint256 balance = balanceOf( address(this) );
    @>	_burn( address(this), balance );

    	emit SALTBurned(balance);
    	}
## Tools Used

## Recommended Mitigation Steps
Add a require statement to check balance is greater than 0.


## Assessed type

Context
Timelock Bypass
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

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

Timelock Bypass: There is no check to reset the activeTimelock if a proposal is rejected. If the confirmationWallet confirms a proposal and then sends less than 0.05 ETH before the changeWallets function is called, the activeTimelock would be set to type(uint256).max, effectively bypassing the timelock.
## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

	// 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
        }
## Tools Used
vs code
## Recommended Mitigation Steps
we have to implement timelock in a different way so that our timelock cannot be bypassed.


## Assessed type

Context
Malicious borrower can spam borrows with less than dust positions leading to many insolvent ans unliquidatable positions in the protocol
Audit: 2024-01-salty-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/CollateralAndLiquidity.sol#L95
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/CollateralAndLiquidity.sol#L115
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/CollateralAndLiquidity.sol#L145


# Vulnerability details

## Impact

Malicious user can spam the protocol with unliquidatable positions leaving the `CollateralAndLiquidity.sol` pool with dead positions.

## POC Summary

This vulnerability is borne form the fact that the user is allowed to repay any amount they like

- Borrower takes up loan
- repays up between 90% to 95% of the loan
- borrower may decide to withdraws max collateral withdrawable after repayment
- price of collateral drops and position becomes undercollateralised
- liquidator calling `liquidateUser(...)` will revert for the outstanding balance and as such position is left underwater or the borrower decides to repay whenever he deems fit spaming the protocol with dead positions
- Borrower can withdraw most of their liquidity.
- There is a higher propensity for this attack when the collateralisation ratio is still close to 110%

## Coded POC

Add the following test case to the `CollateralAndLiquidity.t.sol` file and run `COVERAGE="yes" NETWORK="sep" forge test --mt testLiquidateIsNotProfitableForSpamPositions -vv --rpc-url https://rpc.ankr.com/eth_sepolia` 

```solidity
function testLiquidateIsNotProfitableForSpamPositions() public {

		assertEq( collateralAndLiquidity.numberOfUsersWithBorrowedUSDS(), 0 );

		assertEq(wbtc.balanceOf(address(usds)), 0, "USDS contract should start with zero WBTC");
		assertEq(weth.balanceOf(address(usds)), 0, "USDS contract should start with zero WETH");
		assertEq(usds.balanceOf(alice), 0, "Alice should start with zero USDS");

		// Total needs to be worth at least $2500
		uint256 depositedWBTC = ( 1000 ether *10**8) / priceAggregator.getPriceBTC();
		uint256 depositedWETH = ( 1000 ether *10**18) / priceAggregator.getPriceETH();

        emit log_named_uint("depositedWBTC", depositedWBTC );
        emit log_named_uint("depositedWETH", depositedWETH );

		(uint256 reserveWBTC, uint256 reserveWETH) = pools.getPoolReserves(wbtc, weth);
		assertEq( reserveWBTC, 0, "reserveWBTC doesn't start as zero" );
		assertEq( reserveWETH, 0, "reserveWETH doesn't start as zero" );

		vm.startPrank(alice);
		(,, uint256 aliceInitialAddedLiquidityIncrement) = collateralAndLiquidity.depositCollateralAndIncreaseShare( depositedWBTC, depositedWETH, 0, block.timestamp, false );
        emit log_named_uint("aliceInitialAddedLiquidityIncrement", aliceInitialAddedLiquidityIncrement );
		uint256 maxUSDS = collateralAndLiquidity.maxBorrowableUSDS(alice);
        // emit log_named_uint("initial maxUSDS", maxUSDS );
		assertEq( maxUSDS, 0, "Alice doesn't have enough collateral to borrow USDS" );

		// De