xanoutas/solidity-security-findings huggingface dataset:
Malicious actors could exploit `claim()` function to get lpETH at a discount
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Malicious actors will receive more lpETH than they should relative to the value of the LRT they locked in the contract, ultimately resulting in theft from the protocol and other users who are utilizing it.

## Proof of Concept

If users deposited LRTs instead of ETH, obtaining the corresponding lpETH amount against their LRTs is executed as follows, as stated on the contest page: "The conversion for LRTs happens on each claim by using 0x API. This is triggered by each user." However, while claiming lpETH against the LRT value they've deposited, malicious actors could manipulate the protocol to receive more lpETH than they are entitled to.

This issue arises from the expectation of the protocol team, that when users claim their LRTs, the contract balance would not contain any ETH. Thus, the `claimedAmount`, retrieved as `claimedAmount = address(this).balance;`, where `address(this).balance` is expected to reflect the value of LRT in terms of ETH from the 0x API. Based on `claimedAmount` also is determined the amount of lpETH that the user should receive. However, a malicious actor could exploit this by transferring ETH to the contract before calling the `claim()` function. This vulnerability is profitable if the ETH price is lower than the LRT price in ETH terms. For example, at the time of writing this report, the price of UNIETH is $3,340.88, and the ETH price is $3,132.95. Therefore, this exploitation would allow the malicious actor to receive more lpETH at a cheaper price relative to the LRT.

```solidity
    function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
        internal
        returns (uint256 claimedAmount)
    {
        uint256 userStake = balances[msg.sender][_token];
        ...
         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
            // @audit-issue This can be not the case, as there is receive() function and malicious actor can send ETH to the contract
            _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);
    }
```

## Tools Used

VSCode

## Recommended Mitigation Steps

The amount to be deposited in lpETH should be calculated as follows:

```solidity
    function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
        internal
Invariant is broken, one ETH deposit by user is not convertable to one lpETH, if someone manually deposits eth to contract
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Within the context of invariants, it's emphasized that when a user deposits 1 ETH, it should automatically convert to 1 lpETH. However, if an individual chooses to directly send ETH to the contract, this disrupts the conversion mechanism, diverging from the user's intended perspective.

## Proof of Concept
By incorporating the provided test into the existing test file, it becomes evident that the conversion process is compromised.

```javascript
function testBreakOneToOneConversion(uint256 lockAmount) public {
        lockAmount = bound(lockAmount, 1, 1e36);
        vm.deal(address(this), lockAmount);
        prelaunchPoints.lockETH{value: lockAmount}(referral);

        // Directly deposit 1 eth
        address userDepositETH = makeAddr("user");
        vm.deal(address(userDepositETH), 1e18);
        vm.prank(userDepositETH);
        (bool sent, bytes memory data) = address(prelaunchPoints).call{
            value: 1e18
        }("");
        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);
        prelaunchPoints.claim(
            ETH,
            100,
            PrelaunchPoints.Exchange.UniswapV3,
            emptydata
        );

        uint256 balanceLpETH = (prelaunchPoints.totalLpETH() * lockAmount) /
            prelaunchPoints.totalSupply();

        assertEq(prelaunchPoints.balances(address(this), ETH), 0);
        assertEq(lpETH.balanceOf(address(this)), balanceLpETH);
        assertEq(lpETH.balanceOf(address(this)), lockAmount);
    }
