-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Protocol supports stETH
but doesn't consider its unique transfer logic which would lead to not only a DOS of the depositing/withdrawal channel for this collateral token but also a flaw in multiple other core protocol logic
#10
Comments
stETH
token but doesn't consider its transfer logic which would lead to not only a DOS of the depositing/withdrawal channel for this collateral token but a flaw in other protocol's logicstETH
but doesn't consider its unique transfer logic which would lead to not only a DOS of the depositing/withdrawal channel for this collateral token but also a flaw in multiple other core protocol logic
CloudEllie marked the issue as duplicate of #389 |
alcueca marked the issue as selected for report |
@jatinj615, please review |
@alcueca , I think this report is highlighting somewhat similar issue as #389 around "1-2 corner case". Also I think the safeApprove will fail around these "1-2 corner case" as well before which I think transferFrom will fail. As specified in the #389 -
Also the wstETH integration does not seems to be the mitigation as EigenLayer Strategy only accepts stETH not wstETH. Therefore, can be acknowledged for the time being. lmk if I am missing something here. |
That's right, I did mark #389 as a duplicate of this one previously. I think that your assessment is right. I do not know what is the right mitigation, but I would suggest that you test this issue thoroughly and talk with the folks at EigenLayer to get their view on it. |
alcueca marked the issue as satisfactory |
Yeah what I am wondering about is that we have had stETH deposits enabled and have around 20k stETH deposited in the protocol and never faced this. But will cross check as well. |
High severity warranted on permanent disabling of core functionality. However, I'm quite suspicious on why this hasn't manifested in the live instance. |
Hi @alcueca, thanks for judging! According to the function transferFrom(address _sender, address _recipient, uint256 _amount) external returns (bool) {
_spendAllowance(_sender, msg.sender, _amount);
_transfer(_sender, _recipient, _amount);
return true;
} function _spendAllowance(address _owner, address _spender, uint256 _amount) internal {
uint256 currentAllowance = allowances[_owner][_spender];
if (currentAllowance != INFINITE_ALLOWANCE) {
require(currentAllowance >= _amount, "ALLOWANCE_EXCEEDED");
_approve(_owner, _spender, currentAllowance - _amount);
}
} Then, the function _transfer(address _sender, address _recipient, uint256 _amount) internal {
uint256 _sharesToTransfer = getSharesByPooledEth(_amount);
_transferShares(_sender, _recipient, _sharesToTransfer);
_emitTransferEvents(_sender, _recipient, _amount, _sharesToTransfer);
} function getSharesByPooledEth(uint256 _ethAmount) public view returns (uint256) {
return _ethAmount
.mul(_getTotalShares())
.div(_getTotalPooledEther());
} function _transferShares(address _sender, address _recipient, uint256 _sharesAmount) internal {
require(_sender != address(0), "TRANSFER_FROM_ZERO_ADDR");
require(_recipient != address(0), "TRANSFER_TO_ZERO_ADDR");
require(_recipient != address(this), "TRANSFER_TO_STETH_CONTRACT");
_whenNotStopped();
uint256 currentSenderShares = shares[_sender];
require(_sharesAmount <= currentSenderShares, "BALANCE_EXCEEDED");
shares[_sender] = currentSenderShares.sub(_sharesAmount);
shares[_recipient] = shares[_recipient].add(_sharesAmount);
} Regarding the issue described by this report, after a |
Firstly, I think this report is a bit incorrect and unnecessarily describes the This is clearly stETH's 1-2 wei corner case issue only. And thats exactly whats enough for this issue to be accepted as the Valid High Severity Issue. I'm not going to describe the vulnerabilities here, as I have explained them in detail in my two reports, especially the first one:
Secondly, we have to understand that the 1-2 wei issue can occur often but not always. The current live instances of the protocol only transfer the tokens twice, but these new contracts will transfer 3-4 times (technically 5). So, the likelihood of this issue happening increases massively. As mentioned by the Lido docs, "In the future, when the stETH/share rate is greater, the error can become a bit bigger." When this happens, the whole protocol's stETH functionality will be broken, resulting in users' funds getting frozen in the EigenLayer, as they will be unable to withdraw.
I am uncertain if the I believe this issue is rightly judged as a high-severity issue, and the only good mitigation for this is to transfer the actual token balance instead of the expected amounts. Maybe one of the other reports deserves the "selected for report" tag more. As it seems the sponsor has acknowledged this issue but is not planning to fix it, I would like to request the sponsor @jatinj615 to review this report and revise the decision, as users' 20k stETH are at risk. Thanks! |
@alcueca @jatinj615 there is a good reason this hasn't manifested in the live contracts. This finding is invalid, and there is a misunderstanding of the referenced stETH 1-2 wei corner case in these submissions and the comments above. In stETH, both What this means is it isn't completely accurate to say that 1-2 wei less than passed as parameter are transferred. Rather, the rounded down amount of shares that best approximate the transfer amount is transferred, and these shares may amount to 1-2 wei less than the transfer amount when converted back into balance. With that in mind, it is easy to see what will happen when multiple subsequent transfers with the same amount are executed in the same transaction. The amount will always be converted to the same number of shares, which means all recipients may obtain 1-2 wei less than the original amount, but not than the amount transferred in the preceding transfer. The transfers will all succeed, since the amount of shares that is transferred stays the same and is transferred from one contract to the next. It is, in fact, not a problem for an address to call |
Hi @alcueca, I'd like you to consider the fact that this report includes two parts before making the final decision, information shared by @0xEVom & @rbserver is new to me. However this was even stated in the report:
Which is because the |
Based on the evidence shown, there isn't sufficient proof that an issue exists. The report will be invalidated on this basis, but I recommend the sponsor to stay vigilant in case poof materializes in the future. |
alcueca marked the issue as unsatisfactory: |
alcueca marked the issue as unsatisfactory: |
Lines of code
ttps://github.com/code-423n4/2024-04-renzo/blob/1c7cc4e632564349b204b4b5e5f494c9b0bc631d/contracts/RestakeManager.sol#L491-L576
https://github.com/code-423n4/2024-04-renzo/blob/1c7cc4e632564349b204b4b5e5f494c9b0bc631d/contracts/RestakeManager.sol#L664-L665
https://github.com/code-423n4/2024-04-renzo/blob/1c7cc4e632564349b204b4b5e5f494c9b0bc631d/contracts/Deposits/DepositQueue.sol#L134-L145
https://github.com/code-423n4/2024-04-renzo/blob/1c7cc4e632564349b204b4b5e5f494c9b0bc631d/contracts/Bridge/xERC20/contracts/XERC20Lockbox.sol#L125-L152
Vulnerability details
Proof of Concept
First, would be key to note that
stETH
is a special token when it comes to it's transfer logic, navigating to lido's official docs we can see that there is a special section that talks about it's unique concept, i.e the "1-2 wei corner case", see https://docs.lido.fi/guides/lido-tokens-integration-guide/#1-2-wei-corner-case, quoting them:That's to say at any transfer tx there is a possibility that the amount that actually gets sent is up to
2 wei
different, a minute value you might think, however when we couple this with the fact that protocol heavily usessafeApprove()
to pass on approvals for collateral tokens before depositing or withdrawing, this corner case could then brick the protocol.Now see OpenZeppelin's implementation of
safeApprove()
and how it will revert if the current allowance is non-zero and the approval attempt is also passing a non-zero value.Consider a minimalistic generic scenario:
Allowance is set by user A for user B to
1e18
"wei" stETH tokens.User B attempts to transfer these tokens, however due to the corner case, stETH balance gets converted to shares, integer division happens and rounding down applies, the amount of tokens that are actually transferred would be
1e18 - 2
"wei" tokens.Now user A assumes that user A has expended their allowance and attempts granting them a new allowance of a fresh
1e18
"wei" stETH tokens, doing this with the normalERC20.approve()
is going to go through, however user A attempts to do this withSafeERC20.safeApprove()
which would fail causeSafeERC20.safeApprove()
reverts on non-zero to non-zero approvals and user B is currently being approved of2
wei tokens which they've not spent yet.The scenario above is quite generic but this exact idea can be applied to current protocol's logic of passing on allowances around contracts in scope, for example see
RestakeManager.deposit()
at https://github.com/code-423n4/2024-04-renzo/blob/1c7cc4e632564349b204b4b5e5f494c9b0bc631d/contracts/RestakeManager.sol#L491-L576As hinted by the two @Audit tags, protocol uses
safeApprove()
to grant approval in this case to both thedepositQueue
and the operator delegator, note that the implementations of both operatorDelegator.deposit() and depositQueue.fillERC20withdrawBuffer() include transfers of the allowances they've already been given from the execution ofRestakeManager.deposit()
considering the transfers is going to get rounded down, then minute part of the allowance is going to be left untransferred and in consequent calls tosafeApprove()
when depositingstETH
the call tosafeApprove
is going revert (as shown in the attached openZeppelin'ssafeApprove()
snippet above) and throw an error since an attempt is being made to approve from a non-zero to a non-zero value, effectively bricking/DOS'ing the depositing logic for the supportedstETH
collateral token.Now, parallel to the already explained issue with
safeApprovals
, this1-2
wei corner case is going to also cause protocol to make a wrong assumption on the amount of tokens that were really transferred, would be key to note that the amount ofezETH
that get minted to themsg.sender
is directly proportional to the collateral value of theamount
of tokens that were considered to be "transferred" in to theRestakeManager
during the deposit attempt.Evidently, since during deposits, protocol assumes the amount of tokens that was specified in the
safeTransferFrom()
is actually the amount of tokens that end up getting transferred in, the amount ofezETH
that gets minted for users is going to be inflated as more than what was transferred in is going to be considered as theamount
transferred in when calculating the collateral token being deposited https://github.com/code-423n4/2024-04-renzo/blob/1c7cc4e632564349b204b4b5e5f494c9b0bc631d/contracts/RestakeManager.sol#L506-L508The logic from the last two paragraphs hint that the
stETH
token does somewhat behave like the popular Fee-On-Transfertokens , albeit in this case the discrepancy in the amount of tokens being recieved is due to rounding down and not fees, also this could lead to the depositing/withdrawing logic of theXERC20Lockbox
to also work with flawed data.Impact
This bug cases leads to multiple issues and the root cause is the fact that protocol does not take the 1-2 wei corner case of
stETH
into mind, a few noteworthy impacts would be:When the allowance of the supported
stETH
token is non-zero in instances where protocol thinks it's already zero, all attempts tosafeApprove()
on this collateral token (stETH) is going to fail DOSing the depositing attempts, making protocol's core functionality unavailable to some users.Additionally, the accounting of the backed collateral for minted ezETH could now be flawed since it's going to assume the wrong amount of collaterals are backing already minted assets which covers the main window under the requested bug windows/attack ideas since the integrity on the TVL calculations (ezETH Pricing) is now going to be slightly flawed, i.e users are now going to mint & withdraw at slightly invalid prices considering the
stETH
is a core integrated token and with multiple transfers this minute differences could amount to quite a reasonable sum.So, this means that depositing into the strategy manager for the
stETH
collateral token would also be broken.This bug case also makes it impossible to complete queued withdrawals for
stETH
fromOperatorDelegator.sol
, since the channel is going to be DOS'd when the residual amount of approval already made to deposit queue causes this attempt at a new approval to fail, showcasing how the withdrawal channel is also going to be DOS'd.Another subtle one, would be the Inability to fill up the withdrawal queue buffer when needed for the
stETH
token, (albeit in this case as hinted by protocol the admins can manually unstake to ensure the buffer is at where it needs to be).Finally, there seems to be a subtle edge case, where, if every user is to attempt withdrawing their deposited assets back, the last set of users might not receive their assets it due to protocol not having enough assets backed for ezETH already minted to send to back to users.
Recommended Mitigation Steps
First consider scraping the idea of
safeApprove()
forstETH
, since the all assets being supported right now (ezETH, stETH, wBETH) are standard tokens then the normalERC20.approve()
can be used which wouldn't revert on non-zero to non-zero approvals.For the parallel case in regards to the
amount
specified in the transferral attempt not being the amount that gets transferred in, then the differences in balance could be used to see the real amount that was transferred in and then use this value to calculate the amount ofezETH
to be minted.Alternatively, protocol can just consider integrating
wstETH
instead ofstETH
as suggested by the official Lido docs for more ease in regards to DEFI integration, see how this can be done here.Assessed type
Token-Transfer
The text was updated successfully, but these errors were encountered: