xanoutas/solidity-security-findings huggingface dataset:
Availability of deposit invariant can be bypassed
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#L324


# Vulnerability details

## Impact

One of the [main invariants](https://github.com/code-423n4/2024-05-loop?tab=readme-ov-file#main-invariants) stated in the contest is the following:

> Deposits are active up to the lpETH contract and lpETHVault contract are set

However, there are currently two ways this invariant can be broken, allowing users to gain lpETH without explicitly locking tokens before contracts are set.

1. Sandwich a call to `convertAllETH` by  front-running to directly donate ETH and then back-running to claim lpETH via multiple lock positions

<br/>

2. Bundle a transaction of donation and claiming with a previously locked position of wrapped LRT


This bypasses the `onlyBeforeDate(loopActivation)` modifier included in all lock functions. It also potentially allows no cap in lpETH minted and also discourages users from locking ETH in the `PrelaunchPoints.sol` contract before contract addresses are set (i.e. `loopActivation` is assigned)

## Proof of Concept

### Scenario 1
Bundle a transaction of donation and claiming with a previously locked position of wrapped LRT. User can execute the following in one transaction, assuming the user previously has a locked position with any of the allowed LRTs.

1. Donate ETH directly to contract
2. Call `claim()/claimAndStake()`, claiming all tokens, this swaps users wrapped LRT to ETH via `_fillQuote`.
3. Since `claimedAmount` is set as `address(this).balance` [here](https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L262), `claimedAmount`is computed as the addition of ETH receive from LRT swapped + user donated ETH 
4. `claimedAmount` worth of lpETH is subsequently claimed/staked via `lpETH.deposit()/lpETHVault.stake()`. 

### Scenario 2

Sandwich a call to `convertAllETH` by  front-running to directly donate ETH and then back-running to claim lpETH via multiple lock positions

1. Assume user has 10 positions of ETH/WETH locked each with 1 ether with different addresses, with 100 ETH total locked
2. Front-run call to `convertAllETH` by donating 1 ETH. This sets `totalLpETH` to be 101 ETH as seen [here](https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L324)
3. Ratio of `totalLpETH : totalSupply` computed [here](https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L249) is now 1.01 (1.01)
4. Back-run `convertAllETH` by executing claims for all 10 position, each claim will mint 0.01 ETH extra (1.01 lpETH)

This scenario is less likely than scenario 1 as it requires user to perfectly back-run `convertAllETH()` with claims, although it also breaks another invariant of a 1:1 swap

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


## Tools Used

Manual Analysis

## Recommended Mitigation Steps
1. Fo
Users can claim and mint tokens that they have not locked
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
A user that has locked just 1 wei of the allowed tokens can claim an arbitrary amount of the `lpETH` token.

## Proof of Concept
This is the `_claim()` function:
```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);
    }
```
If a user has locked just 1 wei of any of the allowed tokens, then he can do the following:
1. Directly send Ether to the contract for the amount he wishes to claim
2. Call the `claim()` function with the token he has locked for 1 wei
3. He will end up in the `else` statement as his token is not `ETH`
4. His 1 wei of the token gets swapped to ETH
5. Then, the `claimedAmount` variable is equal to `address(this).balance` which he just increased as he sent Ether directly to the contract

While the implementation of the `lpETH` contract is unclear, such issue can definitely cause a lot of unexpected issues depending on the implementation of `lpETH` and other contracts. Furthermore, that makes the event emission wrong and by the contest page in Code4rena, we can see that they are tracking different events on the backend, potentially causing other issues.
## Tools Used
Manual Review
## Recommended Mitigation Steps
Change the `_fillQuote()` function to return the `boughtETHAmount` variable and use it as the `claimedAmount` instead.





## Assessed type

Other
User can get as much `lpETH` as he wants with low locked amount of tokens
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
User can get as much `lpETH` as he wants with low locked amount of tokens, sabotaging the whole idea of the locking mechanism 

## Proof of Concept
User can easily bypass the locking mechanism by locking a low amount of tokens (different than WETH). If he lock his tokens and wait for the `convertAllETH` function to be called he can additionally send ETH to the contract and then immediately call the `claim`/`claimAndStake` function to claim the `lpETH`. this is possible because of the following block of code in the `_claim` function:
```javascript
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 first person to claim, gets it
            claimedAmount = address(this).balance;
            lpETH.deposit{value: claimedAmount}(_receiver);
``` 
It deposits the whole ETH balance of the contract to the recipient in form of `lpETH`, which means that the user's token deposit and the ETH he sent right before calling the `claim` function are now his in the form of `lpETH`, which sabotages the whole idea of the locking mechanism 
## Tools Used
manual review

## Recommended Mitigation
Just sending the ETH to the owner via `receive` function wont be enough, since a malicious user can transfer ETH to the contract in other ways (for example create a contract with implemented `selfdestruct` function). The best I can think of is to make the `_fillQuote` function return the `boughtETHAmount` variable, and then deposit that amount to the receiver like this:


`_fillQuote` function:
```javascript
function _fillQuote(
        IERC20 _sellToken,
        uint256 _amount,
        bytes calldata _swapCallData
    ) internal returns (uint256 boughtETHAmount) {
        // Track our balance of the buyToken to determine how much we've bought.
        boughtETHAmount = address(this).balance;

        require(_sellToken.approve(exchangeProxy, _amount));

        (bool success, ) = payable(exchangeProxy).call{value: 0}(_swapCallData);
        if (!success) {
            revert SwapCallFailed();
        }

        // Use our current buyToken balance to determine how much we've bought.
        boughtETHAmount = address(this).balance - boughtETHAmount;
        emit SwappedTokens(address(_sellToken), _amount, boughtETHAmount);
    }
```


`_claim` function:
```javascript
    function _claim(
        address _token,
        address _receiver,
        uint8 _percentage,
        Exchange _exchange,
Malicious user can lock all lpETH using wrapped LRT from being claimed via a direct donation
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

In `PreLaunchPoints.sol`, users can claim their lpETH after the `startClaimDate` when all ETH balance within the contract is converted to lpETH via `convertAllETH()`. Users claim lpETH depending on whether ETH/WETH or wrapped LRTs are locked:

1. ETH/WETH - 1:1 exchange of amount locked
2. Wrapped LRTs - A swap is initiated via a uniswapV3 pool and/or other exchanges

However, due to a flaw in `_fillQuote()` revolving how `boughtETHAmount` is computed, it can potentially cause a permanent locking in funds. The impact of this is borderline medium/high, given ALL potential users funds are locked but would require the attacker to potentially donate an equal/greater value worth of ETH. Since it is mentioned explicitly as an [attack vector](https://github.com/code-423n4/2024-05-loop?tab=readme-ov-file#attack-ideas-where-to-focus-for-bugs) to focus on, I will leave as high severity

> - User funds getting locked forever

## Proof of Concept

```solidity
function _fillQuote(IERC20 _sellToken, uint256 _amount, bytes calldata _swapCallData) internal {
    // Track our balance of the buyToken to determine how much we've bought.
    uint256 boughtETHAmount = address(this).balance;

    require(_sellToken.approve(exchangeProxy, _amount));

    (bool success,) = payable(exchangeProxy).call{value: 0}(_swapCallData);
    if (!success) {
        revert SwapCallFailed();
    }

    // Use our current buyToken balance to determine how much we've bought.
    boughtETHAmount = address(this).balance - boughtETHAmount;
    emit SwappedTokens(address(_sellToken), _amount, boughtETHAmount);
}
```

1. Assume that 50e18 amount of wrapped LRTs are locked within the `PrelaunchPoints.sol` contract. When users want to claim the tokens, `_fillQuote()` is executed to initiate the swap, notice how only `userClaim` worth of tokens is approved for swap. 

<br/>

2. Depending on slippage in play and exchange rates, lets assume the maximum amount of ETH that can be exchanged from wrapped LRT would be 51e18 (some tokens could be priced more than ETH). 

<br/>

3. Notice how `boughtETHAmount` is computed. At the point of time `_fillQuote()` is executed, ETH balance would be assumed to be zero. So it is presumed that [`boughtETHAmount` would initially be zero](https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L493), and thus would never underflow when [subsequently recomputed](https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L503) after the swap is completed

<br/>

4. However, a malicious user/whale can simply back-run 
`convertAllETH()`, donate a necessary amount of ETH (For example in this case, 52e18) to essentially lock all users from claiming lpETH from previous positions locked using wrapped LRTs since now the computation of `boughtETHAmount` would underflow (51e18 - 52e18).

<br/>

5.
Attackers could steal the ETH contained in the PrelaunchPoints contract
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
When users claim lpETH and the specified _token is not ETH, if there is ETH in the `PrelaunchPoints` contract transferred by mistake by others, the contract will also convert this part of ETH into lpETH for the user. This is because the amount of lpETH calculated for the user's claim in the `_claim` function is `address(this).balance`, rather than the actual amount of ETH obtained through exchangeProxy with the _token in the `_fillQuote` function.
```solidity
_fillQuote(IERC20(_token), userClaim, _data);

// Convert swapped ETH to lpETH (1 to 1 conversion)
claimedAmount = address(this).balance;
lpETH.deposit{value: claimedAmount}(_receiver);
```
Although the comment states, "At this point there should not be any ETH in the contract," this is an ideal situation. In reality, it is highly possible that someone could mistakenly transfer ETH to the `PrelaunchPoints` contract.

## Proof of Concept
1. User A calls the `lock` function, locking an amount of `_token` into the `PrelaunchPoints` contract.
2. The owner calls the `convertAllETH` function to convert the ETH in the `PrelaunchPoints` contract into lpETH and updates the `startClaimDate` variable, allowing users who have locked assets in the contract to start claiming lpETH.
3. User B mistakenly transfers 10 ETH to the `PrelaunchPoints` contract.
4. User A observes User B's mistaken transfer and immediately calls the `claim` function to claim lpETH. The `claim` function calls the `_claim` function for the actual operation. In the `_claim` function, `_fillQuote` is invoked, which converts User A's _token into ETH held within the `PrelaunchPoints` contract.
5. The contract converts the amount of ETH denoted as `claimedAmount` into lpETH for User A’s specified `_receiver`. Note that the `claimedAmount` here is `address(this).balance`, which includes User B's 10 ETH, not just the `boughtETHAmount` calculated in the final `_fillQuote` function.
6. User A successfully steals User B's 10 ETH.

## Tools Used
None

## Recommended Mitigation Steps
1. It is suggested that the `_fillQuote` function return the final calculated `boughtETHAmount` to the `_claim` function. In the `_claim` function, convert the amount of ETH denoted as `boughtETHAmount` into lpETH for the `_receiver`.
2. It is recommended to implement a revert logic in the `receive` function to prevent users from mistakenly transferring ETH into the contract.


## Assessed type

Other
User can bypass the locking mechanism and still be able to get as big amount of `lpETH` as he wants
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
User can lock low amount worth of any token different than `WETH` and get as much `lpETH` as he wants.

## Proof of Concept
The user will lock via `lock()` function, then after some time when the owner calls the `convertAllETH()` function and everybody is able to claim or stake their `lpETH`, the user will send ether to the contract and immediately call `claim()` or `claimAndStake()` functions. No matter which function he will call, because they are both leading to the following block of code in the `_claim()` function:
```javascript
} 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);
```
It deposits the ETH balance of the contract to the receiver, assuming that there is no ETH left in the contract after the `convertAllETH()` function is called. This sabotages the whole idea of the locking mechanism, making it easy for users to trick the system

## Tools Used
Manual review

## Recommended Mitigation
return the `boughtETHAmount` from `_fillQuote` function and deposit it, instead of depositing the ETH balance of the contract 






## Assessed type

Other
[H-1] `PrelaunchPoints::lock, lockFor` allows a users to lock s small amount of LRT token and then force ether into the contract claiming as much lpETH as they want, removing the risk of locking a lot of tokens and braking the 2nd invariant
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

**Description:**

In the `Prelaunchpoint` the functions `lock` and `lockFor` allow users to lock LRTs or WETH into the contract. After the owner calls `setLoopAddresses` and converts all the ETH, users are able to call the `claim` and `claimAndStake` functions. A user can force ETH into the smart contract and right after call the `claim` function with the LRT Token with a small percentage. Because `claimedAmount` is set to `address(this).balance` this will also get the forced ETH, allowing users to remove the risk of locking a large amount and rather lock a small amount and then force ether to get the desired lpETH.

**Impact:**

1. Allows users to remove the risk of locking a large amount of tokens.
2. Allows users to mint how much ever lpETH they want, as long as they have the capital and a small locked amount of LRT Token.
3. This breaks the 2nd invariant - `Deposits are active up to the lpETH contract and lpETHVault contract are set`

**Proof of Concept:**

1. The user locks a desired amount of an LRT Token
2. The owner sets the loop addresses and the 7 days to withdraw pass
3. The owner converts all the ETH, which allows the user to claim, claim and stake
4. The user forces ETH into the contract
5. The user calls the `claim` function and gets the lpETH for the forced ETH

Paste this into `PrelaunchPoints.t.sol`

```javascript
function testDepositAndStakeAfterTheClaimStartDate() public {
        uint256 lockAmount = 10;
        address userOne = vm.addr(1);

        lrt.mint(userOne, lockAmount);

        vm.startPrank(userOne);
        lrt.approve(address(prelaunchPoints), lockAmount);
        prelaunchPoints.lock(address(lrt), lockAmount, referral);
        vm.stopPrank();

        // Set Loop Contracts and Convert to lpETH
        prelaunchPoints.setLoopAddresses(address(lpETH), address(lpETHVault));
        vm.warp(prelaunchPoints.loopActivation() + prelaunchPoints.TIMELOCK() + 1);
        prelaunchPoints.convertAllETH();

        vm.warp(prelaunchPoints.startClaimDate() + 1);

        bytes memory data = abi.encodeWithSelector(0x415565b0, address(lrt), ETH, ((lockAmount * 1) / 100));

        vm.deal(userOne, 10);
        vm.prank(userOne);
        (bool success,) = address(prelaunchPoints).call{value: 10}("");
        if (!success) revert("Not Successful");

        uint256 temp = lpETH.balanceOf(address(userOne));
        console.log(temp);

        vm.prank(userOne);
        prelaunchPoints.claim(address(lrt), 1, PrelaunchPoints.Exchange.TransformERC20, data);

        temp = lpETH.balanceOf(address(userOne));
        console.log(temp);
    }
