xanoutas/solidity-security-findings huggingface dataset:
Incorrect decimal handling in `Stable2::decodeWellData()` may lead to miscalculations and potential financial losses
Audit: 2024-07-basin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/Stable2.sol#L310-L326


# Vulnerability details

## Description: 

The `Stable2` contract implements a gas-efficient StableSwap pricing function for Wells with 2 tokens. A critical component of this contract is the `decodeWellData()` function, which is responsible for decoding and setting the decimal places for the two tokens involved in the Well. This function plays a crucial role in ensuring accurate calculations throughout the contract's operations.

The `decodeWellData()` function is designed to extract the decimal places of two tokens from the provided `data` parameter. If either token's decimal value is zero, it should default to 18 decimals. However, there is a logical error in the implementation of this function.

The problematic code section is as follows:

```solidity
File: Stable2.sol
310:     function decodeWellData(bytes memory data) public view virtual returns (uint256[] memory decimals) {
311:         (uint256 decimal0, uint256 decimal1) = abi.decode(data, (uint256, uint256));
312: 
313:         // if well data returns 0, assume 18 decimals.
314:         if (decimal0 == 0) {
315:             decimal0 = 18;
316:         }
317:         // @audit : This should be checking decimal1 instead of decimal0
318:         if (decimal0 == 0) {
319:             decimal1 = 18;
320:         }
321:         if (decimal0 > 18 || decimal1 > 18) revert InvalidTokenDecimals();
322: 
323:         decimals = new uint256[](2);
324:         decimals[0] = decimal0;
325:         decimals[1] = decimal1;
326:     }

```

The issue lies in the second conditional statement, which incorrectly checks `decimal0` again instead of `decimal1`. This means that if `decimal1` is zero, it will not be properly set to the default value of 18, potentially leading to incorrect decimal handling for the second token.


## Impact

The incorrect handling of token decimals could lead to severe miscalculations throughout the contract's operations. Functions that rely on accurate decimal information, such as `calcLpTokenSupply()`, `calcReserve()`, and `calcRate()`, may produce incorrect results and loss of funds for users. 


## Proof of Concept

1. A Well is created with two tokens: TokenA with 18 decimals and TokenB with 6 decimals.
2. The `data` parameter passed to `decodeWellData()` correctly encodes these values: `(18, 6)`.
3. In `decodeWellData()`, `decimal0` is correctly set to 18.
4. Due to the bug, the function fails to set `decimal1` to 6, instead leaving it at 0.
5. The function returns `[18, 0]` instead of the correct `[18, 6]`.
6. A user attempts to swap 1,000,000 TokenB for TokenA.
7. Due to the incorrect decimal handling, the contract treats this as 1,000,000,000,000 TokenB (applying 18 decimals instead of 6).
8. The user receives 1000 times more TokenA than they should, draining the pool and causing significant losses to liquidity providers.

## Too
`_authorizeUpgrade` has been implemented incorrectly and will prevent Well upgrades
Audit: 2024-07-basin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/WellUpgradeable.sol#L65-L85


# Vulnerability details

## Impact
There are several issues with the current `_authorizeUpgrade` function which will prevent any upgradeable Well from being upgraded.

