You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository has been archived by the owner on Mar 3, 2024. It is now read-only.
DuplicateA valid issue that is a duplicate of an issue with `Has Duplicates` labelMediumA valid Medium severity issueRewardA payout will be made for this issue
Edge case scenario during LMPVault withdrawals results in the inability to withdraw
Summary
During LMPVault withdrawals, the amount of destination vault shares and debt to burn is potentially rounded up, leading to an underflow error when the amount of assets to pull is less than the amount of debt to decrease. This results in the inability to withdraw from the LMPVault under specific conditions.
Vulnerability Detail
Withdrawals from the LMPVault contract utilize the currently available idle funds and withdraw the remaining needed funds from the destination vaults, configured via the withdrawalQueue.
Destination vaults are iterated and withdrawn from as long as the total assets to pull (totalAssetsToPull) are not reached. The specific amount of destination vault shares to burn to receive the anticipated amount of base assets is calculated with the _calcUserWithdrawSharesToBurn function, which calls the LMPDebt._calcUserWithdrawSharesToBurn function. As a return value, the LMPDebt._calcUserWithdrawSharesToBurn function returns the amount of destination vault shares to burn (sharesToBurn) and the amount of debt to decrease (totalDebtBurn).
The maxAssetsToPull parameter of the LMPDebt._calcUserWithdrawSharesToBurn function is set to info.totalAssetsToPull - Math.max(info.debtDecrease, info.totalAssetsPulled). This limits the amount of assets to withdraw from the destination vault.
However, the subtraction within info.totalAssetsToPull - Math.max(info.debtDecrease, info.totalAssetsPulled) can possibly revert if the subtrahend is larger than the minuend. Specifically, this can happen if info.debtDecrease is larger than info.totalAssetsPulled and even larger than info.totalAssetsToPull.
To cause such a scenario, the LMPDebt._calcUserWithdrawSharesToBurn function needs to return a totalDebtBurn value that is larger than the provided maxAssetsToPull parameter, and the then withdrawn base assets need to be less than the anticipated totalAssetsToPull, to ensure the for loop continues to iterate. The next iteration, info.debtDecrease is potentially greater than info.totalAssetsToPull, causing a revert due to the underflow error.
How can the returned totalDebtBurn value from the LMPDebt._calcUserWithdrawSharesToBurn function be larger than the provided maxAssetsToPull parameter?
Observing the internals of the LMPDebt._calcUserWithdrawSharesToBurn function, we notice that the totalDebtBurn value is the result of two calculations:
Line 291:sharesToBurn = currentDvShares.mulDiv(maxAssetsToPull, currentDvDebtValue, Math.Rounding.Up); Line 296:totalDebtBurn = cachedCurrentDebt.mulDiv(sharesToBurn, cachedDvShares, Math.Rounding.Up);
Both calculations use the Math.Rounding.Up rounding mode, rounding the result up to the next integer.
To demonstrate this behavior, we can use the following, very simplified, example (the same issue persists with large, realistic values as well):
As we can see, the totalDebtBurn value (2) is larger than the provided maxAssetsToPull parameter (1).
In words, this basically means that the destination vault's full debt (2) and all shares (1) should be burned.
Let's now assume that this example was demonstrated with larger and realistic values (i.e., such as 1.000219247063460994160469e24). If the actual received base assets, received after swapping the underlyer tokens to base asset, is not sufficient to cover the funds needed to pull (info.totalAssetsToPull), the loop will continue with the next destination vault.
However, now it will revert due to the above mentioned subtraction issue!
The intended behaviour would be that the value of maxAssetsToPull, passed to the _calcUserWithdrawSharesToBurn function in the current iteration evaluates to 0, leading to no further withdrawal from the destination vault. Depending in the user's configured slippage control in the LMPVaultRouterBase contract, the withdrawal would then succeed.
P.S.: This issue was detected by fuzzing the LMPDebt._calcUserWithdrawSharesToBurn function. Here you can find the used fuzz test (please note that for simplicity, the affected code was taken out of context and simplified - however, the vulnerable logic remains the same):
File: LMPVault.sol
448: function _withdraw(
449: uint256assets,
450: uint256shares,
451: addressreceiver,
452: addressowner453: ) internalvirtualreturns (uint256) {
454: uint256 idle = totalIdle;
455: WithdrawInfo memory info =WithdrawInfo({
456: currentIdle: idle,
457: assetsFromIdle: assets >= idle ? idle : assets,
458: totalAssetsToPull: assets - (assets >= idle ? idle : assets),
459: totalAssetsPulled: 0,
460: idleIncrease: 0,
461: debtDecrease: 0462: });
463:
464: // If not enough funds in idle, then pull what we need from destinations465: if (info.totalAssetsToPull >0) {
466: uint256 totalVaultShares =totalSupply();
467:
468: // Using pre-set withdrawalQueue for withdrawal order to help minimize user gas469: uint256 withdrawalQueueLength = withdrawalQueue.length;
470: for (uint256 i =0; i < withdrawalQueueLength; ++i) {
471: IDestinationVault destVault =IDestinationVault(withdrawalQueue[i]);
472: (uint256sharesToBurn, uint256totalDebtBurn) =_calcUserWithdrawSharesToBurn(
473: destVault,
474: shares,
475: ❌ info.totalAssetsToPull - Math.max(info.debtDecrease, info.totalAssetsPulled),
476: totalVaultShares
477: );
478: if (sharesToBurn ==0) {
479: continue;
480: }
481:
482: uint256 assetPreBal = _baseAsset.balanceOf(address(this));
483: uint256 assetPulled = destVault.withdrawBaseAsset(sharesToBurn, address(this));
484:
485: // Destination Vault rewards will be transferred to us as part of burning out shares486: // Back into what that amount is and make sure it gets into idle487: info.idleIncrease += _baseAsset.balanceOf(address(this)) - assetPreBal - assetPulled;
488: info.totalAssetsPulled += assetPulled;
489: info.debtDecrease += totalDebtBurn;
490:
491: // It's possible we'll get back more assets than we anticipate from a swap492: // so if we do, throw it in idle and stop processing. You don't get more than we've calculated493: if (info.totalAssetsPulled > info.totalAssetsToPull) {
494: info.idleIncrease = info.totalAssetsPulled - info.totalAssetsToPull;
495: info.totalAssetsPulled = info.totalAssetsToPull;
496: break;
497: }
498:
499: // No need to keep going if we have the amount we're looking for500: // Any overage is accounted for above. Anything lower and we need to keep going501: // slither-disable-next-line incorrect-equality502: if (info.totalAssetsPulled == info.totalAssetsToPull) {
503: break;
504: }
... // [...]
232: function _calcUserWithdrawSharesToBurn(
233: DestinationInfo storagedestInfo,
234: IDestinationVault destVault,
235: uint256userShares,
236: uint256maxAssetsToPull,
237: uint256totalVaultShares238: ) externalreturns (uint256sharesToBurn, uint256totalDebtBurn) {
... // [...]282:
283: // Shouldn't pull more than we want284: // Or, we're not in profit so we limit the pull285: if (currentDvDebtValue < maxAssetsToPull) {
286: maxAssetsToPull = currentDvDebtValue;
287: }
288:
289: // Calculate the portion of shares to burn based on the assets we need to pull290: // and the current total debt value. These are destination vault shares.291: ❌ sharesToBurn = currentDvShares.mulDiv(maxAssetsToPull, currentDvDebtValue, Math.Rounding.Up);
292:
293: // This is what will be deducted from totalDebt with the withdrawal. The totalDebt number294: // is calculated based on the cached values so we need to be sure to reduce it295: // proportional to the original cached debt value296: ❌ totalDebtBurn = cachedCurrentDebt.mulDiv(sharesToBurn, cachedDvShares, Math.Rounding.Up);
297: }
Tool used
Manual Review
Recommendation
Consider checking if the totalDebtBurn value in the LMPDebt._calcUserWithdrawSharesToBurn is greater than the provided maxAssetsToPull value, and if so, limit the returned totalDebtBurn value to the maxAssetsToPull value.
sherlock-admin
changed the title
Nice Maroon Frog - Edge case scenario during LMPVault withdrawals results in the inability to withdraw
berndartmueller - Edge case scenario during LMPVault withdrawals results in the inability to withdraw
Oct 3, 2023
Sign up for freeto subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Labels
DuplicateA valid issue that is a duplicate of an issue with `Has Duplicates` labelMediumA valid Medium severity issueRewardA payout will be made for this issue
berndartmueller
medium
Edge case scenario during
LMPVault
withdrawals results in the inability to withdrawSummary
During
LMPVault
withdrawals, the amount of destination vault shares and debt to burn is potentially rounded up, leading to an underflow error when the amount of assets to pull is less than the amount of debt to decrease. This results in the inability to withdraw from theLMPVault
under specific conditions.Vulnerability Detail
Withdrawals from the
LMPVault
contract utilize the currently available idle funds and withdraw the remaining needed funds from the destination vaults, configured via thewithdrawalQueue
.Destination vaults are iterated and withdrawn from as long as the total assets to pull (
totalAssetsToPull
) are not reached. The specific amount of destination vault shares to burn to receive the anticipated amount of base assets is calculated with the_calcUserWithdrawSharesToBurn
function, which calls theLMPDebt._calcUserWithdrawSharesToBurn
function. As a return value, theLMPDebt._calcUserWithdrawSharesToBurn
function returns the amount of destination vault shares to burn (sharesToBurn
) and the amount of debt to decrease (totalDebtBurn
).The
maxAssetsToPull
parameter of theLMPDebt._calcUserWithdrawSharesToBurn
function is set toinfo.totalAssetsToPull - Math.max(info.debtDecrease, info.totalAssetsPulled)
. This limits the amount of assets to withdraw from the destination vault.However, the subtraction within
info.totalAssetsToPull - Math.max(info.debtDecrease, info.totalAssetsPulled)
can possibly revert if the subtrahend is larger than the minuend. Specifically, this can happen ifinfo.debtDecrease
is larger thaninfo.totalAssetsPulled
and even larger thaninfo.totalAssetsToPull
.To cause such a scenario, the
LMPDebt._calcUserWithdrawSharesToBurn
function needs to return atotalDebtBurn
value that is larger than the providedmaxAssetsToPull
parameter, and the then withdrawn base assets need to be less than the anticipatedtotalAssetsToPull
, to ensure thefor
loop continues to iterate. The next iteration,info.debtDecrease
is potentially greater thaninfo.totalAssetsToPull
, causing a revert due to the underflow error.How can the returned
totalDebtBurn
value from theLMPDebt._calcUserWithdrawSharesToBurn
function be larger than the providedmaxAssetsToPull
parameter?Observing the internals of the
LMPDebt._calcUserWithdrawSharesToBurn
function, we notice that thetotalDebtBurn
value is the result of two calculations:Line 291:
sharesToBurn = currentDvShares.mulDiv(maxAssetsToPull, currentDvDebtValue, Math.Rounding.Up);
Line 296:
totalDebtBurn = cachedCurrentDebt.mulDiv(sharesToBurn, cachedDvShares, Math.Rounding.Up);
Both calculations use the
Math.Rounding.Up
rounding mode, rounding the result up to the next integer.To demonstrate this behavior, we can use the following, very simplified, example (the same issue persists with large, realistic values as well):
Assume the following:
currentDvShares
= 1maxAssetsToPull
= 1currentDvDebtValue
= 2cachedCurrentDebt
= 2cachedDvShares
= 1Calculating
sharesToBurn
:0.5
is now rounded up to1
due to theMath.Rounding.Up
rounding mode.Calculating
totalDebtBurn
:As we can see, the
totalDebtBurn
value (2
) is larger than the providedmaxAssetsToPull
parameter (1
).Let's now assume that this example was demonstrated with larger and realistic values (i.e., such as
1.000219247063460994160469e24
). If the actual received base assets, received after swapping the underlyer tokens to base asset, is not sufficient to cover the funds needed to pull (info.totalAssetsToPull
), the loop will continue with the next destination vault.However, now it will revert due to the above mentioned subtraction issue!
The intended behaviour would be that the value of
maxAssetsToPull
, passed to the_calcUserWithdrawSharesToBurn
function in the current iteration evaluates to 0, leading to no further withdrawal from the destination vault. Depending in the user's configured slippage control in theLMPVaultRouterBase
contract, the withdrawal would then succeed.P.S.: This issue was detected by fuzzing the
LMPDebt._calcUserWithdrawSharesToBurn
function. Here you can find the used fuzz test (please note that for simplicity, the affected code was taken out of context and simplified - however, the vulnerable logic remains the same):https://gist.github.com/berndartmueller/932942d01a8e7d3c8e102eec9c8bbde0
Impact
Inability to withdraw from the
LMPVault
in the demonstrated scenario.Code Snippet
src/vault/LMPVault.sol#L475
src/vault/libs/LMPDebt.sol#L291-L296
Tool used
Manual Review
Recommendation
Consider checking if the
totalDebtBurn
value in theLMPDebt._calcUserWithdrawSharesToBurn
is greater than the providedmaxAssetsToPull
value, and if so, limit the returnedtotalDebtBurn
value to themaxAssetsToPull
value.Duplicate of #519
The text was updated successfully, but these errors were encountered: