Summary
The calculation of the funding rate is subject to precision loss making the PerpMarket::getFundingRate function to return an incorrect result.
Vulnarability Details
When calculating the current funding rate, the PerpMarket::getFundingRate function calls the PerpMarket::getCurrentFundingVelocity function:
File: src/perpetuals/leaves/PerpMarket.sol
function getCurrentFundingRate(Data storage self) internal view returns (SD59x18) {
return sd59x18(self.lastFundingRate).add(
getCurrentFundingVelocity(self).mul(getProportionalElapsedSinceLastFunding(self).intoSD59x18())
);
}
function getCurrentFundingVelocity(Data storage self) internal view returns (SD59x18) {
SD59x18 maxFundingVelocity = sd59x18(uint256(self.configuration.maxFundingVelocity).toInt256());
SD59x18 skewScale = sd59x18(uint256(self.configuration.skewScale).toInt256());
SD59x18 skew = sd59x18(self.skew);
if (skewScale.isZero()) {
return SD59x18_ZERO;
}
SD59x18 proportionalSkew = skew.div(skewScale);
SD59x18 proportionalSkewBounded = Math.min(Math.max(unary(SD_UNIT), proportionalSkew), SD_UNIT);
return proportionalSkewBounded.mul(maxFundingVelocity);
}
In the PerpMarket::getCurrentFundingVelocity function, the funding velocity value returned depends on proportionalSkewBounded which is itself subject to conditions as we can see. In the case where proportionalSkewBounded equals proportionalSkew, there will be a loss of precision due to the division of skew by skewScale before multiplying the result obtained by maxFundingVelocity. The result is an inaccurate value for the current funding velocity, and therefore for the current funding rate.
Proof Of Concept
Below's my optimized version of the PerpMarket::getCurrentFundingVelocity function:
File: src/perpetuals/leaves/PerpMarket.sol
function getCurrentFundingVelocity(Data storage self) internal view returns (SD59x18) {
SD59x18 maxFundingVelocity = sd59x18(uint256(self.configuration.maxFundingVelocity).toInt256());
SD59x18 skewScale = sd59x18(uint256(self.configuration.skewScale).toInt256());
SD59x18 skew = sd59x18(self.skew);
if (skewScale.isZero()) {
return SD59x18_ZERO;
}
return proportionalSkewBounded == unary(SD_UNIT) || proportionalSkewBounded == SD_UNIT
? proportionalSkewBounded.mul(maxFundingVelocity)
: skew.mul(maxFundingVelocity).div(skewScale);
}
Modify the GetFundingVelocity_Integration_Test::testFuzz_GivenTheresAMarketCreated test function as follows:
File: /test/integration/perpetuals/perp-market-branch/getFundingVelocity/getFundingVelocity.t.sol
/// *** existing code ***
import { console } from "forge-std/console.sol";
/// *** existing code ***
function testFuzz_GivenTheresAMarketCreated(
uint128 marketId,
uint256 marginValueUsd,
bool isLong,
uint256 timeElapsed
)
external
{
/// *** existing code ***
++ console.log("expectedFundingVelocity", expectedFundingVelocity);
++ console.log("fundingVelocity", fundingVelocity.intoInt256());
// it should return the funding velocity
++ assertLte(fundingVelocity.intoInt256(), expectedFundingVelocity, "invalid funding velocity");
}
Run forge test --mc GetFundingVelocity_Integration_Test -vv.
Impact
The inaccuracy of the funding rate expands to the funding fee per unit and therefore on the unrealized PNL, which affects the liquidation and fulfillment of orders. The incorrect value will affect all future values as it is stored in the PerpMarket::updateFunding function, compounding the error and increasing the impact over time.
Tools Used
Manual review.
Recommendations
Change the PerpMarket::getCurrentFundingVelocity function with an optimized one as shown in the POC.