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
Rank: 25/47
Findings: 1
Award: $142.22
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xnev
Also found by: 0x04bytes, 0xBugSlayer, 0xJoyBoy03, 0xSecuri, 0xrex, Bigsam, DMoore, Evo, Greed, Kirkeelee, Krace, Pechenite, Rhaydden, SBSecurity, Sajjad, TheFabled, Topmark, XDZIBECX, ZanyBonzy, _karanel, bbl4de, btk, d3e4, gumgumzum, nfmelendez, novamanbg, petarP1998, samuraii77, sandy, shaflow2, sldtyenj12, web3er, y4y, yovchev_yoan
142.2222 USDC - $142.22
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
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.
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.
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.
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.
🌟 Selected for report: 0xnev
Also found by: 0x04bytes, 0xBugSlayer, 0xJoyBoy03, 0xSecuri, 0xrex, Bigsam, DMoore, Evo, Greed, Kirkeelee, Krace, Pechenite, Rhaydden, SBSecurity, Sajjad, TheFabled, Topmark, XDZIBECX, ZanyBonzy, _karanel, bbl4de, btk, d3e4, gumgumzum, nfmelendez, novamanbg, petarP1998, samuraii77, sandy, shaflow2, sldtyenj12, web3er, y4y, yovchev_yoan
142.2222 USDC - $142.22
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
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.
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:
claim
function to claim 1000 tokens of a non-ETH token, with a _percentage
of 100._claim
function calculates userClaim
as 1000 tokens and calls _fillQuote
to swap these tokens for ETH._fillQuote
function performs the swap and receives 5 ETH in return._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.
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);
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