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: 30/47
Findings: 1
Award: $71.11
🌟 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
71.1111 USDC - $71.11
https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L262 https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L503
Risk of Fund Loss to Protocol as User can Claim Entire Eth Balance of Contract instead of just their own Eth Claim Value in the PrelaunchPoints.sol contract during claim function call.
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 { 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); } emit Claimed(msg.sender, _token, claimedAmount); }
The function above shows how _claim(...) function is implemented in the PrelaunchPoints.sol contract, it can be noted from the pointers that when token is Non Eth a swap is done before a claim of Eth value is done and deposited to receiver address, from the first pointer, the protocol assumes there should not be any Eth in the contract which is why the last pointer assigns the complete balance of eth in contract as claimed Amount instead of the actual Amount that is to be claimed.
However the implementation of _fillQuote(...) function which handles the swap to determine the Eth value to be claimed as provided below paints a different picture of the Eth that should be in balance. As noted from the pointers below the value of Eth Bought is derived by comparing the Eth balance before and after without assuming it is previously empty, this is the correct way to derived the actual Eth that should be claimed. Now back to the _claim(...) function above, it is this boughtETHAmount
from _fillQuote(...) function that should be used as claimedAmount value and not using the entire balance of the contract as wrongly done in the last pointer above which risks a user claiming the whole contract value instead of just its personal funds, a bad actor simply needs to keep track of the contract balance to claim at the perfect time to get excess fund above its claimable range
function _fillQuote(IERC20 _sellToken, uint256 _amount, bytes calldata _swapCallData) internal { // Track our balance of the buyToken to determine how much we've bought. >>> uint256 boughtETHAmount = address(this).balance; require(_sellToken.approve(exchangeProxy, _amount)); (bool success,) = payable(exchangeProxy).call{value: 0}(_swapCallData); if (!success) { revert SwapCallFailed(); } // Use our current buyToken balance to determine how much we've bought. >>> boughtETHAmount = address(this).balance - boughtETHAmount; emit SwappedTokens(address(_sellToken), _amount, boughtETHAmount); }
Manual Review
As adjusted in the code below the actual boughtEthAmount from sell token should be returned by the _fillQuote(...) function and this value should be used to represent claimedAmount and not the overall balance of the contract as previously done
--- function _fillQuote(IERC20 _sellToken, uint256 _amount, bytes calldata _swapCallData) internal { +++ function _fillQuote(IERC20 _sellToken, uint256 _amount, bytes calldata _swapCallData) internal returns (uint256 boughtETHAmount ) { ... // Use our current buyToken balance to determine how much we've bought. boughtETHAmount = address(this).balance - boughtETHAmount; emit SwappedTokens(address(_sellToken), _amount, boughtETHAmount); }
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) { ... } 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); +++ uint256 boughtETHAmount = _fillQuote(IERC20(_token), userClaim, _data); // Convert swapped ETH to lpETH (1 to 1 conversion) --- claimedAmount = address(this).balance; +++ claimedAmount = boughtETHAmount; lpETH.deposit{value: claimedAmount}(_receiver); } emit Claimed(msg.sender, _token, claimedAmount); }
ETH-Transfer
#0 - c4-judge
2024-05-15T13:30:56Z
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:39:06Z
koolexcrypto marked the issue as partial-75
#3 - c4-judge
2024-06-05T09:38:59Z
koolexcrypto marked the issue as partial-50
#4 - c4-judge
2024-06-05T09:41:00Z
koolexcrypto changed the severity to 3 (High Risk)
#5 - c4-judge
2024-06-05T09:41:27Z
koolexcrypto marked the issue as duplicate of #33
#6 - Topmark1
2024-06-05T14:33:12Z
I believe this finding is worth full reward as it noted the major problem which is the use of address(this).balance for claimedAmount just as noted in the primary report at https://github.com/code-423n4/2024-05-loop-findings/issues/33 and it offers more explanation and elaboration than the bot primary report at https://github.com/code-423n4/2024-05-loop-findings/issues/18 which is also getting full reward, and some other examples just to mention a few, Great Judging @koolexcrypto thanks for the hard work
#7 - koolexcrypto
2024-06-10T08:21:34Z
Hi @Topmark1
The issues doesn't clearly explain where the funds in the contract comes from. It is just assumes to have funds which shouldn't happen normally. For this, it is not fair to give it full credit.
#8 - c4-judge
2024-06-11T07:54:08Z
koolexcrypto marked the issue as partial-25
#9 - Topmark1
2024-06-11T07:58:34Z
I don't seem to understand why this issue is being downgraded again instead of an upgrade???, you gave reasons for giving it a partial 50 which I would have argued but don't want to extended the PQA for a mere duplicate but suddenly downgrading it even lower seems rather unfair, thanks