November 3

Безопасность в Web3: основные уязвимости смарт-контрактов и как их избежать

Привет! 👋 Сегодня поговорим об одной из самых важных тем в Web3 разработке – безопасности смарт-контрактов. За последние годы хакеры украли сотни миллионов долларов, эксплуатируя уязвимости в смарт-контрактах. Давайте разберем основные типы уязвимостей и научимся их предотвращать.

1. Reentrancy-атаки 🔄

Что это?

Reentrancy (повторный вход) происходит, когда вредоносный контракт повторно вызывает функцию целевого контракта до завершения её первого выполнения. Это возможно из-за особенностей работы внешних вызовов в Solidity, когда контракт-получатель может выполнить произвольный код перед возвратом управления.

Типы Reentrancy-атак:

  1. Single-Function Reentrancy
    • Повторный вход в одну и ту же функцию
    • Самый простой и распространённый тип
  2. Cross-Function Reentrancy
    • Повторный вход в другую функцию, которая работает с тем же состоянием
    • Сложнее обнаружить, так как затрагивает несколько функций
  3. Cross-Contract Reentrancy
    • Атака через взаимодействие нескольких контрактов
    • Особенно опасна в DeFi протоколах
  4. Read-Only Reentrancy
    • Использование повторного входа для чтения некорректного состояния
    • Может влиять на оракулы цен и другие механизмы

Примеры уязвимого кода:

  1. Классическая Reentrancy
solidityCopycontract VulnerableBank {
    mapping(address => uint) public balances;

    function withdraw() public {
        uint amount = balances[msg.sender];
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        balances[msg.sender] = 0; // Слишком поздно!
    }

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
}

// Атакующий контракт
contract Attacker {
    VulnerableBank public bank;
    
    constructor(address bankAddress) {
        bank = VulnerableBank(bankAddress);
    }
    
    // Функция для начала атаки
    function attack() public payable {
        bank.deposit{value: msg.value}();
        bank.withdraw();
    }
    
    // Fallback функция для повторного входа
    receive() external payable {
        if (address(bank).balance >= msg.value) {
            bank.withdraw();
        }
    }
}
  1. Cross-Function Reentrancy
solidityCopycontract VulnerableProtocol {
    mapping(address => uint) public deposits;
    mapping(address => bool) public hasBonus;
    
    function withdraw() public {
        uint amount = deposits[msg.sender];
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        deposits[msg.sender] = 0;
    }
    
    function claimBonus() public {
        require(!hasBonus[msg.sender], "Bonus already claimed");
        require(deposits[msg.sender] > 0, "No deposits");
        hasBonus[msg.sender] = true;
        // Отправка бонуса...
    }
}

Реальные примеры атак:

  1. The DAO Hack (2016)
    • Потери: $60 миллионов
    • Метод: Классическая reentrancy в функции разделения DAO
  2. Lendf.me (2020)
    • Потери: $25 миллионов
    • Метод: Cross-function reentrancy в протоколе кредитования
  3. Cream Finance (2021)
    • Потери: $130 миллионов
    • Метод: Complex reentrancy через flash loan

Методы защиты:

  1. Паттерн Checks-Effects-Interactions
solidityCopycontract SecureBank {
    mapping(address => uint) public balances;
    
    function withdraw() public {
        uint amount = balances[msg.sender];    // Checks
        balances[msg.sender] = 0;              // Effects
        (bool success, ) = msg.sender.call{value: amount}(""); // Interactions
        require(success, "Transfer failed");
    }
}
  1. Использование ReentrancyGuard
solidityCopyimport "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureProtocol is ReentrancyGuard {
    mapping(address => uint) public balances;
    
    function withdraw() public nonReentrant {
        uint amount = balances[msg.sender];
        balances[msg.sender] = 0;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}
  1. Собственная реализация защиты от reentrancy
solidityCopycontract CustomSecureContract {
    bool private locked;
    
    modifier noReentrant() {
        require(!locked, "Reentrant call");
        locked = true;
        _;
        locked = false;
    }
    
    function secureFunction() public noReentrant {
        // Безопасный код
    }
}
  1. Использование Pull Payment Pattern
