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: 11/154
Findings: 2
Award: $3,168.53
π Selected for report: 0
π 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#L366
When withdrawing from a vault, the user passes the amount of shares to be burnt, and in return the user receives the corresponding assets as follows:
value = (_freeFunds() * _shares) / totalSupply();
If there is not enough balance in the vault, then it withdraws from strategies in the order they were added in withdrawalQueue
. However, in case the strategies don't have enough balance, the vault burns all user's shares even though s/he hasn't received all of the corresponding assets for it.
In other words, if the user should receive Y tokens when passing X shares and the issue occurs, then X shares will be burnt even though the user receives less than Y which is not fair.
Let's break down _withdraw
's flow:
value = (_freeFunds() * _shares) / totalSupply();
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L365
_burn(_owner, _shares);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L366
Assume thereβs not enough fund in the vault then it will loop through withdrawalQueue
and withdraw what's required to cover the gap.
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L372-L397
Assume the tokens the user should receive is still lower than the balance then update it to the balance
vaultBalance = token.balanceOf(address(this)); if (value > vaultBalance) { value = vaultBalance; }
token.safeTransfer(_receiver, value);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L410
As you can notice, no matter how many tokens the user receives, s/he will always get his/her shares burnt.
Manual analysis
value
For example:
vaultBalance = token.balanceOf(address(this)); if (value > vaultBalance) { value = vaultBalance; => recalculate the shares to be burnt } _burn(_owner, _shares); token.safeTransfer(_receiver, value);
#0 - c4-judge
2023-03-09T15:29:59Z
trust1995 marked the issue as duplicate of #723
#1 - c4-judge
2023-03-09T15:30:05Z
trust1995 marked the issue as satisfactory
π Selected for report: GalloDaSballo
Also found by: GalloDaSballo, Koolex
2028.0263 USDC - $2,028.03
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L385
When withdrawing from a vault, the user passes the amount of shares to be burnt, and in return the user receives the underlying token. If there is not enough balance in the vault, then it withdraws from strategies in the order they were added in withdrawalQueue
till it has enough amount for withdrawal.
However, if a withdraw call to one strategy fails (i.e. reverts) for whatever reason, then the withdrawal will revert even if other strategies could successfully cover the withdrawal amount. Thus, one strategy could block other strategies from being utilised resulting in blocking withdrawal functionality in the protocol.
As the protocol supports multiple strategies, and those strategies basically rely on external DeFis (e.g. lending markets such as Granary), the protocol's withdrawal is vulnerable to being blocked since the hard/heavy dependency. One scenario in Granary that could possibly cause this, is when the lending pool is paused. Here is the sequence flow for clearer picture:
Vault = ReaperVaultV2 Strategy = ReaperStrategyGranarySupplyOnly LendingPool = Granary's Lending Pool
flowchart TD; subgraph Vault Vault.withdraw-->Vault._withdraw(_withdraw) end subgraph Strategy Strategy.withdraw-->Strategy._liquidatePosition(_liquidatePosition); Strategy._liquidatePosition-->Strategy._withdraw(_withdraw); Strategy._withdraw-->Strategy._withdrawUnderlying(_withdrawUnderlying); end subgraph LendingPool LendingPool.withdraw; end Vault-->Strategy Strategy-->LendingPool
Setting this to high severity due to the following:
setWithdrawalQueue
. Which is not a good solution, as the strategy could resume working again (i.e. it was failing temporarily)uint256 loss = IStrategy(stratAddr).withdraw(Math.min(remaining, strategyBal));
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L385
(amountFreed, loss) = _liquidatePosition(_amount);
if (wantBal < _amountNeeded) { _withdraw(_amountNeeded - wantBal); liquidatedAmount = balanceOfWant(); } else { liquidatedAmount = _amountNeeded; }
ILendingPool(ADDRESSES_PROVIDER.getLendingPool()).withdraw(address(want), _withdrawAmount, address(this));
As you will notice in the withdraw function, there is a modifier whenNotPaused. If this function is paused, it reverts, and all sequence calls will do as well. Check the sequence diagram above.
Manual analysis
One possibility is using try/catch, when the strategy withdrawal reverts, keep going to next strategy for withdrawal. This way, the strategy withdrawal impact is isolated and withdrawal from other strategies can still work.
#0 - c4-judge
2023-03-09T08:53:43Z
trust1995 marked the issue as duplicate of #693
#1 - c4-judge
2023-03-09T08:53:48Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-03-09T09:01:37Z
trust1995 changed the severity to 2 (Med Risk)