Part 2

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

Assets can be drained for vaults with LTV greater than certain threshold

Summary

Incorrect application of premiumDiscountFactor in StabilityBranch.getAssetAmountOut, premiumDiscountFactor will inflate the amount of collateral amount out, effectively drain the vault.

Vulnerability Details

Root Cause

In StabilityBranch.getAmountOfAssetOut, premiumDiscountFactor is getting multiplied, instead of being divided

amountOutX18 = usdAmountInX18.div(indexPriceX18).mul(premiumDiscountFactorX18);

Let's see why premiumDiscountFactor should be divided.

UsdTokenSwapConfig.getPremiumDiscountFactor logic can be formalized in the following way:

: vaultAssetsValueUsd

: vaultDebtUsd

: vaultDebtTvlRatioAbs

: premiumDiscountFactor

getPremiumDiscountFactor

According to the following comment:

/// @dev The proposed initial curve is defined as:
/// f(x) = 1 + 9 * ((x - 0.3) / 0.5)^3

we can derive the following:

So for , .

This means for vaults with LTV = 0.8, collateral mount to be returned for 1 USD is inflated by 10 times.

This is the opposite of the purpose of premiumDiscountFactor.

If it was a division, collateral amount will be deflated by 1/10, which will effectively guard against asset draining during high LTV.

POC

import "@zaros/market-making/branches/StabilityBranch.sol";
import { Vault } from "@zaros/market-making/leaves/Vault.sol";
import { UsdTokenSwapConfig } from "@zaros/market-making/leaves/UsdTokenSwapConfig.sol";
import { UD60x18, ud60x18, convert as ud60x18Convert } from "@prb-math/UD60x18.sol";
import "forge-std/Test.sol";
contract MockVault {
function totalAssets() external view returns (uint256) {
return 1000e18;
}
}
contract StabilityBranchTest is StabilityBranch, Test {
using Vault for Vault.Data;
using UsdTokenSwapConfig for UsdTokenSwapConfig.Data;
uint128 vaultId = 1;
uint256 usdAmountInX18 = 100e18;
uint256 indexPriceX18 = 1e18;
address indexToken;
function setUp() external {
indexToken = address(new MockVault());
Vault.Data storage vault = Vault.load(vaultId);
vault.id = vaultId;
vault.indexToken = indexToken;
vault.marketsRealizedDebtUsd = 800e18;
UsdTokenSwapConfig.Data storage swapConfig = UsdTokenSwapConfig.load();
swapConfig.baseFeeUsd = 1e18;
swapConfig.swapSettlementFeeBps = 300;
swapConfig.maxExecutionTime = 100;
swapConfig.pdCurveYMin = 0e18;
swapConfig.pdCurveYMax = 9e18;
swapConfig.pdCurveXMin = 0.3e18;
swapConfig.pdCurveXMax = 0.8e18;
swapConfig.pdCurveZ = 3e18;
}
function test_getAmountOfAssetOut() external {
// D = marketsRealizedDebtUsd = 800
// V = MockVault.totalAssets() * indexPrice = 1000 * 1 = 1000
// x = 0.8
// f(x) = 1 + 9 * ((0.8 - 0.3) / 0.5) ^ 3 = 10
// amountOut = (usdAmountIn * f(x)) / indexPrice = 100 * 10 / 1 = 1000
uint256 amountOutX18 = getAmountOfAssetOut(vaultId, ud60x18(usdAmountInX18), ud60x18(indexPriceX18)).unwrap();
assertEq(amountOutX18, MockVault(indexToken).totalAssets());
}
}

Impact

  • Vaults can be easily drained once its LTV is greater than

Tools Used

Foundry, Manual Review

Recommendations

Should be fixed in the following way:

- amountOutX18 = usdAmountInX18.div(indexPriceX18).mul(premiumDiscountFactorX18);
+ amountOutX18 = usdAmountInX18.div(indexPriceX18).div(premiumDiscountFactorX18);
Updates

Lead Judging Commences

inallhonesty Lead Judge 6 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.