Polynomial Protocol contest - carlitox477's results

The DeFi Derivatives Powerhouse.

General Information

Platform: Code4rena

Start Date: 13/03/2023

Pot Size: $72,500 USDC

Total HM: 33

Participants: 35

Period: 7 days

Judge: Dravee

Total Solo HM: 16

Id: 222

League: ETH

Polynomial Protocol

Findings Distribution

Researcher Performance

Rank: 9/35

Findings: 1

Award: $1,562.54

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: carlitox477

Also found by: KIntern_NA

Labels

bug
3 (High Risk)
primary issue
selected for report
sponsor confirmed
H-13

Awards

1562.5386 USDC - $1,562.54

External Links

Lines of code

https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/Exchange.sol#L196 https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/Exchange.sol#L416

Vulnerability details

Description

wad operations are meant to be done with int/uint which represent numbers with 18 decimals.

While the funding rate follows this representation, a simple time difference does not.

In Exchange.getMarkPrice as well as in Exchange._updateFundingRate a wadMul operation is done to multiply the funding rate per second by a simple time difference, leading to wrong calculation of normalizationFactor and mark price, affecting critical parts of the protocol

POC

In Exchange.getMarkPrice first we get the funding rate per second:

(int256 fundingRate,) = getFundingRate();
fundingRate = fundingRate / 1 days; // Funding rate per second

Immediately after, the total funding since last update is calculated:

int256 currentTimeStamp = int256(block.timestamp);
        int256 fundingLastUpdatedTimestamp = int256(fundingLastUpdated);

        // @audit funding rate X (time interval) / 1e18 = Number without decimal
        int256 totalFunding = wadMul(fundingRate, (currentTimeStamp - fundingLastUpdatedTimestamp));

int256 totalFunding = wadMul(fundingRate, (currentTimeStamp - fundingLastUpdatedTimestamp)); is the same that $TOTAL_ FUNDING_{t_{1}; t_{2}} = \frac{FUNDING_ RATE_{sec} \times (t_{2} - t_{1})}{10^{18}}$

However, the division by $10^{18}$ should not happen, given that time difference does not represent a number with 18 decimals.

This ends up in a miscalculation of totalFunding variable, and as a consequence, a miscalculation of mark price

The same issue happens in _updateFundingRate function

Impact

Here a complete list of function affected by this bug:

  1. Exchange._updateFundingRate
    1. Exchange.openTrade
    2. Exchange.closeTrade
    3. Exchange.addCollateral
    4. Exchange.removeCollateral
    5. Exchange.liquidate
  2. Exchange.getMarkPrice:
    1. Exchange._addCollateral
    2. Exchange._removeCollateral
    3. KangarooVault.getTokenPrice
    4. ShortCollateral.liquidate
    5. ShortCollateral.getMinCollateral
    6. ShortCollateral.canLiquidate
    7. ShortCollateral.maxLiquidatableDebt
    8. LiquidityPool.orderFee
    9. LiquidityPool.getMarkPrice
      1. LiquidityPool.getTokenPrice
      2. LiquidityPool.openLong
      3. LiquidityPool.closeLong
      4. LiquidityPool.openShort
      5. LiquidityPool.closeShort
      6. LiquidityPool.liquidate
      7. KangarooVault.removeCollateral
      8. KangarooVault._openPosition

As it can be seen, this bug affects multiple critical part of the protocol, calculating the correct mark price as well as updating the funding rate is essential for the protocol correct behavior.

Mitigation steps

Simply replace current wad operation for a simple multiplication

    function getMarkPrice() public view override returns (uint256 markPrice, bool isInvalid) {
        // Get base asset price from oracles
        (uint256 baseAssetPrice, bool invalid) = pool.baseAssetPrice();
        isInvalid = invalid;

        // Get funding rate per second
        // max 1% or 1e16
        (int256 fundingRate,) = getFundingRate();
        fundingRate = fundingRate / 1 days;

        int256 currentTimeStamp = int256(block.timestamp);
        int256 fundingLastUpdatedTimestamp = int256(fundingLastUpdated);

-       int256 totalFunding = wadMul(fundingRate, (currentTimeStamp - fundingLastUpdatedTimestamp));
+       int256 totalFunding = fundingRate, (currentTimeStamp - fundingLastUpdatedTimestamp);
        int256 normalizationUpdate = 1e18 - totalFunding;
        uint256 newNormalizationFactor = normalizationFactor.mulWadDown(uint256(normalizationUpdate));

        uint256 squarePrice = baseAssetPrice.mulDivDown(baseAssetPrice, PRICING_CONSTANT);
        markPrice = squarePrice.mulWadDown(newNormalizationFactor);
    }
    function _updateFundingRate() internal {
        (int256 fundingRate,) = getFundingRate();
        
        fundingRate = fundingRate / 1 days;

        int256 currentTimeStamp = int256(block.timestamp);
        int256 fundingLastUpdatedTimestamp = int256(fundingLastUpdated);

-       int256 totalFunding = wadMul(fundingRate, (currentTimeStamp - fundingLastUpdatedTimestamp));
+       int256 totalFunding = fundingRate, (currentTimeStamp - fundingLastUpdatedTimestamp);        

        int256 normalizationUpdate = 1e18 - totalFunding;

        normalizationFactor = normalizationFactor.mulWadDown(uint256(normalizationUpdate));
        
        emit UpdateFundingRate(fundingLastUpdated, normalizationFactor);
        fundingLastUpdated = block.timestamp;
    }

#0 - c4-sponsor

2023-04-04T13:16:39Z

mubaris marked the issue as sponsor confirmed

#1 - c4-judge

2023-04-22T17:26:11Z

JustDravee marked the issue as primary issue

#2 - c4-judge

2023-05-02T22:25:57Z

JustDravee marked the issue as selected for report

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter