Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Безопасность смарт-контрактов
security

Безопасность смарт-контрактов

Reentrancy, CEI-паттерн, tx.origin, integer overflow, front-running, ReentrancyGuard

Безопасность смарт-контрактов

Уязвимость в смарт-контракте — это не ошибка, которую можно исправить патчем. Это дыра в неизменяемом коде с деньгами внутри

#Почему безопасность критична

Код смарт-контракта неизменяем после деплоя (если не используется proxy). Ошибки в контрактах стоили миллиарды долларов: взлом DAO в 2016 ($60M), Parity wallet bug 2017 ($150M заморожены навсегда), Ronin Bridge 2022 ($625M). Понимание атак — обязательное условие для любого разработчика Web3.

#Reentrancy Attack

Самая известная уязвимость блокчейна, именно она привела к взлому DAO.

Суть атаки: контракт-жертва отправляет ETH атакующему до обновления своего состояния. Атакующий в receive() снова вызывает уязвимую функцию — и так по кругу, пока баланс жертвы не опустеет.

Уязвимый контракт:

// УЯЗВИМЫЙ КОД — не использовать в production! contract VulnerableBank { mapping(address => uint256) public balances; function deposit() external payable { balances[msg.sender] += msg.value; } // УЯЗВИМОСТЬ: ETH отправляется ПЕРЕД обновлением баланса function withdraw() external { uint256 amount = balances[msg.sender]; require(amount > 0, "Nothing to withdraw"); // Сначала отправляем ETH... (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); // ...потом обновляем баланс (ПОЗДНО!) balances[msg.sender] = 0; } }

Атакующий контракт вызывает withdraw(), получает ETH, и в момент получения (в receive()) снова вызывает withdraw() — баланс ещё не обнулён, поэтому проверка проходит.

#Паттерн Checks-Effects-Interactions (CEI)

CEI — главный паттерн защиты от reentrancy. Порядок действий в функции:

  1. Checks — все проверки и валидация (require, revert)
  2. Effects — обновление состояния контракта
  3. Interactions — внешние вызовы, отправка ETH
// ПРАВИЛЬНЫЙ КОД — паттерн CEI contract SecureBank { mapping(address => uint256) public balances; function deposit() external payable { balances[msg.sender] += msg.value; } function withdraw() external { // 1. Checks uint256 amount = balances[msg.sender]; require(amount > 0, "Nothing to withdraw"); // 2. Effects — обновляем состояние ДО отправки ETH balances[msg.sender] = 0; // 3. Interactions — теперь безопасно отправлять (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); } }

Дополнительная защита — OpenZeppelin ReentrancyGuard:

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; contract SafeBank is ReentrancyGuard { function withdraw() external nonReentrant { // nonReentrant устанавливает lock перед выполнением // и снимает после — повторный вызов из receive() откатится } }

ReentrancyGuard использует bool флаг в storage: перед вызовом устанавливает _entered = true, после — сбрасывает. Повторный вызов увидит флаг и выбросит revert. Используйте вместе с CEI для двойной защиты.

#tx.origin vs msg.sender

tx.origin — адрес EOA, который инициировал цепочку вызовов (самая первая подпись в цепочке). msg.sender — непосредственный вызывающий текущей функции (может быть другой контракт).

Использование tx.origin для авторизации — уязвимость:

// УЯЗВИМЫЙ КОД — фишинговая атака! contract VulnerableWallet { address public owner; function transfer(address to, uint256 amount) external { require(tx.origin == owner, "Not owner"); // ОПАСНО! payable(to).transfer(amount); } }

Атакующий разворачивает вредоносный контракт и убеждает владельца вызвать его функцию. Вредоносный контракт переадресует вызов в VulnerableWallet.transfer(attacker, balance). tx.origin = owner (он подписал транзакцию), проверка пройдена, средства украдены.

Правило: никогда не используйте tx.origin для авторизации. Только msg.sender.

#Integer Overflow в Solidity < 0.8

До Solidity 0.8.0 переполнение не вызывало revert:

// В Solidity 0.7.x: uint8(255) + 1 == 0 // В Solidity 0.8.x: revert с Panic(0x11)

Исторически это была частая уязвимость. Решение в старом коде — библиотека SafeMath от OpenZeppelin. В Solidity 0.8+ защита встроена. Если нужен wrap-around (кольцевой счётчик) — используйте unchecked { } явно.

#Front-Running

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

Уязвимый сценарий: контракт лотереи, где первый угадавший число выигрывает. Атакующий видит в mempool транзакцию с правильным числом, копирует его и отправляет с большим gasPrice.

Защиты: commit-reveal scheme (сначала отправить хэш ответа, потом раскрыть), использование Flashbots для приватных транзакций, проектирование контрактов так, чтобы front-running не давал преимущества.

#Access Control

Неправильная авторизация — частая уязвимость. Рекомендации:

Используйте OpenZeppelin Ownable или AccessControl вместо ручных проверок. AccessControl поддерживает ролевую модель — MINTER_ROLE, PAUSER_ROLE и т.д.

import "@openzeppelin/contracts/access/AccessControl.sol"; contract ManagedToken is AccessControl { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(MINTER_ROLE, msg.sender); } function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) { // только адреса с MINTER_ROLE могут минтить } }

Всегда ограничивайте административные функции (pause, upgrade, withdraw) конкретными ролями. Не давайте одному адресу все права — используйте multisig (Gnosis Safe) для критических операций.

Далее: Паттерны DeFi