Part 2

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

The market's debt is not correctly updated in `recalculateVaultsCreditCapacity()`

Summary

The market's debt is not correctly updated in recalculateVaultsCreditCapacity().

Vulnerability Details

When recalculateVaultsCreditCapacity() is called, protocol gets market's unrealized debt (which is always 0 in v1) and realized debt, and distributes the debt.

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);
}

And the market's realizedDebtUsdPerVaultShare and unrealizedDebtUsdPerVaultShare is updated based on the market's totalDelegatedCreditUsd.
Market::distributeDebtToVaults():

function distributeDebtToVaults(
Data storage self,
SD59x18 newUnrealizedDebtUsdX18,
SD59x18 newRealizedDebtUsdX18
)
internal
{
// cache the total vault's shares as SD59x18
@> SD59x18 totalVaultSharesX18 = ud60x18(self.totalDelegatedCreditUsd).intoSD59x18();
// if there is zero delegated credit and we're trying to distribute debt to vaults, we should revert and the
// market is considered to be in a panic state
if (totalVaultSharesX18.isZero()) {
revert Errors.NoDelegatedCredit(self.id); // here
}
// update storage values
@> self.realizedDebtUsdPerVaultShare = newRealizedDebtUsdX18.div(totalVaultSharesX18).intoInt256().toInt128();
@> self.unrealizedDebtUsdPerVaultShare = newUnrealizedDebtUsdX18.div(totalVaultSharesX18).intoInt256().toInt128();
}

The problem is that both the market's realizedDebtUsdPerVaultShare and unrealizedDebtUsdPerVaultShare are up to date by the time the debt is distributed, but the market's totalDelegatedCreditUsd is not, and its value is only updated at the end of recalculateVaultsCreditCapacity() when _updateCreditDelegations() is called.

Vault::_updateCreditDelegations():

market.updateTotalDelegatedCredit(creditDeltaUsdX18);

And using outdated totalDelegatedCreditUsd value will get us incorrect realizedDebtUsdPerVaultShare and unrealizedDebtUsdPerVaultShare values.

Impact

realizedDebtUsdPerVaultShare and unrealizedDebtUsdPerVaultShare are later used to get the vault's accumulated debt, credit changes in getVaultAccumulatedValues().

Vault::_recalculateConnectedMarketsState():

// 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)
);

Market::getVaultAccumulatedValues():

realizedDebtChangeUsdX18 = !lastVaultDistributedRealizedDebtUsdPerShareX18.isZero()
? sd59x18(self.realizedDebtUsdPerVaultShare).sub(lastVaultDistributedRealizedDebtUsdPerShareX18).mul(
vaultCreditShareX18.intoSD59x18()
)
: SD59x18_ZERO;
unrealizedDebtChangeUsdX18 = !lastVaultDistributedUnrealizedDebtUsdPerShareX18.isZero()
? sd59x18(self.unrealizedDebtUsdPerVaultShare).sub(lastVaultDistributedUnrealizedDebtUsdPerShareX18).mul(
vaultCreditShareX18.intoSD59x18()
)
: SD59x18_ZERO;

Incorrect realizedDebtUsdPerVaultShare and unrealizedDebtUsdPerVaultShare lead to incorrect value changes, and the state of the Maket Making Engine is not correctly accounted, this can bring huge risk to the whole system.

Tools Used

Manual Review

Recommendations

Update market's totalDelegatedCreditUsd before distribute debt.

Updates

Lead Judging Commences

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

Vault::_recalculateConnectedMarketsState doesn't update market's realized debt per vault share after clearing market's debt, remove the zero check

Support

FAQs

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