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: 21/103
Findings: 3
Award: $330.05
π 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
When a user adds liquidity to the pool, separate calculations are done based on the number of baseTokens
and fractionalTokens
sent. Each of these calculates the number of LP tokens that it expects should be generated based on the number of inputs. Then, the minimum of these two values are taken and this number of LP tokens is minted to the user.
However, the full value of both baseTokens
and fractionalTokens
are taken from the user. This can create a dramatic mismatch between the amount spent by the user and the amount of LP tokens earned.
It is true that this amount can be counteracted by setting a strict minLpTokenAmount
in the function call. However, for most users, they will not be so careful with slippage and can lose unnecessary tokens through this inaccurate math.
baseTokens
than fractionalTokens
baseTokens
submitted should mint X LP tokens, but their fractionalTokens
submitted should mint Y LP tokens, where Y < XbaseTokens
and fractionalTokens
are taken from the userManual Review
After calculating the LP tokens to be minted, return the minimum number of baseTokens
and fractionalTokens
that were required to mint this number of LP tokens, and only take this number of tokens from the user.
#0 - c4-judge
2022-12-28T15:40:05Z
berndartmueller marked the issue as duplicate of #376
#1 - c4-judge
2023-01-10T09:02:24Z
berndartmueller changed the severity to 3 (High Risk)
#2 - c4-judge
2023-01-10T09:02:28Z
berndartmueller marked the issue as satisfactory
239.6304 USDC - $239.63
The price()
function is expected to return the price of one fractional tokens, represented in base tokens, to 18 decimals of precision. This is laid out clearly in the comments:
/// @notice The current price of one fractional token in base tokens with 18 decimals of precision. /// @dev Calculated by dividing the base token reserves by the fractional token reserves. /// @return price The price of one fractional token in base tokens * 1e18.
However, the formula incorrectly calculates the price to be represented in whatever number of decimals the base token is in. Since there are many common base tokens (such as USDC) that will have fewer than 18 decimals, this will create a large mismatch between expected prices and the prices that result from the function.
Prices are calculated with the following formula, where ONE = 1e18
:
return (_baseTokenReserves() * ONE) / fractionalTokenReserves();
We know that fractionalTokenReserves
will always be represented in 18 decimals. This means that the ONE
and the
fractionalTokenReserves
will cancel each other out, and we are left with the baseTokenReserves
number of decimals for the final price.
As an example:
1e9 * 1e18 / 1e21 = 1e6
Manual Review
The formula should use the decimals value of the baseToken
to ensure that the decimals of the resulting price ends up with 18 decimals as expected:
return (_baseTokenReserves() * 10 ** (36 - ERC20(baseToken).decimals()) / fractionalTokenReserves();
This will multiple baseTokenReserves
by 1e18, and then additionally by the gap between 1e18 and its own decimals count, which will result in the correct decimals value for the outputted price.
#0 - c4-judge
2022-12-28T15:40:25Z
berndartmueller marked the issue as duplicate of #53
#1 - c4-judge
2022-12-28T15:41:10Z
berndartmueller marked the issue as selected for report
#2 - c4-sponsor
2023-01-05T15:44:09Z
outdoteth marked the issue as sponsor confirmed
#3 - outdoteth
2023-01-06T17:14:20Z
Fixed in: https://github.com/outdoteth/caviar/pull/5
Always ensure that the exponent is 18 greater than the denominator.
#4 - C4-Staff
2023-01-25T12:21:47Z
CloudEllie marked the issue as not a duplicate
#5 - C4-Staff
2023-01-25T12:22:11Z
CloudEllie marked the issue as primary issue
π 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
When a new Pair is created, it's checked that an identical pair doesn't already exist:
require(pairs[nft][baseToken][merkleRoot] == address(0), "Pair already exists");
Afterwards, we create the pair symbol, create the pair, add it to the mapping and emit an event.
When we create the pair symbol, we call the following functions to gather NFT information:
string memory baseTokenSymbol = baseToken == address(0) ? "ETH" : baseToken.tokenSymbol(); string memory nftSymbol = nft.tokenSymbol(); string memory nftName = nft.tokenName();
With a custom malicious ERC721, these external calls create the opportunity for reentrancy.
Although there is no way to cause any harm to the protocol logic using this, it will cause multiple identical Create
events to be emitted from the contract, which should not be possible.
We implement an ERC721 where one of the functions calls back to Caviar.sol to reenter and create a new pair.
... function tokenSymbol() external returns (string memory) { if (count == TIMES_TO_CALL) return; Caviar(msg.sender).create(address(this), BASE_TOKEN, MERKLE_ROOT); count++; return "NFT"; }
This will result in the create()
function being called with the same parameters TIMES_TO_CALL
times. Each time the pair address will overwrite the previous time, but the event will be emitted each time, causing confusion and possibly disturbing the front end.
Manual Review
Move the requirement check down immediately above where the pair is saved in the mapping, so the flow is:
Although this doesn't conform to the typical rule of "checks - effects - interactions", you aren't able to use that rule because you need the pair address in order to save the information in storage. This accomplishes the same thing in this specific situation, ensuring that the check isn't bypassed when it shouldn't be.
#0 - c4-sponsor
2023-01-05T15:40:25Z
outdoteth marked the issue as disagree with severity
#1 - berndartmueller
2023-01-10T16:52:38Z
Downgrading to QA (NC) due to the C4 judging criteria (https://docs.code4rena.com/awarding/judging-criteria#estimating-risk):
QA (Quality Assurance) Includes both Non-critical (code style, clarity, syntax, versioning, off-chain monitoring (events, etc) ...
#2 - c4-judge
2023-01-10T16:52:55Z
berndartmueller changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-01-16T11:45:18Z
berndartmueller marked the issue as grade-b