Caviar Private Pools - hihen's results

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

General Information

Platform: Code4rena

Start Date: 07/04/2023

Pot Size: $47,000 USDC

Total HM: 20

Participants: 120

Period: 6 days

Judge: GalloDaSballo

Total Solo HM: 4

Id: 230

League: ETH

Caviar

Findings Distribution

Researcher Performance

Rank: 9/120

Findings: 2

Award: $1,107.77

🌟 Selected for report: 1

🚀 Solo Findings: 0

Awards

10.8554 USDC - $10.86

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-419

External Links

Lines of code

https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/Factory.sol#L71 https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/Factory.sol#L168

Vulnerability details

Impact

Malicious users can gain ownership of a PrivatePool and steal all assets deposited in the pool by front-running Factory.create().

Proof of Concept

Suppose a user creates his own PrivatePool and deposits assets using the following script:

  1. Call Factory.predictPoolDeploymentAddress(salt_u) to get the address of the pool to create, assuming it is pool_u.
  2. Send a transaction Factory.create(..., salt_u, ...) to deploy pool_u.
  3. Send a transaction pool_u.deposit() to deposit NFT and base tokens.

An attacker can call Factory.create(..., salt_u, ...) before step 2 above to preemptively deploy pool_u, resulting in

  • The attacker becomes the owner of pool_u.
  • The user's tx - Factory.create() in step 2 will fail.
  • The user's tx -pool_u.deposit() in step 3 will succeed.
  • The attacker can then call pool_u.withdraw() to take both the NFTs and base tokens deposited by the user.

Tools Used

VS Code

We can prevent this attack by including the creator address in the pool contract address generation algorithm.

Here is an implementation to fix this:

diff --git a/src/Factory.sol b/src/Factory.sol index 09cbb4e..6e21ba6 100644 --- a/src/Factory.sol +++ b/src/Factory.sol @@ -89,7 +89,8 @@ contract Factory is ERC721, Owned { } // deploy a minimal proxy clone of the private pool implementation - privatePool = PrivatePool(payable(privatePoolImplementation.cloneDeterministic(_salt))); + bytes32 safeSalt = keccak256(abi.encodePacked(_salt, msg.sender)); + privatePool = PrivatePool(payable(privatePoolImplementation.cloneDeterministic(safeSalt))); // mint the nft to the caller _safeMint(msg.sender, uint256(uint160(address(privatePool)))); @@ -165,7 +166,8 @@ contract Factory is ERC721, Owned { /// @notice Predicts the deployment address of a new private pool. /// @param salt The salt that will used on deployment. /// @return predictedAddress The predicted deployment address of the private pool. - function predictPoolDeploymentAddress(bytes32 salt) public view returns (address predictedAddress) { - predictedAddress = privatePoolImplementation.predictDeterministicAddress(salt, address(this)); + function predictPoolDeploymentAddress(bytes32 salt, address creator) public view returns (address predictedAddress) { + bytes32 safeSalt = keccak256(abi.encodePacked(salt, creator)); + predictedAddress = privatePoolImplementation.predictDeterministicAddress(safeSalt, address(this)); } }

#0 - c4-pre-sort

2023-04-20T17:18:04Z

0xSorryNotSorry marked the issue as duplicate of #419

#1 - c4-judge

2023-04-30T08:58:08Z

GalloDaSballo changed the severity to 2 (Med Risk)

#2 - c4-judge

2023-05-01T07:23:09Z

GalloDaSballo marked the issue as satisfactory

Findings Information

🌟 Selected for report: hihen

Also found by: tanh

Labels

bug
2 (Med Risk)
downgraded by judge
high quality report
primary issue
selected for report
sponsor confirmed
M-12

Awards

1096.9107 USDC - $1,096.91

External Links

Lines of code

https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/PrivatePool.sol#L157 https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/PrivatePool.sol#L623 https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/PrivatePool.sol#L514

