code-423n4 / loopfi-bug-bounty

5 stars 6 forks source link

User Funds Getting Locked Forever #14

Closed c4-bot-5 closed 4 months ago

c4-bot-5 commented 4 months ago

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

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);
}
  1. Users can only withdraw their tokens if the contract is in emergency mode or if the current time is before the startClaimDate.
  2. 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.

Impact

PrelaunchPoints.sol#L284-L306

/**
 * @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 Context 1. Emergency Mode and Start Claim Date:

2. Emergency Mode Activation: PrelaunchPoints.sol#L380-L386

/**
 * @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;
}

3. Start Claim Date Initialization: PrelaunchPoints.sol#L102-L119

constructor(address _exchangeProxy, address _wethAddress, address[] memory _allowedTokens) {
    owner = msg.sender;
    exchangeProxy = _exchangeProxy;
    WETH = IWETH(_wethAddress);

    loopActivation = uint32(block.timestamp + 120 days);
    startClaimDate = 4294967295; // Max uint32 ~ year 2107

    // Allow initial list of tokens
    uint256 length = _allowedTokens.length;
    for (uint256 i = 0; i < length;) {
        isTokenAllowed[_allowedTokens[i]] = true;
        unchecked {
            i++;
        }
    }
    isTokenAllowed[_wethAddress] = true;
}

4. Start Claim Date Update: PrelaunchPoints.sol#L311-L330

/**
 * @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:

2. Validity of the Vulnerability:

3. Severity of the Vulnerability:

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:

Recommended Mitigation Steps

Consider implementing more flexible withdrawal conditions to ensure users can always withdraw their funds under certain circumstances.

c4-bot-9 commented 4 months ago

Discord id(s) for hunter(s): [object Object]