Налаштування моніторингу смарт-контрактів (Forta, OpenZeppelin Defender)

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Налаштування моніторингу смарт-контрактів (Forta, OpenZeppelin Defender)
Середній
від 1 дня до 3 днів
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1306
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1218
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    920
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1147
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    610
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    885

Аудит безпеки NFT-коллекції

NFT-контракти виглядають простіше, ніж DeFi протоколи — немає складної математики, немає liquidity pools. На практиці це хибне відчуття безпеки: NFT колекції регулярно втрачають кошти через вразливості в mint логіці, reentrancy у ERC-721 transfer callback, некоректний randomness та проблеми з royalty. Аудит NFT-контракту — це не формальність, це вимога ринку: більшість маркетплейсів та інвесторів розглядають аудит як baseline.

Що перевіряється в NFT-аудиті

ERC-721 стандарт та реалізація

Перший блок: відповідність ERC-721 стандарту та коректність базової реалізації. OpenZeppelin або власний контракт?

Власна реалізація ERC-721 — невідкладна red flag. Якщо команда не використовує OZ ERC721, потрібно тщательно перевірити:

  • Коректність safeTransferFrom — виклик onERC721Received у контракті-одержувачі
  • Правильність approve та setApprovalForAll логіки
  • Захист від передачі до address(0) без намірення на burn
  • Коректний підрахунок balanceOf та ownerOf

Reentrancy через ERC-721 callback

Це найбільш поширена критична вразливість у NFT. safeTransferFrom викликає onERC721Received на контракті-одержувачі — це зовнішній виклик посередині транзакції.

// ВРАЗЛИВИЙ контракт
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 → викликає onERC721Received → reentrancy!
            _safeMint(msg.sender, tokenId);
            // State оновлюється ПІСЛЯ _safeMint — вразливо
            minted[msg.sender]++;
        }
    }
}

Атака: зловмисник створює контракт з onERC721Received, який при виклику знову викликає mint. Якщо minted лічильник оновлюється після _safeMint — можна перевищити ліміт mint per address.

// ВИПРАВЛЕНА версія: 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 ПЕРШИМИ
    minted[msg.sender] += quantity;
    
    // Interactions ПІСЛЯ
    for (uint256 i = 0; i < quantity; i++) {
        _safeMint(msg.sender, ++_tokenCounter);
    }
}

Randomness: VRF vs blockhash

On-chain randomness для reveal — часта вразливість. block.timestamp, blockhash, prevrandao — все маніпулюється.

// ВРАЗЛИВО: miner/validator може маніпулювати 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 може викликати revealTokens тільки у блоках, де blockhash дає вигідні traits. Це не теоретичний ризик — на крупних колекціях це траплялося.

Правильне рішення: Chainlink VRF v2.

contract SecureNFT is VRFConsumerBaseV2 {
    VRFCoordinatorV2Interface private vrfCoordinator;
    uint64 private subscriptionId;
    bytes32 private keyHash;
    
    uint256 public randomWord;  // отримано від 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 логіка: whitelist, ліміти, timing

// Перевіряємо в аудиті:

// 1. Merkle whitelist — коректність подвійного хешування
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, maxAmount))));
// НЕ: keccak256(abi.encodePacked(msg.sender)) — вразливо до hash collision

// 2. Захист від перевищення supply
require(totalSupply() + quantity <= MAX_SUPPLY, "Exceeds supply");
// Немає off-by-one помилок?

// 3. Team mint не перевищує оголошене
// Перевіряємо що team allocation реально обмежена

// 4. Front-running захист для whitelist
// Merkle proof не містить amount — не можна скопіювати proof іншому адресу?
// Leaf має включати msg.sender!

Механізм Royalty: ERC-2981

// Коректна реалізація ERC-2981
contract NFTWithRoyalty is ERC721, ERC2981 {
    constructor() {
        // Встановлюємо royalty: 5% для owner
        _setDefaultRoyalty(msg.sender, 500); // 500 basis points = 5%
    }
    
    // royaltyInfo має повертати коректні значення
    // Перевіряємо: не перевищує 100%, коректний receiver
    function royaltyInfo(uint256, uint256 salePrice)
        public view override returns (address receiver, uint256 royaltyAmount)
    {
        return super.royaltyInfo(0, salePrice);
    }
    
    // supportsInterface має включати ERC2981
    function supportsInterface(bytes4 interfaceId)
        public view override(ERC721, ERC2981) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}

В аудиті перевіряємо: royalty receiver не дорівнює address(0), процент не аномально високий, немає можливості змінити receiver без governance.

Функція withdraw

Проста, але критична: хто може вивести ETH із контракту?

// Перевіряємо в аудиті:

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

// 2. Немає ли pull payment вразливостей?
// 3. ETH не може застрять у контракті при невдалому transfer?
// Використовуємо call замість transfer:
(bool success, ) = payable(owner()).call{value: address(this).balance}("");
require(success, "Transfer failed");

// 4. Немає ли можливості drain перед reveal/sale через exploit?

Типичні findings по severity

Severity Приклад вразливості Частота
Critical Reentrancy в mint дозволяє mint більше supply Рідко
High Манипульовний randomness для reveal Часто
High Merkle leaf без msg.sender — proof можна украсти Середньо
Medium Withdraw без nonReentrant Часто
Medium totalSupply overflow при великій кількості mint Рідко
Low Відсутність подій для критичних функцій Дуже часто
Informational Gas оптимізація (ERC721A vs ERC721) Завжди

ERC-721A: специфіка аудиту

Багато колекцій використовують ERC-721A (оптимізовану версію Azuki для batch mint). Специфічні перевірки:

  • Коректність _startTokenId() — за замовчуванням 0, але деякі проекти хочуть 1
  • _nextTokenId() коректно ініціалізований
  • Batch transfer не порушує ownership маппинг
  • tokensOfOwner не перевищує gas limit для cold wallets з великою кількістю токенів

Процес аудиту

Статичний аналіз. Запуск Slither — автоматичне виявлення стандартних паттернів (reentrancy, integer overflow, unprotected функції). Не замінює ручний аудит, але відсіває базові проблеми.

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

Ручний review. Перевірка business logic: відповідає ли код whitepaper та оголошеній механіці. Автоматика не зрозуміє, що maxPerWallet = 100 при totalSupply = 10000 порушує оголошену "fair launch" механіку.

Fuzz тестування. Foundry fuzzing для mint логіки та 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-контракту: 1–2 тижні для стандартної коллекції. З кастомною механікою (staking, breeding, on-chain game logic) — 2–4 тижні.