xanoutas/solidity-security-findings huggingface dataset:
Upgraded Q -> 3 from #117 [1718034766745]
Audit: 2024-05-loop-findings
Severity: HIGH

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

 2. Users being able to donate to ETH or lpETH before conversion one of protocol's main invariant
Links to affected code *

https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L392

Impact
From the readme, there's a main invariant -

Users that deposit ETH/WETH get the correct amount of lpETH on claim (1 to 1 conversion)

This 1 to 1 invariant can be easily broken through donations, before the convertAllETH function is called. The function queries the balance of ETH before conerting all to lpETH, and queries the contract's lpETH balance to set the totalLpETH parameter. The conversion rate is 1 to 1.

    function convertAllETH() external onlyAuthorized onlyBeforeDate(startClaimDate) {
        if (block.timestamp - loopActivation <= TIMELOCK) {
            revert LoopNotActivated();
        }

        // deposits all the ETH to lpETH contract. Receives lpETH back
        uint256 totalBalance = address(this).balance;
        lpETH.deposit{value: totalBalance}(address(this));

        totalLpETH = lpETH.balanceOf(address(this));

        // Claims of lpETH can start immediately after conversion.
        startClaimDate = uint32(block.timestamp);

        emit Converted(totalBalance, totalLpETH);
    }
Note the totalSupply tracks ETH deposits only through the _processLock function, meaning it isn't affected by donations. So if tokens had been donated, they'll be converted to lpETH without actually changing the totalSupply.

