Smart contract monitoring setup (Forta, OpenZeppelin Defender)

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Smart contract monitoring setup (Forta, OpenZeppelin Defender)
Medium
from 1 business day to 3 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

NFT Collection Security Audit

NFT contracts appear simpler than DeFi protocols — no complex math, no liquidity pools. In practice, this is false security: NFT collections regularly lose funds due to vulnerabilities in mint logic, reentrancy in ERC-721 transfer callbacks, incorrect randomness, and royalty issues. NFT contract audit is not a formality, it is market requirement: most marketplaces and investors view an audit as baseline.

What is checked in NFT audit

ERC-721 standard and implementation

First block: ERC-721 standard compliance and correct basic implementation. OpenZeppelin or custom contract?

Custom ERC-721 implementation — immediate red flag. If team doesn't use OZ ERC721, thoroughly check:

  • Correctness of safeTransferFrom — calling onERC721Received on recipient contract
  • Correctness of approve and setApprovalForAll logic
  • Protection from transfer to address(0) without intent to burn
  • Correct balanceOf and ownerOf accounting

Reentrancy via ERC-721 callback

This is the most common critical vulnerability in NFT. safeTransferFrom calls onERC721Received on recipient contract — this is external call in middle of transaction.

// VULNERABLE contract
contract VulnerableNFT is ERC721 {
    uint256 public mintPrice = 0.1 ether;
    mapping(address => uint256) public minted;
    
    function mint(uint256 quantity) external payable {
        require(msg.value >= mintPrice * quantity, "Insufficient payment");
        
        for (uint256 i = 0; i < quantity; i++) {
            uint256 tokenId = ++_tokenCounter;
            // safeTransferFrom → calls onERC721Received → reentrancy!
            _safeMint(msg.sender, tokenId);
            // State updated AFTER _safeMint — vulnerable
            minted[msg.sender]++;
        }
    }
}

Attack: attacker creates contract with onERC721Received that calls mint again. If minted counter updated after _safeMint — can exceed mint limit per address.

// FIXED version: Checks-Effects-Interactions
function mint(uint256 quantity) external payable nonReentrant {
    require(msg.value >= mintPrice * quantity, "Insufficient payment");
    require(minted[msg.sender] + quantity <= MAX_PER_WALLET, "Exceeds limit");
    
    // Effects FIRST
    minted[msg.sender] += quantity;
    
    // Interactions AFTER
    for (uint256 i = 0; i < quantity; i++) {
        _safeMint(msg.sender, ++_tokenCounter);
    }
}

Randomness: VRF vs blockhash

On-chain randomness for reveal — frequent vulnerability. block.timestamp, blockhash, prevrandao — all manipulable.

// VULNERABLE: miner/validator can manipulate blockhash
function revealTokens() external onlyOwner {
    for (uint256 i = 1; i <= totalSupply; i++) {
        uint256 randomSeed = uint256(keccak256(abi.encodePacked(
            blockhash(block.number - 1),
            i,
            block.timestamp
        )));
        tokenTraits[i] = _assignTraits(randomSeed);
    }
}

Validator can only call revealTokens in blocks where blockhash gives favorable traits. Not theoretical risk — happened on major collections.

Correct solution: Chainlink VRF v2.

contract SecureNFT is VRFConsumerBaseV2 {
    VRFCoordinatorV2Interface private vrfCoordinator;
    uint64 private subscriptionId;
    bytes32 private keyHash;
    
    uint256 public randomWord;  // obtained from Chainlink
    bool public revealed;
    
    function requestReveal() external onlyOwner {
        require(!revealed, "Already revealed");
        vrfCoordinator.requestRandomWords(
            keyHash,
            subscriptionId,
            3,    // confirmations
            100000, // callbackGasLimit
            1     // numWords
        );
    }
    
    function fulfillRandomWords(uint256, uint256[] memory randomWords) internal override {
        randomWord = randomWords[0];
        revealed = true;
        emit Revealed(randomWord);
    }
    
    function getTraits(uint256 tokenId) public view returns (Traits memory) {
        require(revealed, "Not revealed");
        uint256 seed = uint256(keccak256(abi.encodePacked(randomWord, tokenId)));
        return _assignTraits(seed);
    }
}

