xanoutas/solidity-security-findings huggingface dataset:
User can Claim Entire Eth Balance of Contract instead of just their own Eth Claim Value
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L262
https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L503


# Vulnerability details

## Impact
Risk of Fund Loss to Protocol as User can Claim Entire Eth Balance of Contract instead of just their own Eth Claim Value in the PrelaunchPoints.sol contract during claim function call.
## Proof of Concept
```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)
>>>            claimedAmount = address(this).balance; ❌
            lpETH.deposit{value: claimedAmount}(_receiver);
        }
        emit Claimed(msg.sender, _token, claimedAmount);
    }
```
The function above shows how _claim(...) function is implemented in the PrelaunchPoints.sol contract, it can be noted from the pointers that when token is Non Eth a swap is done before a claim of Eth value is done and deposited to receiver address, from the first pointer, the protocol assumes there should not be any Eth in the contract which is why the last pointer assigns the complete balance of eth in contract as claimed Amount instead of the actual Amount that is to be claimed.
However the implementation of _fillQuote(...) function which handles the swap to determine the Eth value to be claimed as provided below paints a different picture of the Eth that should be in balance. As noted from the pointers below the value of Eth Bought is derived by comparing the Eth balance before and after without assuming it is previously empty, this is the correct way to derived the actual Eth that should be claimed. Now back to the _claim(...) function above, it is this `boughtETHAmount` from _fillQuote(...) function that should be used as claimedAmount value and not using the entire balance of the contract as wrongly done in the last pointer above which risks a user claiming the whole contract value instead of just its personal funds, a bad actor simply needs to keep track of the contract balance to claim at the perfect time to get excess fund above its claimable range
```solidity
function _fillQuote(
Excess ETH (Protocol Fee Refund) Not Returned to User in _fillQuote Function
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details


## Impact
When a user claims tokens other than ETH, the `_claim` function calls the `_fillQuote` function to swap the tokens for ETH using the 0x exchange proxy. If there is any excess ETH (protocol fee refund) sent back from the 0x exchange proxy after the swap, it is not refunded to the user who initiated the claim. Instead, the excess ETH is converted to lpETH and sent to the `_receiver`, which is not the intended behavior. This results in the user losing the protocol fee refund they are entitled to.

## Proof of Concept
The issue occurs in the `_fillQuote` function. Here's a detailed explanation:

In the `_claim` function, when a user claims tokens other than ETH, it calls the `_fillQuote` function to swap the tokens for ETH:

https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L259-L259
```solidity
// PrelaunchPoints.sol
259:            _fillQuote(IERC20(_token), userClaim, _data);
```


After the swap, the `_claim` function converts the swapped ETH to lpETH and sends it to the `_receiver`:

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

```solidity
262:            claimedAmount = address(this).balance;
263:            lpETH.deposit{value: claimedAmount}(_receiver);
```


Now, let's look at the `_fillQuote` function:
https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L491-L505

```solidity
491:    function _fillQuote(IERC20 _sellToken, uint256 _amount, bytes calldata _swapCallData) internal {
492:        // Track our balance of the buyToken to determine how much we've bought.
493:        uint256 boughtETHAmount = address(this).balance;
494: 
495:        require(_sellToken.approve(exchangeProxy, _amount));
496: 
497:        (bool success,) = payable(exchangeProxy).call{value: 0}(_swapCallData);
498:        if (!success) {
499:            revert SwapCallFailed();
500:        }
501: 
502:        // Use our current buyToken balance to determine how much we've bought.
503:        boughtETHAmount = address(this).balance - boughtETHAmount;
504:        emit SwappedTokens(address(_sellToken), _amount, boughtETHAmount);
505:    }
```

The `_fillQuote` function calculates the amount of ETH bought (`boughtETHAmount`) by taking the difference between the contract's balance before and after the swap. However, it does not account for any excess ETH that might be sent back from the 0x exchange proxy as a protocol fee refund.



As a result, any excess ETH
Incorrect Handling of ETH Balance Post-Swap in _claim Function Leading to lpETH Over/Under-Minting
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-loop/blob/40167e469edde09969643b6808c57e25d1b9c203/src/PrelaunchPoints.sol#L262


# Vulnerability details

## Impact

The bug can lead to incorrect issuance of lpETH tokens during the claim process, potentially resulting in financial loss or discrepancy for users. If the contract holds any ETH before a swap, the claimedAmount calculation will be incorrect, causing either over-minting or under-minting of lpETH.

## Root of the bug 

the bug is in the assumption that the entire ETH balance of the contract post-swap corresponds to the amount of _token swapped. 
And The current implementation does not account for any pre-existing ETH balance in the contract.
We have The _claim function is designed to allow users to claim their vested lpETH. 
If the _token is ETH, it calculates claimedAmount based on the user's share of totalLpETH. 
If the _token is not ETH, it calls _fillQuote to swap the token to ETH and then converts the received ETH to lpETH.

```solidity
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;

    // 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);
}