solidityCopycontract PullPayment {
    mapping(address => uint) private payments;
    
    // Асинхронный платёж
    function asyncPay(address payee, uint amount) internal {
        payments[payee] += amount;
    }
    
    // Получатель сам забирает средства
    function withdrawPayment() public {
        uint payment = payments[msg.sender];
        require(payment > 0, "No payment");
        
        payments[msg.sender] = 0;
        
        (bool success, ) = msg.sender.call{value: payment}("");
        require(success, "Transfer failed");
    }
}
  1. Мутекс для сложных операций
solidityCopycontract MutexProtection {
    mapping(address => uint) private balances;
    mapping(bytes32 => bool) private mutexLocks;
    
    modifier mutex(bytes32 lockId) {
        require(!mutexLocks[lockId], "Operation in progress");
        mutexLocks[lockId] = true;
        _;
        mutexLocks[lockId] = false;
    }
    
    function complexOperation() public mutex(keccak256("complex")) {
        // Сложная операция с несколькими внешними вызовами
    }
}

Лучшие практики предотвращения:

  1. Архитектурные решения
    • Использовать паттерн Pull Payment вместо Push Payment
    • Минимизировать внешние вызовы
    • Группировать связанные операции
    • Использовать атомарные транзакции
  2. Написание кода
    • Следовать паттерну Checks-Effects-Interactions
    • Использовать ReentrancyGuard
    • Добавлять мутексы для сложных операций
    • Тщательно документировать порядок операций
  3. Тестирование
    • Писать специфические тесты на reentrancy
    • Использовать инструменты статического анализа
    • Проводить фаззинг-тестирование
    • Тестировать взаимодействие между контрактами
  4. Аудит и мониторинг
    • Регулярно проводить аудит безопасности
    • Мониторить необычные паттерны транзакций
    • Использовать системы обнаружения атак
    • Внедрять механизмы экстренной остановки

Инструменты защиты:

  1. Статические анализаторы
    • Slither: обнаружение reentrancy
    • Mythril: символьное выполнение
    • Securify: формальная верификация
  2. Библиотеки
    • OpenZeppelin ReentrancyGuard
    • DappHub DS-Guard
    • Safe-Math для защиты вычислений
  3. Мониторинг
    • Tenderly: отслеживание транзакций
    • OpenZeppelin Defender: автоматические проверки
    • DeFi Shield: обнаружение атак

2. Overflow и Underflow 🔢

Что это?

Целочисленное переполнение (overflow) и антипереполнение (underflow) - это арифметические уязвимости, которые возникают при выходе за пределы допустимого диапазона чисел.

  • Overflow происходит, когда результат превышает максимальное значение типа данных:
    • uint8: 255 + 1 = 0
    • uint256: 2^256 - 1 + 1 = 0
  • Underflow происходит при уменьшении числа ниже минимального значения:
    • uint8: 0 - 1 = 255
    • uint256: 0 - 1 = 2^256 - 1

Примеры уязвимого кода:

solidityCopy// Уязвимый контракт с overflow
contract VulnerableToken {
    mapping(address => uint256) public balances;
    
    function transfer(address to, uint256 amount) public {
        require(balances[msg.sender] >= amount);
        
        balances[msg.sender] -= amount;  // Возможен underflow
        balances[to] += amount;          // Возможен overflow
    }
}

// Уязвимый контракт с underflow
contract VulnerableGame {
    uint public playerHealth = 100;
    
    function takeDamage(uint damage) public {
        playerHealth -= damage;  // Если damage > playerHealth, произойдет underflow
    }
}

Реальные примеры атак:

  1. Beauty Chain (BEC) Token - в 2018 году хакеры использовали overflow для создания огромного количества токенов
  2. PoWHC - потеря около $1 млн из-за underflow в функции withdraw

Как защититься:

  1. Использовать Solidity ≥ 0.8.0
solidityCopy// Безопасно в Solidity 0.8.0+
contract SafeToken {
    mapping(address => uint256) public balances;
    
    function transfer(address to, uint256 amount) public {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount; // Автоматически проверяется на underflow
        balances[to] += amount;         // Автоматически проверяется на overflow
    }
}
  1. Использовать SafeMath для версий < 0.8.0