Mint logic: whitelist, limits, timing

// Check in audit:

// 1. Merkle whitelist — correct double hashing
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, maxAmount))));
// NOT: keccak256(abi.encodePacked(msg.sender)) — vulnerable to hash collision

// 2. Protection from supply overage
require(totalSupply() + quantity <= MAX_SUPPLY, "Exceeds supply");
// No off-by-one errors?

// 3. Team mint doesn't exceed announced
// Verify that team allocation is really limited

// 4. Front-running protection for whitelist
// Merkle proof doesn't contain amount — can't copy proof to another address?
// Leaf must include msg.sender!

Royalty mechanism: ERC-2981

// Correct ERC-2981 implementation
contract NFTWithRoyalty is ERC721, ERC2981 {
    constructor() {
        // Set royalty: 5% for owner
        _setDefaultRoyalty(msg.sender, 500); // 500 basis points = 5%
    }
    
    // royaltyInfo must return correct values
    // Check: doesn't exceed 100%, correct receiver
    function royaltyInfo(uint256, uint256 salePrice)
        public view override returns (address receiver, uint256 royaltyAmount)
    {
        return super.royaltyInfo(0, salePrice);
    }
    
    // supportsInterface must include ERC2981
    function supportsInterface(bytes4 interfaceId)
        public view override(ERC721, ERC2981) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}

In audit check: royalty receiver not address(0), percentage not anomalously high, no way to change receiver without governance.

Withdraw function

Simple but critical: who can withdraw ETH from contract?

// Check in audit:

// 1. onlyOwner or multisig?
function withdraw() external onlyOwner {
    payable(owner()).transfer(address(this).balance);
}

// 2. No pull payment vulnerabilities?
// 3. ETH can't get stuck in contract on failed transfer?
// Use call instead of transfer:
(bool success, ) = payable(owner()).call{value: address(this).balance}("");
require(success, "Transfer failed");

// 4. No way to drain before reveal/sale via exploit?

Typical findings by severity

Severity Example vulnerability Frequency
Critical Reentrancy in mint allows minting over supply Rare
High Manipulatable randomness for reveal Frequent
High Merkle leaf without msg.sender — proof can be stolen Medium
Medium Withdraw without nonReentrant Frequent
Medium totalSupply overflow with large mint quantity Rare
Low Missing events for critical functions Very frequent
Informational Gas optimization (ERC721A vs ERC721) Always

ERC-721A: audit specifics

Many collections use ERC-721A (Azuki's optimized version for batch mint). Specific checks:

  • Correct _startTokenId() — defaults to 0, but some projects want 1
  • _nextTokenId() correctly initialized
  • Batch transfer doesn't break ownership mapping
  • tokensOfOwner doesn't exceed gas limit for cold wallets with many tokens

Audit process

Static analysis. Run Slither — automatic detection of standard patterns (reentrancy, integer overflow, unprotected functions). Not replacement for manual audit, but filters basic issues.

slither contracts/NFTCollection.sol \
  --solc-remaps "@openzeppelin=node_modules/@openzeppelin" \
  --checklist \
  --markdown-root .

Manual review. Check business logic: does code match whitepaper and announced mechanics. Automation won't understand that maxPerWallet = 100 with totalSupply = 10000 violates announced "fair launch" mechanics.

Fuzz testing. Foundry fuzzing for mint logic and edge cases:

function testFuzz_MintDoesNotExceedSupply(uint256 quantity) public {
    quantity = bound(quantity, 1, 100);
    vm.deal(user, 100 ether);
    vm.prank(user);
    nft.mint{value: 0.1 ether * quantity}(quantity);
    assertLe(nft.totalSupply(), nft.MAX_SUPPLY());
}

NFT contract audit timeline: 1–2 weeks for standard collection. With custom mechanics (staking, breeding, on-chain game logic) — 2–4 weeks.