## Proof of Concept
Here is the current `_authorizeUpgrade` function:
```solidity
    function _authorizeUpgrade(address newImplmentation) internal view override {
        // verify the function is called through a delegatecall.
        require(address(this) != ___self, "Function must be called through delegatecall");


        // verify the function is called through an active proxy bored by an aquifer.
        address aquifer = aquifer();
        address activeProxy = IAquifer(aquifer).wellImplementation(_getImplementation());
        require(activeProxy == ___self, "Function must be called through active proxy bored by an aquifer");


        // verify the new implmentation is a well bored by an aquifier.
        require(
            IAquifer(aquifer).wellImplementation(newImplmentation) != address(0),
            "New implementation must be a well implmentation"
        );


        // verify the new implmentation is a valid ERC-1967 implmentation.
        require(
            UUPSUpgradeable(newImplmentation).proxiableUUID() == _IMPLEMENTATION_SLOT,
            "New implementation must be a valid ERC-1967 implmentation"
        );
    }
````

As we can see `IAquifer(aquifer).wellImplementation` is invoked several times. The issue is that it is being called incorrectly.
`IAquifer(aquifer).wellImplementation` is a mapping of wells pointing to their implementation contracts. When calling `IAquifer(aquifer).wellImplementation`, in order for it to work properly the address of a Well needs to be passed in as the parameter of the call, and the return value will be the address of the Well's implementation. Implementation contracts do not hold the actual protocol state, but are instead used only through delegatecalls.

In the `_authorizeUpgrade` function, whenever `IAquifer(aquifer).wellImplementation` is called, it is always passed an implementation contract as the parameter, not the address of a well. As a result, the return address of the call will most likely be `address(0)`, causing the function to revert.

Here is the `wellImplementation` mapping in the `Aquifer` contract, where we can see that it is a mapping of `well proxies => well implementations`:
```solidity
    // A mapping of Well address to the Well implementation addresses
    // Mapping gets set on Well deployment
    mapping(address => address) public wellImplementation;
