Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: medium
Valid

The logic in `getPremiumDiscountFactor` is inverted: a discount is applied when Vault is in credit and a premium is applied if Vault is in debt

Summary

The current implementation of getPremiumDiscountFactor allow traders to withdraw more assets (a premium) when Vault is in debt and less assets(a discount) when the vault is credit. It should be the other way around.

Vulnerability Details

Traders can exit the protocol and swap their usdToken for a Vault's asset at any time. The amount of assetsOut for the given usdToken amount is calculated by getAmountOfAssetOut.

A premium or discount is applied depending if Vault is in credit respectively debt.

function getAmountOfAssetOut(
uint128 vaultId,
UD60x18 usdAmountInX18,
UD60x18 indexPriceX18
)
public
view
returns (UD60x18 amountOutX18)
{
...
// calculate the premium or discount that may be applied to the vault asset's index price
// note: if no premium or discount needs to be applied, the premiumDiscountFactorX18 will be
// 1e18 (UD60x18 one value)
UD60x18 premiumDiscountFactorX18 =
UsdTokenSwapConfig.load().getPremiumDiscountFactor(vaultAssetsUsdX18, vaultDebtUsdX18);
// get amounts out taking into consideration the CL price and the premium/discount
@> amountOutX18 = usdAmountInX18.div(indexPriceX18).mul(premiumDiscountFactorX18);
}

For example if usdAmountIn is 2000 usd, price is 2000 and the factor is 1.1 the trader will get back 2000 / 2000 * 1.1 = 1.1 asset amount.
Instead, if the factor is 0.8, the trader will receive only 2000/ 2000 * 0.8 = 0.8 amount of asset.

A value of vaultDebtUsdX18 smaller than 0 means that the vault is in credit, while a value bigger than 0 means that the vault is in debt.
The problem is that when Vault is in credit the pdCurveYX18 is subtracted from 1 (UD60x18_UNIT), and summed to 1 when Vault is in debt.
Traders will be penalized when vault is in credit and they will withdraw more funds than they should. This will destabilize the vault more.

function getPremiumDiscountFactor(
Data storage self,
UD60x18 vaultAssetsValueUsdX18,
SD59x18 vaultDebtUsdX18
)
internal
view
returns (UD60x18 premiumDiscountFactorX18)
{
// @audit this comment is correct
@> // calculate the vault's tvl / debt absolute value, positive means we'll apply a discount, negative means
// we'll apply a premium
UD60x18 vaultDebtTvlRatioAbs = vaultDebtUsdX18.abs().intoUD60x18().div(vaultAssetsValueUsdX18);
...
// calculate the y point of the premium or discount curve given the x point
UD60x18 pdCurveYX18 = pdCurveYMinX18.add(
pdCurveYMaxX18.sub(pdCurveYMinX18).mul(
pdCurveXX18.sub(pdCurveXMinX18).div(pdCurveXMaxX18.sub(pdCurveXMinX18)).pow(pdCurveZX18)
)
);
//@audit comment and implementation are wrong
// a premium should be added if Vault is in credit
@> // if the vault is in credit, we apply a discount, otherwise, we apply a premium
@> premiumDiscountFactorX18 =
vaultDebtUsdX18.lt(SD59x18_ZERO) ? UD60x18_UNIT.sub(pdCurveYX18) : UD60x18_UNIT.add(pdCurveYX18);
}

Impact

The getPremiumDiscountFactor acts as a destabilizing factor, allowing traders to withdraw more funds than they are allowed to and in this way the vault will accumulate even more debt. Vault can get drained.

Tools Used

Recommendations

Reverse the logic from getPremiumDiscountFactor :

- vaultDebtUsdX18.lt(SD59x18_ZERO) ? UD60x18_UNIT.sub(pdCurveYX18) : UD60x18_UNIT.add(pdCurveYX18);
+ vaultDebtUsdX18.lt(SD59x18_ZERO) ? UD60x18_UNIT.add(pdCurveYX18) : UD60x18_UNIT.sub(pdCurveYX18);
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

The getPremiumDiscountFactor() function applies premiums and discounts inversely to what would maintain protocol stability

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.