← All Posts | findings | April 25, 2025

Lido – Uncaught exception on creating ejection report

Paweł Kuryłowicz

Paweł Kuryłowicz

Managing Partner & Smart Contract Security Auditor

The report generation cannot be completed and submitted due to this bug.

Vulnerability Details

The get_remaining_forced_validators function is designed to remove validators that are required to exit, even if the withdrawal queue is fully claimable.

def get_remaining_forced_validators(self) -> list[tuple[NodeOperatorGlobalIndex, LidoValidator]]:
        """
        Returns a list of validators from NOs that are requested for forced exit.
        This includes an additional scenario where enough validators have been ejected to fulfill the withdrawal requests,
        but forced ejections are still necessary.
        """
        result: list[tuple[NodeOperatorGlobalIndex, LidoValidator]] = []

        # Extra validators limited by VEBO report
        while self.index != self.max_validators_to_exit:
            for no_stats in sorted(self.node_operators_stats.values(), key=self.no_remaining_forced_predicate):
                if self._no_force_predicate(no_stats) == 0:
                    # The current and all subsequent NOs in the list has no forced validators to exit. Cycle done
                    return result

                if no_stats.predictable_validators:
                    # When found Node Operator
                    self.index += 1
                    gid = (
                        no_stats.node_operator.staking_module.id,
                        no_stats.node_operator.id,
                    )
                    result.append((gid, self._eject_validator(gid)))
                    break

        return result

This function iterates through all node operators, sorting them in descending order based on the number of validators awaiting forced exit. This count is determined by subtracting the force_exit_to limit from predictable_validators.

def _no_force_predicate(node_operator: NodeOperatorStats) -> int:
    return ValidatorExitIterator._get_expected_validators_diff(
        node_operator.predictable_validators,
        node_operator.force_exit_to,
    )

If there is no difference in the count, the loop terminates with a return statement. However, if a difference is detected, the function proceeds to remove the first validator from the exitable_validators list using the _eject_validator function.

The issue arises when a Node Operator has transient validators, which are counted in the predictable_validators but not included in exitable_validators. Consequently, if the function attempts to remove a validator when exitable_validators is empty, an IndexOutOfBounds exception occurs, halting the report generation.

Vulnerable scenario

The following steps lead to the described issue:

  1. A Node Operator has 1 exitable validator and 2 transient validators with no force_exit_to limit set.
  2. The force_exit_to limit is adjusted to 1, necessitating the exit of 2 operators while leaving one.
  3. The Ejector calculates the number of predictable validators as 3.
  4. The difference between the predictable validators and the limit results in 3 - 1 = 2.
  5. The first (and only) validator from the exitable operators is ejected.
  6. In the next iteration of the while loop, the difference becomes 2 - 1 = 1.
  7. The Ejector needs to eject another validator, but when it attempts to remove the first element from the now-empty exitable_validators list, an uncaught exception is raised, interrupting the report processing.

Impact

MEDIUM – The report generation cannot be completed and submitted due to this issue.

Recommendation

  • The while loop should include a check to ensure that exitable validators exist for the Node Operator that has not reached the force_exit_to limit.
  • Additionally, provisions should be made to prevent the possibility of an infinite loop when a Node Operator has not reached the force_exit_to limit and lacks exitable operators. Detecting this scenario is essential to avoid a Denial of Service situation.

References

Join the newsletter now

Please wait...

Thank you for sign up!