```
Thus, the proper way of querying `wellImplementation` is by supplying the address of a well, not an implementation.

## Tools Used
Manual review

## Recommended Mitigation Steps
The `_authorizeUpgrade` function must be entirely reworked to work as expected.


## Assessed type

Other
Incorrect Error Handling
Audit: 2024-07-basin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/Stable2.sol#L314
https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/Stable2.sol#L315


# Vulnerability details

InvalidTokenDecimals Error:
In decodeWellData, the code checks decimal0 == 0 twice
The second check should be for decimal1, not decimal0.

if (decimal1 == 0) {
    decimal1 = 18;
}

Explanation:
The second if condition incorrectly checks decimal0 instead of decimal1. This means if decimal0 is 0, it will set both decimal0 and decimal1 to 18, even if decimal1 should be checked.

Example Exploitation:
If decimal0 is 0 but decimal1 is not set correctly, it could cause incorrect scaling of reserves. This misconfiguration could lead to erroneous calculations, impacting pricing or liquidity management.

Impacts:

Incorrect token decimal handling might result in unexpected contract behavior.
Financial calculations and interactions might be misrepresented, leading to incorrect balances or transactions.

Mitigations:

Correct the condition to check decimal1 in the second if statement like this:

if (decimal1 == 0) {
    decimal1 = 18;
}



## Assessed type

Other
Instant upgrade vulnerability allows permanent DoS and fund lock in Wells
Audit: 2024-07-basin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/WellUpgradeable.sol#L93-L96
https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/WellUpgradeable.sol#L104-L107


# Vulnerability details

## Impact
Permanent DoS due to an upgrade to a malicious implementation that reverts critical functions like `upgradeTo`, `upgradeToAndCall`, `removeLiquidity`, etc. This would result in locked user funds and prevent further upgrades.

## Proof of Concept
The `WellUpgradeable.sol` contract is designed as a template for upgradeable Well logic contracts, with minimal proxies (Wells) created by the Aquifer that delegatecalls to this contract. Well owners have the autonomy to choose which implementation their Wells delegate to and how the logic should operate. However, there is a vulnerability in the current design that could allow an arbitrary malicious user to harm users who have deposited assets into Wells.

While the documentation and whitepapers advise users to exercise caution when depositing into Wells, there is a vulnerability in the current implementation that could be addressed and make the contract more safer. The issue stems from the fact that the current upgrade functionality in `WellUpgradeable` takes effect instantly after a successful transaction. For example, if `upgradeTo` is called and the transaction succeeds, the proxy will immediately delegate calls to the new implementation. This poses a significant risk, as it could be exploited by malicious actors, leading to permanently locked user funds and preventing further upgrades to the Well.

Consider the following scenario:
1. The Basin team deploys an audited `WellUpgradeable` contract, and users start creating minimal proxies (Wells) that point to this logic.
2. Alice deploys a new Well for TokenA and TokenB.
3. Bob deploys a new Well for TokenC and TokenD.
4. Eve deploys a new Well for TokenX and TokenY.
5. Everything functions as expected, with DeFi users depositing and utilizing the Wells deployed by Alice, Bob, and Eve.
6. Suddenly, a malicious user decides to disrupt the system. This user creates a new, malicious UUPS-compatible implementation where critical functions like `upgradeTo`, `upgradeToAndCall`, `removeLiquidity`, etc. are designed to revert.
7. The malicious user then calls `upgradeTo` with this new implementation address. The proxy contract delegate calls the `WellUpgradeable` contract.
8. The [`_authorizeUpgrade`](https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/WellUpgradeable.sol#L65-L85) function will pass because the call originates from a proxy, and the malicious implementation is UUPS-compatible. Since there are no `onlyWellOwner` access control modifiers and the update is instant, user funds will be permanently locked, and the Well will be stuck in this broken state, preventing further upgrades.

---
The following PoC test demon
WellUpgradeable inherits from OwnableUpgradeable but misses restricting critical functions to onlyOwner
Audit: 2024-07-basin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/WellUpgradeable.sol#L93-L96
https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/WellUpgradeable.sol#L104-L107
https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/WellUpgradeable.sol#L33


# Vulnerability details

## Impact

- **Unauthorized Upgrades**: Without ownership restrictions on upgrade functions, anyone could potentially initiate an upgrade, leading to possible unauthorized modifications of the contract logic.
- **False Sense of Security**: The presence of `OwnableUpgradeable` might create a false sense of security, misleading developers and users into thinking the contract has proper access controls when it does not.

## Proof of Concept

The `WellUpgradeable` contract inherits from `OwnableUpgradeable`, but fails to utilize the ownership management functionalities provided by the `OwnableUpgradeable` contract. Specifically, there are no functions restricted to the owner using the `onlyOwner` modifier, which leaves critical functions, including the upgrade functionality, vulnerable to unauthorized access. 

### Code Snippet:
```solidity
@> contract WellUpgradeable is Well, UUPSUpgradeable, OwnableUpgradeable {
    
      ...

 @>   function init(string memory _name, string memory _symbol) external override reinitializer(2) {
        __ERC20Permit_init(_name);
        __ERC20_init(_name, _symbol);
        __ReentrancyGuard_init();
        __UUPSUpgradeable_init();
        __Ownable_init();

        IERC20[] memory _tokens = tokens();
        uint256 tokensLength = _tokens.length;
        for (uint256 i; i < tokensLength - 1; ++i) {
            for (uint256 j = i + 1; j < tokensLength; ++j) {
                if (_tokens[i] == _tokens[j]) {
                    revert DuplicateTokens(_tokens[i]);
                }
            }
        }
    }

    ...
    function _authorizeUpgrade(address newImplmentation) internal view override {
        // verify the function is called through a delegatecall.
        require(address(this) != ___self, "Function must be called through delegatecall");

        // verify the function is called through an active proxy bored by an aquifer.
        address aquifer = aquifer();
        address activeProxy = IAquifer(aquifer).wellImplementation(_getImplementation());
        require(activeProxy == ___self, "Function must be called through active proxy bored by an aquifer");

        // verify the new implmentation is a well bored by an aquifier.
        require(
            IAquifer(aquifer).wellImplementation(newImplmentation) != address(0),
            "New implementation must be a well implmentation"
        );

        // verify the new implmentation is a valid ERC-1967 implmentation.
        require(
            UUPSUpgradeable(newImplmentation).proxiableUUID() == _IMPLEMENTATION_SLOT,
            "New implementation
Incorrect conditional check in `decodeWellData` function 
Audit: 2024-07-basin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/Stable2.sol#L310-L327


# Vulnerability details

## Impact
The `decodeWellData` function contains a logical error that prevents the second token's decimals from being properly set to the default value of 18 when it's 0. This leads to incorrect decimal handling for the second token, potentially causing miscalculations in other parts of the protocol that rely on accurate decimal information. 


This bug leads to incorrect handling of token decimals, particularly when the second token in the pair has 0 decimals. Instead of being set to the default value of 18, it will retain its 0 value. Lets look at the affected functions:

1. `getScaledReserves`:
   This function directly uses the decimals returned by `decodeWellData` to scale the reserves. If `decimal1` is incorrectly 0, it will result in:
   ```solidity
   scaledReserves[1] = reserves[1] * 10 ** (18 - 0) = reserves[1] * 1e18
   ```
   This would massively inflate the scaled reserve for the second token, potentially by a factor of 1e18.

2. `calcLpTokenSupply`:
   Uses `getScaledReserves`, so it would work with incorrectly scaled reserves. This will lead to overestimation of LP token supply and incorrect liquidity representation.
  

3. `calcReserve`:
   Also uses `getScaledReserves`. The bug could cause:
   - Incorrect reserve calculations
   - Potential underflow when scaling back to original precision

4. `calcRate`:
   Relies on `getScaledReserves` and `calcLpTokenSupply`. The bug would result in:
   - Incorrect rate calculations

## Proof of Concept
Take a look at the `decodeWellData` function: https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/Stable2.sol#L310-L327

```solidity
function decodeWellData(bytes memory data) public view virtual returns (uint256[] memory decimals) {
        (uint256 decimal0, uint256 decimal1) = abi.decode(data, (uint256, uint256));

        // if well data returns 0, assume 18 decimals.
        if (decimal0 == 0) {
            decimal0 = 18;
        }
        if (decimal0 == 0) {
            decimal1 = 18;
        }
        if (decimal0 > 18 || decimal1 > 18) revert InvalidTokenDecimals();

        decimals = new uint256[](2);
        decimals[0] = decimal0;
        decimals[1] = decimal1;
    }

```

The second condition incorrectly checks `decimal0` again instead of `decimal1`. As a result:
1. If `decimal0` is 0, it's correctly set to 18.
2. The second condition will always be false because `decimal0` is now 18.
3. `decimal1` is never set to 18 even if it's 0.

This means that if the second token has 0 decimals in the input data, it will be incorrectly processed as having 0 decimals instead of the intended default of 18.


## Tools Used
Manual review

## Recommended Mitigation Steps
The fix is straightforward. Change the second condition to check `decimal1` instead of `decimal0`:

```diff
Unauthorised Upgrade Vulnerability in upgradeTo Function within WellUpgradeable contract
Audit: 2024-07-basin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/WellUpgradeable.sol#L93-L96


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
Title: Unauthorised Upgrade Vulnerability in upgradeTo Function

	•	Severity: High
	•	Impact: Allows unauthorised users to upgrade the contract implementation.
	•	Status: Unresolved
	•	File: WellUpgradeable.sol
	•	Lines Affected: 93-96
The upgradeTo function in the WellUpgradeable contract is designed to allow the contract owner to upgrade the contract to a new implementation. However, a security issue exists where any unregistered user can call this function, potentially leading to unauthorised upgrades of the contract to a registered new implementation address.

Affected Code

The vulnerability is present in the following function within WellUpgradeable.sol:
```sol
function upgradeTo(address newImplementation) public override {
    _authorizeUpgrade(newImplementation);
    _upgradeToAndCallUUPS(newImplementation, new bytes(0), false);
}
```
## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
	•	Access Control: The function is supposed to be protected so that only the contract owner or an authorised address can perform upgrades. However, the lack of access control checks allows any user to call the function.

This vulnerability allows an unauthorised user to upgrade the contract to a new  implementation that has not been planned. This can lead to:
	•	Reputation Damage: Users and investors may lose trust in the protocol due to security breaches.

***Step 1***
Create the following path and file:
```test/WellUpgradeableDebo.t.sol```

***Step 2***
Add the following code in the foundry test.
```sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Test, console} from "forge-std/Test.sol";
import {WellUpgradeable} from "src/WellUpgradeable.sol";
import {IERC20} from "test/TestHelper.sol";
import {WellDeployer} from "script/helpers/WellDeployer.sol";
import {MockPump} from "mocks/pumps/MockPump.sol";
import {Well, Call, IWellFunction, IPump, IERC20} from "src/Well.sol";
import {ConstantProduct2} from "src/functions/ConstantProduct2.sol";
import {Aquifer} from "src/Aquifer.sol";
import {WellDeployer} from "script/helpers/WellDeployer.sol";
import {LibWellUpgradeableConstructor} from "src/libraries/LibWellUpgradeableConstructor.sol";
import {MockToken} from "mocks/tokens/MockToken.sol";
import {WellDeployer} from "script/helpers/WellDeployer.sol";
import {ERC1967Proxy} from "oz/proxy/ERC1967/ERC1967Proxy.sol";
import {MockWellUpgradeable} from "mocks/wells/MockWellUpgradeable.sol";

contract WellUpgradeableDeboTest is Test, WellDeployer {
    address proxyAddress;
    address aquifer;
    address initialOwner;
    address user;
    address mockPumpAddress;
    address wellFunctionAddress;
    address token1Addre
Improper Zero-Value Decimal Handling in the decodeWellData Function
Audit: 2024-07-basin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/Stable2.sol#L317


# Vulnerability details

## Impact

The impact of this finding is significant as it concerns the improper handling of token decimal values in the decodeWellData function. When one of the token decimal values (specifically decimal1) is zero, it fails to default to 18, as intended. This can cause inaccuracies in the downstream calculations that depend on token decimal values, such as computing the liquidity pool (LP) token supply, token reserves, and rates.

## Proof of Concept


Consider the decodeWellData function in the current implementation:

```solidity

function decodeWellData(bytes memory data) public view virtual returns (uint256[] memory decimals) {
    (uint256 decimal0, uint256 decimal1) = abi.decode(data, (uint256, uint256));

    if (decimal0 == 0) {
        decimal0 = 18;
    }
    // This should check for `decimal1` instead of `decimal0`
    if (decimal0 == 0) {
        decimal1 = 18;
    }
    if (decimal0 > 18 || decimal1 > 18) revert InvalidTokenDecimals();

    decimals = new uint256[](2);
    decimals[0] = decimal0;
    decimals[1] = decimal1;
}

```

The error is highlighted in the comment. The second check should verify decimal1, not decimal0.

A Foundry test case can demonstrate this issue:

```solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";

contract TestContract is Test { 
    function testDecodeWellData() public {
        // Setup
        bytes memory data = abi.encode(0, 0); // Simulating zero decimals for both tokens

        InitialStable2 stable2 = new InitialStable2();

        // Decode Well Data
        uint256[] memory decimals = stable2.decodeWellData(data);
        
        // Check for expected default values
        assertEq(decimals[0], 18, "decimal0 should default to 18");
        assertEq(decimals[1], 0, "decimal1 incorrectly defaults to 0"); // Should fail
    }
}

contract InitialStable2 {
    function decodeWellData(bytes memory data) public view virtual returns (uint256[] memory decimals) {
        (uint256 decimal0, uint256 decimal1) = abi.decode(data, (uint256, uint256));
        
        // Incorrect handling: only decimal0 is checked and assigned, but not decimal1
        if (decimal0 == 0) {
            decimal0 = 18;
        }
        if (decimal0 == 0) {
            decimal1 = 18;
        }
        if (decimal0 > 18 || decimal1 > 18) revert();

        decimals = new uint256[](2);
        decimals[0] = decimal0;
        decimals[1] = decimal1;
    }
}

```

The test demonstrates that decimal1 does not default to 18, causing potential downstream issues.

## Tools Used

Solidity
Foundry

## Recommended Mitigation Steps

To resolve this issue, update the decodeWellData function to check and default decimal1 appropriately. The corrected function should look like this:

```solidity

function decodeWellData(bytes memo