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: 38/47
Findings: 1
Award: $70.15
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: pamprikrumplikas
Also found by: 0xnev, 0xrex, ParthMandale, Pechenite, SpicyMeatball, ZanyBonzy, caglankaan, chainchief, cheatc0d3, karsar, krisp, oualidpro, peanuts, popeye, slvDev
70.1538 USDC - $70.15
Wen user locks their WETH token, they call lock()
with the WETH token address and it automatically converts to ETH and gets logged in the ETH mapping.
if (_token == address(WETH)) { WETH.withdraw(_amount); totalSupply = totalSupply + _amount; balances[_receiver][ETH] += _amount;
When they withdraw, they need to call withdraw()
with the ETH token address and it gives them back ETH instead of WETH.
if (_token == ETH) { if (block.timestamp >= startClaimDate){ revert UseClaimInstead(); } totalSupply = totalSupply - lockedAmount; (bool sent,) = msg.sender.call{value: lockedAmount}(""); if (!sent) { revert FailedToSendEther(); } } else { IERC20(_token).safeTransfer(msg.sender, lockedAmount); }
This will confuse some users if it is not indicated that they cannot withdraw WETH but only ETH.
The owner of the contract only has one chance to call setLoopAddress and there is no other way to change it. There is also no failsafe check when setting the loop address (eg zero address check). This is pretty dangerous as a lot of ETH will be at stake if the loop addresses are set wrongly.
function setLoopAddresses(address _loopAddress, address _vaultAddress) external onlyAuthorized onlyBeforeDate(loopActivation) { lpETH = ILpETH(_loopAddress); lpETHVault = ILpETHVault(_vaultAddress); loopActivation = uint32(block.timestamp); emit LoopAddressesUpdated(_loopAddress, _vaultAddress); }
The function can only be called once because loopActivation
will be reset to block.timestamp, which prevent any further function call.
Also, in the event that the function is not called after 120 days, the loop addresses cannot be set anymore.
The only way to resolve any issue is to set emergencyMode
to true to allow everyone to withdraw their funds.
Since this function is already a centralized function, it would be good to have other authorized function to change the loop addresses again.
When claiming their lp token, if the user calls claim()
instead of claimAndStake()
, the user will not have any option to stake their lp tokens anymore.
In claimAndStake()
, the function will call approve()
and lpETHVault.stake()
.
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);
function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data) external onlyAfterDate(startClaimDate) { _claim(_token, msg.sender, _percentage, _exchange, _data); }
Note that _claim()
sets address(this)
as the second parameter, while in claim()
, msg.sender
is set as the second parameter in _claim()
. This means that to stake the lp token, the lp token must be in the contract.
Users that call claim()
and wants to stake their tokens in the future will not be able to do so.
Contrary to the NATSPEC above receive()
, ETH sent to this contract will be converted to lpETH before convertAllETH()
is called or given to the first user that calls claim()
with their LRT staked after convertAllETH()
is called.
/** * Enable receive ETH * @dev ETH sent to this contract directly will be locked forever. */ receive() external payable {}
claimedAmount
is set as address(this).balance
.
// 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;
When claiming ETH deposits, the lpETH to ETH conversion should ideally be 1:1. If a user directly deposit ETH into the contract, totalLpETH
will be greater than totalSupply
which will affect the conversion rate for all user stakes.
if (_token == ETH) { claimedAmount = userStake.mulDiv(totalLpETH, totalSupply); balances[msg.sender][_token] = 0; lpETH.safeTransfer(_receiver, claimedAmount);
Although the depositor loses out, it is still a griefing attack that manipulates the conversion rate.
At convertAllETH()
, it will be better if the totalSupply
is equal to the totalLpETH
, then sweep the remaining ETH in the contract to the owner's address. This makes it such that the conversion rate will always be 1:1.
setOwner()
has no zero address check. If the owner is set incorrectly, all onlyAuthorized
functions will fail. Set a failsafe for setOwner()
function setOwner(address _owner) external onlyAuthorized { owner = _owner; emit OwnerUpdated(_owner); }
If the owner sets the token to true by mistake, he should be able to set it back to false, but the current iteration of the function does not allow the owner to do so.
function allowToken(address _token) external onlyAuthorized { isTokenAllowed[_token] = true; }
Add a bool value in the parameter so that the owner can change the boolean value.
#0 - CloudEllie
2024-05-11T17:28:04Z
#1 - 0xd4n1el
2024-05-13T16:42:06Z
Good report, but L-3 and L-7 should not be considered
#2 - c4-judge
2024-06-03T10:32:34Z
koolexcrypto marked the issue as grade-a
#3 - DecentralDisco
2024-06-11T15:26:37Z
For awarding purposes, staff have marked as 2nd place. Also noting there was a tie for 2nd/3rd place.