Vulnerability details

Impact

Any Factory NFTs deposited into a Factory-PrivatePool can have all assets in the corresponding PrivatePools stolen by malicious users.

Proof of Concept

Suppose there are two PrivatePools p1 and p2, p1.nft = address(Factory), and uint256(p1) and uint256(p2) are deposited into p1. Malicious users can use flashloan() to steal all the base tokens in p1 and p2:

  1. Call p1.flashloan() to borrow the Factory NFT - uint256(p1) from p1.
  2. In the flashloan callback, call p1.withdraw() to withdraw all the base tokens and the factory NFT - uint256(p2) from p1.
  3. Return uint256(p1) to p1.

Suppose there are two PrivatePools p1 and p2, p1.nft = address(Factory), and uint256(p2) is deposited into p1. Malicious users can use flashloan() to steal all the base tokens and NFTs in p2:

  1. Call p1.flashloan() to borrow factory NFT - uint256(p2) from p1.
  2. In the flashloan callback, call p2.withdraw() to steal all the base tokens and NFTs in p2.
  3. Return uint256(p2) to p1.

In addition, malicious users can also steal assets in p2 by:

  1. p1.buy(uint256(p2))
  2. p2.withdraw(...)
  3. p1.sell(uint256(p2).

Tools Used

VS Code

To prevent users from misusing the protocol and causing financial losses, we should prohibit the creation of PrivatePools with the Factory NFT:

diff --git a/src/PrivatePool.sol b/src/PrivatePool.sol index 75991e1..14ec386 100644 --- a/src/PrivatePool.sol +++ b/src/PrivatePool.sol @@ -171,6 +171,8 @@ contract PrivatePool is ERC721TokenReceiver { // check that the fee rate is less than 50% if (_feeRate > 5_000) revert FeeRateTooHigh(); + require(_nft != factory, "Unsupported NFT"); + // set the state variables baseToken = _baseToken; nft = _nft;

#0 - c4-pre-sort

2023-04-18T07:15:13Z

0xSorryNotSorry marked the issue as high quality report

#1 - c4-pre-sort

2023-04-20T19:04:24Z

0xSorryNotSorry marked the issue as primary issue

#2 - c4-sponsor

2023-04-22T09:04:37Z

outdoteth marked the issue as sponsor confirmed

#3 - outdoteth

2023-04-24T17:12:57Z

Fixed in: https://github.com/outdoteth/caviar-private-pools/pull/14

Proposed fix is to add a check that ensures that the nft which is used in the private pool is not a private pool NFT from the factory contract.

// check that the nft is not a private pool NFT
if (_nft == factory) revert PrivatePoolNftNotSupported();

#4 - GalloDaSballo

2023-04-30T09:00:38Z

Judging severity on this finding is contingent on determining if using the factory as NFT is an external requirement or not

#5 - GalloDaSballo

2023-05-01T06:53:26Z

Will seek advice from another Judge, but saying "if they do it, they lose everything" doesn't sound like an external requirement

#6 - c4-judge

2023-05-01T06:53:31Z

GalloDaSballo marked the issue as selected for report

#7 - GalloDaSballo

2023-05-04T09:30:07Z

After further thinking, I believe the finding has shown a vulnerability in how the pool may be used for composability.

I believe that similar "vault like" NFTs may be subject to the same risks, there's another contest happening at this time that may also be subject to the same risk

Those instances, would mostly be categorized as Medium Severity, because the implementations are not known

After discussing with additional judges, given that there are a category of NFTs that should not be Flashloaned (e.g. UniV3, Factory, other factories, etc..) believe it is most appropriate to judge the finding as Medium Severity, with the additional warning that similar "vault like" NFTs should also be examined with care.

The risk doesn't apply to ordinary collections

#8 - c4-judge

2023-05-05T10:18:06Z

GalloDaSballo changed the severity to 2 (Med Risk)

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