Настройка мониторинга смарт-контрактов (Forta, OpenZeppelin Defender)

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Настройка мониторинга смарт-контрактов (Forta, OpenZeppelin Defender)
Средняя
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

Аудит безопасности 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 логики
  • Защита от transfer к address(0) без intent to 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, limits, 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?

Типичные находки по severity

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

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

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

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

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

Статический анализ. Запуск Slither — автоматическое обнаружение стандартных паттернов (reentrancy, integer overflow, unprotected functions). Не заменяет ручной аудит, но отсеивает базовые проблемы.

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

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

Fuzz testing. 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 недели.