Platform: Code4rena
Start Date: 30/10/2023
Pot Size: $49,250 USDC
Total HM: 14
Participants: 243
Period: 14 days
Judge: 0xsomeone
Id: 302
League: ETH
Rank: 69/243
Findings: 3
Award: $63.90
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: bird-flu
Also found by: 00decree, 0xAadi, AS, Audinarey, DeFiHackLabs, Eigenvectors, Fitro, Hama, Kaysoft, Krace, REKCAH, SovaSlava, The_Kakers, Viktor_Cortess, cartlex_, degensec, devival, evmboi32, funkornaut, jacopod, openwide, peanuts, rotcivegaf, smiling_heretic, xAriextz, xiao
25.2356 USDC - $25.24
Protocol will get the highest bid amount instead of the NFT owner during the auction process.
In claimAuction()
, the owner of the ERC721 token is set as ownerOfToken
.
address ownerOfToken = IERC721(gencore).ownerOf(_tokenid);
The winner will get the ERC721 from the ownerOfToken
through the safeTransferFrom function. The contract will then pay owner()
, which is the contract owner, instead of ownerOfToken
, which is the NFT owner.
IERC721(gencore).safeTransferFrom(ownerOfToken, highestBidder, _tokenid); (bool success, ) = payable(owner()).call{value: highestBid}("");
owner()
is used in the Ownable contract, which is also inherited by NextGenCore.sol.
/** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { return _owner; }
This means that the auction money will go to the protocol owner instead of the NFT owner.
If the protocol intends for the money to go to the protocol instead of the NFT owner, the money should not go to the owner of the AuctionDemo contract but rather the MinterContract itself. That way, the amount earned from the auction can be split amongst the artist and the team, like how it is done through mint()
. After the auction, send the money to the minter contract and increase the value of collectionTotalAmount
in the miinter contract. The admin can then call payArtist()
to split the amount accordingly.
collectionTotalAmount[col] = collectionTotalAmount[col] + msg.value;
Manual Review
Change owner()
to ownerOfToken
.
IERC721(gencore).safeTransferFrom(ownerOfToken, highestBidder, _tokenid); - (bool success, ) = payable(owner()).call{value: highestBid}(""); + (bool success, ) = payable(ownerOfToken).call{value: highestBid}("");
or otherwise send the money to the minter contract.
Token-Transfer
#0 - c4-pre-sort
2023-11-14T08:43:05Z
141345 marked the issue as duplicate of #245
#1 - c4-judge
2023-12-08T22:28:41Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: HChang26
Also found by: 0x3b, 0xMAKEOUTHILL, 0xSwahili, 0xarno, ABA, DeFiHackLabs, Eigenvectors, Haipls, Kow, MrPotatoMagic, Neon2835, Nyx, Zac, alexfilippov314, ayden, c3phas, immeas, innertia, lsaudit, merlin, mojito_auditor, oakcobalt, ohm, oualidpro, peanuts, phoenixV110, sces60107, t0x1c, tnquanghuy0512, ubl4nk, volodya, xAriextz
10.9728 USDC - $10.97
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L58 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L105
The latest highest bidder will lose his funds if he participated in the auction after claimAuction()
is called in the same timestamp.
Let's understand how participation and winning works first.
When participating in the auction, the function participateToAuction()
checks whether the bid sent is higher than the highest bid and checks the auction end time. (Take note of the <= sign of the auction end time)
function participateToAuction(uint256 _tokenid) public payable { require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true);
If the current highest bid is 1 ETH, a participant can only bid an amount higher than 1 ETH.
So the participants will keep bidding until the time is > the auction end time. If the end time is 1pm, the participants cannot bid after 1pm, but can still bid at 1pm on the dot.
Next, let's look at how the process works once the auction is over. claimAuction()
is called. The function checks whether the time is >= the auction end time. auctionClaim[_tokenid] == false
means that this function can only be called once.
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true);
If the claimAuction function is 1pm, at 1pm and onwards, the claimAuction()
function can be called. Once claimAuction()
is called, there will be no more refunds to participants. cancelBid()
cannot be called as well.
claimAuction()
to get his ERC721 NFT.claimAuction()
is called, user B calls participateToAuction()
and puts 11 ETH. (time is still 1pm)claimAuction()
is called already and cannot be called again.cancelBid()
anymore.This issue can happen quite often, especially if the auction is a speed auction (only 5 seconds to participate). Also, since NextGen is able to host multiple collection, there can be multiple auction happening, and this issue will definitely surface. Since the highest bidder can also control the claimAuction()
function, he can call the function on the end time and 'frontrun' the next highest bidder, which will lead to the bidder's ETH getting stuck in the contract.
VSCode
A simple fix is to make sure that claimAuction()
can only be called > auction end time, and not >= auction end time. This prevents any errors and only the highest bidder can win the NFT.
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ - require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); + require(block.timestamp > minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true);
DoS
#0 - c4-pre-sort
2023-11-14T23:40:07Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-02T15:33:34Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-02T15:35:47Z
alex-ppg marked the issue as duplicate of #1926
#3 - c4-judge
2023-12-08T18:52:52Z
alex-ppg marked the issue as satisfactory
#4 - c4-judge
2023-12-09T00:21:41Z
alex-ppg changed the severity to 2 (Med Risk)
🌟 Selected for report: MrPotatoMagic
Also found by: 3th, Fulum, JohnnyTime, K42, Kose, SAAJ, Toshii, catellatech, cats, clara, digitizeworx, dimulski, dy, glcanvas, hunter_w3b, ihtishamsudo, niki, peanuts
27.6948 USDC - $27.69
To create another collection, the whole process will restart.
Emoji | Description |
---|---|
:white_check_mark: | Fully Checked, Working as Intended |
:ballot_box_with_check: | Fully Checked, Requires Slight Changes |
:red_circle: | Checked, Something seems off |
Contract | Function | Explanation | Comments | Coverage |
---|---|---|---|---|
MinterContract | setCollectionCosts | Set a Collection Minting Cost | CollectionAdmin, Once set, cannot be changed, collection must be created already | :white_check_mark: |
MinterContract | setCollectionPhases | Set a Collection Start/End Time | CollectionAdmin, Timings are not checked against each other | :ballot_box_with_check: |
MinterContract | airDropTokens | Mints a free NFT to a recipient | Admin, Collection must be created first, checks total limit correctly | :white_check_mark: |
MinterContract | mint | Mints an NFT to a recipient | Payable, users do not get refund, 2 phases and 3 salesOption | :ballot_box_with_check: |
MinterContract | burnToMint | Mints an NFT to a recipient | Payable, initializeBurn must be set to true, only public access, mints 1 only | :ballot_box_with_check: |
MinterContract | mintAndAuction | Mints an NFT to a recipient, set Auction | Admin, mints one NFT per time period, collectionTokenMintIndex repeated | :white_check_mark: |
MinterContract | initializeBurn | Initialize burn to mint | Admin, might be a hassle if there's a lot of users wanting to burnToMint | :white_check_mark: |
MinterContract | initializeExternalBurnOrSwap | Initialize burnOrSwapExternalToMint | Admin, can swap external NFT for a nextGen NFT | :white_check_mark: |
MinterContract | burnOrSwapExternalToMint | Swap external NFT for nextGen NFT | Admin, external NFT will be sent to burnOrSwapAddress | :white_check_mark: |
MinterContract | setPrimaryAndSecondarySplits | Set primary splits | Admin | :white_check_mark: |
MinterContract | proposePrimaryAddressesAndPercentages | Propose primary addresses and percentages | Artist/Admin | :white_check_mark: |
MinterContract | proposeSecondaryAddressesAndPercentages | Propose secondary addresses and percentages | Artist/Admin, unclear of usage in protocol | :ballot_box_with_check: |
MinterContract | acceptAddressesAndPercentages | Accept primary addresses and percentages | Admin | :white_check_mark: |
MinterContract | payArtist | Pay artist and protocol team | Admin, success not checked | :ballot_box_with_check: |
MinterContract | emergencyWithdraw | Withdraw any balance from the contract | Admin, huge centralization (able to withdraw all earnings), success not checked | :ballot_box_with_check: |
MinterContract | getPrice | Get the minting price of collection | Different types of price, descending, ascending, fixed | :white_check_mark: |
NextGenCore | createCollection | Create a Collection | Admin | :white_check_mark: |
NextGenCore | setCollectionData | Set collection data | CollectionAdmin | :white_check_mark: |
NextGenCore | airDropTokens | Airdrops 1 NFT to a recipient | OnlyMinter, airdrops 1 NFT to recipient, supply check works as intended | :white_check_mark: |
NextGenCore | mint | Mint an NFT | OnlyMinter, depends on phase, supply check works as intended | :white_check_mark: |
NextGenCore | burn | Burns an NFT | Public (not sure if intended, requires approval), increases the burn amount | :ballot_box_with_check: |
NextGenCore | burnToMint | Burns an NFT to Mint an NFT | OnlyMinter | :white_check_mark: |
NextGenCore | updateCollectionInfo | Update Collection Info | CollectionAdmin, change collectionInfo, depends on index | :white_check_mark: |
NextGenCore | artistSignature | Signs a collection | CollectionArtistAddress, signs a collection | :white_check_mark: |
NextGenCore | freezeCollection | Freeze a collection | Admin, frozen collection cannot update image, collection, change token data | :white_check_mark: |
AuctionDemo | participateToAuction | Participate in an Auction | before auction end time, must be higher than highest bid | :white_check_mark: |
AuctionDemo | returnHighestBid | Returns the highest bid | If highest bid is withdrawn, the second highest will become highest bid | :white_check_mark: |
AuctionDemo | returnHighestBidder | Returns the highest bidder | Same as highest bid, returns an address | :white_check_mark: |
AuctionDemo | claimAuction | Claim the NFT after an Auction | Winner/Admin, claims the NFT, refunds all participants, some M issues reported | :ballot_box_with_check: |
AuctionDemo | cancelBid | Cancel a bid | Refunds a bid, must be before auction end time | :white_check_mark: |
AuctionDemo | cancelAllBids | Cancel all bids | Cancel and refunds all bids | :white_check_mark: |
mint()
getPrice()
if (collectionPhases[_collectionId].salesOption == 3) { // increase minting price by mintcost / collectionPhases[_collectionId].rate every mint (1mint/period) // to get the price rate needs to be set if (collectionPhases[_collectionId].rate > 0) { return collectionPhases[_collectionId].collectionMintCost + ((collectionPhases[_collectionId].collectionMintCost / collectionPhases[_collectionId].rate) * gencore.viewCirSupply(_collectionId)); } else { return collectionPhases[_collectionId].collectionMintCost;
} else { // fixed price return collectionPhases[_collectionId].collectionMintCost;
Seems like the exponential descending calculation is correct
Seems like the linear descending calculation is correct
MinterContract
Minter Contract
_maxAllowance
is checked, calling the gencore.retrieveTokensMintedALPerAddress
, making sure that the allowlisted address do not mint past the max allowance.Contract | Actor | Description | Severity | Reason |
---|---|---|---|---|
MinterContract | FunctionAdmin | Controls the updates, artists and team splits, and emergency withdraw | Critical | Able to withdraw all earnings |
MinterContract | CollectionAdmin | Sets the collection costs and phase | Low | Artist team |
MinterContract | Artist | Propose Percentages | Low | Artist team |
NextGenCore | FunctionAdmin | Creates Collection , freeze collection, set royalties | Medium | Able to set royalties and freeze collection |
NextGenCore | CollectionAdmin | Update collection info, change metadata | Low | Artist team |
NextGenCore | MinterContract | Interacts with MinterContract to mint, burn etc | None | Contract |
AuctionDemo | Admin | Call claimAuction | Low | Intended Design |
AuctionDemo | Winner | Call claimAuction | Low | Intended Design |
40 hours
#0 - c4-pre-sort
2023-11-27T14:58:23Z
141345 marked the issue as sufficient quality report
#1 - c4-judge
2023-12-07T16:51:05Z
alex-ppg marked the issue as grade-b