ID | Description | Severity |
---|---|---|
H-01 | Ethers sent to the contract can be stolen from any user | High |
User can by passing 0 as a _percentage
, receive lpETH
based on the ethers in the contract.
If the PrelaunchPoints.sol
contract has native ether, any user can get lpETH
for them without sending anything.
The user only needs to have a balance in the contract of the token that passes when he perform the attack to pass this check - it can be 1 wei.
uint256 userStake = balances[msg.sender][_token];
if (userStake == 0) {
revert NothingToClaim();
}
User will call claim()
with:
_token
- anyToken_percentage
- 0Exchange
- TransformERC20_data
:- TRANSFORM_SELECTOR = 0x415565b0
- inputToken = the token provided in _token
- outputToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
- inputTokenAmount = 0
function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
Then in _claim()
it will enter the else statement, userClaim
will become 0 because its - userStake * 0 / 100 = 0
.
Then _validateData()
will pass correctly, his balance will not be changed. When _fillQuote()
is called, nothing will be exchanged because it will pass userClaim
which is 0, so no ethers will enter the contract, and boughtETHAmount
will remain the same. The claimedAmount
will then be the contract balance and will be deposited into lpETH
, by passing him as receiver, causing him to receive lpETH
based on the Ether balance in the contract without having to sent anything**.**
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);
}
function _validateData(address _token, uint256 _amount, Exchange _exchange, bytes calldata _data) internal view {
address inputToken;
address outputToken;
uint256 inputTokenAmount;
address recipient;
bytes4 selector;
if (_exchange == Exchange.UniswapV3) {
(inputToken, outputToken, inputTokenAmount, recipient, selector) = _decodeUniswapV3Data(_data);
if (selector != UNI_SELECTOR) {
revert WrongSelector(selector);
}
// UniswapV3Feature.sellTokenForEthToUniswapV3(encodedPath, sellAmount, minBuyAmount, recipient) requires `encodedPath` to be a Uniswap-encoded path, where the last token is WETH, and sends the NATIVE token to `recipient`
if (outputToken != address(WETH)) {
revert WrongDataTokens(inputToken, outputToken);
}
} else if (_exchange == Exchange.TransformERC20) {
(inputToken, outputToken, inputTokenAmount, selector) = _decodeTransformERC20Data(_data);
if (selector != TRANSFORM_SELECTOR) {
revert WrongSelector(selector);
}
if (outputToken != ETH) {
revert WrongDataTokens(inputToken, outputToken);
}
} else {
revert WrongExchange();
}
if (inputToken != _token) {
revert WrongDataTokens(inputToken, outputToken);
}
if (inputTokenAmount != _amount) {
revert WrongDataAmount(inputTokenAmount);
}
if (recipient != address(this) && recipient != address(0)) {
revert WrongRecipient(recipient);
}
}
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
I think the purpose of boughtETHAmount
was to deposit in lpETH
only if there is some swap that increases the native balance, so _claim()
should be changed to deposit in lpETH
only boughtETHAmount
which is calculated in _fillQuote()
this can be done by returning boughtETHAmount
from _fillQuote()
and then passing it to lpETH.deposit{value: boughtETHAmount}(_receiver)
.