```

The vulnerability Is in this line : 

```

claimedAmount = address(this).balance;

```
Here in this  calculation assumes that the entire ETH balance of the contract after calling _fillQuote is the result of the token swap. This assumption fails if the contract had any pre-existing ETH balance.

The calculation does not isolate the ETH received from the swap but instead uses the total ETH balance of the contract, which can be misleading if there was already ETH in the contract before the swap.


## Proof of Concept
Here is the scenario that I test with: 

Scenario Demonstration

Consider the following scenario:

- let’s say The contract has a pre-existing balance of 10 ETH.
- A user claims 1,000 units of _token that are swapped for 1 ETH via _fillQuote.
- Post-swap, the contract's balance becomes 11 ETH.
- The line claimedAmount = address(this).balance; assigns claimedAmount as 11 ETH.
- lpETH is minted based on 11 ETH instead of the correct 1 ETH, leading to over-minting of lpETH.
- And as result: 
Claimed Amount (ETH): 11
Contract lpETH Balance: 11
It’s show the incorrect claimed amount due to pre-existing ETH in the contract.

## Tools Used
Manual review 
## Recommended Mitigation Steps
the ETH received from the swap should be calculated separately


## Assessed type

Other
Attacker can bypass lock to claim any number of token they wanted
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L254-L263


# Vulnerability details

## Impact

In usual circumstances, to obtain 1 ETH worth of lpETH, a user must lock 1 ETH for a period until it can be claimed. However, due to the incorrect validation in the `_validateData` function, attackers can deposit only a tiny amount of ERC20 Tokens whitelisted by the contract. Subsequently, by creating a malicious ERC20 token, they inflate the contract's ETH balance. Consequently, the contract mistakenly assumes the attacker has locked a sufficient amount of tokens, thus granting the attacker more lpETH than the locked amount.

This is inherently unfair to regular users.

## Proof of Concept

When `claim` is invoked, it calls the `_claim` function. If the passed `_token` is an ERC20 Token, it first checks the passed `data` using `_validateData`. Subsequently, it utilizes `_fillQuote` to perform a UniswapV3 swap to ETH, and finally, it fetches the current contract balance and mints corresponding lpETH to the `_receiver`.

The `_validateData` function only checks `inputToken` and `outputToken`; it does not validate the tokens in the conversion path.

The attacker's attack path is as follows, assuming the ERC20 Token whitelisted is `USDT`, and the attacker aims to obtain 1 ETH worth of lpETH:

1. Create a malicious ERC20 Token, referred to as Evil, with overridden `transferFrom` function to enable specified transfers.
2. Create two uniswap pairs: USDT-Evil and Evil-ETH.
3. The attacker deposits a small amount of USDT using `lock`, for example, 10wei.
4. Wait until `startClaimDate`, then invoke `claim` with the Uniswap path as USDT-Evil and Evil-ETH.
5. The contract executes `_fillQuote` for the conversion, transferring 1 ETH to the contract while passing through Evil.
6. Upon completion of the swap, the contract checks the current balance, presuming 1 ETH has been obtained, and consequently mints 1 ETH worth of lpETH for the attacker.

It's evident that the attacker completely circumvents the locked quantity restriction, claiming any amount of lpETH without the need to lock a substantial amount of tokens.

## Tools Used

manual review

## Recommended Mitigation Steps

validate all the swap path of token  is  in the `isTokenAllowed` array


## Assessed type

Invalid Validation
Users could claim any amount of `lpETH` without locking a corresponding amount of tokens
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Users could claim any amount of `lpETH` by locking only 1 wei worth of tokens in the contract, which breaks the core functionality of the contract.


## Proof of Concept

The `_claim` function uses the current ETH balance of this contract to claim `lpETH` for the `_receiver`. However, any user with any amount of staked tokens could transfer some ETH to this contract to obtain any desired amount of `lpETH`. 
The only restriction is that `balances[msg.sender][_token]` should not be zero. For instance, a user could lock only 1 wei of `LRTToken` and then claim 1 ether of `lpETH` by transferring ETH to this contract before calling `claim`.

```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)
        //@audit it use the ETH of this to claim lpETH,
        //@audit which should be the diff value in `fillQuote`
            claimedAmount = address(this).balance;
            lpETH.deposit{value: claimedAmount}(_receiver);
        }
        emit Claimed(msg.sender, _token, claimedAmount);
    }