```

**Tools Used**

Manual Review

**Recommended Mitigation:**

1. If the `receive` function is called revert



## Assessed type

ETH-Transfer
Users are able to get more lpETH tokens than they have staked for during the locking period
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The `PrelaunchPoints` contract allows a user to lock a very small amount of LRT tokens, and right before or during a claim, the user can transfer a large amount of ETH directly to the contract.

Originally, by design, lpETH tokens should be gained only from the contract by staking LRT tokens during the locking period and then claiming them after the locking period has ended.

However, using the method described above, a user will end up **avoiding uncertainty and risks associated with the staking process** and being able to claim as many LRT tokens as they want **even after the locking period has ended.** Even though the user isn't getting those lpETH tokens for free, the user is **bypassing the staking process** and **avoiding the risks associated with it**.

Following that, the documentation also states, "[`Deposits are active up to the lpETH contract and lpETHVault contract are set`](https://code4rena.com/audits/2024-05-loopfi#top:~:text=Deposits%20are%20active%20up%20to%20the%20lpETH%20contract%20and%20lpETHVault%20contract%20are%20set)" which is **an invariant, that is broken here** and further more confirms this finding.

## Example scenario

_The amounts are simplified for the sake of easier understanding_

- Alice locks 1 LRT token and stakes it.
- Some time passes and the locking period ends.
- Before claiming, Alice sends large amount of ETH directly to the contract to gain more lpETH tokens than they have staked for during the locking period.
- Alice claims the staked amount and receives 10 lpETH tokens.
- Alice now has 10 lpETH tokens, even though they have only staked 1 LRT token during the locking period.

## Proof of Concept

The following Foundry test can be added to `test/PrelaunchPoints.t.sol` to demonstrate this finding:

Run the test using this command:
`forge test --match-test "test_DepositAndStakeAfterTheClaimStartDate" -vv`

```solidity
function test_DepositAndStakeAfterTheClaimStartDate() public {
        uint256 lockAmount = 1;
        address user1 = vm.addr(1);

        lrt.mint(user1, lockAmount);

        vm.startPrank(user1);
        lrt.approve(address(prelaunchPoints), lockAmount);
        // Alice locks 1 LRT token
        prelaunchPoints.lock(address(lrt), lockAmount, referral);
        vm.stopPrank();

        // Set Loop Contracts and Convert to lpETH
        prelaunchPoints.setLoopAddresses(address(lpETH), address(lpETHVault));

        // Locking period ends
        vm.warp(
            prelaunchPoints.loopActivation() + prelaunchPoints.TIMELOCK() + 1
        );
        prelaunchPoints.convertAllETH();

        vm.warp(prelaunchPoints.startClaimDate() + 1);

        bytes memory data = abi.encodeWithSelector(
            0x