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

4 stars 4 forks source link

Users can bypass the conversion of LRT to lpETH by setting a feeRecipient in the 0x transaction #71

Closed howlbot-integration[bot] closed 3 months ago

howlbot-integration[bot] commented 4 months ago

Lines of code

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

Vulnerability details

Impact

Users can farm points without participating in the protocol.

Proof of Concept

When users that deposited LRTs call claim(), the function will route through 0x, convert their LRT tokens to ETH, send ETH to the contract, and swap lpETH to ETH at a 1:1 conversion rate.

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

This means that users who deposited LRT will get lpETH back when claim(). However, there is a way that users can get back ETH and bypass the conversion to lpETH.

The reason for doing so could be to farm points through lock() without participating in getting lpETH. Also, there may be instances where lpETH breaks the 1:1 peg to ETH because of market volatility or a manipulation in the totalLpETH variable, which results in users wanting ETH back and not lpETH.

In 0x, there is a request for feeRecipient and feeRecipientPercentage.

If the feeRecipient and feeRecipientPercentage is set, the percentage of buyToken will be sent to the feeRecipient. This means that the LRT will be converted to ETH, and the percentage of ETH will be sent to the feeRecipient. Note that the fee can be up to 0.99 (apparently 1.0 gets internal server error)

If a user locks 1 rETH for example, and the exchange rate to ETH is 1.1:1, he can get back 1.1 ETH and not 1.1 lpETH.

Send this request to the CLI, get the API-KEY by creating an account on 0x. Note that this is selling 1 USDC for x USDT on polygon.

curl --location --request GET 'https://polygon.api.0x.org/swap/v1/quote?buyToken=0xc2132D05D31c914a87C6611C10748AEb04B58e8F&sellAmount=1000000&sellToken=0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174&buyTokenPercentageFee=0.99&feeRecipient=0x324446D6676123CA1e8EB5d6F171dB201d18e2D5' --header '0x-api-key: [API-KEY]'

This should return the data:

0x415565b00000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa84174000000000000000000000000c2132d05d31c914a87c6611c10748aeb04b58e8f00000000000000000000000000000000000000000000000000000000000f424000000000000000000000000000000000000000000000000000000000000020e000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005200000000000000000000000000000000000000000000000000000000000000620000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000340000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa84174000000000000000000000000c2132d05d31c914a87c6611c10748aeb04b58e8f00000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000024d65736853776170000000000000000000000000000000000000000000000000000000000000000000000000000f424000000000000000000000000000000000000000000000000000000000000f1b2f000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000010f4a785f458bc144e3706575924889954946639000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa84174000000000000000000000000c2132d05d31c914a87c6611c10748aeb04b58e8f000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c2132d05d31c914a87c6611c10748aeb04b58e8f00000000000000000000000000000000000000000000000000000000000ef483000000000000000000000000324446d6676123ca1e8eb5d6f171db201d18e2d5000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c2132d05d31c914a87c6611c10748aeb04b58e8f00000000000000000000000000000000000000000000000000000000000005cc000000000000000000000000ad01c20d5886137e056775af56915de824c8fce5000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa84174000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd0000000000000000000000001000000000000000000000000000000000000011000000000000000000000000000000008e51cc6e20e400ddaacbfbbd094d3ec9

When sent to Metamask, the feeRecipient will get 99% of the buyToken, which is USDT.

Tools Used

Manual Review

Recommended Mitigation Steps

If this bypass is not intended, set the priceFeePercentage to zero and check zero value when validating the data.

Assessed type

Context

c4-judge commented 4 months ago

koolexcrypto marked the issue as primary issue

0xd4n1el commented 4 months ago

Point calculation is done offchain and controlled via events that cannot be manipulated this way

koolexcrypto commented 3 months ago

Points are calculated off-chain based on Claimed events emitted, claimedAmount is address(this).balance, so the user won't get more points.

            // Convert swapped ETH to lpETH (1 to 1 conversion)
            claimedAmount = address(this).balance;
            lpETH.deposit{value: claimedAmount}(_receiver);
        }
        emit Claimed(msg.sender, _token, claimedAmount);
c4-judge commented 3 months ago

koolexcrypto marked the issue as unsatisfactory: Invalid