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
Rank: 9/120
Findings: 2
Award: $1,107.77
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: AkshaySrivastav
Also found by: 0xTheC0der, Dug, GT_Blockchain, Haipls, adriro, bin2chen, carlitox477, dingo2077, fs0c, hasmama, hihen, holyhansss_kr, juancito, ladboy233, philogy, saian, said, sashik_eth, yixxas, zion
10.8554 USDC - $10.86
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
Malicious users can gain ownership of a PrivatePool and steal all assets deposited in the pool by front-running Factory.create()
.
Suppose a user creates his own PrivatePool and deposits assets using the following script:
Factory.predictPoolDeploymentAddress(salt_u)
to get the address of the pool to create, assuming it is pool_u
.Factory.create(..., salt_u, ...)
to deploy pool_u
.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
Factory.create()
in step 2 will fail.pool_u.deposit()
in step 3 will succeed.pool_u.withdraw()
to take both the NFTs and base tokens deposited by the user.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
1096.9107 USDC - $1,096.91
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
Any Factory NFTs deposited into a Factory-PrivatePool can have all assets in the corresponding PrivatePools stolen by malicious users.
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:
p1.flashloan()
to borrow the Factory NFT - uint256(p1)
from p1.p1.withdraw()
to withdraw all the base tokens and the factory NFT - uint256(p2)
from p1.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:
p1.flashloan()
to borrow factory NFT - uint256(p2)
from p1.p2.withdraw()
to steal all the base tokens and NFTs in p2.uint256(p2)
to p1.In addition, malicious users can also steal assets in p2 by:
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)