Part 2

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

Vault.getTotalCreditCapacityUsd is incorrect.

Summary

When usdTokens are issuanced or received from perpEngin, only market.netUsdTokenIssuance is updated.
This value is only related the realizedDebt of market.
Therefore zlpVault.realizedDebt is not updated.

Vulnerability Details

https://github.com/Cyfrin/2025-01-zaros-part-2/blob/main/src/market-making/branches/CreditDelegationBranch.sol#L246

246:function withdrawUsdTokenFromMarket(uint128 marketId, uint256 amount) external onlyRegisteredEngine(marketId) {
// loads the market's data and connected vaults
Market.Data storage market = Market.loadLive(marketId);
uint256[] memory connectedVaults = market.getConnectedVaultsIds();
// once the unrealized debt is distributed update credit delegated
// by these vaults to the market
Vault.recalculateVaultsCreditCapacity(connectedVaults);
// cache the market's total debt and delegated credit
SD59x18 marketTotalDebtUsdX18 = market.getTotalDebt();
UD60x18 delegatedCreditUsdX18 = market.getTotalDelegatedCreditUsd();
// calculate the market's credit capacity
SD59x18 creditCapacityUsdX18 = Market.getCreditCapacityUsd(delegatedCreditUsdX18, marketTotalDebtUsdX18);
// enforces that the market has enough credit capacity, if it's a listed market it must always have some
// delegated credit, see Vault.Data.lockedCreditRatio.
// NOTE: additionally, the ADL system if functioning properly must ensure that the market always has credit
// capacity to cover USD Token mint requests. Deleverage happens when the perps engine calls
// CreditDelegationBranch::getAdjustedProfitForMarketId.
// NOTE: however, it still is possible to fall into a scenario where the credit capacity is <= 0, as the
// delegated credit may be provided in form of volatile collateral assets, which could go down in value as
// debt reaches its ceiling. In that case, the market will run out of mintable USD Token and the mm engine
// must settle all outstanding debt for USDC, in order to keep previously paid USD Token fully backed.
if (creditCapacityUsdX18.lte(SD59x18_ZERO)) {
revert Errors.InsufficientCreditCapacity(marketId, creditCapacityUsdX18.intoInt256());
}
// uint256 -> UD60x18
// NOTE: we don't need to scale decimals here as it's known that USD Token has 18 decimals
UD60x18 amountX18 = ud60x18(amount);
// prepare the amount of usdToken that will be minted to the perps engine;
// initialize to default non-ADL state
uint256 amountToMint = amount;
// now we realize the added usd debt of the market
// note: USD Token is assumed to be 1:1 with the system's usd accounting
if (market.isAutoDeleverageTriggered(delegatedCreditUsdX18, marketTotalDebtUsdX18)) {
// if the market is in the ADL state, it reduces the requested USD
// Token amount by multiplying it by the ADL factor, which must be < 1
UD60x18 adjustedUsdTokenToMintX18 =
market.getAutoDeleverageFactor(delegatedCreditUsdX18, marketTotalDebtUsdX18).mul(amountX18);
amountToMint = adjustedUsdTokenToMintX18.intoUint256();
market.updateNetUsdTokenIssuance(adjustedUsdTokenToMintX18.intoSD59x18());
} else {
// if the market is not in the ADL state, it realizes the full requested USD Token amount
market.updateNetUsdTokenIssuance(amountX18.intoSD59x18());
}
// loads the market making engine configuration storage pointer
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
// mint USD Token to the perps engine
UsdToken usdToken = UsdToken(marketMakingEngineConfiguration.usdTokenOfEngine[msg.sender]);
usdToken.mint(msg.sender, amountToMint);
// emit an event
emit LogWithdrawUsdTokenFromMarket(msg.sender, marketId, amount, amountToMint);
}

https://github.com/Cyfrin/2025-01-zaros-part-2/blob/main/src/market-making/leaves/Vault.sol#L245
https://github.com/Cyfrin/2025-01-zaros-part-2/blob/main/src/market-making/leaves/Vault.sol#L226
https://github.com/Cyfrin/2025-01-zaros-part-2/blob/main/src/market-making/leaves/Vault.sol#L218

unsettledRealizedDebtUsd = marketsRealizedDebtUsd - depositedUsdc
getTotalDebt = UnsettledRealizedDebt + marketsUnrealizedDebtUsd
TotalCreditCapacityUsd = totalAssetsUsd - getTotalDebt

https://github.com/Cyfrin/2025-01-zaros-part-2/blob/main/src/market-making/branches/StabilityBranch.sol#L401-L409

vault.marketsRealizedDebtUsd -= int128(ctx.amountIn);
// burn usd amount from address(this)
ctx.usdToken.burn(ctx.amountIn);
// transfer the required assets from the vault to the mm engine contract before distributions
// note: as the swap fee stays in the ZLP Vault, it is technically a net gain to share holders, i.e it is auto
// accumulated to the contract
IERC20(ctx.asset).safeTransferFrom(vault.indexToken, address(this), ctx.amountOut + ctx.protocolReward);
// distribute protocol reward value
marketMakingEngineConfiguration.distributeProtocolAssetReward(ctx.asset, ctx.protocolReward);

When users swap the usdToken to assets in the zlpVault, the totalAssets and marketRealizedDebt are decreased.
As a result, even if marketMakingEngin issuance usdTokens and users swap these to assets, the vault's TotalCreditCapacity is not changed.

Impact

Vault.getTotalCreditCapacityUsd is incorrect.
Even if vault have no funds, it could supply credit to market, leading to users loss.

Recommendations

Consider reflecting market.netUsdTokenIssuance in vault.realizedDebut.

Updates

Lead Judging Commences

inallhonesty Lead Judge
5 months ago
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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