Platform: Code4rena
Start Date: 16/02/2023
Pot Size: $144,750 USDC
Total HM: 17
Participants: 154
Period: 19 days
Judge: Trust
Total Solo HM: 5
Id: 216
League: ETH
Rank: 10/154
Findings: 3
Award: $3,838.19
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: hyh
Also found by: Koolex, Parad0x, chaduke, hansfriese, jasonxiale, koxuan
1140.5041 USDC - $1,140.50
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L399-L402 https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L364-L366
At the end of _withdraw
, if vault balance after withdrawal from strategies is not enough to cover for user share value, user share value will be capped at vault balance after withdrawal from strategies. I believe that the cap is meant to limit the maximum number of shares that user can withdraw depending on the availability of funds in vault and strategies. However, the limit is implemented incorrectly and instead causes loss of funds to user as the amount they receive is lesser than what their shares are worth.
In _withdraw
for ReaperVaultV2
, we can see here that value
is capped at whatever vaultBalance has after withdrawing all available funds from all strategies. I believe the intention for this is to limit the number of shares user can redeem, but instead the shares are burned and the share value is capped to the available vaultBalance after withdrawal from strategies.
vaultBalance = token.balanceOf(address(this)); if (value > vaultBalance) { value = vaultBalance; }
Front part of withdraw
that shows how user share value
is calculated. Note that value
has the loss of the strategies being deducted from it when withdrawing from strategies.
require(_shares != 0, "Invalid amount"); value = (_freeFunds() * _shares) / totalSupply(); _burn(_owner, _shares);
Manual Review
Recommend reverting in the event user tries to withdraw an amount of share that is more than what is available. Another way is to calculate the amount available from all strategies and limit the user to only withdraw the number of shares that vault and strategies can allow.
#0 - c4-judge
2023-03-09T14:38:33Z
trust1995 marked the issue as duplicate of #723
#1 - c4-judge
2023-03-09T14:38:38Z
trust1995 marked the issue as satisfactory
2636.4342 USDC - $2,636.43
P is asserted to never be zero in _updateRewardSumAndProduct
which is called for every liquidation. However, there is an edge case that can cause P to be zero, causing DOS to certain liquidations.
In _updateRewardSumAndProduct
notice assert(newP > 0);
. However, the SCALE_FACTOR check is insufficient in ensuring P is always more than zero. Three cycles of very small P can cause P to drop to zero and hence causing revert for that particular liquidation of troves. POC steps are as follows,
First liquidation newProductFactor is 1e2, 1e18*1e2/1e18 = 1e2, since its less than scale factor. 1e9 is multiplied before dividing with 1e18. P = 1e11 after 1st liquidation cycle.
Second liquidation newProductFactor is 1e3, 1e11*1e3/1e18 = 0, since its less than scale factor. 1e9 is multipled. P = 1e5
Third liquidation newProductFactor is 1e3, 1e5*1e3/1e8 = 0, since its less than scale factor, 1e9 is multipled. However, even after 1e9 is multiplied it will result in 1e17, which is less than DECIMAL_PRECISION of 1e18. P = 0.
Revert due to assert(newP > 0);
.
// If the Stability Pool was emptied, increment the epoch, and reset the scale and product P if (newProductFactor == 0) { currentEpoch = currentEpochCached.add(1); emit EpochUpdated(currentEpoch); currentScale = 0; emit ScaleUpdated(currentScale); newP = DECIMAL_PRECISION; // If multiplying P by a non-zero product factor would reduce P below the scale boundary, increment the scale } else if (currentP.mul(newProductFactor).div(DECIMAL_PRECISION) < SCALE_FACTOR) { newP = currentP.mul(newProductFactor).mul(SCALE_FACTOR).div(DECIMAL_PRECISION); currentScale = currentScaleCached.add(1); emit ScaleUpdated(currentScale); } else { newP = currentP.mul(newProductFactor).div(DECIMAL_PRECISION); } assert(newP > 0); P = newP; emit P_Updated(newP); }
Reasons for putting high is because
It breaks the invariant of P being > 0 which according to the docs would break deposit tracking when Pool is not empty.
Even though in some lucky cases, smaller liquidation can be made if batch liquidation is done, in the event that P is at a precarious place whereby all permutations of liquidations done will result in P being 0, liquidation will be DOSed which can be detrimental to protocol as they cannot liquidate bad debt and hence might lead it to insolvency.
Manual Review
Recommend setting SCALE_FACTOR to 1e18, even though the docs did explain that 1e9 is used instead of 1e18 to ensure negligible precision loss, the alternative option is redesigning the mitigation mechanism of rounding error for P.
#0 - c4-judge
2023-03-10T10:11:11Z
trust1995 marked the issue as unsatisfactory: Invalid
#1 - trust1995
2023-03-10T10:12:11Z
Code will not allow P == 0 , so it's not really an issue. Even if they change to 1e18 SCALE_FACTOR, a similar situation could occur.
#2 - vkabc
2023-03-22T07:07:32Z
Hi @trust1995, this is a duplicate of #444. Both POCs are also similar whereby both requires 3 liquidations to result in newP being 0.
#3 - c4-judge
2023-03-22T10:26:15Z
trust1995 marked the issue as satisfactory
#4 - c4-judge
2023-03-22T10:26:25Z
trust1995 marked the issue as duplicate of #444
#5 - c4-judge
2023-03-22T10:34:14Z
trust1995 changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-03-22T10:35:10Z
trust1995 marked the issue as selected for report