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: 72/120
Findings: 1
Award: $31.00
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: AkshaySrivastav
Also found by: 0x5rings, 0xbepresent, ABA, Bauchibred, BenRai, DadeKuma, ElKu, RaymondFam, Rolezn, adriro, btk, chaduke, devscrooge, dingo2077, minhtrng, nemveer, p0wd3r, rbserver, ulqiorra
30.9954 USDC - $31.00
It is a good practice to give time for users to react and adjust to critical changes with a mandatory time window between them. The first step merely broadcasts to users that a particular change is coming, and the second step commits that change after a suitable waiting period. This allows users that do not accept the change to withdraw within the grace period. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate (less risk of a malicious Owner making any malicious or ulterior intention). Specifically, privileged roles could use front running to make malicious changes just ahead of incoming transactions, or purely accidental negative effects could occur due to the unfortunate timing of changes.
Considder implementing a time look feature for chritical parameter that influance the calculation of the amound a user needs to pay to the pool or reseives from the pool like the following:
function setProtocolFeeRate(uint16 _protocolFeeRate) public onlyOwner
function setVirtualReserves(uint128 newVirtualBaseTokenReserves, uint128 newVirtualNftReserves) public onlyOwner function setFeeRate(uint16 newFeeRate) public onlyOwner function setPayRoyalties(bool newPayRoyalties) public onlyOwner
Indexing event fields makes the field more quickly accessible to off-chain tools that parse events. However, note that each index field costs extra gas during emission, so it’s not necessarily best to index the maximum allowed per event (threefields). Each event should use three indexed fields if there are three or more fields, and gas usage is not particularly of concern for the events in question. If there are fewer than three fields, all of the fields should be indexed.
There are 12 instenses of this issue:
event Create(address indexed privatePool, uint256[] tokenIds, uint256 baseTokenAmount);
event Initialize(address indexed baseToken, address indexed nft, uint128 virtualBaseTokenReserves, uint128 virtualNftReserves, uint56 changeFee, uint16 feeRate, bytes32 merkleRoot, bool useStolenNftOracle, bool payRoyalties); event Buy(uint256[] tokenIds, uint256[] tokenWeights, uint256 inputAmount, uint256 feeAmount, uint256 protocolFeeAmount, uint256 royaltyFeeAmount); event Sell(uint256[] tokenIds, uint256[] tokenWeights, uint256 outputAmount, uint256 feeAmount, uint256 protocolFeeAmount, uint256 royaltyFeeAmount); event Deposit(uint256[] tokenIds, uint256 baseTokenAmount); event Withdraw(address indexed nft, uint256[] tokenIds, address token, uint256 amount); event Change(uint256[] inputTokenIds, uint256[] inputTokenWeights, uint256[] outputTokenIds, uint256[] outputTokenWeights, uint256 feeAmount, uint256 protocolFeeAmount); event SetVirtualReserves(uint128 virtualBaseTokenReserves, uint128 virtualNftReserves); event SetMerkleRoot(bytes32 merkleRoot); event SetFeeRate(uint16 feeRate); event SetUseStolenNftOracle(bool useStolenNftOracle); event SetPayRoyalties(bool payRoyalties);
As a good practice an interface Import should come from the main file and not from a file that imports the interface itselve:
import {IERC3156FlashBorrower} from "openzeppelin/interfaces/IERC3156FlashLender.sol"
Use a comment to make it clear that if the base token address is 0, ETH is used as a base token. This would make the code easier to understand.
When assigning values to address state variables there is no check for address(0x0). This can be especially problematic in the constructor of PrivatePool.sol since the address state variables for factory, royaltyRegistry and stolenNftOracle are immutable and can not be changed. This can lead to lose of functionality in the case of a wrong royaltyRegistry and stolenNftOracle or even to lose of the protocol fees for the factory owner in case of the factory address. Loosing functionality might lead to the need of redeploying the pool and therefore to additional costs for removing all NFTs and baseTokens from the pool and redeploying the pool the right way.
252: // add the royalty fee amount to the net input aount
574: Sets the whether or not to use the stolen NFT oracle
Since the royalties are in % of the sales price they can never be higher than the sales price. Check and definition of custom error can be removed for lower deployment gas
801: if (royaltyFee > salePrice) revert InvalidRoyaltyFee(); 79: error InvalidRoyaltyFee();
There is no tracking of the fees the pool has earned and since the owner can set the virtualBaseTokenReserves freely without been restricted by the base tokens held by the pool there is no way to calculate the fees earned so far. (balanceOfContract – virtualBaseTokenReserves can not be used to calculate the fees earned). This makes it inconvenient for the owner of the pool to withdraw the earned fees. I would suggest to add a variable that tracks the fees earned by the pool and that is set to zero when the owner withdraws equal or more than the fees earned.
The variables of the addresses stolenNftOracle and royaltyRegistry are immutable. In case the addresses of those two variables need to be changed (eg. because one needs to use an other provider of this service due to the current service not working any more) there is no possibility to do so. In this case the owner would need to withdraw all funds/nfts from the pool and redeploy it with the new addresses and the address of the pool would change. Therefore I suggest removing the immutable keyword for this variables and adding setter functions for both to be more flexible.
The fees for the function change() are calculated based on the inputweight of the NFTs the user is putting into the pool. 414: (feeAmount, protocolFeeAmount) = changeFeeQuote(inputWeightSum); This is contra intuitive and can lead to a bad user experience. If the user puts in NFTs that are more valuable than the NFTs he gets out the pool already profits from that. If the user in addition has to pay fees based on the value of the NFT he puts in he is disadvantaged twice. Therefore I would suggest to base the calculation of the fees for the change() function on the outputWeightSum not the inputWeightSum.
For the functions buy(), sell(), change(), deposit() and withdraw() there is no check if the alleged current owner really owns the NFTs in the tokenIds array. To save gas in case one of the Ids is not owned by the alleged owner (what would result in the whole transaction to fail) I suggest adding a check if the alleged owner really owns the Nft and if not fail the transaction early. Such a check is already implemented in the function flashLoan() with the call of the function availableForFlashLoan().
#0 - GalloDaSballo
2023-04-30T19:47:29Z
NC
Ignoring
R
NC
L
NC
Typo
Invalid
R
Ignoring
L
NC
#1 - GalloDaSballo
2023-05-01T07:34:46Z
2L from dups
#2 - GalloDaSballo
2023-05-01T07:36:54Z
2L 2R 4NC
4L 2R 4NC
#3 - c4-judge
2023-05-01T09:17:01Z
GalloDaSballo marked the issue as grade-b