Sample report — NFT mint contract
Realistic AEDSC report on an NFT mint with allowlist. 1 HIGH (weak randomness for rarity roll) + 2 MED + 3 LOW. Standard 2026 NFT-mint bug suite.
F-01 (weak randomness in _rollRarity) is exploitable for rarity sniping if rarity affects future sale floors. Either bring in Chainlink VRF (recommended for genesis drops > $100k) or remove on-chain rarity rolls entirely and reveal off-chain. F-02 and F-03 are fixable in minutes.
- HIGHF-01 · weak-prng + manual reviewGenesisMint.sol:168–175
Weak randomness in _rollRarity() — block.timestamp + msg.sender
Rarity tier is decided on-chain by hashing block.timestamp + msg.sender + tokenId. A miner can simulate the hash in the same block and only include the transaction if it would mint a legendary, dropping it otherwise. A regular user can do the same by submitting from a contract that reverts on non-legendary outcomes. Both attacks are publicly documented since 2021.
GenesisMint.solvulnerablefunction _rollRarity(uint256 tokenId) internal view returns (uint8) { uint256 rand = uint256( keccak256(abi.encodePacked(block.timestamp, msg.sender, tokenId)) ); return uint8(rand % 100); // 0–99 }suggested patch drop-in fix// Replace _rollRarity() entirely with a commit-reveal or VRF callback. // See OpenZeppelin's VRFConsumerBaseV2 example.Fix: Replace with Chainlink VRF (or equivalent). Alternatively, commit-reveal: store keccak256(secret) at mint time, reveal secret + rarity in a second tx. Most clean: pre-reveal images, generate rarity off-chain after sellout, upload metadata once.weak-prngminer-extractable-valueCWE-330 - MEDIUMF-02 · incorrect-equalityGenesisMint.sol:94
msg.value comparison uses == instead of >=
mint() requires msg.value == price. If price is later updated via setPrice() to a lower value but the front-end caches the old value, every mint reverts. Worse, if msg.value sends slightly more (rounding in some wallets), the user loses the excess.
GenesisMint.solvulnerablerequire(msg.value == price, "wrong price");suggested patch drop-in fix- require(msg.value == price, "wrong price"); + require(msg.value >= price, "underpaid"); + if (msg.value > price) { + payable(msg.sender).transfer(msg.value - price); + }Fix: require(msg.value >= price) and refund the excess to msg.sender.incorrect-equality - MEDIUMF-03 · tx-originGenesisMint.sol:118
tx.origin used in allowlist check
allowlist[tx.origin] is checked instead of allowlist[msg.sender]. A malicious contract can phish an allowlisted user into calling its function, which then calls mint() — tx.origin is the user, msg.sender is the contract.
GenesisMint.solvulnerablerequire(allowlist[tx.origin], 'not allowlisted');Fix: Replace tx.origin with msg.sender. tx.origin should never be used for authentication.tx-originCWE-477 - LOWF-04 · calls-with-valueGenesisMint.sol:246
External call to recipient in royalty payout — gas-griefing risk
royalty payout uses .transfer (2300 gas). A contract recipient with a fallback that consumes more than 2300 gas will revert the payout. Not exploitable for theft but causes failed mints.
GenesisMint.solvulnerableroyaltyReceiver.transfer(royalty);Fix: Use .call{value: x}('') with a gas-limit check, or implement pull payments where receiver claims.gas-grief - LOWF-05 · missing-input-validationGenesisMint.sol:133
Missing supply cap check in batchMint()
batchMint(uint256 qty) allows minting qty tokens but only enforces the cap after the loop. A re-entrant call from _safeMint could push past MAX_SUPPLY.
GenesisMint.solvulnerablefor (uint256 i; i < qty; i++) _safeMint(msg.sender, ++tokenId);Fix: require(tokenId + qty <= MAX_SUPPLY) BEFORE the loop.missing-input-validation - LOWF-06 · erc721-receiverGenesisMint.sol:138
ERC-721 _safeMint to contract not checked for receiver interface
_safeMint correctly checks IERC721Receiver, but the contract's mint() is also reachable from a contract that doesn't implement the receiver — token gets stuck.
GenesisMint.solvulnerable_safeMint(msg.sender, tokenId);Fix: Already correct via _safeMint. Add a NatSpec note to the front-end so contract-callers know they need to implement onERC721Received.erc721 - INFOF-07 · pragmaGenesisMint.sol:1
Pragma version floats
pragma solidity ^0.8.24
GenesisMint.solvulnerablepragma solidity ^0.8.24;Fix: Pin to 0.8.26.pragma - INFOF-08 · magic-numberGenesisMint.sol:172
Magic numbers for rarity tiers
rand % 100, then if < 5 legendary, < 25 epic, etc. Hardcoded thresholds make audit + maintenance harder.
GenesisMint.solvulnerableif (roll < 5) return LEGENDARY;Fix: uint8 public constant LEGENDARY_THRESHOLD = 5; (and friends).style - INFOF-09 · uri-emptyGenesisMint.sol:201
tokenURI() returns empty string when baseURI not set
If setBaseURI is forgotten, every token returns '' as URI — wallets show broken metadata.
GenesisMint.solvulnerablereturn bytes(baseURI).length > 0 ? ... : "";Fix: require(bytes(baseURI).length > 0, 'baseURI unset') in tokenURI(), or initialize in constructor.metadata - INFOF-10 · centralization-riskGenesisMint.sol:278
Withdraw function uses fixed receiver — not configurable
withdraw() sends to a hardcoded constant. If the team key rotates, ETH is stranded.
GenesisMint.solvulnerablepayable(TEAM_WALLET).transfer(address(this).balance);Fix: Replace constant with a settable treasury address, behind a timelock.centralization-risk
Get one of these for your contract.
Free for the first scan. Founder Pro €29/mo with a 7-day free trial unlocks unlimited scans, priority queue, and a human triaging every report — rate locked for life.