LoopFi - _karanel'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: 31/47

Findings: 1

Award: $71.11

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

71.1111 USDC - $71.11

Labels

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

External Links

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.

        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.

    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.totalSupply(), 2*lockAmount);
        // `totalLpETH` variable is 3*lockAmount
        assertEq(prelaunchPoints.totalLpETH(), 3*lockAmount);
        // The contract has 3*lockAmount of lpETH token
        assertEq(lpETH.balanceOf(address(prelaunchPoints)), 3*lockAmount);
        assertEq(prelaunchPoints.startClaimDate(), block.timestamp);

        vm.warp(prelaunchPoints.startClaimDate() + 1);
        vm.prank(address(1));
        // User 1 claims his `lpETH` tokens
        prelaunchPoints.claim(ETH, 100, PrelaunchPoints.Exchange.UniswapV3, emptydata);

        // User 1 should have received lockAmount amount of lpETH tokens
        // But User 1 received 3*lockAmount/2 amount of lpETH tokens
        // Any user aware of any mistakenly deposited ETH can lock ETH and claim more lpETH tokens
        // Mistakenly deposited ETH can be checked through the block explorer
        // Mistakenly deposited ETH can't be withdrawn by the contract owner
        assertEq(lpETH.balanceOf(address(1)), 3*lockAmount/2);
    }

Tools Used

Manual Review

Instead of converting the whole balance of PrelaunchPoints contract, only convert the amount stored in PrelaunchPoints::totalSupply variable to lpETH tokens, and send the remaining ETH to the owner. This prevents giving unfair disadvantage to those who locked LRTs.

    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));
+       lpETH.deposit{value: totalSupply}(address(this));

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

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

+       owner.transfer(address(this).balance);

        emit Converted(totalBalance, totalLpETH);
    }

Assessed type

Token-Transfer

#0 - c4-judge

2024-05-15T14:19:22Z

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:37Z

koolexcrypto changed the severity to 2 (Med Risk)

#3 - c4-judge

2024-06-05T09:26:36Z

koolexcrypto marked the issue as partial-50

#4 - c4-judge

2024-06-05T09:41:01Z

koolexcrypto changed the severity to 3 (High Risk)

#5 - c4-judge

2024-06-05T09:41:18Z

koolexcrypto marked the issue as duplicate of #33

#6 - c4-judge

2024-06-11T08:15:01Z

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