LoopFi - Rhaydden's results

A dedicated lending market for Ethereum carry trades. Users can supply a long tail of Liquid Restaking Tokens (LRT) and their derivatives as collateral to borrow ETH for increased yield exposure.

General Information

Platform: Code4rena

Start Date: 01/05/2024

Pot Size: $12,100 USDC

Total HM: 1

Participants: 47

Period: 7 days

Judge: Koolex

Id: 371

League: ETH

LoopFi

Findings Distribution

Researcher Performance

Rank: 25/47

Findings: 1

Award: $142.22

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

142.2222 USDC - $142.22

Labels

bug
3 (High Risk)
partial-50
sufficient quality report
upgraded by judge
:robot:_42_group
duplicate-33

External Links

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

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

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

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

#0 - c4-judge

2024-05-15T13:58:10Z

koolexcrypto marked the issue as duplicate of #18

#1 - c4-judge

2024-06-03T09:03:32Z

koolexcrypto changed the severity to 3 (High Risk)

#2 - c4-judge

2024-06-05T07:29:36Z

koolexcrypto changed the severity to 2 (Med Risk)

#3 - c4-judge

2024-06-05T09:41:00Z

koolexcrypto changed the severity to 3 (High Risk)

#4 - c4-judge

2024-06-05T09:41:25Z

koolexcrypto marked the issue as duplicate of #33

#5 - c4-judge

2024-06-05T09:53:15Z

koolexcrypto marked the issue as satisfactory

#6 - c4-judge

2024-06-11T07:38:30Z

koolexcrypto marked the issue as partial-25

#7 - Rhaydden

2024-06-11T07:54:12Z

Hello @koolexcrypto, thank you for the efforts.

Respectfully. I think marking this as a partial 25 is a mistake based on your comment here because this report talks about the direct transfer of funds and the bypassing of lock. Please have a second look at it again

#8 - koolexcrypto

2024-06-11T08:23:43Z

Hello @koolexcrypto, thank you for the efforts.

Respectfully. I think marking this as a partial 25 is a mistake based on your comment here because this report talks about the direct transfer of funds and the bypassing of lock. Please have a second look at it again

Hi @Rhaydden

Where do you mention the direct transfer?

#9 - koolexcrypto

2024-06-11T08:25:45Z

My bad

If additional ETH is deposited into the contract (not through the standard lockETH or convertAllETH functions

I missed this.

#10 - c4-judge

2024-06-11T08:26:02Z

koolexcrypto marked the issue as partial-50

#11 - Rhaydden

2024-06-11T08:27:16Z

Thank you @koolexcrypto for your understanding. I believe a partial 75 is deserved just as in #57

#12 - koolexcrypto

2024-06-11T08:29:42Z

There is no mentioning of bypassing locking duration.

This misalignment could inflate the lpETH supply without proper backing which could devalue the lpETH

This impact also is inaccurate. lpETH is not devalued due to this. ETH are actually converted and deposited into the vault.

#13 - koolexcrypto

2024-06-11T08:30:33Z

#57 mentions this

more tokens via claimAndStake() than he had initially locked

It should take full credit, but other factors (quality of report) was taken into account.

Awards

142.2222 USDC - $142.22

Labels

bug
3 (High Risk)
partial-25
sufficient quality report
upgraded by judge
:robot:_07_group
duplicate-33

External Links

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

// 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

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:

// 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).balance - initialBalance;
    return boughtETHAmount;
}

// Adjust _claim function to use the returned value from _fillQuote
uint256 ethReceived = _fillQuote(IERC20(_token), userClaim, _data);
claimedAmount = ethReceived;
lpETH.deposit{value: claimedAmount}(_receiver);

Assessed type

ETH-Transfer

#0 - c4-judge

2024-05-15T13:29:25Z

koolexcrypto marked the issue as duplicate of #18

#1 - c4-judge

2024-06-05T07:29:36Z

koolexcrypto changed the severity to 2 (Med Risk)

#2 - c4-judge

2024-06-05T07:35:37Z

koolexcrypto marked the issue as partial-50

#3 - c4-judge

2024-06-05T09:39:52Z

koolexcrypto marked the issue as partial-25

#4 - c4-judge

2024-06-05T09:40:08Z

koolexcrypto marked the issue as partial-50

#5 - c4-judge

2024-06-05T09:41:00Z

koolexcrypto changed the severity to 3 (High Risk)

#6 - c4-judge

2024-06-05T09:41:28Z

koolexcrypto marked the issue as duplicate of #33

#7 - c4-judge

2024-06-11T07:55:19Z

koolexcrypto marked the issue as partial-25

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter