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: 90/243
Findings: 3
Award: $26.77
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: btk
Also found by: 00xSEV, 0x175, 0x180db, 0x3b, 0xAlix2, 0xJuda, 0xpiken, 0xraion, 3th, 836541, Al-Qa-qa, AvantGard, Aymen0909, Beosin, ChrisTina, DarkTower, DeFiHackLabs, EricWWFCP, Kose, Kow, KupiaSec, MrPotatoMagic, Neo_Granicen, PENGUN, PetarTolev, Ruhum, Soul22, SovaSlava, SpicyMeatball, Talfao, The_Kakers, Toshii, Tricko, VAD37, Viktor_Cortess, ZdravkoHr, _eperezok, alexxander, audityourcontracts, ayden, bird-flu, bronze_pickaxe, codynhat, critical-or-high, danielles0xG, degensec, droptpackets, evmboi32, fibonacci, flacko, gumgumzum, ilchovski, immeas, innertia, jacopod, joesan, ke1caM, kk_krish, mojito_auditor, nuthan2x, phoenixV110, pontifex, r0ck3tz, sces60107, seeques, sl1, smiling_heretic, stackachu, t0x1c, trachev, turvy_fuzz, ubl4nk, ustas, xAriextz, xuwinnie, y4y
0.152 USDC - $0.15
Reentrancy in the mint function allows users from allowlist to bypass amount restrictions.
During the minting process in phase 1, the following line of the code in the mint()
function checks how many tokens the user from allowlist has minted and if this amount is not more than allowed:
213,217: require(_maxAllowance >= gencore.retrieveTokensMintedALPerAddress(col, msg.sender) + _numberOfTokens, "AL limit");
The issue is that the rewriting of the number of tokens minted per address is made after the call to the minting function, allowing the receiver of the NFT to call the mint function from the Minter Contract again without changes in the mapping tokensMintedAllowlistAddress
:
function mint(uint256 mintIndex, address _mintingAddress , address _mintTo, string memory _tokenData, uint256 _saltfun_o, uint256 _collectionID, uint256 phase) external { require(msg.sender == minterContract, "Caller is not the Minter Contract"); collectionAdditionalData[_collectionID].collectionCirculationSupply = collectionAdditionalData[_collectionID].collectionCirculationSupply + 1; if (collectionAdditionalData[_collectionID].collectionTotalSupply >= collectionAdditionalData[_collectionID].collectionCirculationSupply) { _mintProcessing(mintIndex, _mintTo, _tokenData, _collectionID, _saltfun_o); if (phase == 1) { //@audit here tokensMintedAllowlistAddress[_collectionID][_mintingAddress] = tokensMintedAllowlistAddress[_collectionID][_mintingAddress] + 1; } else { tokensMintedPerAddress[_collectionID][_mintingAddress] = tokensMintedPerAddress[_collectionID][_mintingAddress] + 1; } } }
As a result the gencore.retrieveTokensMintedALPerAddress(col, msg.sender) will return 0 allowing the user endless minting.
POC for phase 1 is here:
https://gist.github.com/viktorcortess/381f37cf04173528b679fb1f454aedaa
The similar situation with phase 2. Require statements check if gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) + _numberOfTokens <= gencore.viewMaxAllowance(col)
:
} else if (block.timestamp >= collectionPhases[col].publicStartTime && block.timestamp <= collectionPhases[col].publicEndTime) { phase = 2; console.log("publicStartTime"); require(_numberOfTokens <= gencore.viewMaxAllowance(col), "Change no of tokens"); require(gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) + _numberOfTokens <= gencore.viewMaxAllowance(col), "Max"); //@Mid look here mintingAddress = msg.sender; tokData = '"public"'; }
But the tokensMintedPerAddress mapping is rewritten after the call to the _mintProcessing:
if (collectionAdditionalData[_collectionID].collectionTotalSupply >= collectionAdditionalData[_collectionID].collectionCirculationSupply) { _mintProcessing(mintIndex, _mintTo, _tokenData, _collectionID, _saltfun_o); if (phase == 1) { console.log("phase count"); tokensMintedAllowlistAddress[_collectionID][_mintingAddress] = tokensMintedAllowlistAddress[_collectionID][_mintingAddress] + 1; console.log("tokensMintedAllowlistAddress[_collectionID][_mintingAddress]",tokensMintedAllowlistAddress[_collectionID][_mintingAddress]); } else { tokensMintedPerAddress[_collectionID][_mintingAddress] = tokensMintedPerAddress[_collectionID][_mintingAddress] + 1; } }
vs
The rewriting of variables should be performed before calling the _mintProcessing function:
if (phase == 1) { tokensMintedAllowlistAddress[_collectionID][_mintingAddress] = tokensMintedAllowlistAddress[_collectionID][_mintingAddress] + 1; } else { tokensMintedPerAddress[_collectionID][_mintingAddress] = tokensMintedPerAddress[_collectionID][_mintingAddress] + 1; } _mintProcessing(mintIndex, _mintTo, _tokenData, _collectionID, _saltfun_o);
Reentrancy
#0 - c4-pre-sort
2023-11-16T05:48:22Z
141345 marked the issue as duplicate of #51
#1 - c4-pre-sort
2023-11-26T14:02:08Z
141345 marked the issue as duplicate of #1742
#2 - c4-judge
2023-12-08T16:34:19Z
alex-ppg marked the issue as satisfactory
#3 - c4-judge
2023-12-08T16:40:11Z
alex-ppg marked the issue as partial-50
#4 - c4-judge
2023-12-08T19:17:16Z
alex-ppg marked the issue as satisfactory
#5 - c4-judge
2023-12-09T00:18:52Z
alex-ppg changed the severity to 3 (High Risk)
🌟 Selected for report: The_Kakers
Also found by: 00xSEV, 0xAsen, 0xDetermination, 0xJuda, 0xWaitress, 0xhunter, 0xlemon, 0xpiken, Al-Qa-qa, Arabadzhiev, CSL, CaeraDenoir, DarkTower, DeFiHackLabs, Greed, Haipls, MaNcHaSsS, NentoR, NoamYakov, PENGUN, Ruhum, Soul22, SovaSlava, Talfao, Toshii, TuringConsulting, VAD37, Vagner, Valix, Viktor_Cortess, ZdravkoHr, audityourcontracts, btk, codynhat, flacko, funkornaut, glcanvas, gumgumzum, immeas, innertia, ke1caM, lanrebayode77, lsaudit, mrudenko, niki, nmirchev8, openwide, oualidpro, r0ck3tz, rvierdiiev, trachev, yojeff
1.3844 USDC - $1.38
Function claimAuction() uses a loop for sending ether to the auction participants. This makes it prone to a DDoS attack blocking users from receiving of funds.
After the auction is ended winner or admin should call the function claimAuction() where ether is sent within the loop:
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ //@audit who pays for gas require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); auctionClaim[_tokenid] = true; uint256 highestBid = returnHighestBid(_tokenid); address ownerOfToken = IERC721(gencore).ownerOf(_tokenid); address highestBidder = returnHighestBidder(_tokenid); for (uint256 i=0; i< auctionInfoData[_tokenid].length; i ++) { if (auctionInfoData[_tokenid][i].bidder == highestBidder && auctionInfoData[_tokenid][i].bid == highestBid && auctionInfoData[_tokenid][i].status == true) { IERC721(gencore).safeTransferFrom(ownerOfToken, highestBidder, _tokenid); (bool success, ) = payable(owner()).call{value: highestBid}(""); emit ClaimAuction(owner(), _tokenid, success, highestBid); } else if (auctionInfoData[_tokenid][i].status == true) { (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } }
The problem is that contracts also can participate in the auction. And if one of these contracts implements an endless loop in its receive fallback this will lead to the stuck transaction:
contract RevertOnReceive { uint256 x; receive() external payable { // This contract will revert on any incoming ether transfer while (true) { x = x + 1 ; // This creates an endless loop } } }
vs
Avoid sending funds in a loop to unknown addresses. Consider implementing logic where every user withdraws his personal funds.
Loop
#0 - c4-pre-sort
2023-11-15T09:33:37Z
141345 marked the issue as duplicate of #843
#1 - c4-pre-sort
2023-11-16T13:35:41Z
141345 marked the issue as duplicate of #486
#2 - c4-judge
2023-12-01T22:58:18Z
alex-ppg marked the issue as not a duplicate
#3 - c4-judge
2023-12-01T22:58:37Z
alex-ppg marked the issue as duplicate of #1782
#4 - c4-judge
2023-12-08T21:03:03Z
alex-ppg marked the issue as partial-50
🌟 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
When claiming an auction the token is transferred from the owner of the token to the winner but the highest bid i.e. the funds received for the token's sale is sent to the owner of the auction contract, not the owner of the token.
A quote from the contests's invariants:
The highest bidder will receive the token after an auction finishes, the owner of the token will receive the funds and all other participants will get refunded.
Function claimAuction() transfers the token from its owner to the winner and returns the funds of other participants. But the highest bid goes to the owner of the auction contract not the owner of the token.
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); auctionClaim[_tokenid] = true; uint256 highestBid = returnHighestBid(_tokenid); address ownerOfToken = IERC721(gencore).ownerOf(_tokenid); address highestBidder = returnHighestBidder(_tokenid); for (uint256 i=0; i< auctionInfoData[_tokenid].length; i ++) { if (auctionInfoData[_tokenid][i].bidder == highestBidder && auctionInfoData[_tokenid][i].bid == highestBid && auctionInfoData[_tokenid][i].status == true) { IERC721(gencore).safeTransferFrom(ownerOfToken, highestBidder, _tokenid); (bool success, ) = payable(owner()).call{value: highestBid}(""); emit ClaimAuction(owner(), _tokenid, success, highestBid); } else if (auctionInfoData[_tokenid][i].status == true) { (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } }
vs
Variable ownerOfToken should be used:
(bool success, ) = payable(ownerOfToken).call{value: highestBid}("");
Context
#0 - c4-pre-sort
2023-11-16T05:43:09Z
141345 marked the issue as duplicate of #245
#1 - c4-judge
2023-12-08T22:26:16Z
alex-ppg marked the issue as satisfactory
#2 - c4-judge
2023-12-09T00:22:20Z
alex-ppg changed the severity to 2 (Med Risk)