solidityCopyimport "@openzeppelin/contracts/utils/math/SafeMath.sol";

contract SafeToken {
    using SafeMath for uint256;
    mapping(address => uint256) public balances;
    
    function transfer(address to, uint256 amount) public {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] = balances[msg.sender].sub(amount);
        balances[to] = balances[to].add(amount);
    }
}
  1. Использовать проверки границ
solidityCopycontract SafeGame {
    uint public constant MAX_HEALTH = 100;
    uint public playerHealth = MAX_HEALTH;
    
    function heal(uint amount) public {
        require(playerHealth + amount <= MAX_HEALTH, "Health overflow");
        playerHealth += amount;
    }
    
    function takeDamage(uint damage) public {
        require(damage <= playerHealth, "Health underflow");
        playerHealth -= damage;
    }
}
  1. Использовать библиотеку с безопасной математикой
solidityCopylibrary SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");
        return c;
    }
    
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b <= a, "SafeMath: subtraction underflow");
        return a - b;
    }
    
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) return 0;
        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");
        return c;
    }
}

Лучшие практики предотвращения:

  1. Всегда проверять границы
    • Устанавливать максимальные и минимальные значения
    • Проверять результаты до присваивания
  2. Использовать правильные типы данных
    • Выбирать типы с достаточным диапазоном
    • Учитывать возможный рост значений
  3. Тестирование граничных случаев
    • Проверять максимальные значения
    • Тестировать операции с нулевыми значениями
    • Использовать фаззинг-тестирование
  4. Аудит кода
    • Использовать статические анализаторы
    • Проверять все арифметические операции
    • Документировать предполагаемые диапазоны значений

3. Фронт-раннинг (Front-Running) 🏃‍♂️

Что это?

Front-running – это тип атаки, при которой злоумышленник отслеживает транзакции в mempool и размещает свою транзакцию перед целевой, используя более высокую цену на газ. Это возможно, потому что майнеры обычно включают в блок транзакции с более высокой ценой на газ первыми.

Типы фронт-раннинга:

  1. Displacement (Замещение)
    • Атакующий полностью вытесняет транзакцию жертвы
    • Пример: перехват редкого NFT при минтинге
  2. Insertion (Вставка)
    • Атакующий вставляет свою транзакцию перед целевой
    • Пример: покупка токена перед большой сделкой на DEX
  3. Suppression (Подавление)
    • Атакующий задерживает транзакцию жертвы
    • Пример: блокировка ликвидации позиции

Примеры уязвимых ситуаций:

  1. DEX с фиксированной ценой
solidityCopycontract VulnerableDEX {
    function swap(address token, uint amount) public {
        uint price = getPrice(token);  // Цена фиксируется здесь
        // ... некоторое время проходит ...
        executeSwap(token, amount, price);  // Используется старая цена
    }
}
  1. Наивный NFT минтинг
solidityCopycontract VulnerableNFT {
    function mintSpecialNFT(uint tokenId) public {
        require(!_exists(tokenId), "Already minted");
        _mint(msg.sender, tokenId);  // Можно перехватить редкий tokenId
    }
}
  1. Уязвимый аукцион
solidityCopycontract VulnerableAuction {
    function bid() public payable {
        require(msg.value > highestBid, "Bid too low");
        payable(highestBidder).transfer(highestBid);  // Возврат предыдущей ставки
        highestBidder = msg.sender;
        highestBid = msg.value;
    }
}

Реальные примеры атак:

  1. Salmonella (2022)
    • Потери: ~$8.1 млн
    • Метод: фронт-раннинг транзакций при mint событиях
  2. Sandwich атаки на Uniswap
    • Регулярные потери пользователей из-за сэндвич-атак
    • Работает путем покупки токена перед крупной сделкой и продажи сразу после

Как защититься:

  1. Commit-Reveal схема
solidityCopycontract SafeNFTMint {
    mapping(address => bytes32) public commits;
    mapping(bytes32 => bool) public usedCommits;
    uint public commitDeadline;
    uint public revealDeadline;
    
    // Шаг 1: Пользователь отправляет хэш своего выбора
    function commit(bytes32 commitHash) public {
        require(block.timestamp < commitDeadline, "Commit phase ended");
        commits[msg.sender] = commitHash;
    }
    
    // Шаг 2: Пользователь раскрывает свой выбор
    function reveal(uint tokenId, bytes32 salt) public {
        require(block.timestamp >= commitDeadline, "Commit phase not ended");
        require(block.timestamp < revealDeadline, "Reveal phase ended");
        
        bytes32 commitHash = keccak256(abi.encodePacked(tokenId, salt, msg.sender));
        require(commits[msg.sender] == commitHash, "Invalid reveal");
        require(!usedCommits[commitHash], "Already revealed");
        
        usedCommits[commitHash] = true;
        _mint(msg.sender, tokenId);
    }
}
  1. Batch-аукционы
solidityCopycontract BatchAuction {
    struct Bid {
        address bidder;
        uint amount;
    }
    
    Bid[] public bids;
    uint public auctionEnd;
    bool public finalized;
    
    // Все ставки собираются в течение периода аукциона
    function bid() public payable {
        require(block.timestamp < auctionEnd, "Auction ended");
        bids.push(Bid(msg.sender, msg.value));
    }
    
    // Победитель определяется после окончания аукциона
    function finalize() public {
        require(block.timestamp >= auctionEnd, "Auction not ended");
        require(!finalized, "Already finalized");
        
        finalized = true;
        // Сортировка ставок и определение победителя
        _settleBids();
    }
}
  1. Использование флеш-займов для обеспечения атомарности
solidityCopycontract FlashLoanProtection {
    bool private _locked;
    
    modifier noFlashLoan() {
        require(!_locked, "No flash loan allowed");
        _locked = true;
        _;
        _locked = false;
    }
    
    function protectedFunction() public noFlashLoan {
        // Код защищенной функции
    }
}
  1. Внедрение MEV-защиты
solidityCopycontract MEVProtection {
    // Минимальная задержка между транзакциями
    uint public constant MIN_DELAY = 1 blocks;
    mapping(address => uint) public lastActionBlock;
    
    modifier mevProtected() {
        require(block.number >= lastActionBlock[msg.sender] + MIN_DELAY, 
                "Too frequent transactions");
        lastActionBlock[msg.sender] = block.number;
        _;
    }
    
    function protectedSwap() public mevProtected {
        // Код свапа
    }
}

Лучшие практики предотвращения:

  1. Архитектурные решения
    • Использовать batch-обработку транзакций
    • Внедрять временные задержки
    • Применять commit-reveal схемы
    • Использовать случайность (через oracle или VRF)
  2. Ценообразование
    • Использовать скользящие средние цены
    • Внедрять механизмы против манипуляций
    • Добавлять проскальзывание (slippage)
  3. Мониторинг
    • Отслеживать необычные паттерны транзакций
    • Использовать инструменты анализа mempool
    • Внедрять системы алертов
  4. Дополнительные меры
    • Использовать приватные мемпулы
    • Внедрять механизмы предотвращения фронт-раннинга на уровне протокола
    • Применять zero-knowledge доказательства

Инструменты защиты:

  1. Flashbots Protect
    • Отправка транзакций через приватный мемпул
    • Защита от сэндвич-атак
  2. MEV-Blocker
    • Защита от MEV через специализированные RPC
  3. OpenZeppelin Defender
    • Мониторинг и защита от фронт-раннинг атак
    • Приватный релейер транзакций

4. Неправильное управление доступом 🔐

Что это?

Уязвимости управления доступом возникают, когда смарт-контракт некорректно контролирует доступ к привилегированным функциям или данным. Это может привести к несанкционированному доступу, изменению критических параметров или краже средств.

Типы уязвимостей доступа:

  1. Отсутствие проверок доступа
solidityCopy// Уязвимый контракт
contract VulnerableContract {
    address public owner;
    uint public price;
    
    function setPrice(uint newPrice) public {  // Нет проверки доступа!
        price = newPrice;
    }
}
  1. Неправильная инициализация
solidityCopycontract VulnerableInit {
    address public owner;
    
    // Забыли инициализировать owner в конструкторе
    function initialize() public {  // Любой может вызвать!
        owner = msg.sender;
    }
}
  1. Недостаточная глубина проверки
