Разработка смарт-контрактов с Diamond Standard (EIP-2535)

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка смарт-контрактов с Diamond Standard (EIP-2535)
Сложная
~1-2 недели
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1258
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1170
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    873
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1092
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    563
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    830

Разработка смарт-контрактов с Diamond Standard (EIP-2535)

Контракт вырос до 23KB — лимит размера EVM (EIP-170: 24KB на deployed bytecode). Добавить ещё одну фичу некуда. Переписывать всё — это migration, downtime, потеря истории транзакций и потенциально миллионы в TVL под риском. Diamond Standard (EIP-2535) решает эту проблему системно: вместо одного монолитного контракта — один Diamond proxy с произвольным количеством facets, каждый из которых несёт часть логики.

Как работает Diamond: маршрутизация через fallback

Diamond-контракт сам по себе содержит минимум логики. Его fallback() перехватывает все вызовы, смотрит в DiamondStorage — mapping от function selector до адреса facet, и делегирует вызов нужному facet через delegatecall.

fallback() external payable {
    DiamondStorage storage ds = diamondStorage();
    address facet = ds.selectorToFacet[msg.sig];
    require(facet != address(0), "Diamond: function not found");
    assembly {
        calldatacopy(0, 0, calldatasize())
        let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
        returndatacopy(0, 0, returndatasize())
        switch result
        case 0 { revert(0, returndatasize()) }
        default { return(0, returndatasize()) }
    }
}

Весь state хранится в Diamond (потому что delegatecall исполняет код facet в storage контекста Diamond). Facets — это stateless логика. Это означает, что все facets разделяют одно и то же storage space, что порождает главную специфическую проблему Diamond.

Storage collision: самая опасная ловушка EIP-2535

В стандартном proxy паттерне (EIP-1967) storage collision решается тем, что proxy хранит адрес имплементации по заранее известному слоту, вычисленному через keccak256('eip1967.proxy.implementation') - 1. В Diamond несколько facets разделяют весь storage контракта.

Если Facet A объявляет uint256 public totalSupply в slot 0, а Facet B объявляет address public owner в slot 0 — при чтении owner вернётся интерпретация totalSupply как address. Контракт работает «без ошибок» и даёт неправильные результаты.

Diamond Storage Pattern — решение из EIP-2535: каждый facet хранит свои данные в именованной struct, размещённой по псевдослучайному storage slot:

library LibToken {
    bytes32 constant STORAGE_POSITION = 
        keccak256("diamond.storage.token.v1");
    
    struct TokenStorage {
        uint256 totalSupply;
        mapping(address => uint256) balances;
        mapping(address => mapping(address => uint256)) allowances;
    }
    
    function tokenStorage() internal pure returns (TokenStorage storage ts) {
        bytes32 position = STORAGE_POSITION;
        assembly {
            ts.slot := position
        }
    }
}

Каждый facet использует LibToken.tokenStorage() вместо прямых переменных. Коллизия возможна только если два разных STORAGE_POSITION совпадут — при использовании уникальных строк это практически невозможно.

DiamondCut: добавление и замена facets

Управление facets происходит через diamondCut() — единственная функция, которая меняет routing таблицу Diamond. Это точка контроля для апгрейдов.

struct FacetCut {
    address facetAddress;
    FacetCutAction action; // Add, Replace, Remove
    bytes4[] functionSelectors;
}

function diamondCut(
    FacetCut[] calldata _diamondCut,
    address _init,
    bytes calldata _calldata
) external;

_init + _calldata — optional: адрес контракта и calldata, который будет вызван через delegatecall сразу после изменения facets. Используется для миграции storage при замене facet (analogous to OpenZeppelin's upgradeAndCall).

Право вызывать diamondCut должно быть защищено. Стандартный паттерн: OwnershipFacet контролирует доступ, diamondCut доступен только owner. Для DAO-управляемых протоколов — governance через TimelockController + Governor, который вызывает diamondCut после голосования.

Сравнение с альтернативными proxy паттернами

Паттерн Лимит размера Апгрейдируемость Сложность Gas overhead
Transparent Proxy (EIP-1967) 24KB на логику Полная замена Низкая ~2000 gas
UUPS (EIP-1822) 24KB на логику Полная замена Средняя ~1500 gas
Beacon Proxy 24KB, один beacon Групповая замена Средняя ~2500 gas
Diamond (EIP-2535) Безлимитно Частичная замена Высокая ~3000 gas

Diamond — не всегда правильный выбор. Для контракта до 15KB с простой логикой UUPS проще и дешевле. Diamond оправдан когда: контракт уже близок к лимиту размера, нужна гранулярная апгрейдируемость (обновить только один модуль без замены всего контракта), или логика разрабатывается несколькими командами независимо.

Инструментарий и аудит Diamond

Louper.dev — UI для инспекции Diamond контрактов. Показывает все facets, их function selectors, адреса. Обязательный инструмент для аудиторов и разработчиков.

hardhat-diamond-abi — собирает ABI из всех facets в один файл. Нужен для frontend — frontend видит один контракт, не множество facets.

Nick Mudge's diamond-3 — reference implementation от автора EIP-2535. Используем как базу, не как copy-paste — важно понять каждую строку.

Аудит Diamond-контрактов требует специфической экспертизы: аудиторы проверяют storage layout всех facets на коллизии, корректность diamondCut access control, отсутствие selector clashes (два facet с одним selector). Slither имеет частичную поддержку Diamond, но manual review обязателен.

Процесс разработки

Проектирование facet структуры (2-3 дня). Разбиваем логику на логические модули: TokenFacet, GovernanceFacet, RewardsFacet, AdminFacet. Проектируем storage namespaces для каждого. Это самый важный этап — переработка storage layout после деплоя катастрофична.

Разработка (1.5-2 недели). Каждый facet разрабатывается и тестируется изолированно. Интеграционные тесты — против полного Diamond.

Storage collision check. Перед деплоем запускаем кастомный скрипт, который сравнивает все STORAGE_POSITION значений всех facets на уникальность. Пересечение — блокирующая ошибка.

Деплой и верификация. Diamond деплоится первым, затем каждый facet отдельно, потом diamondCut инициализирует routing. Каждый facet верифицируется на Etherscan. Louper.dev используем для финальной проверки конфигурации.

Сроки: 1-2 недели для системы из 3-5 facets, до месяца для крупного протокола с 10+ facets и сложной governance.