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: 31/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#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
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); }
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); }
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); }
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