Part 2

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

Wrong implementation of `Market::getCreditCapacityUsd` allows infinite debt accumulation

Summary
Function Market::getCreditCapacityUsd adds delegatedCredit to totalDebt inflating the market's credit capacity. The more debt a market has, the more credit capacity allowing accumulating even more debt.

Vulnerability Details

In getCreditCapacityUsd the sum delegatedCredit + totalDebt is returned.

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

This function is called from getAdjustedProfitForMarketId and withdrawUsdTokenFromMarketId

function getAdjustedProfitForMarketId(...) public view returns(...) {
...
SD59x18 marketTotalDebtUsdX18 = market.getTotalDebt();
// caches the market's delegated credit & credit capacity
UD60x18 delegatedCreditUsdX18 = market.getTotalDelegatedCreditUsd();
SD59x18 creditCapacityUsdX18 = Market.getCreditCapacityUsd(delegatedCreditUsdX18, marketTotalDebtUsdX18);

In both cases the values returned by market.getTotalDebt() and market.getTotalDelegatedCreditUsd() are passed further to getCreditCapacityUsd to calculate how much credit the market has left, if any.
While getTotalDelegatedCreditUsd returns a positive number, representing the credit value, getTotalDebt can also return negative values.
This function returns the sum of unrealized Debt and realized debt. As mentioned in contest known issues, PerpsEngine::getUnrealizedDebt returns a zero value which means getTotalDebt returns the value from getRealizedDebtUsd

//Market.sol
function getTotalDebt(Data storage self) internal view returns (SD59x18 totalDebtUsdX18) {
totalDebtUsdX18 = getUnrealizedDebtUsd(self).add(getRealizedDebtUsd(self));
}
function getUnrealizedDebtUsd(Data storage self) internal view returns (SD59x18 unrealizedDebtUsdX18) {
unrealizedDebtUsdX18 = sd59x18(IEngine(self.engine).getUnrealizedDebt(self.id)); // @audit returns a zero value
}

NOTE: getRealizedDebtUsd has another issue reported in another submission.

For simplicity let's suppose creditDepositsValueUsdX18 from below is 0.
Market's netUsdTokenIssuance is increased each time usdToken is minted (to pay for profitable traders) and decreased when the market makes a profit.
This means that a negative value returned by getRealizedDebtUsd or getTotalDebt is a credit for market.

function getRealizedDebtUsd(Data storage self) internal view returns (SD59x18 realizedDebtUsdX18) {
// prepare the credit deposits usd value variable;
UD60x18 creditDepositsValueUsdX18;
// if the credit deposits usd value cache is up to date, return the stored value
if (block.timestamp <= self.lastCreditDepositsValueRehydration) {
creditDepositsValueUsdX18 = ud60x18(self.creditDepositsValueCacheUsd);
} else {
// otherwise, we'll need to loop over credit deposits to calculate it
creditDepositsValueUsdX18 = getCreditDepositsValueUsd(self);
}
// finally after determining the market's latest credit deposits usd value, sum it with the stored net usd
// token issuance to return the net realized debt usd value
realizedDebtUsdX18 = creditDepositsValueUsdX18.intoSD59x18().add(sd59x18(self.netUsdTokenIssuance));
}

Going back to the getCreditCapacityUsd function, it can be seen why it returns the wrong value. The debt amount should be subtracted from the delegated credit, not added together as in the current implementation

Impact

Market credit capacity can increase indefinitely, increasing the risk for traders and vault LPs.
If debt is much bigger than allocated credit, traders will be paid in a token with no value (internal usdToken) since there will be no assets left in vaults to cover for debt.

Tools Used

Recommendations

In getCreditCapacityUsd the debt should be deducted from allocated credit.

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

Lead Judging Commences

inallhonesty Lead Judge 7 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.