solidityCopycontract VulnerableProxy {
    address public implementation;
    
    function upgrade(address newImplementation) public {
        require(msg.sender == owner);  // Проверяем только владельца
        // Нет проверки, является ли newImplementation контрактом!
        implementation = newImplementation;
    }
}

Реальные примеры атак:

  1. Poly Network Hack (2021)
    • Потери: $611 миллионов
    • Причина: Уязвимость в механизме проверки прав доступа
  2. Parity Multisig Wallet (2017)
    • Потери: $30 миллионов
    • Причина: Неправильная инициализация библиотеки
  3. The DAO Hack (2016)
    • Потери: $60 миллионов
    • Причина: Комбинация reentrancy и некорректных проверок доступа

Методы защиты:

  1. Базовое управление доступом с OpenZeppelin
solidityCopyimport "@openzeppelin/contracts/access/Ownable.sol";

contract SecureContract is Ownable {
    uint public price;
    
    function setPrice(uint newPrice) public onlyOwner {
        price = newPrice;
    }
    
    // Безопасная передача владения
    function transferOwnership(address newOwner) public override onlyOwner {
        require(newOwner != address(0), "New owner is zero address");
        super.transferOwnership(newOwner);
    }
}
  1. Многоуровневая система ролей
solidityCopyimport "@openzeppelin/contracts/access/AccessControl.sol";

contract SecureProtocol is AccessControl {
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
    
    constructor() {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setRoleAdmin(OPERATOR_ROLE, ADMIN_ROLE);
        _setRoleAdmin(UPGRADER_ROLE, ADMIN_ROLE);
    }
    
    function adminFunction() public onlyRole(ADMIN_ROLE) {
        // Только админ может вызвать
    }
    
    function operatorFunction() public onlyRole(OPERATOR_ROLE) {
        // Только оператор может вызвать
    }
    
    function upgradeFunction() public onlyRole(UPGRADER_ROLE) {
        // Только upgrader может вызвать
    }
}
  1. Временные блокировки для критических операций
solidityCopycontract TimelockController {
    uint public constant DELAY = 2 days;
    mapping(bytes32 => bool) public pendingOperations;
    mapping(bytes32 => uint) public operationTimestamps;
    
    function scheduleOperation(bytes32 operationId) public onlyOwner {
        require(!pendingOperations[operationId], "Already scheduled");
        pendingOperations[operationId] = true;
        operationTimestamps[operationId] = block.timestamp + DELAY;
    }
    
    function executeOperation(bytes32 operationId) public onlyOwner {
        require(pendingOperations[operationId], "Not scheduled");
        require(block.timestamp >= operationTimestamps[operationId], "Too early");
        
        pendingOperations[operationId] = false;
        // Выполнение операции
    }
}
  1. Multisig управление
solidityCopycontract MultiSigWallet {
    address[] public owners;
    mapping(address => bool) public isOwner;
    uint public required;
    
    struct Transaction {
        address to;
        uint value;
        bytes data;
        bool executed;
        mapping(address => bool) confirmations;
    }
    
    Transaction[] public transactions;
    
    constructor(address[] memory _owners, uint _required) {
        require(_owners.length >= _required, "Invalid required number");
        require(_required > 0, "Required must be positive");
        
        for (uint i = 0; i < _owners.length; i++) {
            address owner = _owners[i];
            require(owner != address(0), "Invalid owner");
            require(!isOwner[owner], "Duplicate owner");
            
            isOwner[owner] = true;
            owners.push(owner);
        }
        required = _required;
    }
    
    function submitTransaction(address to, uint value, bytes memory data) 
        public
        returns (uint transactionId)
    {
        require(isOwner[msg.sender], "Not owner");
        transactionId = transactions.length;
        transactions.push(Transaction({
            to: to,
            value: value,
            data: data,
            executed: false
        }));
        confirmTransaction(transactionId);
    }
    
    function confirmTransaction(uint transactionId) public {
        require(isOwner[msg.sender], "Not owner");
        Transaction storage transaction = transactions[transactionId];
        transaction.confirmations[msg.sender] = true;
        if (isConfirmed(transactionId)) {
            executeTransaction(transactionId);
        }
    }
    
    function isConfirmed(uint transactionId) public view returns (bool) {
        uint count = 0;
        for (uint i = 0; i < owners.length; i++) {
            if (transactions[transactionId].confirmations[owners[i]])
                count += 1;
            if (count >= required)
                return true;
        }
        return false;
    }
    
    function executeTransaction(uint transactionId) public {
        require(isConfirmed(transactionId), "Not confirmed");
        Transaction storage transaction = transactions[transactionId];
        require(!transaction.executed, "Already executed");
        
        transaction.executed = true;
        (bool success,) = transaction.to.call{value: transaction.value}(transaction.data);
        require(success, "Execution failed");
    }
}

