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: 8/47
Findings: 1
Award: $386.08
🌟 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
284.4444 USDC - $284.44
The protocol defines that users cannot lock tokens/ETH anymore after the loopActivation
time, but this breaks the rule, and can potentially affect other state variables.
For any locking operations, they all call _processLock
internally:
function _processLock(address _token, uint256 _amount, address _receiver, bytes32 _referral) internal onlyBeforeDate(loopActivation) { if (_amount == 0) { revert CannotLockZero(); } if (_token == ETH) { totalSupply = totalSupply + _amount; balances[_receiver][ETH] += _amount; } else { if (!isTokenAllowed[_token]) { revert TokenNotAllowed(); } IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); if (_token == address(WETH)) { WETH.withdraw(_amount); totalSupply = totalSupply + _amount; balances[_receiver][ETH] += _amount; } else { balances[_receiver][_token] += _amount; } } emit Locked(_receiver, _amount, _token, _referral); }
As we can see, this function handles the transfer of ETH and other ERC20 tokens. Also the onlyBeforeDate
modifier implies this function is only supposed to be called before loopActivation
timestamp.
Another timestamp is startClaimDate
, this indicates when the claiming starts. In constructor, it's set to the max value of uint32, and updated when an admin comes in and calls convertAllETH
to set the value to block.timestamp
. This implies lpETH
are ready to be claimed and distributed to whoever locked their tokens in the protocol.
In _claim
function, which is called internally by both claim
and claimAndStake
function converts the locked assets to lpETH
and deposit to the receivers:
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; // <=(1) lpETH.deposit{value: claimedAmount}(_receiver); } emit Claimed(msg.sender, _token, claimedAmount); }
The problem lies here. In (1), we see all ETH balance will be deposited to lpETH
. Also from the comment above we know it's expected for the contract to have 0 ETH at the moment of calling such function, but in receive
function, there are no restrictions on who or when ETH can be sent. This leaves a way for anyone to send ETH right before claim of lpETH
, and getting more lpETH
than the user has locked. This breaks the intention of locking period, and makes depositors to get more lpETH
than intented. Also, in this way of getting more lpETH
, totalSupply
is not updated, which may also cause some problems in the protocol.
Moreover, if such action is done in claimAndStake
, it may cause permanent DoS because:
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 claimed amount will be approved and staked, but if the staked amount is increased using the unintented way, eventually lpETH
of contract will run out, making other users not being able to stake, and hence DoS.
Manual review.
Only allow WETH
and other approved addresses to send ETH to the contract.
Context
#0 - c4-judge
2024-05-15T14:17:41Z
koolexcrypto marked the issue as duplicate of #6
#1 - c4-judge
2024-05-31T09:58:30Z
koolexcrypto marked the issue as duplicate of #33
#2 - c4-judge
2024-06-05T09:53:50Z
koolexcrypto marked the issue as satisfactory