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;
}
UD60x18 marketDebtRatio = totalDebtUsdX18.div(sdDelegatedCreditUsdX18).intoUD60x18();
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
UD60x18 unscaledDeleverageFactor = Math.min(marketDebtRatio, autoDeleverageEndThresholdX18).sub(
autoDeleverageStartThresholdX18
).div(autoDeleverageEndThresholdX18.sub(autoDeleverageStartThresholdX18));
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)) {
UD60x18 adjustedUsdTokenToMintX18 =
market.getAutoDeleverageFactor(delegatedCreditUsdX18, marketTotalDebtUsdX18).mul(amountX18);
amountToMint = adjustedUsdTokenToMintX18.intoUint256();
market.updateNetUsdTokenIssuance(adjustedUsdTokenToMintX18.intoSD59x18());
} else {
market.updateNetUsdTokenIssuance(amountX18.intoSD59x18());
}
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
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 {
PerpMarketCreditConfig memory fuzzMarketConfig = getFuzzPerpMarketCreditConfig(marketId);
uint128 startThreshold = marketMakingEngine.workaround_getAutoDeleverageStartThreshold(fuzzMarketConfig.marketId);
UD60x18 delegatedCreditUsdX18 =
marketMakingEngine.workaround_getTotalDelegatedCreditUsd(fuzzMarketConfig.marketId);
uint128 targetDebt = uint128(startThreshold) * delegatedCreditUsdX18.intoUint128() / 1e18;
marketMakingEngine.workaround_setMarketUsdTokenIssuance(fuzzMarketConfig.marketId, int128(targetDebt));
SD59x18 totalDebtUsdX18 = marketMakingEngine.workaround_getTotalMarketDebt(fuzzMarketConfig.marketId);
UD60x18 autoDeleverageFactorX18 = marketMakingEngine.workaround_getAutoDeleverageFactor(
fuzzMarketConfig.marketId, delegatedCreditUsdX18, totalDebtUsdX18
);
assertEq(autoDeleverageFactorX18.intoUint256(), 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