DeFiFoundry
60,000 USDC
View results
Submission Details
Severity: high
Invalid

PerpMarket::getMarkPrice() doesn't calculate the given market's mark price correctly due to precision loss

Summary

In the PerpMarket contract, the getMarketPrice function, doesn't calculate the given market's mark price correctly due to precision loss.

Vulnerability Details

In the PerpMarket::getMarketPricefunction, lies a precision loss due to division before multiplication when calculating the mark price of the give market.

function getMarkPrice(
Data storage self,
SD59x18 skewDelta,
UD60x18 indexPriceX18
)
internal
view
returns (UD60x18 markPrice)
{
SD59x18 skewScale = sd59x18(self.configuration.skewScale.toInt256());
SD59x18 skew = sd59x18(self.skew);
SD59x18 priceImpactBeforeDelta = skew.div(skewScale);
SD59x18 newSkew = skew.add(skewDelta);
SD59x18 priceImpactAfterDelta = newSkew.div(skewScale);
SD59x18 cachedIndexPriceX18 = indexPriceX18.intoSD59x18();
UD60x18 priceBeforeDelta =
cachedIndexPriceX18.add(cachedIndexPriceX18.mul(priceImpactBeforeDelta)).intoUD60x18();
UD60x18 priceAfterDelta =
cachedIndexPriceX18.add(cachedIndexPriceX18.mul(priceImpactAfterDelta)).intoUD60x18();
markPrice = priceBeforeDelta.add(priceAfterDelta).div(ud60x18Convert(2));
}

Let's breakdown each line from bottom to top.

markPrice = priceBeforeDelta.add(priceAfterDelta).div(ud60x18Convert(2));
= cachedIndexPriceX18.add(cachedIndexPriceX18.mul(priceImpactBeforeDelta)).intoUD60x18().add(
cachedIndexPriceX18.add(cachedIndexPriceX18.mul(priceImpactAfterDelta)).intoUD60x18()
).div(ud60x18Convert(2));
@--> = cachedIndexPriceX18.add(cachedIndexPriceX18.mul(skew.div(skewScale))).intoUD60x18().add(
@--> cachedIndexPriceX18.add(cachedIndexPriceX18.mul(newSkew.div(skewScale))).intoUD60x18()
).div(ud60x18Convert(2));

As we can see, we divide the skew by the skewScale when calculating the priceImpactBeforeDelta and so do we to the newSkew when calculating the priceImpactAfterDelta.

Proof Of Concept

Below's my optimized version of the PerpMarket::getMarkPrice function:

File: src/perpetuals/leaves/PerpMarket.sol
function getMarkPrice(
Data storage self,
SD59x18 skewDelta,
UD60x18 indexPriceX18
)
internal
view
returns (UD60x18 markPrice)
{
SD59x18 skewScale = sd59x18(self.configuration.skewScale.toInt256());
SD59x18 skew = sd59x18(self.skew);
SD59x18 newSkew = skew.add(skewDelta);
SD59x18 cachedIndexPriceX18 = indexPriceX18.intoSD59x18();
markPrice = cachedIndexPriceX18.add(
cachedIndexPriceX18.mul(skew.add(newSkew)).div(
sd59x18Convert(2).mul(skewScale)
)
).intoUD60x18();
}

Modify the GetMarkPrice_Integration_Test::testFuzz_GivenTheresAMarketCreated test function as follows:

File: /test/integration/perpetuals/perp-market-branch/getMarkPrice/getMarkPrice.t.sol
/// *** existing lines ***
++ import "forge-std/console.sol";
/// *** existing lines ***
function testFuzz_GivenTheresAMarketCreated(
uint128 marketId,
uint256 marginValueUsd,
bool isLong,
uint256 timeElapsed
)
external
{
/// *** existing code ***
++ console.log("expectedMarkPrice", expectedMarkPrice);
++ console.log("markPrice", markPrice.intoUint256());
// it should return the funding velocity
assertEq(markPrice.intoUint256(), expectedMarkPrice, "invalid mark price");
}

Run forge test --mc GetMarkPrice_Integration_Test -vv.

The test output shows a significant loss of precision in PerpMarket::getMarketPrice().

Impact

This precision loss leads to a wrong calculation of the market's mark price and affect every feature/function that uses the PerpMarket::getMarketPrice function from liquidation to order filling.

Tools Used

Manual review.

Recommendations

Change the PerpMarket::getMarketPrice function with an optimized one as shown in the POC.

Updates

Lead Judging Commences

inallhonesty Lead Judge
over 1 year ago
inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!