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: 30/120
Findings: 4
Award: $185.58
π Selected for report: 0
π Solo Findings: 0
26.761 USDC - $26.76
https://github.com/code-423n4/2023-04-caviar/blob/main/src/EthRouter.sol#L152 https://github.com/code-423n4/2023-04-caviar/blob/main/src/EthRouter.sol#L219 https://github.com/code-423n4/2023-04-caviar/blob/main/src/EthRouter.sol#L270
in ETHRouter, on sell, deposit and change methods, ETHRouter approves the pool to transfer NFTs from the ETHRouter. For example:
// approve the pair to transfer NFTs from the router ERC721(sells[i].nft).setApprovalForAll(sells[i].pool, true);
So it approves the pool for all token Ids under the NFT. However, it doesn't revoke the approval after the operation is finished. Now this might seem not risky on the surface. However, if any NFTs get stuck in (or sent to) ETHRouter, and because the pool is approvalForAll by ETHRouter, the pool owner could withdraw them via PrivatePool.execute
. Please note that it is easy to get the approval of ETHRouter by doing the following:
In short, the ETHRouter should be permission-less and no contract should have the authority to withdraw tokens from it to mitigate any unknown exploits scenarios that could possibly occur in future.
See sell, deposit and change methods of EthRouter. they don't revoke the approval after the operation is finished.
Manual analysis
Revoke the ERC721's approvalForAll of the pool after the operation (e.g. deposit, sell or change) is finished
#0 - c4-pre-sort
2023-04-20T16:40:12Z
0xSorryNotSorry marked the issue as duplicate of #184
#1 - c4-judge
2023-05-01T19:20:20Z
GalloDaSballo changed the severity to 3 (High Risk)
#2 - c4-judge
2023-05-01T19:21:26Z
GalloDaSballo marked the issue as satisfactory
5.9827 USDC - $5.98
https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L733
PrivatePool.change
method is used to change a set of NFTs that the caller owns for another set of NFTs in the pool. The caller has to pay the pool a change fee. To calculate the change fee, PrivatePool.changeFeeQuote
is used. In the first line of the method, it deducts 4 from baseToken's decimals
// multiply the changeFee to get the fee per NFT (4 decimals of accuracy) uint256 exponent = baseToken == address(0) ? 18 - 4 : ERC20(baseToken).decimals() - 4;
Thus, getting the exponent which is used to calculate the feePerNft. The issue is that it doesn't take into account baseTokens with decimals less than 4. If the baseToken is less than 4, changeFeeQuote will always revert. This makes the PrivatePool.change
method is unusable as it will always revert when calculating the fee.
PrivatePool.changeFeeQuote
methodfunction changeFeeQuote(uint256 inputAmount) public view returns (uint256 feeAmount, uint256 protocolFeeAmount) { // multiply the changeFee to get the fee per NFT (4 decimals of accuracy) uint256 exponent = baseToken == address(0) ? 18 - 4 : ERC20(baseToken).decimals() - 4; uint256 feePerNft = changeFee * 10 ** exponent; feeAmount = inputAmount * feePerNft / 1e18; protocolFeeAmount = feeAmount * Factory(factory).protocolFeeRate() / 10_000; }
PrivatePool.change
method at line 416.// calculate the fee amount (feeAmount, protocolFeeAmount) = changeFeeQuote(inputWeightSum);
Manual analysis
Consider setting the exponent to a fixed value if the decimals is less than 4.
#0 - c4-pre-sort
2023-04-20T15:22:15Z
0xSorryNotSorry marked the issue as duplicate of #858
#1 - c4-judge
2023-05-01T07:14:19Z
GalloDaSballo marked the issue as satisfactory
π Selected for report: AkshaySrivastav
Also found by: 0xRobocop, Koolex, adriro, bin2chen, bshramin, chaduke, cryptonue, rbserver, rvierdiiev, saian, sashik_eth, tallo
40.7364 USDC - $40.74
https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L211
on PrivatePool.buy
, total royalty fee amount is calculated by adding the royaltyFee for all tokenIds.
for (uint256 i = 0; i < tokenIds.length; i++) { // transfer the NFT to the caller ERC721(nft).safeTransferFrom(address(this), msg.sender, tokenIds[i]); if (payRoyalties) { // get the royalty fee for the NFT (uint256 royaltyFee,) = _getRoyalty(tokenIds[i], salePrice); // add the royalty fee to the total royalty fee amount royaltyFeeAmount += royaltyFee; } }
After that, it adds the royalty fee amount to the net input amount
netInputAmount += royaltyFeeAmount;
Then after transferring the base token from the caller to the contract, It loops through tokenIds again to transfer the royalty fees to their respective recipients only If the recipient's address is not zero
if (royaltyFee > 0 && recipient != address(0)) { if (baseToken != address(0)) { ERC20(baseToken).safeTransfer(recipient, royaltyFee); } else { recipient.safeTransferETH(royaltyFee); } }
The issue is that the buyer will pay extra ether (to the pool) if at least one recipient's address is zero.
When calculating the total royalty fee amount, it doesn't skip the royalty fee for zero recipient's addresses. However, only when transferring the fee, it does. An example:
Note: this is applicable for ERC20 baseTokens as well.
Manual analysis
Skip adding the fee to the total if recipient's address is zero.
#0 - c4-pre-sort
2023-04-20T16:49:48Z
0xSorryNotSorry marked the issue as duplicate of #596
#1 - c4-judge
2023-05-01T07:16:10Z
GalloDaSballo marked the issue as satisfactory
112.1045 USDC - $112.10
https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L301
PrivatePool.sell
method is used to sell NFTs into the pool and transfers base tokens to the caller. The royalty recipient receives royalty fee on sale. However, in case the baseToken is ETH, the royalty recipient could censor (i.e. block) the selling operation to certain pools. Therefore, not allowing the NFT owner to sell his token to those pools.
in PrivatePool.sell
, the following code transfer the royalty fee to the recipient.
// transfer the royalty fee to the recipient if it's greater than 0 if (royaltyFee > 0 && recipient != address(0)) { if (baseToken != address(0)) { ERC20(baseToken).safeTransfer(recipient, royaltyFee); } else { recipient.safeTransferETH(royaltyFee); } }
If the recipient is a contract, it could simply revert upon receiving ether in receive()
method of the recipient's contract.
Manual analysis
Favor pull over push. for example:
#0 - c4-pre-sort
2023-04-20T17:47:20Z
0xSorryNotSorry marked the issue as primary issue
#1 - outdoteth
2023-04-21T16:37:05Z
It's assumed the royalty recipients are generally honest actors (collection founders). If it's not the case then a private pool owner can turn off the payRoyalties
flag and disable royalty payments.
#2 - c4-sponsor
2023-04-21T16:37:11Z
outdoteth marked the issue as sponsor acknowledged
#3 - c4-sponsor
2023-04-21T16:37:16Z
outdoteth marked the issue as disagree with severity
#4 - GalloDaSballo
2023-04-27T06:54:31Z
Looks valid, not convinced by High because no funds are lost, NFTs can be withdrawn
#5 - c4-judge
2023-04-27T06:54:37Z
GalloDaSballo changed the severity to 2 (Med Risk)
#6 - GalloDaSballo
2023-04-27T06:55:41Z
The Warden has shown a privilege that the royalty owner can abuse, by reverting when receiving ETH the royalty recipient can prevent trades from being done
Mitigation would require either transferring WETH or changing to a Pull System
#7 - c4-judge
2023-05-01T07:04:22Z
GalloDaSballo marked issue #263 as primary and marked this issue as a duplicate of 263
#8 - c4-judge
2023-05-01T07:04:43Z
GalloDaSballo marked the issue as satisfactory