```


### POC

Add the test to `test/PrelaunchPointsTest.sol` and run it with `forge test --match-test testClaimLRT  -vv`.
```solidity
    function testClaimLRT() public {
        // user only needs to lock 1 wei LRT, then he could cliam any amount he want
        uint256 lockAmount = 1;
        lrt.approve(address(prelaunchPoints), lockAmount);
        prelaunchPoints.lock(address(lrt), lockAmount, referral);

        prelaunchPoints.setLoopAddresses(address(lpETH), address(lpETHVault));
        vm.warp(prelaunchPoints.loopActivation() + prelaunchPoints.TIMELOCK() + 1);
        prelaunchPoints.convertAllETH();

        vm.warp(prelaunchPoints.startClaimDate() + 1);
        bytes4 y =  bytes4(0x415565b0);
        bytes memory da = abi.encodeWithSelector(y, address(lrt), (ETH), 0);
        // user deposit eth to this and call claim to get lp
        address(prelaunchPoints).call{value: 1 ether}("")
Claim and stake more than initially locked
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-05-loop/blob/1a4dc278f1b149eec7e49ffc28e02ea18c52eff3/src/PrelaunchPoints.sol#L262


# Vulnerability details



## Impact
User can claim and stake more tokens via `claimAndStake()` than he had initially locked.

## Proof of Concept
Based on ["Users who provide liquidity for lpETH and lock the respective LP tokens with Loop, earn a points multiplier compared to just depositing ETH/LRTs and holding lpETH."](https://docs.loopfi.xyz/the-launch/the-points-program#:~:text=Users%20who%20provide%20liquidity%20for%20lpETH%20and%20lock%20the%20respective%20LP%20tokens%20with%20Loop%2C%20earn%20a%20points%20multiplier%20compared%20to%20just%20depositing%20ETH/LRTs%20and%20holding%20lpETH.) it seems staking lpETH via `claimAndStake()` confers an advantage over staking directly oneself (perhaps based on the emitted `claimedAmount`).

By transferring ETH directly to the PrelaunchPoints contract before calling `claimAndStake()` on non-ETH tokens this ETH will be added to the [`claimedAmount = address(this).balance;`](https://github.com/code-423n4/2024-05-loop/blob/1a4dc278f1b149eec7e49ffc28e02ea18c52eff3/src/PrelaunchPoints.sol#L262) and then deposited in lpETH and staked. The user is thus able to stake via `claimAndStake()` (or the amount claimed via `claim()`) for funds he had not locked.

## Recommended Mitigation Steps
Count the difference before and after `_fillQuote()`.


## Assessed type

Context
User can skip locking mechanism funnelling in ether deposits to mint `lpETH` when the time deemed to do so is long past
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
A user can still deposit more and end up minting more `lpETH` even at a time when it is no longer possible to be eligible to do so by funneling in ether deposits on every small `lpETH` claim which resolves to mint more `lpETH` than expected by the protocol without the supposed full amount being locked in `ETH`, `WETH` or `LRTs` in the first place.

This breaches one of the protocol's invariants:
> Deposits are active up to the lpETH contract and lpETHVault contract are set

## Proof of Concept
- Alice locks 1 `LRT` or less. Let's assume the `LRT` is Renzo ETH in this case `ezETH`

[PrelaunchPoints.sol#L143-L148](https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L143-L148)
```solidity
function lock(address _token, uint256 _amount, bytes32 _referral) external {
        if (_token == ETH) {
            revert InvalidToken();
        }
@>      _processLock(_token, _amount, msg.sender, _referral); // @note processes Alice's deposit of 1 ezETH
    }
```

- We skip to the time when locking is no longer possible as the loop has been activated. We can see in the `_processLock()` function below, that there is a modifier to stop locking when the loop is activated `onlyBeforeDate()`

[PrelaunchPoints.sol#L172-L175](https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L172-L175)
```solidity
function _processLock(address _token, uint256 _amount, address _receiver, bytes32 _referral)
        internal
@>      onlyBeforeDate(loopActivation) // @note blocks locking attempts after loop activation
    {
...

modifier onlyBeforeDate(uint256 limitDate) {
        if (block.timestamp >= limitDate) { // @note this reverts if loop activated
            revert NoLongerPossible();
        }
        _;
    }
```

[PrelaunchPoints.sol#L211-L216](https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L211-L216)
- She calls `claim()`
```solidity
function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
        external
        onlyAfterDate(startClaimDate)
    {
        _claim(_token, msg.sender, _percentage, _exchange, _data);
    }
```

- Which executes `_claim()`

[PrelaunchPoints.sol#L240-L266](https://github.com/code-423n4/2024-05-loop/blob/main/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]; // @note 1 ezETH
        if (userStake == 0) {
            revert NothingToClaim();
        }
        if (_token == ETH) {
            claimedAmount = userStake.mulDiv(totalLpETH, totalSupply);
            balances[msg.sender][_token] = 0;
            lpETH.safeTransfer(_receiver, claimedAmount);
        } else {
Potential Over-Issuance of lpETH Due to Unaccounted ETH Deposits Before Claim and Stake Operations
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details


## Impact

This could lead to the over-issuance of lpETH tokens relative to the actual amount of ETH backing them. If additional ETH is deposited into the contract (not through the standard `lockETH` or `convertAllETH` functions) and is not accounted for in `totalLpETH`, then when `claimAndStake` is executed, this extra ETH could be wrongly converted into lpETH. This misalignment could inflate the lpETH supply without proper backing which could devalue the lpETH.


## Proof of Concept

The issue in the `claimAndStake` function, which allows users to claim their vested lpETH and immediately stake it. The function internally calls `_claim`, which handles the conversion of tokens to lpETH. 
https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L226-L235

```solidity
function claimAndStake(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
    external
    onlyAfterDate(startClaimDate)
{
    uint256 claimedAmount = _claim(_token, address(this), _percentage, _exchange, _data);
    lpETH.approve(address(lpETHVault), claimedAmount);
    lpETHVault.stake(claimedAmount, msg.sender);

    emit StakedVault(msg.sender, claimedAmount);
}
```

The `_claim` function includes logic for converting non-ETH tokens to ETH and then to lpETH:
https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L252-L266

```solidity
if (_token != ETH) {
    uint256 userClaim = userStake * _percentage / 100;
    _validateData(_token, userClaim, _exchange, _data);
    balances[msg.sender][_token] = userStake - userClaim;

    // 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);
}
```

If additional ETH is present in the contract that was not converted through `convertAllETH`, it will be included in the `address(this).balance` calculation, leading to an incorrect amount of lpETH being minted based on the user's original stake.


## Tools Used
Manual review

## Recommended Mitigation Steps

Track any ETH deposited into the contract outside of the standard functions (`lockETH`, `convertAllETH`). Probably by modifying the fallback function to update a specific balance or state variable.
Also, before converting ETH to lpETH in the `_claim` function, ensure that only the ETH amount corresponding to the user's stake (after token-to-ETH conversion) is used for lpETH conversion.


## Assessed type

Error