Closed code423n4 closed 1 year ago
0xSorryNotSorry marked the issue as primary issue
Requires the user to make a mistake in their inputs.
outdoteth marked the issue as disagree with severity
outdoteth marked the issue as sponsor confirmed
Fixed in: https://github.com/outdoteth/caviar-private-pools/pull/3
Proposed fix is to remove the nft
field from the Buy, Sell, and Change structs and instead replace it with a direct call to
PrivatePool or Pair to fetch the nft
using the nft() method.
// fetch the nft address (PrivatePool and Pair both have an nft() method)
address nft = PrivatePool(buys[i].pool).nft();
GalloDaSballo changed the severity to QA (Quality Assurance)
Downgrading to QA because the user would have to order the wrong NFT from the incorrect Pool
I believe the finding is worth fixing, am awarding it 3 bonus points
L + 3
GalloDaSballo marked the issue as grade-b
Lines of code
https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/EthRouter.sol#L49-L50 https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/EthRouter.sol#L59-L60 https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/EthRouter.sol#L70-L71 https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/EthRouter.sol#L219-L222
Vulnerability details
Impact
The functions buy(), sell(), deposit(), and change() in EthRouter take the pool and NFT address as input. However, in reality,
PrivatePool(pool).nft()
may not match the NFT address passed in the transaction parameters:https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/EthRouter.sol#L48-L50
This can result in ordinary users losing their NFTs.
Proof of Concept
Consider a scenario where there are two private pools:
Both collections have an NFT with
id=123
, and in both pools, themerkleRoot
is equal to zero (i.e., the weights of all NFTs are considered equal and the Merkle tree is not used):https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/PrivatePool.sol#L666-L669
A trader wants to buy an expensive NFT from the 0xRICH collection with
id=123
and calls the EthRouter.buy() function, but mistakenly passes the following structure:The EthRouter successfully spends part of the client's money and buys the expensive NFT from the 0xRICH collection in the POOL_RICH pool:
https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/EthRouter.sol#L129-L132
In a normal situation, without the involvement of a hacker, this transaction will be reverted because EthRouter will attempt to transfer not the expensive NFT with id=123, but the NFT with the same ID from the cheaper 0xPOOR collection accidentally passed by the trader in the arguments (
buys[i].nft = 0xPOOR
):https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/EthRouter.sol#L134-L137
But, in our case, the hacker sandwiches the victim's transaction and buys the NFT with id=123 from the 0xPOOR collection for cheap and transfers it to the EthRouter.
Thus, at the moment of the victim's transaction execution, the EthRouter already holds the NFT 0xPOOR with the same ID. Therefore, the transfer of the NFT in the victim's transaction is successful, and the victim receives a cheap NFT from the 0xPOOR collection in their wallet, while the expensive NFT 0xRICH remains on the EthRouter.
Immediately after the user's transaction, the hacker withdraws the expensive NFT from the EthRouter to their own wallet. The hacker can do this in many different ways.
The first way to withdraw the NFT involves using the same vulnerability related to the mismatch of NFT addresses. The hacker creates POOL_FAKE, which trades the 0xFAKE NFT for a fake tokens, and calls buy():
As a result of this operation, the hacker receives a super expensive NFT 0xRICH with ID=123, on which the victim spent a lot of money, and a fake NFT from the hacker remains on EthRouter.
There is another way to remove NFT from EthRouter - to use the infinite approves that EthRouter gives out. The hacker creates an empty hackerPool with the 0xRICH collection, and calls deposit() with an empty list of NFTs from the 0xRICH collection:
In this case, EthRouter sets the hackerPool as an operator for the 0xRICH NFT:
https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/EthRouter.sol#L244
Then the hacker calls
hackerPool.execute()
with arguments that invokeIERC721(0xRICH).transferFrom(ethRouter, hackerAddress, 123)
.Result. A realistic scenario is described in which the victim loses a super valuable NFT and instead receives an unnecessary cheap NFT, so the vulnerability is assigned a HIGH status.
Recommended Mitigation Steps
It is recommended to take the NFT collection address from the parameters of the pool itself or to verify that they match.