← Назад к обучению

Разработка смарт-контрактов

Практическое руководство по созданию DeFi-смарт-контрактов производственного уровня. Охватывает современные паттерны Solidity, оптимизацию газа, лучшие практики безопасности и полный современный фронтенд-стек Web3.

Современные лучшие практики Solidity

Solidity 0.8.24+ — сегодня базовая версия для любого нового DeFi-проекта. Встроенная защита от переполнения, пользовательские ошибки для экономии газа и зрелый инструментарий (Foundry и Hardhat 3) делают опыт разработчика значительно лучше, чем даже два года назад.

Хорошо структурированный контракт наследует проверенные в бою базы OpenZeppelin: ReentrancyGuard для защиты от повторного входа, Ownable или AccessControl для управления ролями и Pausable для аварийного отключения. Каждая внешняя функция должна проверять входные данные, генерировать события для внецепочечного индексирования и следовать паттерну checks-effects-interactions.

Шаблон структуры контракта
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";

contract DeFiProtocol is ReentrancyGuard, Ownable, Pausable {
    mapping(address => uint256) public userDeposits;
    uint256 public totalDeposits;

    // Пользовательские ошибки — экономят газ (только 4-байтовый селектор)
    error InsufficientBalance();
    error InvalidAmount();
    error TransferFailed();

    event Deposit(address indexed user, uint256 amount, uint256 timestamp);
    event Withdrawal(address indexed user, uint256 amount, uint256 rewards);

    // OZ v5: the initial owner is a constructor argument
    constructor() Ownable(msg.sender) {}

    function deposit(uint256 amount) external nonReentrant whenNotPaused {
        if (amount == 0) revert InvalidAmount();
        // ... checks-effects-interactions
    }
}

Техники оптимизации газа

Затраты на газ напрямую влияют на пользовательский опыт. Каждая оптимизация делает ваш протокол более доступным, особенно в основной сети Ethereum. Три наиболее эффективные техники — это пользовательские ошибки, упаковка структур и unchecked-арифметика там, где это безопасно.

Пользовательские ошибки заменяют строки require и существенно экономят газ — 4-байтовый селектор вместо полностью ABI-кодированной строки. Упаковка структур помещает несколько значений в один 32-байтовый слот хранения вместо траты слотов на мелкие типы. Блоки unchecked пропускают проверки переполнения, когда входные данные уже проверены.

Примеры оптимизации газа
// Пользовательские ошибки vs строки require
// Плохо:  require(amount > 0, "Amount must be greater than zero");
// Хорошо: if (amount == 0) revert InvalidAmount();

// Упаковка структур — 2 слота вместо 3
struct UserInfo {
    uint128 amount;      // 16 байт ─┐
    uint128 rewardDebt;  // 16 байт ─┘ слот 1
    bool isActive;       // 1 байт  ──── слот 2
}

// Unchecked-арифметика, когда переполнение невозможно
function calculateReward(uint256 principal, uint256 rate)
    internal pure returns (uint256)
{
    unchecked {
        return principal * rate / 10000;
    }
}

Паттерны безопасности

Безопасность в DeFi не подлежит обсуждению. Одна уязвимость может увести миллионы. Паттерн checks-effects-interactions и OpenZeppelin ReentrancyGuard — первая линия защиты от повторного входа. Контроль доступа с ролевыми разрешениями (AccessControl) предотвращает несанкционированные изменения состояния.

Интеграция оракула требует особой осторожности. Всегда проверяйте свежесть цены (порог устаревания), убеждайтесь, что цена положительна, и реализуйте резервную логику на случай сбоев оракула. Паттерн try/catch вокруг Chainlink latestRoundData позволяет вашему протоколу деградировать плавно, а не полностью откатываться.

Интеграция оракула с резервом
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract PriceAwareProtocol {
    AggregatorV3Interface internal priceFeed;
    uint256 public constant STALENESS_THRESHOLD = 3600; // 1 час
    uint256 public fallbackPrice;

    function getPrice() public view returns (uint256) {
        try priceFeed.latestRoundData() returns (
            uint80, int256 price, uint256, uint256 updatedAt, uint80
        ) {
            if (block.timestamp - updatedAt > STALENESS_THRESHOLD)
                return fallbackPrice;
            if (price <= 0) return fallbackPrice;
            return uint256(price);
        } catch {
            return fallbackPrice;
        }
    }
}

