Part 2

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

Complete loss of profits to traders in edge case debt ratio equals the auto deleverage start threshold

Summary

When a registered engine wishes to mint usd token to profitable traders they will call withdrawUsdTokenFromMarket. There is a check to see if the market is in a an auto deleveraged state. The problem is that it checks the debt ratio is greater than or equal to rather than just greater than the start threshold. This contradicts the factor calculation leading to an amount of 0 calculated for the user.

Vulnerability Details

A registered engine calls withdrawUsdTokenFromMarket to withdraw usd tokens for profitable traders. If the market is in a deleveraged state, the amount of usd tokens to be minted will be reduced by a certain factor. The logic to check if the market is in this state is done through isAutoDeleverageTriggered. It will return true if the delegated credit is less than or equal to the total debt and the market debt ratio is greater than or equal to the starting threshold.

function isAutoDeleverageTriggered(
Data storage self,
UD60x18 delegatedCreditUsdX18,
SD59x18 totalDebtUsdX18
)
internal
view
returns (bool triggered)
{
SD59x18 sdDelegatedCreditUsdX18 = delegatedCreditUsdX18.intoSD59x18();
if (sdDelegatedCreditUsdX18.lte(totalDebtUsdX18) || sdDelegatedCreditUsdX18.isZero()) {
return false;
}
// market_debt_ratio = total_debt / delegated_credit
UD60x18 marketDebtRatio = totalDebtUsdX18.div(sdDelegatedCreditUsdX18).intoUD60x18();
// trigger ADL if marketRatio >= ADL start threshold
triggered = marketDebtRatio.gte(ud60x18(self.autoDeleverageStartThreshold));
}

There is an edge case on the last check that the market ratio is >= the start threshold. If the ratio were to be equal to, there will be unexpected behavior when the factor is calculated in getAutoDeleverageFactor. If the market ratio equals the start threshold the factor will be calculated to zero here

// first, calculate the unscaled delevarage factor
UD60x18 unscaledDeleverageFactor = Math.min(marketDebtRatio, autoDeleverageEndThresholdX18).sub(
autoDeleverageStartThresholdX18
).div(autoDeleverageEndThresholdX18.sub(autoDeleverageStartThresholdX18));
// finally, raise to the power scale
autoDeleverageFactorX18 = unscaledDeleverageFactor.pow(autoDeleverageExponentZX18);

Then this zero amount will be multiplied by the amount to mint and the function will attempt to mint a zero amount to the msg.sender and revert.
https://github.com/Cyfrin/2025-01-zaros-part-2/blob/35deb3e92b2a32cd304bf61d27e6071ef36e446d/src/market-making/branches/CreditDelegationBranch.sol#L288C12-L304C49

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

POC

add this function to getAdjustedProfitForMarketId.t.sol and run forge test --match-test test_WhenMarketDebtRatioEqualsStartThreshold

function test_WhenMarketDebtRatioEqualsStartThreshold(uint256 marketId) external whenTheMarketIsLive {
// Setup
PerpMarketCreditConfig memory fuzzMarketConfig = getFuzzPerpMarketCreditConfig(marketId);
// Get start threshold
uint128 startThreshold = marketMakingEngine.workaround_getAutoDeleverageStartThreshold(fuzzMarketConfig.marketId);
// Get delegated credit
UD60x18 delegatedCreditUsdX18 =
marketMakingEngine.workaround_getTotalDelegatedCreditUsd(fuzzMarketConfig.marketId);
// Calculate and set debt to exactly match start threshold
// If startThreshold is 0.75, and delegatedCredit is 1000
// then we need totalDebt = 750 to get a ratio of 0.75
uint128 targetDebt = uint128(startThreshold) * delegatedCreditUsdX18.intoUint128() / 1e18;
marketMakingEngine.workaround_setMarketUsdTokenIssuance(fuzzMarketConfig.marketId, int128(targetDebt));
SD59x18 totalDebtUsdX18 = marketMakingEngine.workaround_getTotalMarketDebt(fuzzMarketConfig.marketId);
// Get auto deleverage factor
UD60x18 autoDeleverageFactorX18 = marketMakingEngine.workaround_getAutoDeleverageFactor(
fuzzMarketConfig.marketId, delegatedCreditUsdX18, totalDebtUsdX18
);
// Factor should be 0 since (startThreshold - startThreshold) = 0
assertEq(autoDeleverageFactorX18.intoUint256(), 0);
// Test that adjusted profit is 0 due to factor being 0
uint256 testProfit = 1000e18;
UD60x18 adjustedProfitUsdX18 =
marketMakingEngine.getAdjustedProfitForMarketId(fuzzMarketConfig.marketId, testProfit);
assertEq(adjustedProfitUsdX18.intoUint256(), 0);
}

Impact

Call to this function will revert and usd tokens will not be minted for profitable traders

Tools Used

Manual Review

Recommendations

The check should be that the market debt ratio is greater than the threshold and not equal to

Updates

Lead Judging Commences

inallhonesty Lead Judge
5 months ago
inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Too generic
Assigned finding tags:

[INVALID] The equality case in `CreditDelegationBranch::isAutoDeleverageTriggered` (market ratio is == the start threshold) causes a small dos.

Support

FAQs

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