Summary
When the protocol will be well established, there might be user wanting to withdraw substantial amount of ETH, but the call will revert because of an overflow that shouldn't happen.
Vulnerability Details
In withdraw()
or unstakeEth()
, the amount that the user want to get is converted from the LSD to ETH by calling the function _ethConversion(...)
:
function _ethConversion(uint256 vault, uint88 amount) private view returns (uint88) {
uint256 zethTotalNew = vault.getZethTotal();
uint88 zethTotal = s.vault[vault].zethTotal;
if (zethTotalNew >= zethTotal) {
return amount;
} else {
return amount.mulU88(zethTotalNew).divU88(zethTotal);
}
}
But as we can see in the else case, amount
is multiplied by zethTotalNew
using the PRBMathHelper::mulU88
function.
This function reverts when the result of the calculation is greater than U88.
But there is realistic situations (amount: 333 ETH, zethTotalNew: 1M ETH, see total LSD staked here: 11.3M ETH staked) where this can happen.
The thing is that what matters in this calculation, is that the final result fits in a U88.
That means, only the division should check the overflow, but its not the case.
Impact
User request to withdraw or unstake will revert.
Proof of Concept
Add this test to Yield.t.sol
:
function test_audit_withdrawInvalidAmount() public {
uint88 revertWithdrawal = 333 ether;
uint88 zethTotal = 1_000_000 ether;
uint88 zethTotalNew = 1_000_001 ether;
deal(_reth, sender, revertWithdrawal+100);
deal(_reth, extra, zethTotal);
vm.prank(extra);
diamond.deposit(_bridgeReth, zethTotal);
vm.prank(sender);
diamond.deposit(_bridgeReth, revertWithdrawal+100);
deal(_reth, _bridgeReth, zethTotalNew);
vm.prank(sender);
diamond.withdraw(_bridgeReth, revertWithdrawal);
}
Tools Used
Manual review
Recommended Mitigation Steps
replace the first mulU88 by a mul:
diff --git a/contracts/facets/BridgeRouterFacet.sol b/contracts/facets/BridgeRouterFacet.sol
index c9ff4e5..3680ae6 100644
--- a/contracts/facets/BridgeRouterFacet.sol
+++ b/contracts/facets/BridgeRouterFacet.sol
@@ -176,12 +176,12 @@ contract BridgeRouterFacet is Modifiers {
function _ethConversion(uint256 vault, uint88 amount) private view returns (uint88) {
uint256 zethTotalNew = vault.getZethTotal();
uint88 zethTotal = s.vault[vault].zethTotal;
if (zethTotalNew >= zethTotal) {
// when yield is positive 1 zeth = 1 eth
return amount;
} else {
// negative yield means 1 zeth < 1 eth
- return amount.mulU88(zethTotalNew).divU88(zethTotal);
+ return amount.mul(zethTotalNew).divU88(zethTotal); //@audit-issue revert if amount*zethTotalNew > u88, replace mulU88 by mul
}
}