Лучшие практики:

  1. Проектирование системы доступа
    • Определить все роли заранее
    • Следовать принципу наименьших привилегий
    • Документировать права доступа
    • Использовать многоуровневую систему ролей
  2. Реализация
    • Использовать проверенные библиотеки (OpenZeppelin)
    • Внедрять многоподписные механизмы для критических операций
    • Добавлять временные задержки
    • Реализовать систему логирования доступа
  3. Безопасная инициализация
    • Всегда инициализировать роли в конструкторе
    • Проверять корректность адресов
    • Использовать initializer для прокси-контрактов
    • Добавлять защиту от повторной инициализации
  4. Мониторинг и аудит
    • Отслеживать события смены ролей
    • Проводить регулярный аудит прав доступа
    • Внедрить систему оповещения о критических изменениях

Инструменты и библиотеки:

  1. OpenZeppelin Contracts
    • Ownable для базового контроля
    • AccessControl для сложных систем ролей
    • TimelockController для временных блокировок
  2. Gnosis Safe
    • Мультиподпись
    • Модульная система доступа
    • Социальное восстановление
  3. OpenZeppelin Defender
    • Управление ролями
    • Мониторинг доступа
    • Автоматизация проверок

5. Отсутствие проверок входных данных ⚠️

Что это?

Отсутствие или недостаточная валидация входных параметров функций может привести к неожиданному поведению контракта, потере средств или манипуляциям с состоянием. Эта уязвимость часто становится причиной эксплойтов в комбинации с другими уязвимостями.

Типы уязвимостей входных данных:

  1. Отсутствие проверки нулевого адреса