Паттерны DeFi: пулы ликвидности

AMM с постоянным произведением (x * y = k) — основа децентрализованных бирж. Пул ликвидности хранит два токена и оценивает их на основе их соотношения. Добавление ликвидности чеканит LP-токены пропорционально внесённой доле; обмен применяет комиссию (обычно 0,3 %) перед расчётом вывода через инвариант.

Реализация ниже показывает основную механику. В продакшене вы бы добавили защиту от проскальзывания (минимальные суммы вывода), TWAP-оракулы, диапазоны концентрированной ликвидности и устойчивое к флэш-займам ценообразование.

Упрощённый обмен AMM
function swap(uint256 amountIn, bool aToB) external returns (uint256 amountOut) {
    require(amountIn > 0, "Invalid input amount");

    uint256 reserveIn  = aToB ? reserveA : reserveB;
    uint256 reserveOut = aToB ? reserveB : reserveA;

    // Применить комиссию 0,3%
    uint256 amountInWithFee = amountIn * 997;
    amountOut = (amountInWithFee * reserveOut)
              / (reserveIn * 1000 + amountInWithFee);

    require(amountOut > 0, "Insufficient output amount");
    // ... перевести токены, обновить резервы, сгенерировать событие Swap
}

Паттерны DeFi: доходное фермерство

Доходное фермерство распределяет токены вознаграждения стейкерам с течением времени. Стандартный паттерн отслеживает глобальный аккумулятор rewardPerToken и снимок «оплачено до» для каждого пользователя. При каждом стейкинге, выводе или запросе вознаграждения рассчитываются путём умножения баланса пользователя на разницу между текущим и последним увиденным аккумулятором.

Этот подход эффективен по памяти (два маппинга плюс несколько скаляров) и эффективен по газу (O(1) на взаимодействие пользователя независимо от числа стейкеров).

Паттерн аккумулятора вознаграждений
modifier updateReward(address account) {
    rewardPerTokenStored = rewardPerToken();
    lastUpdateTime = block.timestamp;
    if (account != address(0)) {
        rewards[account] = earned(account);
        userRewardPerTokenPaid[account] = rewardPerTokenStored;
    }
    _;
}

function earned(address account) public view returns (uint256) {
    return (balances[account] *
        (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18
        + rewards[account];
}

Фронтенд-стек Web3

Современный фронтенд-стек DeFi строится вокруг React 19+, Next.js 15+, wagmi 2.x, viem 2.x и RainbowKit 2.x для подключения кошельков. TanStack Query занимается кэшированием и повторным получением ончейн-данных. Эта комбинация даёт типобезопасное чтение и запись контрактов, автоматическое переключение сетей и отточенный UX подключения кошелька прямо из коробки.

Структурируйте проект с отдельными каталогами для переиспользуемых UI-компонентов, специфичных для Web3 компонентов (подключение кошелька, селектор сети) и компонентов DeFi-протокола (формы депозита, отображение пулов). Пользовательские хуки, такие как useContractRead и useContractWrite, абстрагируют взаимодействие с контрактами за чистым API, а компонент TransactionModal даёт пользователям понятную обратную связь о ходе выполнения.

Настройка wagmi + RainbowKit
// lib/wagmi.ts
import { getDefaultConfig } from '@rainbow-me/rainbowkit';
import { mainnet, polygon, arbitrum, base } from 'wagmi/chains';

export const config = getDefaultConfig({
  appName: 'CryptaCore DeFi',
  projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
  chains: [mainnet, polygon, arbitrum, base],
  ssr: true,
});
TipВсегда сначала разворачивайте в тестовой сети. И forge script в Foundry, и фреймворк развёртывания Hardhat поддерживают многосетевое развёртывание с верификацией. Запустите полный набор тестов на форке основной сети перед любым продакшен-развёртыванием.