```

## Tools Used
Manual review

## Recommended Mitigation Steps
To resolve this issue, removing the receive function can be an effective solution. The receive function can be found in the contract code [here](https://github.com/code-423n4/2024-05-loop/blob/0dc8467ccff27230e7c0530b619524cc8401e22a/src/PrelaunchPoints.sol#L392).


## Assessed type

Other
Users can still "deposit" in order to mint more `lpETH` than they have locked during the lock-up period
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


# Vulnerability details

## Impact

The main invariant : "Deposits are active up to the lpETH contract and lpETHVault contract are set" is broken.

Even though the deposits are not active anymore, users are still able to gain the benefits from depositing and bypass the `LRT` lock-up period enforced by the protocol to get more `lpETH` than they are owed.

## Proof of concept

The core idea of the `PrelaunchPoints.sol` contract is that users lock their `LRT` tokens for a certain period and are able to claim `lpETH` based upon their stake when this period comes to an end.

When administrators call `setLoopAddresses()` users can't deposit their `LRT` anymore and the amount of `lpETH` they are able to mint is settled.

These `lpETH` can be claimed using the `claim()` function after the `startClaimDate` set by administrators has been reached.

The issue is when the `LRT` tokens users are trying to `claim()` from is not `ETH` nor `WETH`, the `LRT` tokens are first swapped to `native ETH` through the `_fillQuote()` function.

Right after, the contract uses its new ETH balance to mint the corresponding amount of `lpETH` which should only be equal to the amount received from the swap.

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

```js
// Swap LRT to ETH
_fillQuote(IERC20(_token), userClaim, _data);
// Gets the contract's balance
claimedAmount = address(this).balance;
// Mints lpETH using the contract's balance
lpETH.deposit{value: claimedAmount}(_receiver);
```

However if a user transfers `native ETH` to the contract before calling `claim()`, the amount of `lpETH` to mint will include the amount of ETH transfered.

This will end up in the user artificially depositing without having to deal with the lock-up period, thus, minting more `lpETH` tokens than they should have based upon their initial stake.

In the below PoC, we are claiming 100% of our initial deposit but it is possible to claim way less and repeatitively execute the exploit to mint more `lpETH`

The following PoC will demonstrate the steps to take advantage of the issue.

Add this test to `test/PrelaunchPoints0x.test.ts` and execute it with

*Make sure you have added your 0x API token to your `.env` file*

```sh
npx hardhat test --grep "it should claim more"
```

```js
it(`it should claim more`, async function () {
    let token = tokens[2]; // ezETH
    lockToken = (await ethers.getContractAt(
      "IERC20",
      token.address
    )) as unknown as IERC20

    // Impersonate whale
    const depositorAddress = token.whale
    await impersonateAccount(depositorAddress)
    const depositor = await ethers.getSigner(depositorAddress)
    await setBalance(depositorAddress, parseEther("100"))

    // Get pre-lock balances
    const tokenBalanceBefore = await lock
The `_claim()` function `claimedAmount` calculations causes the `lpETH` to `ETH` conversion rate inflate and not equal 1:1
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
When `claimedAmount` is calculated in the `(_token == ETH)` branch, the amount of `lpETH` received may be greater than the staked `ETH` in case there was an accidental direct deposit of `ETH` to the contract. The user making this transfer of `ETH` rightfully losses it without a way to recover, the issue is that the `ETH` can be considered to be lost as much as it would be when sent to a dummy address, as there is no way to recover it from the contract - you can only recover ERC20s or use the `withdraw()` function as a user. The excess balance causes `totalLpETH` to be greater than `totalSupply` because of the following lines in `convertAllETH`:
```javascript
uint256 totalBalance = address(this).balance;
lpETH.deposit{value: totalBalance}(address(this));
totalLpETH = lpETH.balanceOf(address(this));
```

Therefore, it increases the `claimedAmount` for each user. This is causing the conversion rate between `ETH` and `lpETH` to not equal 1. Moreover, considering the `(_token != ETH)` branch, a user can `_fillQuote` with amount equal to `userClaim`, however the `claimedAmount` can be increased by simply sending `ETH` directly to the contract right before the call to `claim()` or `claimAndStake()` functions. Again, this makes the conversion to change from 1:1, because `ETH` sent directly to the contract is NOT accessible by anyone including the owner.

Both of these situations make the `lpETH` inflationary as users essentially get more `lpETH` for the same amount of _real_ stake if they do what's explained above for both `_claim()` function token branches.

## Proof of Concept

To run a PoC proving how the received `lpETH` is indeed increased when the user sends `ETH` directly to the contract right before calling `claim()` change the code in `PrelaunchPoints0x.test.ts` to the code in this [gist](https://gist.github.com/bbl4de/86e35e2806d53f33d97ab68c8a1a5ba4) and run the test with `yarn hardhat test ./test/PrelaunchPoints0x.test.ts`.

## Tools Used
Manual Review, Hardhat 

## Recommended Mitigation Steps

To mitigate this issue consider changing the approach from push to pull, meaning that in `convertAllETH` the owner does not deposit all `ETH` to the `lpETH` contract. Thanks to this, `totalSupply` and `totalLpETH` variables can be removed and the user gets the appropriate amount of `lpETH` based on their share of the contract's balance.


## Assessed type

Token-Transfer
It's possible for an user to lock and get more lpETH even after `loopActivation` timestamp
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
The protocol defines that users cannot lock tokens/ETH anymore after the `loopActivation` time, but this breaks the rule, and can potentially affect other state variables.

## Proof of Concept
For any locking operations, they all call `_processLock` internally:

```solidity
    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;
            } else {
                balances[_receiver][_token] += _amount;
            }
        }

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

As we can see, this function handles the transfer of ETH and other ERC20 tokens. Also the `onlyBeforeDate` modifier implies this function is only supposed to be called before `loopActivation` timestamp.

Another timestamp is `startClaimDate`, this indicates when the claiming starts. In constructor, it's set to the max value of uint32, and updated when an admin comes in and calls `convertAllETH` to set the value to `block.timestamp`. This implies `lpETH` are ready to be claimed and distributed to whoever locked their tokens in the protocol.

In `_claim` function, which is called internally by both `claim` and `claimAndStake` function converts the locked assets to `lpETH` and deposit to the receivers:

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

            // C
Malicious users could bypass the lock process to claim any amount of lpETH
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
During `claim` process, `_claim` is called to claim `lpETH` token to user, whose amount `claimedAmount` is calculated based on user staked amount during lock period.
However, for the scenario that user stake allowed token to claim `lpETH`, `claimedAmount` is calculated using PrelaunchPoints's balance. 
So malicious users could send any amount of ETH to PrelaunchPoints and call claim in a single transaction to claim any amount of `lpETH` they wants, which will make the whole lock process useless.
## Proof of Concept
1. Alice lock `x_amount` allowed Token to PrelaunchPoints by calling `lock`
2. After `convertAllETH` and time passed `startClaimDate`, Alice can claim `lpETH`, whose amount should equals to the amount of ETH swapped by `x_amount` staked token. Let's define the correct amount of `lpETH` Alice should get is `x_amt_ETH`.
https://github.com/code-423n4/2024-05-loop/blob/40167e469edde09969643b6808c57e25d1b9c203/src/PrelaunchPoints.sol#L502-L504
```solidity
        // Use our current buyToken balance to determine how much we've bought.
        boughtETHAmount = address(this).balance - boughtETHAmount;
        emit SwappedTokens(address(_sellToken), _amount, boughtETHAmount);
```
3. However, Alice send `y_amount` ETHs to PrelaunchPoints and call `claim` in a single transaction, so the actual amount `lpETH` she can get is `y_amount + x_amt_ETH`. Since this `y_amount` can be any number, which means that Alice could claim arbitrary amount of `lpETH` she wants and thusly make lock process useless.
https://github.com/code-423n4/2024-05-loop/blob/40167e469edde09969643b6808c57e25d1b9c203/src/PrelaunchPoints.sol#L261-L263 
```solidity
            // Convert swapped ETH to lpETH (1 to 1 conversion)
            claimedAmount = address(this).balance;
            lpETH.deposit{value: claimedAmount}(_receiver);
```
## Tools Used
Manual Review

## Recommended Mitigation Steps
use the swapped amount of ETH in `_fillQuote` as `claimedAmount` rather than `address(this).balance`


## Assessed type

Context
Inaccurate Calculation of lpETH During Token Claiming
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The `_claim` function  inaccurately calculates the amount of `lpETH` to be received when users claim tokens. This inaccuracy stems from directly using `address(this).balance` to determine the claimed amount, which reflects the total ETH balance of the contract, since the contract implements a `receive` function any one can send ETH to this contract. This approach doesn't account for the actual amount received after token swapping, leading to potential inconsistencies in the claimed lpETH amount.


Users may receive incorrect amounts of `lpETH` during the claiming process.

The `_claim` function swaps tokens other than ETH for ETH and then converts the resulting ETH to lpETH using a 1-to-1 conversion ratio.

However, the claimed amount is determined solely based on the total ETH balance of the contract, which does not accurately represent the amount received after token swapping. anyone can send ETh to this contract.

## Proof of Concept
Suppose a user claims 100 tokens, expecting to receive 100 `lpETH` based on the 1-to-1 conversion ratio. However, due to market conditions, the actual amount of ETH received from the token swap may differ. If the contract directly uses address(this).balance to calculate the claimed lpETH, the user may receive more or fewer lpETH than anticipated, leading to inconsistencies and potential dissatisfaction.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Implement a mechanism to determine the exact amount of ETH received from the token swap.
Use this actual amount of ETH to calculate the equivalent lpETH and transfer it to the user accordingly.
Ensure that the claimed amount accurately reflects the converted token amount, maintaining fairness and consistency in the claiming process.



## Assessed type

Invalid Validation
User can maliciously lock specific tokens (`ETH` and `WETH`) to gain additional `lpETH` tokens unfairly, if `ETH` was mistakenly deposited by someone.
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#L321-L324
https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L179-L195


# Vulnerability details

## Impact
When someone mistakenly deposits `ETH` to the `PrelaunchPoints` contract it is supposed to be locked forever, however when the `owner` calls `PrelaunchPoints::convertAllETH` function all `ETH` balance is converted into `lpETH` including the mistakenly sent `ETH`.

Now the variable `totalSupply` only got updated inside `PrelaunchPoints::_processLock` function when `ETH`/`WETH` was deposited. Meanwhile `totalLpETH` was set inside `PrelaunchPoints::convertAllETH` method and could be a much larger value.

When a user who locked `ETH`/`WETH` calls the `PrelaunchPoints::claim` or `PrelaunchPoints::claimAndStake` functions, they receive a much larger amount of `lpETH` than the `ETH`/`WETH` they originally locked. 
However, users who locked any other LRTs don't receive this added benefit from the tokens which were supposed to be locked forever, and thereby giving them unfair disadvantage.

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

## Proof of Concept

Paste this in `PrelaunchPointsTest` contract inside `test/PrelaunchPoints.t.sol` file.

```javascript
    function testUnfairRewards(uint256 lockAmount) public {
        lockAmount = bound(lockAmount, 1, 1e36);
        vm.deal(address(1), lockAmount);
        vm.deal(address(2), lockAmount);

        // Someone deposits `lockAmount` ETH to the contract mistakenly.
        vm.deal(address(prelaunchPoints), lockAmount);

        // User 1 locks `lockAmount` ETH
        vm.prank(address(1));
        prelaunchPoints.lockETH{value: lockAmount}(referral);
        // User 2 locks `lockAmount` ETH
        vm.prank(address(2));
        prelaunchPoints.lockETH{value: lockAmount}(referral);

        prelaunchPoints.setLoopAddresses(address(lpETH), address(lpETHVault));

        vm.warp(prelaunchPoints.loopActivation() + prelaunchPoints.TIMELOCK() + 1);
        prelaunchPoints.convertAllETH();

        // `totalSupply` variable is 2*lockAmount
        assertEq(prelaunchPoints.totalS
User can manipulate locking mechanism by locking small amount of wrapped `LRT` then send the rest of the ethers at the claim date
Audit: 2024-05-loop-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

# Summary
The Prelaunch Points system will lock fund of the users for at least 
7 days after the authorized owner of the smart contract set 
the `lpETH` via `setLoopAddresses()` function. Within that period, 
the users can withdraw their fund. If the users withdraw their 
fund, they can't claim `lpETH` anymore. But this mechanism can be manipulated 
by only locking small amount of wrapped `LRT` and then send the rest of 
the fund (in the form of ethers) at the time the users claim the `lpETH`. 
Within the locking and before claim period, the users can use the rest of 
their fund to do something else.

# Vulnerability Detail
User locks small amount of wrapped `LRT`. When the claim date comes,
before calling `claim()` function, the user sends ethers to the contract.
When the `claim()` function is called, all of its balance will be converted into
`lpETH` (including the ethers sent by the user).

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

Now the user has `lpETH` from conversion of the amount of locked wrapped `LRT` plus the 
amount of ethers sent.

# Impact
The user can manipulate locking mechanism by locking a small amount of wrapped `LRT`
and provide the rest of the ethers at the claim date to get `lpETH`.

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

# Tool used
Manual Review

# Recommendation
Only deposit the amount of ethers bought from exchange when user uses wrapped `LRT` in order to claim `lpETH`.






## Assessed type

Other