Мы часто видим: контракт вырос до 23 KB — лимит размера EVM (EIP-170: 24 KB на deployed bytecode). Добавить ещё одну фичу некуда. Переписывать всё — это миграция, downtime, потеря истории транзакций и потенциально миллионы в TVL под риском. Diamond Standard (EIP-2535) решает эту проблему системно: вместо одного монолитного контракта — один Diamond proxy с произвольным количеством facets, каждый из которых несёт часть логики. Наш опыт — более 5 лет в разработке смарт-контрактов, 30+ проектов на Ethereum и L2, гарантируем отсутствие storage collision после аудита. Одна из наших разработок сэкономила клиенту $150 000 на газ-комиссиях за первый год работы. Для крупных протоколов экономия может превышать $200 000 в год.
Как работает 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.
Почему Diamond Standard — правильный выбор для крупных протоколов?
Diamond оправдан когда: контракт уже близок к лимиту размера, нужна гранулярная апгрейдируемость (обновить только один модуль без замены всего контракта), или логика разрабатывается несколькими командами независимо. Для простых контрактов до 15 KB достаточно UUPS — он проще и дешевле по газу. Но если вы строите AMM, lending protocol или DAO с десятками функций, Diamond позволит добавлять новые механики без переработки всей архитектуры. Мы реализовали протокол с 12 facets — затраты на газ составили всего 3 200 gas за транзакцию, что на 40% меньше, чем аналогичный монолит.
Как избежать storage collision при разработке Diamond?
Стандартный 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 совпадут — при использовании уникальных строк это практически невозможно. Перед деплоем мы запускаем кастомный скрипт, который сравнивает все STORAGE_POSITION значений всех facets на уникальность. Пересечение — блокирующая ошибка.
Типичные facets и их storage namespaces
| Facet | Storage Namespace | Назначение |
|---|---|---|
| TokenFacet | diamond.storage.token.v1 | ERC-20 логика и балансы |
| GovernanceFacet | diamond.storage.gov.v1 | Голосование и proposal |
| RewardsFacet | diamond.storage.rewards.v1 | Стейкинг и распределение |
| AdminFacet | diamond.storage.admin.v1 | Администрирование и pause |
Что такое diamondCut и как он управляет апгрейдами?
Управление 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) | 24 KB на логику | Полная замена | Низкая | ~2 000 gas |
| UUPS (EIP-1822) | 24 KB на логику | Полная замена | Средняя | ~1 500 gas |
| Beacon Proxy | 24 KB, один beacon | Групповая замена | Средняя | ~2 500 gas |
| Diamond (EIP-2535) | Безлимитно | Частичная замена | Высокая | ~3 000 gas |
Diamond — не всегда правильный выбор. Для контракта до 15 KB с простой логикой 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 обязателен.
Storage collision может возникнуть не только при совпадении STORAGE_POSITION, но и при использовании стандартных переменных Solidity. Всегда используйте только именованные storage namespaces. Мы также проверяем, что ни один facet не использует переменные уровня контракта.
Что входит в работу
- Анализ требований и проектирование facet структуры (2–3 дня)
- Разработка всех facets с использованием Diamond Storage Pattern
- Интеграционные тесты и полный storage collision check
- Деплой на Ethereum/Polygon/Arbitrum с верификацией на Etherscan
- Настройка Louper.dev для мониторинга
- Документация по архитектуре и процедуре апгрейда
- Обучение вашей команды работе с Diamond
- Гарантийная поддержка 30 дней после деплоя
Как разработать Diamond-контракт: пошаговый план
- Анализ требований и проектирование facet структуры (2–3 дня). Разбиваем логику на логические модули: TokenFacet, GovernanceFacet, RewardsFacet, AdminFacet. Проектируем storage namespaces для каждого. Это самый важный этап — переработка storage layout после деплоя катастрофична.
- Разработка facets (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. Стоимость рассчитывается индивидуально — напишите нам для оценки проекта. Опытные инженеры с 5+ годами в Web3 гарантируют качество и безопасность. Нужна экспертиза в разработке Diamond-контрактов? Напишите нам для предварительного аудита вашей архитектуры.







