Part 2

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

isAutoDeleverageTriggered() will revert when market total debt is negative

Summary

Auto deleverage system is used to reduce requested USD tokens for profitable users when the market is in net debt, but protocol fails to fill trader's order and to provide AutoDeleverageFactor when market total debt is negative because of mistake in marketDebtRatio calculation which leads to revert due to underflow.

Vulnerability Details

CreditDelegationBranch.sol:withdrawUsdTokenFromMarket() function gives USD tokens to perpsEngine as incentive for trader if his old position had positive pnl then credit that to the trader (function called in SettlementBranch.sol:_fillOrder()):

/// @notice Mints the requested amount of USD Token to the caller and updates the market's
/// debt state.
/// @dev Called by a registered engine to mint USD Token to profitable traders.
/// @dev USD Token association with an engine's user happens at the engine contract level.
/// @dev We assume `amount` is part of the market's reported unrealized debt.
/// @dev Invariants:
/// The Market of `marketId` MUST exist.
/// The Market of `marketId` MUST be live.
/// @param marketId The engine's market id requesting USD Token.
/// @param amount The amount of USD Token to mint.
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);
}

But let's imagine that market has marketTotalDebtUsdX18 = -10_000 and delegatedCreditUsdX18 = 20_000:
We will pass credit capacity requirement because creditCapacityUsdX18 = marketTotalDebtUsdX18 + delegatedCreditUsdX18 = 10_000:

if (creditCapacityUsdX18.lte(SD59x18_ZERO)) {
revert Errors.InsufficientCreditCapacity(marketId, creditCapacityUsdX18.intoInt256());
}

Which means that protocol is ready to incentive us with some amount of USD tokens.

Next we go to the line that checks if ADL system is triggered:

if (market.isAutoDeleverageTriggered(delegatedCreditUsdX18, marketTotalDebtUsdX18)) {

Market.sol:isAutoDeleverageTriggered():

/// @notice Returns whether the market has reached the auto deleverage start threshold, i.e, if the ADL system
/// must be triggered or not.
/// @param self The market storage pointer.
/// @param delegatedCreditUsdX18 The market's credit delegated by vaults in USD, used to determine the ADL state.
/// @param totalDebtUsdX18 The market's total debt in USD, used to determine the ADL state.
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;
}
//@AUDIT Function will revert if totalDebtUsdX18 is negative due to uint256 conversion
// 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));
}

sdDelegatedCreditUsdX18 > totalDebtUsdX18, means we will go to marketDebtRatio calculation which will revert because totalDebtUsdX18 is negative:

UD60x18 marketDebtRatio = -10000/20000 = uint256(-0.5);//revert

Impact

Protocol fails to fill trader's order and to provide him USD tokens when market total debt is negative and traders PnL is positive.

Tools Used

Manual Review

Recommendations

Use abs() for marketDebtRatio calculation:

UD60x18 marketDebtRatio = totalDebtUsdX18.abs().div(sdDelegatedCreditUsdX18).intoUD60x18();
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.