Issue:
The withdraw function allows users to withdraw their tokens, but only under certain conditions. If these conditions are not met, users may not be able to withdraw their funds, potentially locking them forever.
PrelaunchPoints.sol#L284-L306
function withdraw(address _token) external {
if (!emergencyMode) {
if (block.timestamp >= startClaimDate) {
revert NoLongerPossible();
}
}
uint256 lockedAmount = balances[msg.sender][_token];
balances[msg.sender][_token] = 0;
if (lockedAmount == 0) {
revert CannotWithdrawZero();
}
if (_token == address(WETH)) {
if (block.timestamp >= startClaimDate){
revert UseClaimInstead();
}
totalSupply -= lockedAmount;
}
IERC20(_token).safeTransfer(msg.sender, lockedAmount);
emit Withdrawn(msg.sender, _token, lockedAmount);
}
Users can only withdraw their tokens if the contract is in emergency mode or if the current time is before the startClaimDate.
If the contract is not in emergency mode and the startClaimDate has passed, users will not be able to withdraw their tokens, locking their funds forever.
/**
* @dev Called by a staker to withdraw all their ETH or LRT
* Note Can only be called before claiming lpETH has started.
* In emergency mode can be called at any time.
* @param _token Address of the token to withdraw
*/
function withdraw(address _token) external {
if (!emergencyMode) {
if (block.timestamp >= startClaimDate) {
revert NoLongerPossible();
}
}
uint256 lockedAmount = balances[msg.sender][_token];
balances[msg.sender][_token] = 0;
if (lockedAmount == 0) {
revert CannotWithdrawZero();
}
if (_token == address(WETH)) {
if (block.timestamp >= startClaimDate){
revert UseClaimInstead();
}
totalSupply -= lockedAmount;
}
IERC20(_token).safeTransfer(msg.sender, lockedAmount);
emit Withdrawn(msg.sender, _token, lockedAmount);
}
Referenced Code and Context1. Emergency Mode and Start Claim Date:
The withdrawal function checks if the contract is in emergencyMode or if the current time is before startClaimDate.
If neither condition is met, the function reverts, preventing the withdrawal.
/**
* @param _mode boolean to activate/deactivate the emergency mode
* @dev On emergency mode all withdrawals are accepted at
*/
function setEmergencyMode(bool _mode) external onlyAuthorized {
emergencyMode = _mode;
}
/**
* @dev Called by a owner to convert all the locked ETH to get lpETH
*/
function convertAllETH() external onlyAuthorized onlyBeforeDate(startClaimDate) {
if (block.timestamp - loopActivation <= TIMELOCK) {
revert LoopNotActivated();
}
// deposits all the WETH to lpETH contract. Receives lpETH back
WETH.approve(address(lpETH), totalSupply);
lpETH.deposit(totalSupply, address(this));
// If there is extra lpETH (sent by external actor) then it is distributed among all users
totalLpETH = lpETH.balanceOf(address(this));
// Claims of lpETH can start immediately after conversion.
startClaimDate = uint32(block.timestamp);
emit Converted(totalSupply, totalLpETH);
}
1. Logic Behind the Vulnerability:
The withdrawal function is designed to allow users to withdraw their tokens only before the startClaimDate or during emergency mode.
If the startClaimDate has passed and the contract is not in emergency mode, the function reverts with NoLongerPossible(), preventing any withdrawals.
This logic ensures that users cannot withdraw their tokens once the claiming process for lpETH has started, except in emergency mode.
2. Validity of the Vulnerability:
The vulnerability is valid because it creates a scenario where users cannot withdraw their tokens if the startClaimDate has passed and the contract is not in emergency mode.
This could lead to user funds being locked forever if the owner does not activate emergency mode or if there is a failure in the claiming process.
3. Severity of the Vulnerability:
Impact: High. Users may lose access to their funds permanently if the conditions for withdrawal are not met.
Likelihood: Medium. If the owner fails to manage the contract properly or does not activate emergency mode when needed, the risk increases.
Overall Severity: High. The potential for permanent loss of user funds makes this a high-severity issue.
Proof of Concept
1. Initialization:
When the contract is deployed, startClaimDate is set to a very high value and loopActivation is set to 120 days from the deployment time.
2. Setting Loop Addresses:
The owner can call convertAllETH to convert all locked ETH to lpETH and set the startClaimDate to the current timestamp.
3. Withdrawal Conditions:
Users can only withdraw their tokens if the contract is in emergencyMode or if the current time is before startClaimDate.
4. Scenario:
Assume a user locks their tokens into the contract.
The owner calls convertAllETH, setting startClaimDate to the current timestamp.
After startClaimDate, users can no longer withdraw their tokens unless emergencyMode is activated.
5. Issue:
If the owner does not activate emergencyMode and the startClaimDate has passed, users will be unable to withdraw their tokens, leading to their funds being locked forever.
1. User Locks Tokens:
// User locks 100 WETH
lock(address(WETH), 100 ether, referralCode);
2. Owner Converts ETH to lpETH:
// Owner converts all ETH to lpETH and sets startClaimDate
convertAllETH();
3. User Attempts to Withdraw AfterstartClaimDate:
// User tries to withdraw after startClaimDate
withdraw(address(WETH)); // This will revert with "NoLongerPossible"
4. Emergency Mode Not Activated:
// Owner does not activate emergency mode
setEmergencyMode(false);
5. User Funds Locked:
Since startClaimDate has passed and emergencyMode is not activated, the user's funds are locked in the contract.
Recommended Mitigation Steps
Consider implementing more flexible withdrawal conditions to ensure users can always withdraw their funds under certain circumstances.
Lines of code
https://github.com/LoopFi/loop-prelaunch-contracts/blob/c8b13474aa4f319eec368fc4827bf51eddad080f/src/PrelaunchPoints.sol#L284-L306 https://github.com/LoopFi/loop-prelaunch-contracts/blob/c8b13474aa4f319eec368fc4827bf51eddad080f/src/PrelaunchPoints.sol#L284-L306 https://github.com/LoopFi/loop-prelaunch-contracts/blob/c8b13474aa4f319eec368fc4827bf51eddad080f/src/PrelaunchPoints.sol#L380-L386 https://github.com/LoopFi/loop-prelaunch-contracts/blob/c8b13474aa4f319eec368fc4827bf51eddad080f/src/PrelaunchPoints.sol#L102-L119 https://github.com/LoopFi/loop-prelaunch-contracts/blob/c8b13474aa4f319eec368fc4827bf51eddad080f/src/PrelaunchPoints.sol#L311-L330
Vulnerability details
Bug: User Funds Getting Locked Forever
Issue: The withdraw function allows users to withdraw their tokens, but only under certain conditions. If these conditions are not met, users may not be able to withdraw their funds, potentially locking them forever. PrelaunchPoints.sol#L284-L306
startClaimDate
.startClaimDate
has passed, users will not be able to withdraw their tokens, locking their funds forever.Impact
PrelaunchPoints.sol#L284-L306
Referenced Code and Context 1. Emergency Mode and Start Claim Date:
2. Emergency Mode Activation: PrelaunchPoints.sol#L380-L386
3. Start Claim Date Initialization: PrelaunchPoints.sol#L102-L119
4. Start Claim Date Update: PrelaunchPoints.sol#L311-L330
1. Logic Behind the Vulnerability:
startClaimDate
or during emergency mode.startClaimDate
has passed and the contract is not in emergency mode, the function reverts withNoLongerPossible()
, preventing any withdrawals.lpETH
has started, except in emergency mode.2. Validity of the Vulnerability:
startClaimDate
has passed and the contract is not in emergency mode.3. Severity of the Vulnerability:
Proof of Concept
1. Initialization:
When the contract is deployed,
startClaimDate
is set to a very high value andloopActivation
is set to 120 days from the deployment time.2. Setting Loop Addresses:
The owner can call
convertAllETH
to convert all locked ETH to lpETH and set thestartClaimDate
to the current timestamp.3. Withdrawal Conditions:
Users can only withdraw their tokens if the contract is in
emergencyMode
or if the current time is beforestartClaimDate
.4. Scenario:
Assume a user locks their tokens into the contract. The owner calls
convertAllETH
, settingstartClaimDate
to the current timestamp. AfterstartClaimDate
, users can no longer withdraw their tokens unlessemergencyMode
is activated.5. Issue:
If the owner does not activate
emergencyMode
and thestartClaimDate
has passed, users will be unable to withdraw their tokens, leading to their funds being locked forever.1. User Locks Tokens:
2. Owner Converts ETH to
lpETH
:3. User Attempts to Withdraw After
startClaimDate
:4. Emergency Mode Not Activated:
5. User Funds Locked:
startClaimDate
has passed andemergencyMode
is not activated, the user's funds are locked in the contract.Recommended Mitigation Steps
Consider implementing more flexible withdrawal conditions to ensure users can always withdraw their funds under certain circumstances.