← All sample reports
sample-org/contracts/GenesisMint.sol·312 LOC · scanned May 20, 2026, 8:21 PM
1 HIGH2 MED3 LOW4 INFO

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.

Recommendation

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.

  1. 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.solvulnerable
    function _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
  2. 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.solvulnerable
    require(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
  3. 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.solvulnerable
    require(allowlist[tx.origin], 'not allowlisted');
    Fix: Replace tx.origin with msg.sender. tx.origin should never be used for authentication.
    tx-originCWE-477
  4. 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.solvulnerable
    royaltyReceiver.transfer(royalty);
    Fix: Use .call{value: x}('') with a gas-limit check, or implement pull payments where receiver claims.
    gas-grief
  5. 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.solvulnerable
    for (uint256 i; i < qty; i++) _safeMint(msg.sender, ++tokenId);
    Fix: require(tokenId + qty <= MAX_SUPPLY) BEFORE the loop.
    missing-input-validation
  6. 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
  7. INFOF-07 · pragmaGenesisMint.sol:1

    Pragma version floats

    pragma solidity ^0.8.24

    GenesisMint.solvulnerable
    pragma solidity ^0.8.24;
    Fix: Pin to 0.8.26.
    pragma
  8. 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.solvulnerable
    if (roll < 5) return LEGENDARY;
    Fix: uint8 public constant LEGENDARY_THRESHOLD = 5; (and friends).
    style
  9. 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.solvulnerable
    return bytes(baseURI).length > 0 ? ... : "";
    Fix: require(bytes(baseURI).length > 0, 'baseURI unset') in tokenURI(), or initialize in constructor.
    metadata
  10. 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.solvulnerable
    payable(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.