solidityCopycontract VulnerableToken {
    mapping(address => uint) public balances;
    
    function transfer(address to, uint amount) public {
        // Нет проверки нулевого адреса!
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}
  1. Некорректная проверка значений
solidityCopycontract VulnerableLending {
    function borrow(uint amount, uint collateral) public {
        // Нет проверки соотношения займа к обеспечению!
        // Нет проверки минимального размера займа!
        _transferCollateral(msg.sender, collateral);
        _transferLoan(msg.sender, amount);
    }
}
  1. Манипуляция с массивами
solidityCopycontract VulnerableAirdrop {
    function multiSend(address[] memory recipients, uint[] memory amounts) public {
        // Нет проверки длины массивов!
        // Нет проверки суммы всех amounts!
        for(uint i = 0; i < recipients.length; i++) {
            payable(recipients[i]).transfer(amounts[i]);
        }
    }
}

Реальные примеры атак:

  1. Poly Network (2021)
    • Потери: $611 миллионов
    • Причина: Некорректная валидация входных параметров в кросс-чейн бридже
  2. Wormhole Bridge (2022)
    • Потери: $320 миллионов
    • Причина: Недостаточная проверка входных данных в системе верификации
  3. Uranium Finance (2021)
    • Потери: $57 миллионов
    • Причина: Некорректная проверка параметров в функции обмена

Методы защиты:

  1. Базовые проверки безопасности
solidityCopycontract SecureToken {
    mapping(address => uint) public balances;
    uint public constant MAX_TRANSFER_AMOUNT = 1000000 * 10**18;
    
    function transfer(address to, uint amount) public {
        // Проверка адреса
        require(to != address(0), "Zero address not allowed");
        require(to != address(this), "Cannot transfer to contract");
        
        // Проверка суммы
        require(amount > 0, "Amount must be positive");
        require(amount <= MAX_TRANSFER_AMOUNT, "Amount too large");
        require(balances[msg.sender] >= amount, "Insufficient balance");
        
        // Проверка переполнения
        require(balances[to] + amount >= balances[to], "Overflow check");
        
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}
  1. Проверки для сложных операций
solidityCopycontract SecureLending {
    uint public constant MIN_COLLATERAL_RATIO = 150; // 150%
    uint public constant MIN_LOAN_AMOUNT = 100 * 10**18; // 100 tokens
    uint public constant MAX_LOAN_AMOUNT = 100000 * 10**18; // 100,000 tokens
    
    function borrow(uint amount, uint collateral) public {
        // Базовые проверки
        require(amount >= MIN_LOAN_AMOUNT, "Loan too small");
        require(amount <= MAX_LOAN_AMOUNT, "Loan too large");
        require(collateral > 0, "No collateral provided");
        
        // Проверка соотношения займа к обеспечению
        uint collateralRatio = (collateral * 100) / amount;
        require(collateralRatio >= MIN_COLLATERAL_RATIO, 
                "Insufficient collateral ratio");
        
        // Проверка ликвидности протокола
        require(_getAvailableLiquidity() >= amount, 
                "Insufficient protocol liquidity");
        
        _transferCollateral(msg.sender, collateral);
        _transferLoan(msg.sender, amount);
    }
}
  1. Безопасная работа с массивами
solidityCopycontract SecureAirdrop {
    uint public constant MAX_BATCH_SIZE = 200;
    uint public constant MAX_AMOUNT_PER_TRANSFER = 1000 * 10**18;
    
    function multiSend(
        address[] memory recipients, 
        uint[] memory amounts
    ) public {
        // Проверка длины массивов
        require(recipients.length > 0, "Empty recipients array");
        require(recipients.length == amounts.length, 
                "Arrays length mismatch");
        require(recipients.length <= MAX_BATCH_SIZE, 
                "Batch too large");
        
        // Проверка суммы и адресов
        uint totalAmount = 0;
        for(uint i = 0; i < recipients.length; i++) {
            require(recipients[i] != address(0), 
                    "Zero address in recipients");
            require(amounts[i] > 0, "Zero amount in amounts");
            require(amounts[i] <= MAX_AMOUNT_PER_TRANSFER, 
                    "Amount too large");
            
            totalAmount += amounts[i];
        }
        
        // Проверка общей суммы
        require(totalAmount <= balanceOf(msg.sender), 
                "Insufficient balance");
        
        // Выполнение переводов
        for(uint i = 0; i < recipients.length; i++) {
            _transfer(msg.sender, recipients[i], amounts[i]);
        }
    }
}
  1. Модификаторы для часто используемых проверок
solidityCopycontract SecureProtocol {
    modifier validAddress(address addr) {
        require(addr != address(0), "Zero address not allowed");
        require(addr != address(this), "Cannot use contract address");
        _;
    }
    
    modifier validAmount(uint amount) {
        require(amount > 0, "Amount must be positive");
        require(amount <= maxAmount, "Amount too large");
        _;
    }
    
    modifier validArrays(address[] memory addrs, uint[] memory amounts) {
        require(addrs.length > 0, "Empty array");
        require(addrs.length == amounts.length, "Arrays length mismatch");
        require(addrs.length <= maxBatchSize, "Batch too large");
        _;
    }
    
    function secureFunction(
        address to, 
        uint amount
    ) public 
      validAddress(to) 
      validAmount(amount) 
    {
        // Код функции
    }
}

Лучшие практики:

  1. Валидация адресов
    • Проверка на нулевой адрес
    • Проверка на адрес контракта
    • Проверка на специальные адреса (precompiled contracts)
    • Валидация контрольной суммы адреса
  2. Валидация числовых значений
    • Проверка на ноль
    • Проверка минимальных/максимальных значений
    • Проверка бизнес-ограничений
    • Защита от переполнения
  3. Валидация массивов
    • Проверка длины
    • Проверка соответствия длин связанных массивов
    • Ограничение размера батча
    • Проверка элементов массива
  4. Дополнительные проверки
    • Проверка временных ограничений
    • Валидация подписей
    • Проверка состояния контракта
    • Проверка внешних условий

Инструменты и библиотеки:

  1. OpenZeppelin
    • SafeERC20 для безопасных операций с токенами
    • SafeMath для безопасной арифметики
    • Address для проверок адресов
  2. Инструменты анализа
    • Slither для статического анализа
    • Mythril для поиска уязвимостей
    • Echidna для фаззинг-тестирования
  3. Инструменты разработки
    • Hardhat для локального тестирования
    • Tenderly для мониторинга транзакций
    • Etherscan для верификации контрактов

Заключение 🎯

Ключевые выводы по безопасности смарт-контрактов

  1. Фундаментальные принципы безопасности:
    • Принцип наименьших привилегий
    • Безопасность по умолчанию
    • Глубокая защита (defense in depth)
    • Fail-safe defaults
    • Экономическая безопасность
  2. Процесс разработки безопасных контрактов: Планирование → Разработка → Тестирование → Аудит → Развертывание → Мониторинг
  3. Чеклист безопасности при разработке:
    • Все функции имеют необходимые модификаторы доступа
    • Используются последние версии зависимостей
    • Проведено тестирование граничных случаев
    • Реализованы экстренные механизмы остановки
    • Добавлены события для критических операций
    • Проведен аудит безопасности
    • Настроен мониторинг

Комплексный подход к безопасности

  1. Превентивные меры:
contract SecureContract {
    // Экстренная остановка
    bool public paused;
    modifier whenNotPaused() {
        require(!paused, "Contract paused");
        _;
    }
    
    // Ограничение по времени
    modifier onlyDuringOperatingHours() {
        require(block.timestamp % 86400 >= 32400 &&  // 9:00 AM
               block.timestamp % 86400 <= 64800,     // 6:00 PM
               "Outside operating hours");
        _;
    }
    
    // Ограничение частоты
    mapping(address => uint) public lastActionTime;
    modifier rateLimited() {
        require(block.timestamp >= lastActionTime[msg.sender] + 1 hours,
                "Rate limited");
        lastActionTime[msg.sender] = block.timestamp;
        _;
    }
}

2. Механизмы восстановления:

contract RecoverableContract {
    // Возможность обновления
    address public implementation;
    function upgrade(address newImplementation) external onlyOwner {
        require(newImplementation.code.length > 0, "Not a contract");
        implementation = newImplementation;
    }
    
    // Восстановление активов
    function recoverTokens(
        address token,
        address recipient,
        uint amount
    ) external onlyOwner {
        IERC20(token).transfer(recipient, amount);
    }
}

План действий при обнаружении уязвимости

  1. Немедленные действия:
    • Активация emergency stop
    • Оповещение команды безопасности
    • Временная приостановка операций
  2. Анализ и исправление:
    • Определение масштаба проблемы
    • Разработка патча
    • Тестирование исправления
    • Аудит изменений
  3. Коммуникация и восстановление:
    • Оповещение пользователей
    • Внедрение исправлений
    • Компенсация потерь
    • Обновление процедур безопасности

Непрерывное улучшение безопасности

  1. Регулярные проверки: plaintextCopyЕжедневно: - Мониторинг транзакций - Проверка алертов Еженедельно: - Обзор кода - Обновление зависимостей Ежемесячно: - Пентестинг - Обновление документации Ежеквартально: - Внешний аудит - Обновление процедур
  2. Образование и развитие:
    • Изучение новых уязвимостей
    • Участие в bug bounty программах
    • Обмен опытом с сообществом
    • Проведение внутренних тренингов

Рекомендуемые ресурсы для изучения

  1. Документация и стандарты:
  2. Инструменты безопасности:
    • Статические анализаторы: Slither, Mythril
    • Фаззеры: Echidna
    • Формальная верификация: Certora, Act
  3. Сообщества и ресурсы:
    • Smart Contract Security Discord
    • Immunefi Bug Bounty Platform
    • ETHSecurity Community

Подпишись !!!

Спасибо за чтение ! Подпишись что бы не пропускать дальнейшие статьи!

Телеграм: https://t.me/one_eyes