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():
SD59x18 vaultDebtUsdX18 = vault.getTotalDebt();
UD60x18 assetPriceX18 = vault.collateral.getPrice();
@> 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:
Vault::getTotalDebt():
function getTotalDebt(Data storage self) internal view returns (SD59x18 totalDebtUsdX18) {
@> totalDebtUsdX18 = getUnsettledRealizedDebt(self).add(sd59x18(self.marketsUnrealizedDebtUsd));
}
Vault::getUnsettledRealizedDebt():
function getUnsettledRealizedDebt(Data storage self)
internal
view
returns (SD59x18 unsettledRealizedDebtUsdX18)
{
@> unsettledRealizedDebtUsdX18 =
@> sd59x18(self.marketsRealizedDebtUsd).add(unary(ud60x18(self.depositedUsdc).intoSD59x18()));
}
Vault::_recalculateConnectedMarketsState():
(
uint128[] memory updatedConnectedMarketsIdsCache,
@> SD59x18 vaultTotalRealizedDebtChangeUsdX18,
SD59x18 vaultTotalUnrealizedDebtChangeUsdX18,
UD60x18 vaultTotalUsdcCreditChangeX18,
UD60x18 vaultTotalWethRewardChangeX18
) = _recalculateConnectedMarketsState(self, connectedMarketsIdsCache, true);
if (!vaultTotalRealizedDebtChangeUsdX18.isZero()) {
@> self.marketsRealizedDebtUsd = sd59x18(self.marketsRealizedDebtUsd).add(
@> vaultTotalRealizedDebtChangeUsdX18
@> ).intoInt256().toInt128();
}
if (!market.getTotalDelegatedCreditUsd().isZero()) {
(
@> 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)
);
...
@> 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():
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():
ctx.marketUnrealizedDebtUsdX18 = market.getUnrealizedDebtUsd();
ctx.marketRealizedDebtUsdX18 = market.getRealizedDebtUsd();
if (!ctx.marketUnrealizedDebtUsdX18.isZero() || !ctx.marketRealizedDebtUsdX18.isZero()) {
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.
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());