NextGen - Jorgect's results

Advanced smart contracts for launching generative art projects on Ethereum.

General Information

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

NextGen

Findings Distribution

Researcher Performance

Rank: 76/243

Findings: 2

Award: $35.61

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 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

Awards

0 USDC - $0.00

Labels

bug
3 (High Risk)
satisfactory
edited-by-warden
duplicate-1323

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L105

Vulnerability details

The AuctionDemo contract allow user to participate in nft auctions, to start an auction admin have to call the mintAndAuction function in minter contract then users can call participateToAuction function in the AunctionDemo contract to participate.

Let´s go straight to the two problem that allow an attacker drain all the contract:

  1. There is a time where the claimAuction colapse with the cancelAllBids allowing call the both function in the same block.timestamp. and this time is exactly the end time of the auction:
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ ---> require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); ... } function cancelBid(uint256 _tokenid, uint256 index) public { ---> require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); ... } function cancelAllBids(uint256 _tokenid) public { ---> require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); ... }

[Link] [Link] [Link]

  1. The status of the bid is not setting to false when a user get refunded via claimAuction:
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); ... 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 {} } }

[Link]

With that been said a malicious user have to:

  1. participate several times in the auction.
  2. make sure tha he won the auction (this can be made it easily if the auction is not "popular" or implementing backrun and front run techniques).
  3. call claimAuction and then call cancelAllBids in the same block.timestamp.

The malicius user is get paid twice one for the refund of the claimAuction and one for the cancelation of the bid, this is onlyPossible if the owner of the nft that is in the auction had already give the approve to the auctionContract which is going to transferFrom the nft from the owner to the winner.

Impact

Malicious user can drain all the contract if he participate several times in a auction making sure that he win.

Proof of Concept

(https://gist.github.com/jorgect207/5731a1bdf59786930178ee580eed3448) The three files in the gist have to be copied in the test file of the nextgen repository and have to install foundry in the hardhat repository

Run the following test in the TestFoundry.sol:

function test_gettingPaidTwoTimes() public { test_setActuation(); vm.deal(user2, 1 ether); // user2 just have 1 ether vm.deal(user3, 100 ether); vm.deal(address(_DossContract), 100 ether); vm.prank(user2); _auctionDemo.participateToAuction{value: 1 ether}(30000000000); vm.prank(user3); _auctionDemo.participateToAuction{value: 3 ether}(30000000000); vm.warp(block.timestamp + 10 days); // forward time to exactly the end of the actuation. //aproving caller vm.prank(user1); _NextGenCore.approve(address(_auctionDemo), 30000000000); //claim auction _auctionDemo.claimAuction(30000000000); vm.prank(user2); _auctionDemo.cancelBid(30000000000, 0); uint256 balanceUser2 = user2.balance; assert(balanceUser2 == 2 ether); // balance is twice thant the first one }

This is a simplification of the attack in this proof of concept user get paid twice, this is only possible if the approve of the nft is already give to the auction contract which is pretty possible. this attack can be scalated to drain all the contract if the attacker participate several times in the auction.

In this cases i made user1 owner of the nft but it can be the admin as well (if he is the owner of the nft).

Tools Used

foundry, manual

I have to say that the claim function go again the good practices consider change to the pull patter, with that been said:

  1. Change the time to claim the nft, insted of require(block.timestamp >= minter.getAuctionEndTime(_tokenid) do this require(block.timestamp > minter.getAuctionEndTime(_tokenid) .

  2. When user is refund via claimAuction function change the status of the bid adding auctionInfoData[_tokenid][index].status = false;.

function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid, this.claimAuction.selector) { require( ... for (uint256 i = 0; i < auctionInfoData[_tokenid].length; i++) { ... } else if (auctionInfoData[_tokenid][i].status == true) { -----> auctionInfoData[_tokenid][index].status = false; (bool success,) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } }

Assessed type

Other

#0 - c4-pre-sort

2023-11-14T15:40:29Z

141345 marked the issue as duplicate of #1904

#1 - c4-pre-sort

2023-11-14T23:31:31Z

141345 marked the issue as duplicate of #962

#2 - c4-judge

2023-12-01T15:46:57Z

alex-ppg marked the issue as not a duplicate

#3 - c4-judge

2023-12-01T15:47:06Z

alex-ppg marked the issue as duplicate of #1788

#4 - c4-judge

2023-12-08T18:17:06Z

alex-ppg marked the issue as satisfactory

Awards

35.614 USDC - $35.61

Labels

bug
2 (Med Risk)
partial-50
duplicate-1275

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L540

Vulnerability details

saleOption == 2 is used to give the below behavior of the price:

decreases exponentially every time period collectionPhases[_collectionId].timePeriod sets the time period for decreasing the mintcost if just public mint set the publicStartTime = allowlistStartTime if rate = 0 exponetialy decrease if rate is set the linear decrase each period per rate

The main problem is that the getPrice function is not entring in the salesOption==2 is the yime is equal to publicEndTime which is the last time in the mint.

Impact

User receive unfair price in the last time of the public mint due that the decreasing in price doesn´t apply.

Proof of Concept

file:2023-10-nextgen/smart-contracts/MinterContract.sol function getPrice(uint256 _collectionId) public view returns (uint256) { ... } else if (collectionPhases[_collectionId].salesOption == 2 && block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp < collectionPhases[_collectionId].publicEndTime){ ... }

[Link]

As you can see block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp < collectionPhases[_collectionId].publicEndTime) the last time of the public mint the contract is not applying the decreasing but users can mint in this time:

file:2023-10-nextgen/smart-contracts/MinterContract.sol function mint(uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o) public payable { ... } else if (block.timestamp >= collectionPhases[col].publicStartTime && block.timestamp <= collectionPhases[col].publicEndTime) { ...} ... }

[Link]

Tools Used

Manual review

Add the last time in the getPrice function :

function getPrice(uint256 _collectionId) public view returns (uint256) { ... } else if (collectionPhases[_collectionId].salesOption == 2 && block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp <= collectionPhases[_collectionId].publicEndTime){ ... }

Assessed type

Other

#0 - c4-pre-sort

2023-11-16T01:42:37Z

141345 marked the issue as duplicate of #1391

#1 - c4-judge

2023-12-08T21:41:50Z

alex-ppg marked the issue as partial-50

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter