Assessment reports>Lido Fixed Income>Critical findings>Unsafe handling of over-100% early exit fees
Category: Coding Mistakes

Unsafe handling of over-100% early exit fees

Critical Severity
Critical Impact
Medium Likelihood

Description

During a vault's earning period, a fixed depositor can call withdraw() to withdraw their stake and back out of the vault. If they do so, after the withdraw is finalized and they call finalizeVaultOngoingFixedWithdrawals() to claim their ETH, they are charged an early exit fee that depends on the timestamp they exited.

This early exit fee is calculated in the function claimFixedVaultOngoingWithdrawal, which returns the ETH amount ultimately transferred to the user:

function claimFixedVaultOngoingWithdrawal(address user) internal returns (uint256) {
  if (user == address(0)) return 0;

  WithdrawalRequest memory request = fixedToVaultOngoingWithdrawalRequestIds[user];
  uint256[] memory requestIds = request.requestIds;
  require(requestIds.length != 0, "WNR");

  uint256 upfrontPremium = userToFixedUpfrontPremium[user];

  delete userToFixedUpfrontPremium[user];
  delete fixedToVaultOngoingWithdrawalRequestIds[user];

  uint256 arrayLength = fixedOngoingWithdrawalUsers.length;
  for (uint i = 0; i < arrayLength; i++) {
    if (fixedOngoingWithdrawalUsers[i] == user) {
      delete fixedOngoingWithdrawalUsers[i];
    }
  }

  uint256 amountWithdrawn = claimWithdrawals(msg.sender, requestIds);

  uint256 earlyExitFees = calculateFixedEarlyExitFees(upfrontPremium, request.timestamp);

  // add earlyExitFees to earnings for variable side
  feeEarnings += earlyExitFees

  emit LidoWithdrawalFinalized(user, requestIds, FIXED, isStarted(), isEnded());

  return amountWithdrawn - Math.min(earlyExitFees, amountWithdrawn);
}

Note that according to the documentation, the early exit fee starts at 110% of their deposited stake.

The feeEarnings variable, when increased, directly represents fees able to be withdrawn by the variable-side depositors. Because of the Math.min(earlyExitFees, amountWithdrawn), this fee is given to the variable-side depositors even when the fee cannot be removed, due to lack of funds, from the amount withdrawn by the fixed-side caller.

Impact

If the Math.min call above happens while earlyExitFees > amountWithdrawn, the difference can be claimed by the variable side even though no portion of the contract's actual balance includes it. In normal operation, this means that the last few variable-side users to claim the fee will not be able to claim it until more ETH enters the contract.

This issue creates a significant perverse economic exploit. If the first user to deposit into the vault is a fixed depositor who does not fill up most of the fixed space, then an attacker can fill up the entire remaining fixed and variable space and then immediately withdraw all of their fixed deposits. Since the attacker owns the entire variable space, they are entitled to all the fees that their fixed holdings were charged, and since zero time has passed, there will be no protocol fees for their fixed withdrawal. So, if the attacker claims their fixed ongoing withdrawal immediately after the vault ends, the extra 10% of the attacker's large fixed position is paid for by the victim's smaller fixed position, and the victim cannot withdraw due to the contract running out of native ETH.

Recommendations

If the early exit fees exceed the amount withdrawn, then the entire amount withdrawn should be added to feeEarnings instead of an amount that exceeds it.

Alternatively, if the suggestion in Discussion ref is implemented, then this would not be an issue since the early exit fees would always be less than the amount withdrawn, both denominated in stETH.

Remediation

This issue has been acknowledged by Saffron, and a fix was implemented in commit a9cc64b4.

Zellic © 2024Back to top ↑