Caviar contest - obront's results

A fully on-chain NFT AMM that allows you to trade every NFT in a collection.

General Information

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

Caviar

Findings Distribution

Researcher Performance

Rank: 21/103

Findings: 3

Award: $330.05

QA:
grade-b

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Awards

40.2564 USDC - $40.26

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-376

External Links

Lines of code

https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L417-L428

Vulnerability details

Impact

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.

Proof of Concept

  • A user submits a much larger value of baseTokens than fractionalTokens
  • The math is performed which concludes that their baseTokens submitted should mint X LP tokens, but their fractionalTokens submitted should mint Y LP tokens, where Y < X
  • In the end, Y LP tokens are minted
  • However, the full value of baseTokens and fractionalTokens are taken from the user
  • As a result, the user overpays for their LP Tokens

Tools Used

Manual 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

Findings Information

🌟 Selected for report: obront

Also found by: 0xmuxyz, 8olidity, CRYP70, Tricko, cozzetti, cryptostellar5, koxuan, ktg, ladboy233, yixxas

Labels

bug
2 (Med Risk)
primary issue
selected for report
sponsor confirmed
M-02

Awards

239.6304 USDC - $239.63

External Links

Lines of code

https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L387-L392

Vulnerability details

Impact

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.

Proof of Concept

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:

  • We have $1000 USDC in reserves, which at 6 decimals is 1e9
  • We have 1000 fractional tokens in reserve, which at 18 decimals is 1e21
  • The price calculation is 1e9 * 1e18 / 1e21 = 1e6
  • While the value should be 1 token, the 1e6 will be interpreted as just 1/1e12 tokens if we expect the price to be in 1e18

Tools Used

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

Awards

50.16 USDC - $50.16

Labels

bug
disagree with severity
downgraded by judge
grade-b
QA (Quality Assurance)
Q-09

External Links

Lines of code

https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Caviar.sol#L28-L43

Vulnerability details

Impact

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.

Proof of Concept

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.

Tools Used

Manual Review

Move the requirement check down immediately above where the pair is saved in the mapping, so the flow is:

  • get the token data
  • create the pair
  • check to ensure the mapping doesn't already contain the pair
  • add the pair to the mapping
  • emit the event

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter