Part 2

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

Decimal Normalization Flaw in Vault Credit Capacity Calculation

Summary

A critical flaw exists in Vault.getTotalCreditCapacityUsd() where collateral assets are not properly normalized to 18 decimals before USD conversion. This causes the vault's credit capacity to be miscalculated by a factor of 10^(18-collateralDecimals), leading to severe miscalculation of available credit and system insolvency.

Vulnerability Details

The protocol uses a standardized 18-decimal system for internal calculations, particularly for USD values. This is evident in how the protocol handles token amounts elsewhere:

// In Math.sol - The standard way to convert token amounts to 18 decimals
function convertTokenAmountToUd60x18(uint8 decimals, uint256 amount) internal pure returns (UD60x18) {
if (Constants.SYSTEM_DECIMALS == decimals) {
return ud60x18(amount);
}
return ud60x18(amount * 10 ** (Constants.SYSTEM_DECIMALS - decimals));
}

This conversion is consistently used throughout the codebase for token amount normalization:

// Example from VaultRouterBranch.sol
ctx.assetsX18 = Math.convertTokenAmountToUd60x18(ctx.vaultAssetDecimals, assets);
// Example from FeeDistributionBranch.sol
UD60x18 amountX18 = collateral.convertTokenAmountToUd60x18(amount);

However, in getTotalCreditCapacityUsd() of vault.sol leaves, this normalization is missing:

function getTotalCreditCapacityUsd(Data storage self) internal view returns (SD59x18 creditCapacityUsdX18) {
Collateral.Data storage collateral = self.collateral;
// CRITICAL: Raw token amount cast to UD60x18 without decimal normalization
UD60x18 totalAssetsX18 = ud60x18(IERC4626(self.indexToken).totalAssets());
// Price is in 18 decimals, but totalAssets isn't normalized
UD60x18 totalAssetsUsdX18 = collateral.getAdjustedPrice().mul(totalAssetsX18);
creditCapacityUsdX18 = totalAssetsUsdX18.intoSD59x18().sub(getTotalDebt(self));
}

The problem compounds because:

  1. totalAssets() returns the raw token balance in the token's native decimals

  2. The debt calculation is normalized with 18 decimals.

Impact

The vulnerability leads to:

  1. Massively inflated/dinflated credit capacity for low-decimal tokens (e.g., USDC)

  2. Severely undervalued credit capacity for high-decimal tokens , and underflow reverts for correct actions

Tools Used

Manual review and mathematical analysis

Recommendations

Replace the direct casting with proper decimal normalization:

function getTotalCreditCapacityUsd(Data storage self) internal view returns (SD59x18 creditCapacityUsdX18) {
Collateral.Data storage collateral = self.collateral;
// Get raw assets and properly normalize to 18 decimals
uint256 rawTotalAssets = IERC4626(self.indexToken).totalAssets();
UD60x18 totalAssetsX18 = collateral.convertTokenAmountToUd60x18(rawTotalAssets);
UD60x18 totalAssetsUsdX18 = collateral.getAdjustedPrice().mul(totalAssetsX18);
creditCapacityUsdX18 = totalAssetsUsdX18.intoSD59x18().sub(getTotalDebt(self));
}
Updates

Lead Judging Commences

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

`totalAssets()` is not properly scaled to ZAROS precision

Support

FAQs

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