Platform: Code4rena
Start Date: 11/08/2022
Pot Size: $40,000 USDC
Total HM: 8
Participants: 108
Period: 4 days
Judge: hickuphh3
Total Solo HM: 2
Id: 152
League: ETH
Rank: 18/108
Findings: 2
Award: $123.05
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: rbserver
Also found by: 0xc0ffEE, CodingNameKiki, Deivitto, Diraco, IllIllI, KIntern_NA, Lambda, Noah3o6, Treasure-Seeker, ignacio, oyc_109, zeesaw
13.1705 USDC - $13.17
https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTCollection.sol#L262-L274 https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTDropCollection.sol#L171-L187
When calling the following _mint
or mintCountTo
function for minting an NFT of a NFT collection or NFT drop collection, the OpenZeppelin's ERC721Upgradeable
contract's _mint
function is used to mint the NFT to a receiver. If such receiver is a contract that does not support the ERC721 protocol, the NFT will be locked and cannot be retrieved.
https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTCollection.sol#L262-L274
function _mint(string calldata tokenCID) private onlyCreator returns (uint256 tokenId) { require(bytes(tokenCID).length != 0, "NFTCollection: tokenCID is required"); require(!cidToMinted[tokenCID], "NFTCollection: NFT was already minted"); unchecked { // Number of tokens cannot overflow 256 bits. tokenId = ++latestTokenId; require(maxTokenId == 0 || tokenId <= maxTokenId, "NFTCollection: Max token count has already been minted"); cidToMinted[tokenCID] = true; _tokenCIDs[tokenId] = tokenCID; _mint(msg.sender, tokenId); emit Minted(msg.sender, tokenId, tokenCID, tokenCID); } }
https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTDropCollection.sol#L171-L187
function mintCountTo(uint16 count, address to) external onlyMinterOrAdmin returns (uint256 firstTokenId) { require(count != 0, "NFTDropCollection: `count` must be greater than 0"); unchecked { // If +1 overflows then +count would also overflow, unless count==0 in which case the loop would exceed gas limits firstTokenId = latestTokenId + 1; } latestTokenId = latestTokenId + count; require(latestTokenId <= maxTokenId, "NFTDropCollection: Exceeds max tokenId"); for (uint256 i = firstTokenId; i <= latestTokenId; ) { _mint(to, i); unchecked { ++i; } } }
For reference, OpenZeppelin's documentation for _mint
states: "Usage of this method is discouraged, use _safeMint whenever possible".
The following steps can occur when minting an NFT of a NFT collection or NFT drop collection.
_mint
or mintCountTo
function is called with msg.sender
or the to
input corresponding to a contract.ERC721Upgradeable
contract's _mint
function is called with msg.sender
or to
used in Step 1 as the receiver address.ERC721Upgradeable
contract's _mint
function does not execute the same contract's _checkOnERC721Received
function, it is unknown if the receiving contract inherits from the IERC721ReceiverUpgradeable
interface and implements the onERC721Received
function or not. It is possible that the receiving contract does not support the ERC721 protocol, which causes the minted NFT to be locked.VSCode
https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTCollection.sol#L271 can be changed to the following code.
_safeMint(msg.sender, tokenId);
Also, https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTDropCollection.sol#L182 can be changed to the following code.
_safeMint(to, i);
#0 - HardlyDifficult
2022-08-18T13:45:24Z
Agree will fix.
Generally we are inclined to skip "safe" by default - it can introduce reentrancy & reverting risk and increase gas costs. When a user is making an action to buy or mint an NFT for themselves, it's very clear that they are trying to acquire an NFT - so using safe to ensure that they support NFTs seems like a Low risk concern and we are inclined to avoid potential reentrancy/reverts and save costs for the common user paths.
However in this scenario the part that stood out as different is instead of minting for yourself (the msg.sender) we support minting to an arbitrary to
address, e.g. for an airdrop type use case. Here specifically it does seem that sending to a list of addresses could be error prone, where a contract address without 721 support was incorrectly captured. To guard against that scenario specifically we are moving forward with this change.
Then for consistency we have decided to use safeMint for both collection types because the difference is nuanced.
🌟 Selected for report: Saw-mon_and_Natalie
Also found by: 0x1f8b, 0x52, 0xDjango, 0xNazgul, 0xSmartContract, 0xSolus, 0xackermann, 0xmatt, 0xsolstars, Aymen0909, Bnke0x0, Chom, Deivitto, DevABDee, Dravee, ElKu, IllIllI, JC, Kumpa, Lambda, LeoS, MiloTruck, PwnedNoMore, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, TomJ, Treasure-Seeker, Vexjon, Waze, Yiko, __141345__, apostle0x01, auditor0517, berndartmueller, bin2chen, bobirichman, brgltd, bulej93, c3phas, carlitox477, cccz, cryptphi, csanuragjain, d3e4, danb, delfin454000, durianSausage, erictee, fatherOfBlocks, gogo, iamwhitelights, joestakey, jonatascm, ladboy233, mics, oyc_109, rbserver, ret2basic, robee, rokinot, rvierdiiev, shenwilly, sikorico, simon135, thank_you, wagmi, yash90, zeesaw, zkhorse
109.8795 USDC - $109.88
When calling the createFixedPriceSale
function for creating a fixed price sale drop, the price
input can be set as low as 0, which is also confirmed by the following code comment.
function createFixedPriceSale( address nftContract, uint80 price, uint16 limitPerAccount ) external { ... // Any price is supported, including 0. ... }
When the mintFromFixedPriceSale
function is called, mintCost
, which is uint256(saleConfig.price) * count
, is used for calling the _distributeFunds
function as shown below.
function mintFromFixedPriceSale( address nftContract, uint16 count, address payable buyReferrer ) external payable returns (uint256 firstTokenId) { ... // Calculate the total cost, considering the `count` requested. uint256 mintCost; unchecked { // Can not overflow as 2^80 * 2^16 == 2^96 max which fits in 256 bits. mintCost = uint256(saleConfig.price) * count; } ... // Distribute revenue from this sale. (uint256 totalFees, uint256 creatorRev, ) = _distributeFunds( nftContract, firstTokenId, saleConfig.seller, mintCost, buyReferrer ); ... }
When the _distributeFunds
function is called, mintCost
is further used as the price
input for calling the _getFees
function.
function _distributeFunds( address nftContract, uint256 tokenId, address payable seller, uint256 price, address payable buyReferrer ) internal returns ( uint256 totalFees, uint256 creatorRev, uint256 sellerRev ) { address payable[] memory creatorRecipients; uint256[] memory creatorShares; uint256 buyReferrerFee; (totalFees, creatorRecipients, creatorShares, sellerRev, buyReferrerFee) = _getFees( nftContract, tokenId, seller, price, buyReferrer ); ... // Pay the protocol fee _sendValueWithFallbackWithdraw(getFoundationTreasury(), totalFees, SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT); // Pay the buy referrer fee if (buyReferrerFee != 0) { _sendValueWithFallbackWithdraw(buyReferrer, buyReferrerFee, SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT); emit BuyReferralPaid(nftContract, tokenId, buyReferrer, buyReferrerFee, 0); unchecked { // Add the referrer fee back into the total fees so that all 3 return fields sum to the total price for events totalFees += buyReferrerFee; } } }
In the _getFees
function, mintCost
is used as price
for calculating totalFees = price / PROTOCOL_FEE_DENOMINATOR
. If mintCost
is smaller than PROTOCOL_FEE_DENOMINATOR
, totalFees
will be 0 per Solidity's behavior. This also means that buyReferrerFee
is 0 because buyReferrerFee = totalFees / BUY_REFERRER_PROTOCOL_FEE_DENOMINATOR
.
function _getFees( address nftContract, uint256 tokenId, address payable seller, uint256 price, address payable buyReferrer ) private view returns ( uint256 totalFees, address payable[] memory creatorRecipients, uint256[] memory creatorShares, uint256 sellerRev, uint256 buyReferrerFee ) { // Calculate the protocol fee unchecked { // SafeMath is not required when dividing by a non-zero constant. totalFees = price / PROTOCOL_FEE_DENOMINATOR; } ... if (buyReferrer != address(0) && buyReferrer != msg.sender && buyReferrer != seller && buyReferrer != creator) { unchecked { buyReferrerFee = totalFees / BUY_REFERRER_PROTOCOL_FEE_DENOMINATOR; // buyReferrerFee is always <= totalFees totalFees -= buyReferrerFee; } } }
mintCost
can be small if price
per NFT is set to a low value, including 0, when calling the createFixedPriceSale
function and count
is also set to a low value when calling the mintFromFixedPriceSale
function. This would lead to a case where the protocol and buyer referrer fees are 0. If this is as expected, the documentation can be updated to at least indicate that buyer referrer will receive no fees in this situation. Otherwise, please consider implementing minimum fees for the protocol and buyer referrer.
For the initialize
function of the NFTCollection
contract, the _symbol
input is not further checked. In the NFTDropCollection
contract, require(bytes(_symbol).length > 0, "NFTDropCollection: '_symbol' must be set");
is executed in the initialize
function. To ensure that _symbol
is set as expected, a similar require
statement can be added for the NFTCollection
contract's initialize
function.
https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTCollection.sol#L105-L112
function initialize( address payable _creator, string memory _name, string memory _symbol ) external initializer onlyContractFactory { __ERC721_init(_name, _symbol); _initializeSequentialMintCollection(_creator, 0); }
Comment regarding todo indicates that there is an unresolved action item for implementation, which need to be addressed before protocol deployment.
(totalFees, creatorRecipients, creatorShares, sellerRev, ) = _getFees( nftContract, tokenId, seller, price, // TODO add referral info payable(0) );
When a function has unused named returns and used return statement, these named returns become redundant. To improve readability and maintainability, these variables for the named returns can be removed while keeping the return statements for the functions associated with the following lines.
contracts\NFTCollectionFactory.sol 294: ) external returns (address collection) { 333: ) external returns (address collection) { 372: ) external returns (address collection) {
The following imports are not used. Please consider removing them for better readability and maintainability.
contracts\NFTDropCollection.sol 20: import "./mixins/shared/Constants.sol"; contracts\NFTDropMarket.sol 47: import "./mixins/shared/Constants.sol";
NatSpec comments provide rich code documentation. NatSpec comments are missing for the following functions. Please consider adding them.
contracts\NFTCollection.sol 262: function _mint(string calldata tokenCID) private onlyCreator returns (uint256 tokenId) { contracts\NFTCollectionFactory.sol 386: function _createNFTDropCollection( 448: function _getSalt(address creator, uint256 nonce) private pure returns (bytes32) { contracts\libraries\AddressLibrary.sol 34: function callAndReturnContractAddress(CallWithoutValue memory call) contracts\mixins\collections\SequentialMintCollection.sol 62: function _initializeSequentialMintCollection(address payable _creator, uint32 _maxTokenId) internal onlyInitializing {
#0 - HardlyDifficult
2022-08-18T14:57:55Z
[L-01] PROTOCOL AND BUY REFERRER FEES CAN BE 0 WHEN CALLING createFixedPriceSale
This is working as intended. 1% of 0 is 0.. It is fair to say that our documentation could be more clear about how this would work.
[L-02] _symbol INPUT CAN BE CHECKED
Agree that we were inconsistent with these checks. We have moved the NFTDropCollection requirement into the factory so that both collection types are implemented in a similar fashion, and we went with the factory instead of the collection init in order to follow the best practice of fail fast.
[L-03] UNRESOLVED TODO COMMENT
Agree, will fix.
[N-01] REDUNDANT NAMED RETURNS
Agree, we have opted to use the named returns instead of return ..
. This is more consistent with other code in our repo and saves a bit of on the contract size. We also like named returns as a way of improving natspec, and typechain (particularly when a tuple is returned).
[N-02] UNUSED IMPORTS
Agree, will fix.
[N-03] MISSING NATSPEC COMMENTS
Fair feedback -- for natspec we aim for complete coverage of the public interfaces but for internal/private/libraries we have some gaps in order to reduce redundancy, for those we aim to include comments where things are unclear or not obvious from the function and variable names.