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: 123/243
Findings: 2
Award: $11.12
🌟 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
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L224 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L236 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L193-L198
Attacker is able to bypass the check for tokensMintedPerAddress
(number of tokens minted per address) during public-sale (phase 2) and due to that he is able to mint all the available NFTs (number of tokens remaining until totalSupply) in 1 transaction.
MinterContract#mint
function and it wants to mint 1 NFT (assume maxCollectionPurchases
is 1).gencore.mint(mintIndex, mintingAddress, _mintTo, tokData, _saltfun_o, col, phase);
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) { tokensMintedAllowlistAddress[_collectionID][_mintingAddress] = tokensMintedAllowlistAddress[_collectionID][_mintingAddress] + 1; } else { tokensMintedPerAddress[_collectionID][_mintingAddress] = tokensMintedPerAddress[_collectionID][_mintingAddress] + 1; } } }
function _mintProcessing(uint256 _mintIndex, address _recipient, string memory _tokenData, uint256 _collectionID, uint256 _saltfun_o) internal { tokenData[_mintIndex] = _tokenData; collectionAdditionalData[_collectionID].randomizer.calculateTokenHash(_collectionID, _mintIndex, _saltfun_o); tokenIdsToCollectionIds[_mintIndex] = _collectionID; _safeMint(_recipient, _mintIndex); }
_mintProcessing
calls safeMint
which triggers the function onERC721Received
of transaction-caller (if caller is a contract)onERC721Received
function of caller-contract, and it re-enters to MinterContract#mint
function.Manual Review
Change NexGenCore#mint function like this:
--- a/smart-contracts/NextGenCore.sol +++ b/smart-contracts/NextGenCore.sol @@ -190,12 +190,12 @@ contract NextGenCore is ERC721Enumerable, Ownable, ERC2981 { 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) { 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-16T13:33:12Z
141345 marked the issue as duplicate of #51
#1 - c4-pre-sort
2023-11-16T13:33:24Z
141345 marked the issue as duplicate of #51
#2 - c4-pre-sort
2023-11-26T13:59:59Z
141345 marked the issue as duplicate of #1742
#3 - c4-judge
2023-12-08T16:37:11Z
alex-ppg marked the issue as satisfactory
#4 - c4-judge
2023-12-08T16:40:27Z
alex-ppg marked the issue as partial-50
#5 - c4-judge
2023-12-08T19:17:22Z
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#L57-L61 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L104-L120
Participant calls participateToAuction
and winner calls claimAuction
, if both transactions are executed at auctionEndTime
(in same block where block.timestamp = auctionEndTime), if the transaction of winner is executed earlier, the participant will lost their funds and the funds will be locked in contract for ever.
Let's take a look at participateToAuction
:
// @audit `participateToAuction` doesn't check if the auction is already claimed or not function participateToAuction(uint256 _tokenid) public payable { require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true); // ... }
1. The above condition lets Participant's transaction to be executed at auctionEndTime
(because it checks block.timestamp <=
auctionEndTime and not <
)
And look at claimAuction
:
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); // ...
2. The above condition lets Winner's claim transaction to be executed at auctionEndTime
(because it checks block.timestamp >=
auctionEndTime and not >
)
So what we get from 1 and 2 ? if the transaction of a participant and the transaction of winner are executed in same block (for example block N
where timestamp of block N
is equal to auctionEndTime
) -> then if transaction of winner executes earlier, the participant has lost their funds forever. (In other words, Winner transaction can be executed at x
and Participate transaction can be executed at x
, so if Winner claim transaction is executed earlier, it means auction is claimed, so which auction does the participant participate in? While the auction has been claimed by the winner)
Scenario:
N
and timestamp for block N
is 123456123456
in block N
(it means, auctionEndTime = 123456 = block.timestamp)claimAuction
and Alice (a participant) calls participateToAuction
.N
) and transaction of Winner executes earlier, Alice funds will be lost forever (because the both transactions are allowed to be executed at exactly auctionEndTime
) and there is no way for Alice for get their funds back (because the auction is already claimed and there is no any function to cancel the bid after auction ends).Manual Review
Add the following line in participateToAuction
:
diff --git a/smart-contracts/AuctionDemo.sol b/smart-contracts/AuctionDemo.sol index 95533fb..435c0cd 100644 --- a/smart-contracts/AuctionDemo.sol +++ b/smart-contracts/AuctionDemo.sol @@ -55,6 +55,7 @@ contract auctionDemo is Ownable { // participate to auction function participateToAuction(uint256 _tokenid) public payable { + require(auctionClaim[_tokenid] == false, "It's too late"); require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true); auctionInfoStru memory newBid = auctionInfoStru(msg.sender, msg.value, true); auctionInfoData[_tokenid].push(newBid);
Invalid Validation
#0 - c4-pre-sort
2023-11-15T09:39:45Z
141345 marked the issue as duplicate of #1172
#1 - c4-pre-sort
2023-11-27T10:55:42Z
141345 marked the issue as not a duplicate
#2 - c4-pre-sort
2023-11-27T10:55:52Z
141345 marked the issue as duplicate of #962
#3 - c4-judge
2023-12-02T15:33:32Z
alex-ppg marked the issue as not a duplicate
#4 - c4-judge
2023-12-02T15:35:45Z
alex-ppg marked the issue as duplicate of #1926
#5 - c4-judge
2023-12-08T18:52:47Z
alex-ppg marked the issue as satisfactory
#6 - c4-judge
2023-12-09T00:21:41Z
alex-ppg changed the severity to 2 (Med Risk)