Part 2

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

Vault credit capacity may not be correctly calculated

Summary

Vault credit capacity may not be correctly calculated due to unadjusted collateral asset price is used.

Vulnerability Details

When calculates the net credit capacity of a given vault in getVaultCreditCapacity(), it uses vaultDebtUsdX18 and assetPriceX18 to convert the vault debt value in USD to the equivalent amount of assets to be credited or debited.

VaultRouterBranch::getVaultCreditCapacity():

// we use the vault's net sum of all debt types coming from its connected markets to determine the swap rate
SD59x18 vaultDebtUsdX18 = vault.getTotalDebt();
// get collateral asset price
UD60x18 assetPriceX18 = vault.collateral.getPrice();
// convert the vault debt value in USD to the equivalent amount of assets to be credited or debited
@> SD59x18 vaultDebtInAssetsX18 = vaultDebtUsdX18.div(assetPriceX18.intoSD59x18());

The assetPriceX18 is collateral asset price which is unadjusted, however, vaultDebtUsdX18 is computed from the same collateral's adjusted price.

Let's go back through:

  • getTotalDebt() calls getUnsettledRealizedDebt() to get the vault's total unsettled debt in USD:

Vault::getTotalDebt():

function getTotalDebt(Data storage self) internal view returns (SD59x18 totalDebtUsdX18) {
@> totalDebtUsdX18 = getUnsettledRealizedDebt(self).add(sd59x18(self.marketsUnrealizedDebtUsd));
}
  • In getUnsettledRealizedDebt(), the unsettled debt is calculated based on marketsRealizedDebtUsd and depositedUsdc.

Vault::getUnsettledRealizedDebt():

function getUnsettledRealizedDebt(Data storage self)
internal
view
returns (SD59x18 unsettledRealizedDebtUsdX18)
{
@> unsettledRealizedDebtUsdX18 =
@> sd59x18(self.marketsRealizedDebtUsd).add(unary(ud60x18(self.depositedUsdc).intoSD59x18()));
}
  • marketsRealizedDebtUsd is updated by the returned value from _recalculateConnectedMarketsState().

Vault::_recalculateConnectedMarketsState():

// iterate over each connected market id and distribute its debt so we can have the latest credit
// delegation of the vault id being iterated to the provided `marketId`
(
uint128[] memory updatedConnectedMarketsIdsCache,
@> SD59x18 vaultTotalRealizedDebtChangeUsdX18,
SD59x18 vaultTotalUnrealizedDebtChangeUsdX18,
UD60x18 vaultTotalUsdcCreditChangeX18,
UD60x18 vaultTotalWethRewardChangeX18
) = _recalculateConnectedMarketsState(self, connectedMarketsIdsCache, true);
// gas optimization: only write to storage if values have changed
//
// updates the vault's stored unsettled realized debt distributed from markets
if (!vaultTotalRealizedDebtChangeUsdX18.isZero()) {
@> self.marketsRealizedDebtUsd = sd59x18(self.marketsRealizedDebtUsd).add(
@> vaultTotalRealizedDebtChangeUsdX18
@> ).intoInt256().toInt128();
}
  • The returned value from _recalculateConnectedMarketsState() is in turn retrieved from getVaultAccumulatedValues() in Market.

if (!market.getTotalDelegatedCreditUsd().isZero()) {
// get the vault's accumulated debt, credit and reward changes from the market to update its stored
// values
(
@> ctx.realizedDebtChangeUsdX18,
ctx.unrealizedDebtChangeUsdX18,
ctx.usdcCreditChangeX18,
ctx.wethRewardChangeX18
) = market.getVaultAccumulatedValues(
ud60x18(creditDelegation.valueUsd),
sd59x18(creditDelegation.lastVaultDistributedRealizedDebtUsdPerShare),
sd59x18(creditDelegation.lastVaultDistributedUnrealizedDebtUsdPerShare),
ud60x18(creditDelegation.lastVaultDistributedUsdcCreditPerShare),
ud60x18(creditDelegation.lastVaultDistributedWethRewardPerShare)
);
...
// update the vault's state by adding its share of the market's latest state variables
@> vaultTotalRealizedDebtChangeUsdX18 = vaultTotalRealizedDebtChangeUsdX18.add(ctx.realizedDebtChangeUsdX18);

In Market, we can see the value we are looking for is based on realizedDebtUsdPerVaultShare.

Market::getVaultAccumulatedValues():

realizedDebtChangeUsdX18 = !lastVaultDistributedRealizedDebtUsdPerShareX18.isZero()
@> ? sd59x18(self.realizedDebtUsdPerVaultShare).sub(lastVaultDistributedRealizedDebtUsdPerShareX18).mul(
vaultCreditShareX18.intoSD59x18()
)
: SD59x18_ZERO;

And realizedDebtUsdPerVaultShare is updated in distributeDebtToVaults().

Market::distributeDebtToVaults():

// update storage values
self.realizedDebtUsdPerVaultShare = newRealizedDebtUsdX18.div(totalVaultSharesX18).intoInt256().toInt128();
self.unrealizedDebtUsdPerVaultShare = newUnrealizedDebtUsdX18.div(totalVaultSharesX18).intoInt256().toInt128();

distributeDebtToVaults() is called by Vault's _recalculateConnectedMarketsState(), and the value passed in is from Market's getRealizedDebtUsd().

Vault::_recalculateConnectedMarketsState():

// first we cache the market's unrealized and realized debt
ctx.marketUnrealizedDebtUsdX18 = market.getUnrealizedDebtUsd();
ctx.marketRealizedDebtUsdX18 = market.getRealizedDebtUsd();
// if market has debt distribute it
if (!ctx.marketUnrealizedDebtUsdX18.isZero() || !ctx.marketRealizedDebtUsdX18.isZero()) {
// distribute the market's debt to its connected vaults
market.distributeDebtToVaults(ctx.marketUnrealizedDebtUsdX18, ctx.marketRealizedDebtUsdX18);
}

Market's getRealizedDebtUsd() calls getCreditDepositsValueUsd() to get credit deposit value which is essentially part of the realized debt.

Market::getRealizedDebtUsd():

creditDepositsValueUsdX18 = getCreditDepositsValueUsd(self);

Finally, we can find it in getCreditDepositsValueUsd() that the collateral's adjusted price is used instead of the unadjusted price.

// update the total credit deposits value
creditDepositsValueUsdX18 =
@> creditDepositsValueUsdX18.add((collateral.getAdjustedPrice().mul(ud60x18(value))));

Impact

Vault credit capacity is used to the swap rate from index token to collateral asset when user redeems, incorrect credit capacity leads to incorrect tokens be swapped out, causes loss to the protocol or the user.

Tools Used

Manual Review

Recommendations

When calculates vault creidt capacity, uses adjusted price to as the collateral asset price.

// we use the vault's net sum of all debt types coming from its connected markets to determine the swap rate
SD59x18 vaultDebtUsdX18 = vault.getTotalDebt();
// get collateral asset price
- UD60x18 assetPriceX18 = vault.collateral.getPrice();
+ UD60x18 assetPriceX18 = vault.collateral.getAdjustedPrice();
// convert the vault debt value in USD to the equivalent amount of assets to be credited or debited
SD59x18 vaultDebtInAssetsX18 = vaultDebtUsdX18.div(assetPriceX18.intoSD59x18());
Updates

Lead Judging Commences

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

`VaultRouterBranch::getVaultCreditCapacity` should use the adjusted collateral price

Support

FAQs

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