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.