When claiming, the claimed amount a user is entitled to is calculated as userStake * totalLpETH/totalSupply. And as it has been established that totalLpETH was a 1 to 1 exchange, this means that it will be more than totalSupply (since donations had increased address(this).balance but didn't affect totalSupply). As totalLpETH is more than totalSupply, a user's claimed amount will not be in a 1 to 1 ratio with his staked amount, but rather more.

    function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
        internal
        returns (uint256 claimedAmount)
    {
        uint256 userStake = balances[msg.sender][_token];
        if (userStake == 0) {
            revert NothingToClaim();
        }
        if (_token == ETH) {
            claimedAmount = userStake.mulDiv(totalLpETH, totalSupply);
            balances[msg.sender][_token] = 0;
            lpETH.safeTransfer(_receiver, claimedAmount);
        }
        ...
The same also applies if lpETH is donated to the contract before ETH is converted, due to the use of lpETH.balanceOf(address(this)); to set totalLpETH.

Important to note that this doesn't affect ERC20 stakers, so these donations introduce a disbalance in expected exchange rate of lpETH to ETH and actual exchange rate.

Recommended Mitigation Steps
Best way to fix it is to convert only totalSupply of ETH, and setting tot
Ethers sent to the contract can be stolen from any user
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-loop/blob/40167e469edde09969643b6808c57e25d1b9c203/src/PrelaunchPoints.sol#L240-L266
https://github.com/code-423n4/2024-05-loop/blob/40167e469edde09969643b6808c57e25d1b9c203/src/PrelaunchPoints.sol#L405-L442
https://github.com/code-423n4/2024-05-loop/blob/40167e469edde09969643b6808c57e25d1b9c203/src/PrelaunchPoints.sol#L491-L505


# Vulnerability details

## Impact

User can by passing 0 as a `_percentage`, receive `lpETH` based on the ethers in the contract.

## Proof of Concept

If the `PrelaunchPoints.sol` contract has native ether, any user can get `lpETH` for them without sending anything.

The user only needs to have a balance in the contract of the token that passes when he perform the attack to pass this check - it can be 1 wei.

```solidity
uint256 userStake = balances[msg.sender][_token];
if (userStake == 0) {
    revert NothingToClaim();
}
```

User will call `claim()` with:

- `_token` - anyToken
- `_percentage` - 0
- `Exchange` - TransformERC20
- `_data`:
    - TRANSFORM_SELECTOR = 0x415565b0
    - inputToken = the token provided in _token
    - outputToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
    - inputTokenAmount = 0

```solidity
function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
```

Then in `_claim()` it will enter the else statement, `userClaim` will become 0 because its - **`userStake * 0 / 100 = 0`**.

Then `_validateData()` will pass correctly, his balance will not be changed. When `_fillQuote()` is called, nothing will be exchanged because it will pass **`userClaim` which is 0**, so no ethers will enter the contract, and `boughtETHAmount` will remain the same. The `claimedAmount` will then be the contract balance and will be deposited into `lpETH`, by passing him as receiver, causing him to receive `lpETH` based on the Ether balance in the contract without having to sent anything**.**

[PrelaunchPoints.sol#L240-L266](https://github.com/code-423n4/2024-05-loop/blob/40167e469edde09969643b6808c57e25d1b9c203/src/PrelaunchPoints.sol#L240-L266)

```solidity
function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
    internal
    returns (uint256 claimedAmount)
{
    uint256 userStake = balances[msg.sender][_token];
    if (userStake == 0) {
        revert NothingToClaim();
    }
    if (_token == ETH) {
        claimedAmount = userStake.mulDiv(totalLpETH, totalSupply);
        balances[msg.sender][_token] = 0;
        lpETH.safeTransfer(_receiver, claimedAmount);
    } else {
        uint256 userClaim = userStake * _percentage / 100;
        _validateData(_token, userClaim, _exchange, _data);
        balances[msg.sender][_token] = userStake - userClaim;

        // At this point there should not be any ETH in the contract
        // Swap token to ETH
        _fillQuote(IERC20(_token), userClaim, _data);

        // Convert swapped ETH to lpETH (1 to 1 conversion)
        c
Assumption that if the swap is purely on `UniswapV3` then the selector is the `UNI_SELECTOR` is incorrect causing the `_validateData()` function to revert
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L412
https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L414


# Vulnerability details

## Impact
Calling the 0xAPI for an LRT is might result in `TransformERC20()` (`0x415565b0`) selector being returned, even if the exchange that is the `source` of the data is `Uniswap_V3`. This causes the `_validate()` data to revert with `WrongSelector("0x415565b0")` every time the user tries to claim/claim and stake their `lpETH`. The `outputToken` checked by manually decoding the API return data in Remix IDE with the `_decodeTransformERC20()` function is set to `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` - `ETH`, so the call to `TransformERC20()` should NOT be reverted. This means, that the API return data does not have to necessarily return the `UNI_SELECTOR`, when the source is set as `Uniswap_V3`.

The vulnerability is caused by the mitigation of [WP-M3] finding from LoopFi / Prelaunch / LRT #2 audit.
## Proof of Concept

To better understand this problem paste the code in this [gist](https://gist.github.com/6b1504e567a5a5aae4f5510bc809112f.git) to the `PrelaunchPoints0x.test.ts` test file, and run it with `yarn hardhat test ./test/PrelaunchPoints0x.test.ts`. It is a simplified version of the original test, where we consider only the `rswETH` token for clarity. Data resulting from the API call indeed begins with `0x415565b0`, suggesting that we should be using the `TransformERC20()` function in `_fillQuote`. However, we never get to filling the quote, because `orders[0].source` is `Uniswap_V3` meaning that the `exchangeCode` is `0`. In `_validateData` the first branch is executed and the code fails due to `WrongSelector(0x415565b0)` error as explained before.
## Tools Used
Manual review
## Recommended Mitigation Steps

This problem has to be further investigated and only when 0x API return data is fully predictable, an appropriate mitigation can be developed. However, to simulate successful data validation the following `_validateData()` function implementation, where the `_exchange` enum parameter is ignored to allow the TransformERC20 selector- based data to be decoded ( !!! note that this is NOT a safe nor faul proof solution to this vulnerability !!!):

```javascript
  function _validateData(
        address _token,
        uint256 _amount,
        Exchange _exchange,
        bytes calldata _data
    ) internal view {
        address inputToken;
        address outputToken;
        uint256 inputTokenAmount;
        address recipient;
        bytes4 selector;

        // // if exchange is uniswap _exchange == "UniswapV3" from the 0x query
        // if (_exchange == Exchange.UniswapV3) {
        //     (
        //         inputToken,
        //         outputToken,
        //         inputTokenAmount,
        //         recipient,
        //         selector
Unexpected Gains at the Expense of Users Who Sent ETH by Mistake
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L389-L392
https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L252-L265


# Vulnerability details

## Impact
PrelaunchPoints.sol line 256 state: `ETH sent to this contract directly will be locked forever.` but that is not true:

After `convertAllETH` phase, when a user `claim` will get unexpected earnings at expenses of another user that sent by mistake ETH to the contract and also this problem  creates a front running competence to monitor mempool and claim just after the transaction that sends ETH to the contract.

## Proof of Concept

POC showing the unexpected gain if user send 30 ETH (number really big to make clear the example)

Add `NotExpectedGains.test.ts` to $project/test/ and  run `npx hardhat test`

```javascript
import { expect } from "chai"
import hre, { ethers } from "hardhat"
import {
  time,
  impersonateAccount,
  setBalance,
} from "@nomicfoundation/hardhat-toolbox/network-helpers"
import fetch from "node-fetch"
import "dotenv/config"
import {
  IERC20,
  MockLpETH,
  MockLpETHVault,
  PrelaunchPoints,
} from "../typechain"
import { parseEther } from "ethers"

const ZEROX_API_KEY = process.env.ZEROX_API_KEY

const tokens = [
  {
    name: "ezETH",
    address: "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110",
    whale: "0x267ed5f71EE47D3E45Bb1569Aa37889a2d10f91e",
  }
]

describe("Smart Contract Audit", function () {
  const ETH = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
  const exchangeProxy = "0xdef1c0ded9bec7f1a1670819833240f027b25eff"

  const sellAmount = ethers.parseEther("1")
  const referral = ethers.encodeBytes32String("")

  // Contracts
  let lockToken: IERC20
  let prelaunchPoints: PrelaunchPoints
  let lpETH: MockLpETH
  let lpETHVault: MockLpETHVault

  before(async () => {
    const LpETH = await hre.ethers.getContractFactory("MockLpETH")
    lpETH = (await LpETH.deploy()) as unknown as MockLpETH

    const LpETHVault = await hre.ethers.getContractFactory("MockLpETHVault")
    lpETHVault = (await LpETHVault.deploy()) as unknown as MockLpETHVault
  })

  beforeEach(async () => {
    const PrelaunchPoints = await hre.ethers.getContractFactory(
      "PrelaunchPoints"
    )
    prelaunchPoints = (await PrelaunchPoints.deploy(
      exchangeProxy,
      "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
      tokens.map((token) => token.address)
    )) as unknown as PrelaunchPoints
  })

  const token = tokens[0];

  it(`User will get unexpected gains at expenses of a user that sent ETH by mistake`, async function () {
    lockToken = (await ethers.getContractAt(
      "IERC20",
      token.address
    )) as unknown as IERC20

    const depositorAddress = token.whale
    await impersonateAccount(depositorAddress)
    const depositor = await ethers.getSigner(depositorAddress)


    const tokenBalanceBefore = await lockToken.balanceOf(depositor)
    console.log(`tokenBalanceBefore ${tokenBalanceBefore}`)
Claims that are not paused during `emergencymode` can cause users to get more `lpETH` than the amount of `ETH` they have locked
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L240-L266


# Vulnerability details

## Impact

Users get more `lpETH` than the amount of `ETH` they have locked

## Proof of Concept

The LoopFi protocol has an emergency mode feature which allows users to withdraw their assets even though the `loopActivation` and `startClaimDate` periods have passed. However, in emergency mode, users can still claim `lpETH` by exchanging the `ETH` they have with the `claim()` function. This can be a problem because the total supply of `ETH` changes and affects the exchange rate of `lpETH` and `ETH`.

### Exploit Scenario

1. Normally, the user initially locks his `ETH` with the `lockETH()` or `lockETHfor()` function until a certain time limit.
2. Then after a certain time, owner calls the `setLoopAddresses()` function and starts the first epoch. And after 7 days the owner calls the `convertAllETH()` function to start the claim period for users to be able to claim `lpETH`.
3. For example, it is assumed that the data obtained:
    - totalLpETH    :   1000
    - totalSupply   :    1000
4. Because there was a problem, the owner activated the `emergencyMode` feature by calling the `setEmergencyMode()` function
5. We assume that some users withdraw their `ETH` and the data obtained becomes:
    - totalLpETH    :   1000
    - totalSupply   :    500
6. Bob knows this condition and has a locked ETH balance (i.e. 100 `ETH`) calls the `claim()` function to exchange it for `lpETH`.

```solidity
        if (_token == ETH) {
            claimedAmount = userStake.mulDiv(totalLpETH, totalSupply);
            balances[msg.sender][_token] = 0;
            lpETH.safeTransfer(_receiver, claimedAmount);
```

7. With the formula above, Bob should have gotten 100 `lpETH` tokens but now he gets 200 `lpETH` tokens because the exchange rate has changed due to the change in the total supply of `ETH`.

NOTE :

1. This also affects the [claimAndStake()](https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L226-L235) function

2. If this condition occurs, the main variant will be broken

> **Users that deposit ETH/WETH get the correct amount of lpETH on claim (1 to 1 conversion)**
> 

3. Another user after the first claimer will get less `lpETH` than they should

## Tools Used

Manual review

## Recommended Mitigation Steps

Consider pause `claim()` function if protocol on `emergencyMode` state

```solidity
    function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
        internal
        returns (uint256 claimedAmount)
    {
        if (emergencyMode) {
            revert CurrentlyNotPossible();
	}
		    
        uint256 userStake = balances[msg.sender][_token];
        if (userStake == 0) {
            revert NothingToClaim();
        }
        if (_token == ETH) {
            claimedAmount = us
Loss of funds for users who claim vested `lpETH` by swapping via `UniswapV3`
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L192
https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L252-L263
https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L491-L505
https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L405


# Vulnerability details

## Impact
Users who lock `wrapped LRTs` can claim their `vested lpETH` by swapping their tokens via `UniswapV3` or `TransformERC20`.

If they use `UniswapV3`, their token must be swapped for `WETH`. The problem is that the `WETH` received from the `swap` is never unwrapped to `ETH`, so when the protocol attempts to send `lpETH` to the user, it will convert `ETH` to `lpETH` by a 1:1 conversion ratio, but since the `WETH` was never unwrapped to `ETH`, that amount will be 0.

In addition `WETH` is not recoverable because it is always in the `isTokenAllowed` list, which causes `recoverERC20()` to revert. Therefore users will lose all funds and their `WETH` may be locked in the contract forever.

## Proof of Concept
The `PrelaunchPoints` contract allows users to lock `ETH`, `WETH`, and `wrapped LRTs`.

```javascript
    function _processLock(address _token, uint256 _amount, address _receiver, bytes32 _referral)
        internal
        onlyBeforeDate(loopActivation)
    {
        if (_amount == 0) {
            revert CannotLockZero();
        }

@>      if (_token == ETH) {
            totalSupply = totalSupply + _amount;
            balances[_receiver][ETH] += _amount;
        } else {
@>          if (!isTokenAllowed[_token]) {
                revert TokenNotAllowed();
            }
            IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);

@>          if (_token == address(WETH)) {
                WETH.withdraw(_amount);
                totalSupply = totalSupply + _amount;
                balances[_receiver][ETH] += _amount;
            // @audit if token is not WETH or ETH, but is in the `isTokenAllowed` list, it is accepted
 @>         } else {
                balances[_receiver][_token] += _amount;
            }
        }

        emit Locked(_receiver, _amount, _token, _referral);
    }
```

Once the `owner` calls `convertAllETH()`, all the `ETH` that was `locked` by users is converted to `lpETH`

```javascript
    function convertAllETH() external onlyAuthorized onlyBeforeDate(startClaimDate) {
        if (block.timestamp - loopActivation <= TIMELOCK) {
            revert LoopNotActivated();
        }

        // deposits all the ETH to lpETH contract. Receives lpETH back
        uint256 totalBalance = address(this).balance;
        lpETH.deposit{value: totalBalance}(address(this));

        totalLpETH = lpETH.balanceOf(address(this));

        // Claims of lpETH can start immediately after conversion.
        startClaimDate = uint32(block.timestamp);

        emit Converted(totalBalance, totalLpETH);
    }
```

Users can claim their `lpETH` by c
Incorrect Handling of Contract's ETH Balance in _claim Function
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L257-L266
https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L491-L505


# Vulnerability details

## Impact
When a user claims a non-ETH token, the `_claim` function incorrectly calculates the amount of lpETH to be minted and transferred to the user. If the contract holds any ETH balance before the token swap, this pre-existing ETH will be included in the lpETH minting, resulting in the user receiving more lpETH than they should. This could lead to inflation of the lpETH supply and loss of funds for the protocol.


## Proof of Concept
The `_claim` function in the contract assumes that the entire ETH balance of the contract after a token-to-ETH swap is the result of that swap. This is incorrect if the contract already held some ETH before the swap. Here's the relevant part of the code:
https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L257-L266

```solidity
// At this point there should not be any ETH in the contract
// Swap token to ETH
_fillQuote(IERC20(_token), userClaim, _data);

// Convert swapped ETH to lpETH (1 to 1 conversion)
claimedAmount = address(this).balance;
lpETH.deposit{value: claimedAmount}(_receiver);
```

Let's consider the following scenario:
1. The contract holds a balance of 10 ETH from previous interactions.
2. A user calls the `claim` function to claim 1000 tokens of a non-ETH token, with a `_percentage` of 100.
3. The `_claim` function calculates `userClaim` as 1000 tokens and calls `_fillQuote` to swap these tokens for ETH.
4. The `_fillQuote` function performs the swap and receives 5 ETH in return.
5. After the swap, the contract's balance is now 15 ETH (10 ETH from before + 5 ETH from the swap).
6. The `_claim` function then calculates `claimedAmount` as the entire balance of the contract (15 ETH) and mints 15 lpETH to the user.

However, the user should only receive 5 lpETH, as that's the amount of ETH received from swapping their claimed tokens. The extra 10 ETH should not be included in the minting.



## Tools used
Manual review

## Recommended Mitigation Steps
Modify the `_claim` function to track the ETH balance before and after the swap, and only convert the difference to lpETH by adjusting the `_fillQuote` function to return the amount of ETH bought, and using this value in the `_claim` function:


```solidity
// In _fillQuote function, return the bought ETH amount
function _fillQuote(IERC20 _sellToken, uint256 _amount, bytes calldata _swapCallData) internal returns (uint256) {
    uint256 initialBalance = address(this).balance;
    require(_sellToken.approve(exchangeProxy, _amount));
    (bool success,) = payable(exchangeProxy).call{value: 0}(_swapCallData);
    if (!success) {
        revert SwapCallFailed();
    }
    uint256 boughtETHAmount = address(this).