YearnProvider freezes yearn tokens on partial withdrawal
Summary
YearnProvider's withdraw() doesn't account for partial withdrawal situation, which isn't rare, so the unused part of user shares end up being stuck on the contract balance as there is no mechanics to retrieve them thereafter.
Vulnerability Detail
Full amount of the shares user requested to be burned is transferred to YearnProvider, but only part of it can be utilized by Yearn withdrawal.
Liquidity shortage (squeeze) is common enough situation, for example it can occur whenever part of the Yearn strategy is tied to a lending market that have high utilization at the moment of the call.
Impact
Part of protocol funds can be permanently frozen on YearnProvider contract balance (as it's not operating the funds itself, always referencing the caller Vault).
As Provider's withdraw is routinely called by Vault managing the aggregated funds distribution, the freeze amount can be massive enough and will be translated to a loss for many users.
Code Snippet
withdraw() transfers all the requested _amount from the user, but do not return the remainder _yToken amount:
# NOTE: We have withdrawn everything possible out of the withdrawal queue
# but we still don't have enough to fully pay them back, so adjust
# to the total amount we've freed up through forced withdrawals
if value > vault_balance:
value = vault_balance
# NOTE: Burn # of shares that corresponds to what Vault has on-hand,
# including the losses that were incurred above during withdrawals
shares = self._sharesForAmount(value + totalLoss)
# NOTE: This loss protection is put in place to revert if losses from
# withdrawing are more than what is considered acceptable.
assert totalLoss <= maxLoss * (value + totalLoss) / MAX_BPS
# Burn shares (full value of what is being withdrawn)
self.totalSupply -= shares
self.balanceOf[msg.sender] -= shares
log Transfer(msg.sender, ZERO_ADDRESS, shares)
self.totalIdle -= value
# Withdraw remaining balance to _recipient (may be different to msg.sender) (minus fee)
self.erc20_safe_transfer(self.token.address, recipient, value)
log Withdraw(recipient, shares, value)
return value
The remaining part of the shares, maxShares - shares, end up left on the YearnProvider balance.
Provider's withdraw() is used by the Vault, where amountReceived is deemed corresponding to the full shares spent:
hyh
high
YearnProvider freezes yearn tokens on partial withdrawal
Summary
YearnProvider's withdraw() doesn't account for partial withdrawal situation, which isn't rare, so the unused part of user shares end up being stuck on the contract balance as there is no mechanics to retrieve them thereafter.
Vulnerability Detail
Full amount of the shares user requested to be burned is transferred to YearnProvider, but only part of it can be utilized by Yearn withdrawal.
Liquidity shortage (squeeze) is common enough situation, for example it can occur whenever part of the Yearn strategy is tied to a lending market that have high utilization at the moment of the call.
Impact
Part of protocol funds can be permanently frozen on YearnProvider contract balance (as it's not operating the funds itself, always referencing the caller Vault).
As Provider's withdraw is routinely called by Vault managing the aggregated funds distribution, the freeze amount can be massive enough and will be translated to a loss for many users.
Code Snippet
withdraw() transfers all the requested
_amount
from the user, but do not return the remainder_yToken
amount:https://github.com/sherlock-audit/2023-01-derby/blob/main/derby-yield-optimiser/contracts/Providers/YearnProvider.sol#L44-L66
uAmountReceived
can correspond only to a part of shares_amount
obtained from the caller.Yearn withdrawal is not guaranteed to be full,
value
returned andshares
burned depend on availability (i.e.shares < maxShares
is valid case):https://github.com/yearn/yearn-vaults/blob/master/contracts/Vault.vy#L1144-L1167
The remaining part of the shares,
maxShares - shares
, end up left on the YearnProvider balance.Provider's withdraw() is used by the Vault, where
amountReceived
is deemed corresponding to the fullshares
spent:https://github.com/sherlock-audit/2023-01-derby/blob/main/derby-yield-optimiser/contracts/Vault.sol#L303-L326
Tool used
Manual Review
Recommendation
Consider returning the unused shares to the caller:
https://github.com/sherlock-audit/2023-01-derby/blob/main/derby-yield-optimiser/contracts/Providers/YearnProvider.sol#L44-L66