Skip to content

Instantly share code, notes, and snippets.

@OpenCoreCH
Created February 22, 2024 09:36
Show Gist options
  • Save OpenCoreCH/a19e6e8147eb4b76407a97ae8883232e to your computer and use it in GitHub Desktop.
Save OpenCoreCH/a19e6e8147eb4b76407a97ae8883232e to your computer and use it in GitHub Desktop.

High - EthStrategy: Cannot receive ETH

The EthStrategy contract does not have a receive function and therefore cannot receive ETH when unstaking from Lido.

Recommendation

Add a receive function.

High - StakingManager: No way to recover ETH

There are two ways how ETH can end up in the StakingManager contract:

  • Removing a strategy, which calls strategy.withdraw, but does not send the received ETH anywhere.
  • Specifically for EthStrategy: Calling ownerWithdraw, which sends ETH directly to the staking manager.

However, the staking manager contract does not have a method to recover this ETH and it will be lost.

Recommendation

It should be defined what happens with these funds. Depending on how centralized the flow should be, one possibility would be to let the owner withdraw it.

Medium - ewETH may become undercollaterized because of slashing events in strategies

EdgelessDeposit.mintEthBasedOnStakedAmount allows to mint up to the current asset total of the strategies. However, this value may decrease in the future, making ewETH undercollaterized. More specifically for the LIDO strategy (which is the only at the moment), it can happen because of slashing events, but future strategies may have some different conditions.

Recommendation

Completely avoiding this is not really possible in practice, but the impact could be alleviated. For instance, an insurance pool could be introduced or the losses could be distributed among all other users (unlike in the current design, where the last users will bear all the losses, incentivizing withdrawing everything as soon as this happens).

Medium - EdgelessDeposit: Redemptions may use funds of other users

The function withdrawEth calls StakingManager._withdrawEth, which calls strategy.withdraw and sends to the deposit contract only the funds that it received. This may be lower than the funds that were requested (for the ETH strategy, this happens because the tokens are staked). In such a case, the withdrawal will typically still succeed, but use the ETH that other users deposited. Because the deposit contract is then technically undercollaterized, this may cause a bank run and the last redemptions will then fail (until an admin manually unstakes).

Recommendation

With the current design, this is hard to avoid completely. To not have situations where the deposit contract is temporarily undercollaterized, one possibility would be to revert when the withdrawal does not result in enough funds (although it is not clear if that is better because no users could withdraw in that case).

A different design that some platforms (e.g. eBTC) use is to not integrate with LIDO directly, but to perform ETH / stETH swaps (e.g., on Uniswap). This allows immediate conversion of stETH to ETH and there is no need for owner interventions.

Medium - EthStrategy.claimLidoWithdrawals time complexity

The function LIDO_WITHDRAWAL_ERC721.findCheckpointHints performs a binary search for every request ID and therefore has a time complexity of O(numRequests * log(numCheckpoints)). While numRequests is admin-controlled (and splitting IDs would be possible), numCheckpoints grows continuously. At some point, this may therefore run out of gas, making withdrawals impossible.

Recommendation

There are two approaches to solve this:

  • Passing in the hints (this would be more efficient anyways because it can then be calculated off-chain without any gas constraints, so the owner saves gas when claiming)
  • Not hard-coding the start (currently always 1) for findCheckpoints, but making it configurable.

Low - WrappedToken: Unnecessary events for mint / burn

The mint and burn functions both emit a dedicated event. Because they internally call _mint / _burn, which already does emit a special Transfer event in these scenarios, these events are unnecessary and could be removed to save gas.

Recommendation

Consider removing the events.

Low - Unused parameters

There are a few parameters that are not used and can be removed:

  • EdgelessDeposit.initialize: The _staker argument is not used (except for checking that it is non-zero). I recommend removing it.
  • StakingManager.stake: The function has an argument asset, but only supports staking ETH (at the moment). The argument is ignored except for the event. While this interface may have been chosen to be able to add new assets in the future, the current implementation is pretty confusing because it would technically support passing in an arbitrary asset that is then also emitted in the event. In practice, this can never happen with the current deposit contract (that is only allowed to call this function) because it only passes in the ETH address, but if the idea is to support more assets in the future, you might want to consider already adding validation for this (i.e. reverting for non-supported assets). Moreover, the interface is not consistent because withdraw does not have an asset parameter, so if the reason for this argument was to have a future-proof interface, this is not the case because withdraw needs to be changed anyways.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment