code-423n4 / 2024-01-salty-findings

11 stars 6 forks source link

Individual TWAP feeds should not be multiplied or divided to calculate final value in getTwapWBTC() #289

Open c4-bot-3 opened 9 months ago

c4-bot-3 commented 9 months ago

Lines of code

https://github.com/code-423n4/2024-01-salty/blob/main/src/price_feed/CoreUniswapFeed.sol#L112

Vulnerability details

Impact

getTwapWBTC() calculates TWAP value of WBTC/USD by first fetching WBTC/WETH and WETH/USDC TWAPS, and then dividing them to return the result. This is incorrect maths and unlike spot prices, two TWAPS can not be multiplied or divided to calculate the third TWAP. Just generally in mathematics, two averages when multiplied or divided are not supposed to necessarily yield the value of a third average:

        return ( uniswapWETH_USDC * 10**18) / uniswapWBTC_WETH;

This particular report will highlight the fact that 2 different feeds can not be divided or multiplied to obtain the TWAP price of another i.e.:

Attempting to do so results in an incorrect value. The more volatile the price feeds are, the more extreme this result is. Example presented in the PoC section.

Some additional input

Note that this particular issue is distinct from some of the other ones, like the issue titled "Incorrect maths calculates inverse TWAP" which highlighted the improper inverting of token0/token1 TWAP feed while calculating token1/token0. That's because in the Multiplication style mentioned above, there is no "virtual inverse" happening (otherwise one could have made an argument that in the Division style example, we effectively calculated 1/TWAP of tokenY/tokenA TWAP and then multiplied, choosing to represent it as a division, which would make this issue quite similar to the other issue "Incorrect maths calculates inverse TWAP").
The current issue persists even in the absence of an inverse TWAP and is hence distinct. Consider this:

So basically, no contribution was made by the "inverse issue" to any error which may happen here.
Now we move on to verify the issue at hand - that of incorrectly using 2 TWAPS to calculate a third one.

Proof of Concept

We'll formulate a simplified example to show the issue. The actual Uniswap TWAP has cumulative prices stored which are then used for further calculations, but the following example is good enough to highlight the fundamental issue. Let's assume that the following prices exist at different timestamps: t tokenX spot price tokenY spot price tokenA spot price tokenX/tokenA tokenA/tokenY
1 10000 50 250 40 5
2 11000 54 270 40.74074 5
3 10800 55 260 41.53846 4.72727
- TWAP: (10000+11000+10800)/3 = 10600 TWAP: (50+54+55)/3 = 53 TWAP: (250+270+260)/3 = 260 TWAP: (40+40.74074+41.53846)/3 = 40.75973 TWAP: (5+5+4.72727)/3 = 4.90909

As per the protocol's current logic:

TWAP of tokenX/tokenY = TWAP_tokenX/tokenA * TWAP_tokenA/tokenY = 40.75973 * 4.90909 = 200.09318

The actual TWAP however is:

TWAP of tokenX/tokenY = average of spot price ratios at each timestamp = (10000/50 + 11000/54 + 10800/55) / 3 = (200 + 203.70370 + 196.36363) / 3 = 200.02244 

Protocol's calculation has a deviation of 0.035% from the actual TWAP price.

One can imagine much more volatile prices or difference in degree of fluctuations between tokens would result in a much higher price deviation from the actual figure.

Mathematical PoC

One can also provide a proof mathematically. Assuming - t tokenX spot price tokenY spot price tokenA spot price tokenX/tokenA tokenA/tokenY
1 x1 y1 a1 xa1 = x1/a1 ay1 = a1/y1
2 x2 y2 a2 xa2 = x2/a2 ay2 = a2/y2
3 x3 y3 a3 xa3 = x3/a3 ay3 = a3/y3
- TWAP: (x1+x2+x3)/3 TWAP: (y1+y2+y3)/3 TWAP: (a1+a2+a3)/3 TWAP: (xa1+xa2+xa3)/3 TWAP: (ay1+ay2+ay3)/3

As per the protocol's current logic:

TWAP of tokenX/tokenY = TWAP_tokenX/tokenA * TWAP_tokenA/tokenY = (xa1+xa2+xa3)/3 * (ay1+ay2+ay3)/3 = (xa1+xa2+xa3) * (ay1+ay2+ay3) / 9 = (x1/a1 + x2/a2 + x3/a3) * (a1/y1 + a2/y2 + a3/y3) / 9

The actual TWAP however is:

TWAP of tokenX/tokenY = average of spot price ratios at each timestamp = (x1/y1 + x2/y2 + x3/y3) / 3 

It's trivial to see from here on that the two expressions are not equal.

Tools used

Manual inspection

Recommended Mitigation Steps

Assessed type

Oracle

c4-judge commented 9 months ago

Picodes marked the issue as primary issue

c4-sponsor commented 8 months ago

othernet-global (sponsor) acknowledged

Picodes commented 8 months ago

The 2 expressions are not equal but the goal of the protocol is achieved: what they use for the twap price is more resilient to a sudden drop in price than the spot price. I consider that this is an instance of "function not working as in specs" so of Low severity and not an instance of Medium severity in the absence of convincing evidence that this could lead to real harm being done.

c4-judge commented 8 months ago

Picodes changed the severity to QA (Quality Assurance)

t0x1cC0de commented 8 months ago

Hi @Picodes,
Thanks for your judgement & the comments provided. I would like to highlight a few important facts and dispute the QA tag awarded to this finding as well as Issue #283, also raised by me. Since both are related to TWAP (different problems though), I am clubbing my points together here to make it easier to read -

We are forcing incorrect prices throughout the protocol, despite the fix being simple enough - use the correct TWAP feeds instead of doing internal calculations. The impact seems quite obvious and apparent, spread to almost all of the protocol. Is the protocol as well as the end-user aware of this impact, this error which has crept in due to use of inverse TWAP calculations or their multiplication/divisions? Put differently, from a business perspective, would an end-user be okay knowing that the prices being calculated for them are almost always not reflecting the sources they have been promised?

Hence my request to please review.

Thank you