Part 2

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

Incorrect `CreditCapacity` Calculation

Summary

When calculating the CreditCapacity, there is an adding delegatedCredit and totalDebt instead of subtracting them.

function getCreditCapacityUsd(
UD60x18 delegatedCreditUsdX18,
SD59x18 totalDebtUsdX18
)
internal
pure
returns (SD59x18 creditCapacityUsdX18)
{
creditCapacityUsdX18 = delegatedCreditUsdX18.intoSD59x18().add(totalDebtUsdX18);
}

Vulnerability Details

function isAutoDeleverageTriggered(
...
)
...
{
SD59x18 sdDelegatedCreditUsdX18 = delegatedCreditUsdX18.intoSD59x18();
if (sdDelegatedCreditUsdX18.lte(totalDebtUsdX18) || sdDelegatedCreditUsdX18.isZero()) {
340: return false;
}
...
}

When delegatedCredit <= totalDebt, the isAutoDeleverageTriggered function return false.

246:function withdrawUsdTokenFromMarket(uint128 marketId, uint256 amount) external onlyRegisteredEngine(marketId) {
...
SD59x18 marketTotalDebtUsdX18 = market.getTotalDebt();
UD60x18 delegatedCreditUsdX18 = market.getTotalDelegatedCreditUsd();
// calculate the market's credit capacity
260: SD59x18 creditCapacityUsdX18 = Market.getCreditCapacityUsd(delegatedCreditUsdX18, marketTotalDebtUsdX18);
...
if (creditCapacityUsdX18.lte(SD59x18_ZERO)) {
revert Errors.InsufficientCreditCapacity(marketId, creditCapacityUsdX18.intoInt256());
}
...
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
295: 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);
}

When delegatedCredit <= totalDebt, the withdrawUsdTokenFromMarket function send all pnl(amount) to perpEngin, instead of reverting or deducting.

Market.sol
264:function getTotalDebt(Data storage self) internal view returns (SD59x18 totalDebtUsdX18) {
totalDebtUsdX18 = getUnrealizedDebtUsd(self).add(getRealizedDebtUsd(self));
}
function getRealizedDebtUsd(Data storage self) internal view returns (SD59x18 realizedDebtUsdX18) {
...
248: realizedDebtUsdX18 = creditDepositsValueUsdX18.intoSD59x18().add(sd59x18(self.netUsdTokenIssuance));
}

If totalDebt exceeds delegatedCredit due to price fluctuations in the collateral, the withdrawUsdTokenFromMarket function will issue full requsted usdToken without reverting or deducting.
And UsdTokenIssuance is increased. Therefore, totalDebt is also increased.

Impact

The market falls into an irreversible state, and users lose money by owning usdToken that were issued without collateral.

Recommendations

function getCreditCapacityUsd(
UD60x18 delegatedCreditUsdX18,
SD59x18 totalDebtUsdX18
)
internal
pure
returns (SD59x18 creditCapacityUsdX18)
{
-197: creditCapacityUsdX18 = delegatedCreditUsdX18.intoSD59x18().add(totalDebtUsdX18);
+ creditCapacityUsdX18 = delegatedCreditUsdX18.intoSD59x18().sub(totalDebtUsdX18);
}
Updates

Lead Judging Commences

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

Market::getCreditCapacityUsd overestimates the credit capacity (it adds instead of substracting)

Support

FAQs

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