Ми часто бачимо: контракт виріс до 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 (аналогічно 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-контрактів? Напишіть нам для попереднього аудиту вашої архітектури.







