Platform: Code4rena
Start Date: 12/12/2022
Pot Size: $36,500 USDC
Total HM: 8
Participants: 103
Period: 7 days
Judge: berndartmueller
Id: 193
League: ETH
Rank: 51/103
Findings: 2
Award: $57.15
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: minhquanym
Also found by: 0x52, 0xDecorativePineapple, Apocalypto, BAHOZ, ElKu, Franfran, HE1M, Jeiwan, KingNFT, Koolex, SamGMK, Tointer, Tricko, UNCHAIN, __141345__, ak1, aviggiano, bytehat, carrotsmuggler, cccz, chaduke, cozzetti, dipp, eyexploit, fs0c, haku, hansfriese, hihen, immeas, izhelyazkov, koxuan, ladboy233, lumoswiz, rajatbeladiya, rjs, rvierdiiev, seyni, supernova, unforgiven, yixxas
6.9881 USDC - $6.99
In the current Caviar protocol, anybody can supply initial liquidity to a newly created pool.
An LP who intends to create a new pool and add liqiduity could execute the following transactions:
pair = Caviar.create()
pair.add(..., uint256 minLpTokenAmount)
If the case where minLpTokenAmount
allows for some slippage, an MEV searcher could force the LP to pay more for transaction 2 than is necessary by detecting LP transaction 1, and then sandwiching LP transaction 2 as follows:
pair = Caviar.create()
pair.add(...)
pair.add(..., uint256 minLpTokenAmount)
pair.remove(...)
The legitimate LP would be forced to pay the MEV searcher a slippage amount, or abandon their attempt to provide liquidity.
Anybody can call Pair.add()
after Caviar.create()
.
In Pair.sol#L77, the lpTokenAmount
that will be minted for the LP invoking Pair.add()
is determined by addQuote
.
In addQuote
Pair.sol#L419 the contract checks if there is already liquidity in the pool:
419: if (lpTokenSupply > 0) {
If there is no liquidity in the pool (i.e. lpTokenSupply == 0
), Pair.sol#L424-L427 executes:
424: } else { 425: // if there is no liquidity then init 426: return Math.sqrt(baseTokenAmount * fractionalTokenAmount); 427: }
Thus, the first party to call Pair.add()
can control the initial value of k
in the AMM k = x*y
formula.
As a result, the legitimate LP provider is forced to either:
minLpTokenAmount
value equal to Math.sqrt(baseTokenAmount * fractionalTokenAmount)
Pair.add()
operation to proceed; the MEV searcher earns Math.sqrt(baseTokenAmount * fractionalTokenAmount) - minLpTokenAmount
in this case.The problem with option 1, is that an MEV searcher could effectively hold the contract 'hostage' by just executing pair.add()
after Caviar.create()
. While the legitimate LP won't be 'forced' to pay the 'bribe', in reality if they don't pay the MEV searcher what they are looking for, then they will be forced to abandon their attempt to provide liquidity. In my opinion, this forced loss of value qualifies as a high risk issue.
You could argue that this issue is also present in subsequent liquidity provisioning operations, however I think it's clear that the contract author's intention is to accept slippage during ordinary market operations as a known issue by virtue of the slippage parameter minLpTokenAmount
.
The issue I am describing here differs from this ordinary course of operations because in the state immediately after pool creation there is 0 liquidity and therefore there cannot be slippage, and as a result an MEV searcher can hold the pool hostage until a 'bribe' is paid. After that point, ordinary market operations commence.
I suggest making Caviar.create()
an internal
function, and providing a new public
function e.g. Caviar.createWithLiquidity()
that calls Caviar.create()
and pair.add()
in a single transaction.
An alternative would be to implement a more complex permission model for adding initial liquidity (e.g. limiting it to the pool creator), but then you run the risk of griefing (i.e. the pool creator never sends liquidity). To deal with that would introduce even more complexity, and I think that clearly goes against the current very clean, permisionless architecture of the contracts.
#0 - c4-judge
2022-12-28T14:35:05Z
berndartmueller marked the issue as duplicate of #442
#1 - c4-judge
2023-01-10T09:17:25Z
berndartmueller marked the issue as satisfactory
🌟 Selected for report: 0xSmartContract
Also found by: 0xGusMcCrae, 8olidity, Bnke0x0, IllIllI, JC, RaymondFam, Rolezn, SleepingBugs, UNCHAIN, ahayashi, aviggiano, caventa, cozzetti, h0wl, helios, immeas, ladboy233, minhquanym, obront, rjs, rvierdiiev, shung, unforgiven, yixxas
50.16 USDC - $50.16
The Caviar codebase is very well written and follows a clean, permisionless model.
ID | Finding | Instances |
---|---|---|
L-01 | Anybody can create a pair without funding | N/A |
ID | Finding | Instances |
---|---|---|
N-01 | Use WETH instead of native ETH | N/A |
Caviar.create()
is not permissioned, and there is no cost other than a gas cost for invoking the operation.
As a result, it would be possible to pollute the Caviar
contract with unfunded pairs.
This is a low-risk issue because front-ends could easily choose to filter out inactive or unfunded pairs.
In the Pair
contract there are various branches of logic to handle native ETH vs. ERC20 tokens. It's not clear why native ETH support is necessary, since WETH is available.
According to the authors own specification, Caviar is based on Uniswap V2. One of the big changes from Uniswap V1 to Uniswap V2 was to move from native ETH to WETH. While Uniswap offers much more functionality than Caviar, one of the benefits that still applies to Caviar is a simpler codebase: having extra branches to handle ETH increases the contract complexity, and therefore the risk of defects.
#0 - c4-judge
2023-01-16T11:28:17Z
berndartmueller marked the issue as grade-b