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: 2/103
Findings: 2
Award: $3,442.16
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: Jeiwan
Also found by: 0xxm, 9svR6w, BAHOZ, Bobface, CRYP70, Chom, HE1M, Junnon, RaymondFam, UNCHAIN, __141345__, bytehat, carlitox477, caventa, cccz, chaduke, hansfriese, hihen, koxuan, mauricio1802, minhquanym, minhtrng, nicobevi, obront, shung, unforgiven, wait
40.2564 USDC - $40.26
The Pair
contract does not return excess funds to the user when adding liquidity through add()
or nftAdd()
which can result in loss of funds through external bugs.
Pair.add()
calls into Pair.addQuote
to calculate how much LP tokens to mint given the input of base tokens and fractional tokens (fTokens). The share of both inputs is calculated and the lower share is used to calculate the LP token amount through Math.min(baseTokenShare, fractionalTokenShare)
.
Through an external bug, such as an UI bug or an error in the integration of Caviar in a third-party service, users could overpay by an infinite amount by supplying more of one of the two assets than would be neccessary. These excess funds would get split evenly across all LP token holders instead of being returned to the user.
Affected functions:
add()
nftAdd()
since it calls into add()
Calculate excess funds when adding liquidity and return them to the user.
Paste the following unit test into the test/Pair/unit
directory and run it with forge test -vv -m testOverpay
. The -vv
flag is important in order to see the console.log
output.
The PoC demonstrates a user overpaying and losing 4499 ETH.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "forge-std/Test.sol"; import "forge-std/console.sol"; import "../../shared/Fixture.t.sol"; contract OverpayTest is Fixture { function setUp() public { // Setup ETH and fToken balance deal(address(this), 10_000 ether); deal(address(ethPair), address(this), 100 ether, true); // Setup the pool with 1 ETH and 50 fToken // This state is considered to have been created by other third-parties. // They also own the LP token, that's why it's sent out below. ethPair.add{value: 1 ether}(1 ether, 50 ether, 1 ether); // Send out the LP tokens ethPairLpToken.transfer( address(1), ethPairLpToken.balanceOf(address(this)) ); } function testOverpay() public { // Save balances before add & remove uint256 ethBefore = address(this).balance; uint256 tokensBefore = ethPair.balanceOf(address(this)); // Add liquidity while overpaying significantly in ETH ethPair.add{value: 9_000 ether}(9_000 ether, 50 ether, 1 ether); // Remove liquidity to see how much we get back ethPair.remove(ethPairLpToken.balanceOf(address(this)), 0, 0); // Get balances and calculate diff uint256 ethAfter = address(this).balance; uint256 tokensAfter = ethPair.balanceOf(address(this)); int256 ethDiff = int256(ethAfter) - int256(ethBefore); int256 tokensDiff = int256(tokensAfter) - int256(tokensBefore); console.log("ETH diff:"); console.logInt(ethDiff / 1 ether); console.log("fToken diff:"); console.logInt(tokensDiff / 1 ether); } }
#0 - c4-judge
2022-12-28T15:49:04Z
berndartmueller marked the issue as duplicate of #376
#1 - c4-judge
2023-01-10T09:02:31Z
berndartmueller changed the severity to 3 (High Risk)
#2 - c4-judge
2023-01-10T09:02:37Z
berndartmueller marked the issue as satisfactory
3401.904 USDC - $3,401.90
https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L63 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L107 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L147 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L182 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L275 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L294 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L310 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L323
The Pair
contract does not allow users to submit a deadline for their action. This missing feature enables pending transactions to be maliciously executed at a later point.
AMMs should provide their users with an option to limit the execution of their pending actions, such as swaps or adding and removing liquidity. The most common solution is to include a deadline timestamp as a parameter (for example see Uniswap V2). If such an option is not present, users can unknowingly perform bad trades:
fTokens
) for 1 ETH and later sell the 1 ETH for 1000 DAI. She signs the transaction calling Pair.sell
with inputAmount = 100 fTokens
and minOutputAmount = 0.99 ETH
to allow for some slippage.ETH
could have drastically changed. She will still at least get 0.99 ETH
due to minOutputAmount
, but the DAI
value of that output might be significantly lower. She has unknowingly performed a bad trade due to the pending transaction she forgot about.An even worse way this issue can be maliciously exploited is through MEV:
fToken
has gone up significantly since the transaction was signed, meaning Alice would receive a lot more ETH
when the swap is executed. But that also means that her minOutputAmount
value is outdated and would allow for significant slippage.minOutputAmount
now allows for high slippage, the bot sandwiches Alice, resulting in significant profit for the bot and significant loss for Alice.The affected functions in Pair.sol
are:
add()
remove()
buy()
sell()
nftAdd()
nftRemove()
nftBuy()
nftSell()
Introduce a deadline
parameter to the mentioned functions.
Categorizing this issue into medium versus high was not immediately obvious. I came to the conclusion that this is a high-severity issue for the following reason:
I run an arbitrage MEV bot myself, which also tracks pending transactions in the mempool, though for another reason than the one mentioned in this report. There is a significant amount of pending and even dropped transactions: over 200,000
transactions that are older than one month. These transactions do all kinds of things, from withdrawing from staking contracts to sending funds to CEXs and also performing swaps on DEXs like Uniswap. This goes to show that this issue will in fact be very real, there will be very old pending transactions wanting to perform trades without a doubt. And with the prevalence of advanced MEV bots, these transactions will be exploited as described in the second example above, leading to losses for Caviar's users.
Omitted in this case, since the exploit is solely based on the fact that there is no limit on how long a transaction is allowed to be pending, which can be clearly seen when looking at the mentioned functions.
#0 - Shungy
2022-12-21T20:18:27Z
Seems valid.
I disagree with severity though. It requires user to not set a proper slippage tolerance for lack of deadline to cause loss to the user. Issues conditional on user mistake has historically been considered Low on C4.
#1 - c4-judge
2022-12-29T12:06:38Z
berndartmueller marked the issue as duplicate of #116
#2 - c4-judge
2023-01-10T16:50:02Z
berndartmueller changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-01-10T16:50:21Z
berndartmueller marked the issue as not a duplicate
#4 - c4-judge
2023-01-10T16:50:33Z
berndartmueller marked the issue as primary issue
#5 - c4-judge
2023-01-10T16:51:10Z
berndartmueller marked the issue as selected for report
#6 - outdoteth
2023-01-20T12:39:44Z
Fixed in https://github.com/outdoteth/caviar/pull/6
Add a deadline check.