Проксі-контракти є важливим інструментом для розробників смарт-контрактів. Сьогодні в контрактній системі існує безліч проксі-режимів і відповідних правил використання. Раніше ми описували найкращі методи безпеки контракту проксі-сервера з можливістю оновлення.
У цій статті ми познайомимося з іншою моделлю проксі-сервера, яка популярна в спільноті розробників, — модель алмазного проксі-сервера.
Діамантові проксі-контракти, також відомі як «діаманти», — це шаблон дизайну для смарт-контрактів Ethereum, представлений Пропозицією щодо покращення Ethereum (EIP) 2535.
Діамантовий режим дозволяє контракту мати необмежену функціональність, розділяючи його функціональність на менші контракти (також влучно звані «аспектами»). Diamond діє як проксі, функція маршрутизації викликає відповідний аспект.
Дизайн алмазної моделі може вирішити проблему обмеження максимального розміру контракту мережі Ethereum. Розбиваючи великий контракт на більш дрібні аспекти, ромбовидний шаблон дозволяє розробникам створювати більш складні та багатофункціональні смарт-контракти без обмежень розміру.
Diamond Brokerage пропонує надзвичайну гнучкість у порівнянні з традиційними контрактами з можливістю оновлення. Вони дозволяють оновлювати частини контракту, додавати, замінювати або видаляти вибрані частини функцій, не торкаючись інших частин.
У цій статті наведено огляд EIP-2535, включаючи порівняння з широко використовуваним прозорим режимом проксі та режимом проксі UUPS, а також міркування безпеки для спільноти розробників.
У контексті EIP-2535 «діамант» — це проксі-контракт, функціональна реалізація якого забезпечується різними логічними контрактами, які називаються «аспектами».
Уявіть, що справжній діамант має різні сторони, які називаються гранями, і відповідні алмазні контракти Ethereum також мають різні грані. Кожна функція запозичення алмазів має окрему сторону або грань.
Алмазний стандарт використовує аналогію, щоб розширити можливості «алмазного огранювання» для додавання, заміни або видалення граней і функцій.
Крім того, Diamond Standard надає функцію «Diamond Loupe», яка повертає інформацію про грані та наявність діаманта.
У порівнянні з традиційною проксі-моделлю, «діамант» еквівалентний проксі-контракту, а різні «аспекти» відповідають реалізації контракту. Різні аспекти алмазного агента можуть спільно використовувати внутрішні функції, бібліотеки та змінні стану. Основні компоненти діаманта такі:
Центральний контракт, який діє як проксі, функція маршрутизації викликає відповідний аспект. Він містить відображення селекторів функцій на адреси "аспектів".
Єдиний контракт, який реалізує певну функцію. Кожна грань містить набір функцій, які можуть бути викликані алмазом.
це набір стандартних функцій, визначених у EIP-2535, які надають інформацію про селектори граней і функцій, що використовуються в алмазах. Diamond Loupe дозволяє розробникам і користувачам перевірити та зрозуміти структуру алмазів.
Функції для додавання, заміни або видалення граней у діаманті та їхні відповідні селектори функцій. Лише авторизовані адреси (наприклад, власник діаманта або договір з кількома підписами) можуть виконувати ограновування алмазів.
Подібно до традиційних агентів, коли виклик функції виконується на алмазному агенті, запускається резервна функція агента (резервна функція). Основна відмінність від алмазного проксі полягає в тому, що у резервній функції є відображення selectorToFacet, яке зберігає та визначає, яка адреса логічного контракту має реалізацію викликаної функції. Потім він використовує виклик delegate для виконання функції, як і традиційний проксі.
Усі проксі використовують функцію fallback() для делегування викликів функцій на зовнішні адреси. Нижче наведено реалізацію діамантового проксі та реалізацію традиційного проксі.
Варто зазначити, що їхні блоки асемблерного коду дуже схожі, тому єдина різниця полягає в адресі аспекту у виклику делегату алмазного проксі та адресі impl у виклику делегату традиційного проксі.
Основна відмінність полягає в тому, що в алмазному проксі адреса аспекту визначається хеш-картою від msg.sig (селектора функції) абонента до адреси аспекту, тоді як у традиційному проксі адреса impl не залежить від входить абонент.
Резервна функція алмазного агента
Традиційна резервна функція проксі
Відображення SelectorToFacet визначає, який контракт містить реалізацію кожного селектора функції. Працівникам проекту часто доводиться додавати, замінювати або видаляти це відображення контракту вибору функції на реалізацію. EIP-2535 передбачає: для досягнення цієї мети має бути функція diamondCut(). Нижче наведено приклад інтерфейсу.
Кожна структура FacetCut містить адресу фасету та чотирибайтовий масив селектора функцій, який потрібно оновити в алмазному проксі-контракті. FaceCutAction дозволяє додавати, замінювати та видаляти селектори функцій. Реалізація функції diamondCut() повинна включати адекватний контроль доступу, запобігання зіткненням слотів, відновлення після відмови тощо.
Щоб дізнатися, які функції та грані має алмазний агент, ми використовуємо «алмазне збільшувальне скло». «Діамантова лупа» — це спеціальний аспект, який реалізує такий інтерфейс, визначений у EIP-2535:
Функція facets() має повертати адреси всіх фасетів та їхніх чотирибайтових селекторів функцій. Функція facetFunctionSelectors() має повертати всі селектори функцій, які підтримуються певним аспектом. Функція facetAddresses() має повертати всі адреси граней, які використовує ромб.
Функція facetAddress() має повертати аспект, який підтримує заданий селектор, або адресу (0), якщо не знайдено. Зауважте, що не повинно бути більше однієї адреси аспекту з тим самим селектором функції.
З огляду на те, що алмазні проксі-сервери делегують виклики різних функцій різним контрактам реалізації, важливо правильно керувати слотами для зберігання, щоб запобігти конфліктам. EIP-2535 згадує декілька методів керування слотами пам’яті.
Цей аспект може оголошувати змінні стану в структурі. Цей аспект може використовувати будь-яку кількість структур, кожна з яких має інше місце зберігання. Кожна структура має певне місце в контрактному сховищі. Аспекти можуть оголошувати власні змінні стану, але вони не можуть конфліктувати з місцями зберігання змінних стану, оголошених іншими аспектами. Зразок бібліотеки та договору про зберігання алмазів наведено в EIP-2535, як показано на наступному малюнку:
Сховище програм – це більш спеціалізована версія алмазного сховища. Цей шаблон використовується для більш зручного та легкого обміну змінними стану аспектів. Структура магазину додатків визначається так, щоб містити будь-яку кількість і тип змінних стану, необхідних для програми. Аспект завжди оголошує структуру AppStorage як першу та єдину змінну стану в позиції 0 слота для зберігання. Потім різні аспекти можуть отримати доступ до змінних із цієї структури.
Існують також інші стратегії керування слотами для зберігання, включаючи гібрид Diamond Storage та AppStorage. Наприклад, деякі структури є спільними для різних аспектів, а деякі є специфічними для конкретного аспекту. У всіх випадках дуже важливо запобігати випадковим зіткненням слотів.
Порівняння з Transparent Proxy і UUPS Proxy
Два основних режими проксі, які зараз використовуються спільнотою розробників Web3, — це прозорий режим проксі та режим проксі UUPS. У цьому розділі ми коротко порівняємо режим алмазного проксі з режимами прозорого проксі та проксі UUPS.
1.EPI-2535:
2.EPI-1967:
3. Реалізація еталонного проксі-сервера Diamond:
4. Реалізація OpenZeppelin:
Проксі-сервери та масштабовані рішення є більш складними системами, і OpenZeppelin надає кодові бази та вичерпну документацію для масштабованих проксі-серверів UUPS, Transparent і Beacon. Однак для режиму діамантового проксі, хоча OpenZeppelin підтвердив його переваги, вони все ж вирішили не включати реалізацію діаманта EIP-2535 у свою бібліотеку.
Тому розробники, які використовують існуючі бібліотеки сторонніх розробників або самі впроваджують це рішення, повинні впроваджувати його з особливою обережністю. Тут ми склали контрольний перелік найкращих практик безпеки, який варто взяти до уваги спільноті розробників.
Розбиваючи логіку контракту на менші, більш керовані модулі, розробники можуть легше тестувати та перевіряти свій код.
Крім того, такий підхід дозволяє розробникам зосередитися на конкретних аспектах створення та підтримки контрактів, а не на управлінні складною монолітною кодовою базою. Кінцевим результатом є більш гнучка і модульна кодова база, яку можна легко оновлювати та змінювати, не впливаючи на інші частини контракту.
Джерело: Aavegotchi Github
Коли розгортається алмазний проксі-контракт, він повинен додати адресу алмазного контракту DiamondCutFacet до алмазного проксі-контракту та реалізувати функцію diamondCut(). Функція diamondCut() використовується для додавання, видалення або заміни фасетів і функцій, без DiamondCutFacet і diamondCut() алмазний агент не може працювати належним чином.
Джерело: Mugen's Diamond-3-Hardhat
Додаючи нову змінну стану до структури зберігання в смарт-контракті, її потрібно додати в кінець структури. Додавання нової змінної стану на початку або в середині структури призведе до того, що нова змінна стану перезапише існуючі дані змінної стану, і будь-яка змінна стану після нової змінної стану може посилатися на неправильне розташування пам’яті.
Шаблон AppStorage вимагає, щоб одна і тільки одна структура була оголошена для алмазного проксі, і щоб ця структура була спільною для всіх аспектів. Якщо потрібно кілька структур, слід використовувати шаблон DiamondStorage.
Не розміщуйте структуру безпосередньо всередині іншої структури, якщо ви не впевнені, що не збираєтеся додавати більше змінних стану до внутрішньої структури. Неможливо додати нові змінні стану до внутрішніх структур під час оновлення без перезапису слотів зберігання змінних, оголошених після структури.
Обхідним шляхом є додавання нових змінних стану до відображеної в пам’яті структури замість розміщення «структури» безпосередньо в «структурі». Слоти зберігання змінних на карті обчислюються по-різному і не є суміжними в пам’яті.
На розмір масиву впливатиме розмір структури. Коли до структури додається нова змінна стану, вона змінює розмір і макет цієї структури.
Це може спричинити проблеми, якщо структура використовується як елемент у масиві. Якщо розмір і компонування структури змінюються, розмір і компонування масиву також зміняться, що може спричинити проблеми з індексуванням або іншими операціями, які залежать від узгодженого розміру та компонування структури.
Подібно до інших шаблонів проксі, кожна змінна повинна мати унікальний слот для зберігання. В іншому випадку дві різні структури в одному місці перезапишуть одна одну.
Функція initialize() зазвичай використовується для встановлення важливих змінних, таких як адреси привілейованих ролей. Якщо його не ініціалізовано під час розгортання контракту, зловмисник може викликати контракт і контролювати його.
Рекомендується додати відповідний контроль доступу до функції ініціалізації/налаштування або переконатися, що функція викликається під час розгортання контракту та не може бути викликана повторно.
Якщо будь-який аспект контракту може викликати функцію selfdestruct(), він може знищити весь контракт, що призведе до втрати коштів або даних. Це надзвичайно небезпечно в режимі діамантового проксі-сервера, оскільки кілька аспектів можуть отримати доступ до сховища та даних проксі-контракту.
Зараз ми бачимо все більше і більше проектів, які використовують модель алмазного проксі у своїх смарт-контрактах. Він пропонує гнучкість та інші переваги порівняно з традиційними проксі-серверами.
Однак додаткова гнучкість також може означати ширшу поверхню атаки для зловмисників. Ми сподіваємося, що ця стаття допоможе спільноті розробників зрозуміти механізми моделі алмазного проксі та її аспекти безпеки.
У той же час команда проекту повинна проводити ретельне тестування та сторонні аудити, щоб зменшити ризик уразливості, пов’язаної з виконанням контрактів з алмазним агентством.
Переглянути оригінал
Контент має виключно довідковий характер і не є запрошенням до участі або пропозицією. Інвестиційні, податкові чи юридичні консультації не надаються. Перегляньте Відмову від відповідальності , щоб дізнатися більше про ризики.
Найкращі методи безпеки для контрактів з алмазним агентством
Проксі-контракти є важливим інструментом для розробників смарт-контрактів. Сьогодні в контрактній системі існує безліч проксі-режимів і відповідних правил використання. Раніше ми описували найкращі методи безпеки контракту проксі-сервера з можливістю оновлення.
У цій статті ми познайомимося з іншою моделлю проксі-сервера, яка популярна в спільноті розробників, — модель алмазного проксі-сервера.
Діамантові проксі-контракти, також відомі як «діаманти», — це шаблон дизайну для смарт-контрактів Ethereum, представлений Пропозицією щодо покращення Ethereum (EIP) 2535.
Діамантовий режим дозволяє контракту мати необмежену функціональність, розділяючи його функціональність на менші контракти (також влучно звані «аспектами»). Diamond діє як проксі, функція маршрутизації викликає відповідний аспект.
Дизайн алмазної моделі може вирішити проблему обмеження максимального розміру контракту мережі Ethereum. Розбиваючи великий контракт на більш дрібні аспекти, ромбовидний шаблон дозволяє розробникам створювати більш складні та багатофункціональні смарт-контракти без обмежень розміру.
Diamond Brokerage пропонує надзвичайну гнучкість у порівнянні з традиційними контрактами з можливістю оновлення. Вони дозволяють оновлювати частини контракту, додавати, замінювати або видаляти вибрані частини функцій, не торкаючись інших частин.
У цій статті наведено огляд EIP-2535, включаючи порівняння з широко використовуваним прозорим режимом проксі та режимом проксі UUPS, а також міркування безпеки для спільноти розробників.
У контексті EIP-2535 «діамант» — це проксі-контракт, функціональна реалізація якого забезпечується різними логічними контрактами, які називаються «аспектами».
Уявіть, що справжній діамант має різні сторони, які називаються гранями, і відповідні алмазні контракти Ethereum також мають різні грані. Кожна функція запозичення алмазів має окрему сторону або грань.
Алмазний стандарт використовує аналогію, щоб розширити можливості «алмазного огранювання» для додавання, заміни або видалення граней і функцій.
Крім того, Diamond Standard надає функцію «Diamond Loupe», яка повертає інформацію про грані та наявність діаманта.
У порівнянні з традиційною проксі-моделлю, «діамант» еквівалентний проксі-контракту, а різні «аспекти» відповідають реалізації контракту. Різні аспекти алмазного агента можуть спільно використовувати внутрішні функції, бібліотеки та змінні стану. Основні компоненти діаманта такі:
Центральний контракт, який діє як проксі, функція маршрутизації викликає відповідний аспект. Він містить відображення селекторів функцій на адреси "аспектів".
Єдиний контракт, який реалізує певну функцію. Кожна грань містить набір функцій, які можуть бути викликані алмазом.
це набір стандартних функцій, визначених у EIP-2535, які надають інформацію про селектори граней і функцій, що використовуються в алмазах. Diamond Loupe дозволяє розробникам і користувачам перевірити та зрозуміти структуру алмазів.
Функції для додавання, заміни або видалення граней у діаманті та їхні відповідні селектори функцій. Лише авторизовані адреси (наприклад, власник діаманта або договір з кількома підписами) можуть виконувати ограновування алмазів.
Подібно до традиційних агентів, коли виклик функції виконується на алмазному агенті, запускається резервна функція агента (резервна функція). Основна відмінність від алмазного проксі полягає в тому, що у резервній функції є відображення selectorToFacet, яке зберігає та визначає, яка адреса логічного контракту має реалізацію викликаної функції. Потім він використовує виклик delegate для виконання функції, як і традиційний проксі.
Усі проксі використовують функцію fallback() для делегування викликів функцій на зовнішні адреси. Нижче наведено реалізацію діамантового проксі та реалізацію традиційного проксі.
Варто зазначити, що їхні блоки асемблерного коду дуже схожі, тому єдина різниця полягає в адресі аспекту у виклику делегату алмазного проксі та адресі impl у виклику делегату традиційного проксі.
Основна відмінність полягає в тому, що в алмазному проксі адреса аспекту визначається хеш-картою від msg.sig (селектора функції) абонента до адреси аспекту, тоді як у традиційному проксі адреса impl не залежить від входить абонент.
Резервна функція алмазного агента
Традиційна резервна функція проксі
Відображення SelectorToFacet визначає, який контракт містить реалізацію кожного селектора функції. Працівникам проекту часто доводиться додавати, замінювати або видаляти це відображення контракту вибору функції на реалізацію. EIP-2535 передбачає: для досягнення цієї мети має бути функція diamondCut(). Нижче наведено приклад інтерфейсу.
Кожна структура FacetCut містить адресу фасету та чотирибайтовий масив селектора функцій, який потрібно оновити в алмазному проксі-контракті. FaceCutAction дозволяє додавати, замінювати та видаляти селектори функцій. Реалізація функції diamondCut() повинна включати адекватний контроль доступу, запобігання зіткненням слотів, відновлення після відмови тощо.
Щоб дізнатися, які функції та грані має алмазний агент, ми використовуємо «алмазне збільшувальне скло». «Діамантова лупа» — це спеціальний аспект, який реалізує такий інтерфейс, визначений у EIP-2535:
Функція facets() має повертати адреси всіх фасетів та їхніх чотирибайтових селекторів функцій. Функція facetFunctionSelectors() має повертати всі селектори функцій, які підтримуються певним аспектом. Функція facetAddresses() має повертати всі адреси граней, які використовує ромб.
Функція facetAddress() має повертати аспект, який підтримує заданий селектор, або адресу (0), якщо не знайдено. Зауважте, що не повинно бути більше однієї адреси аспекту з тим самим селектором функції.
З огляду на те, що алмазні проксі-сервери делегують виклики різних функцій різним контрактам реалізації, важливо правильно керувати слотами для зберігання, щоб запобігти конфліктам. EIP-2535 згадує декілька методів керування слотами пам’яті.
Цей аспект може оголошувати змінні стану в структурі. Цей аспект може використовувати будь-яку кількість структур, кожна з яких має інше місце зберігання. Кожна структура має певне місце в контрактному сховищі. Аспекти можуть оголошувати власні змінні стану, але вони не можуть конфліктувати з місцями зберігання змінних стану, оголошених іншими аспектами. Зразок бібліотеки та договору про зберігання алмазів наведено в EIP-2535, як показано на наступному малюнку:
Сховище програм – це більш спеціалізована версія алмазного сховища. Цей шаблон використовується для більш зручного та легкого обміну змінними стану аспектів. Структура магазину додатків визначається так, щоб містити будь-яку кількість і тип змінних стану, необхідних для програми. Аспект завжди оголошує структуру AppStorage як першу та єдину змінну стану в позиції 0 слота для зберігання. Потім різні аспекти можуть отримати доступ до змінних із цієї структури.
Існують також інші стратегії керування слотами для зберігання, включаючи гібрид Diamond Storage та AppStorage. Наприклад, деякі структури є спільними для різних аспектів, а деякі є специфічними для конкретного аспекту. У всіх випадках дуже важливо запобігати випадковим зіткненням слотів.
Порівняння з Transparent Proxy і UUPS Proxy
Два основних режими проксі, які зараз використовуються спільнотою розробників Web3, — це прозорий режим проксі та режим проксі UUPS. У цьому розділі ми коротко порівняємо режим алмазного проксі з режимами прозорого проксі та проксі UUPS.
1.EPI-2535:
2.EPI-1967:
3. Реалізація еталонного проксі-сервера Diamond:
4. Реалізація OpenZeppelin:
Проксі-сервери та масштабовані рішення є більш складними системами, і OpenZeppelin надає кодові бази та вичерпну документацію для масштабованих проксі-серверів UUPS, Transparent і Beacon. Однак для режиму діамантового проксі, хоча OpenZeppelin підтвердив його переваги, вони все ж вирішили не включати реалізацію діаманта EIP-2535 у свою бібліотеку.
Тому розробники, які використовують існуючі бібліотеки сторонніх розробників або самі впроваджують це рішення, повинні впроваджувати його з особливою обережністю. Тут ми склали контрольний перелік найкращих практик безпеки, який варто взяти до уваги спільноті розробників.
Розбиваючи логіку контракту на менші, більш керовані модулі, розробники можуть легше тестувати та перевіряти свій код.
Крім того, такий підхід дозволяє розробникам зосередитися на конкретних аспектах створення та підтримки контрактів, а не на управлінні складною монолітною кодовою базою. Кінцевим результатом є більш гнучка і модульна кодова база, яку можна легко оновлювати та змінювати, не впливаючи на інші частини контракту.
Джерело: Aavegotchi Github
Коли розгортається алмазний проксі-контракт, він повинен додати адресу алмазного контракту DiamondCutFacet до алмазного проксі-контракту та реалізувати функцію diamondCut(). Функція diamondCut() використовується для додавання, видалення або заміни фасетів і функцій, без DiamondCutFacet і diamondCut() алмазний агент не може працювати належним чином.
Джерело: Mugen's Diamond-3-Hardhat
Додаючи нову змінну стану до структури зберігання в смарт-контракті, її потрібно додати в кінець структури. Додавання нової змінної стану на початку або в середині структури призведе до того, що нова змінна стану перезапише існуючі дані змінної стану, і будь-яка змінна стану після нової змінної стану може посилатися на неправильне розташування пам’яті.
Шаблон AppStorage вимагає, щоб одна і тільки одна структура була оголошена для алмазного проксі, і щоб ця структура була спільною для всіх аспектів. Якщо потрібно кілька структур, слід використовувати шаблон DiamondStorage.
Не розміщуйте структуру безпосередньо всередині іншої структури, якщо ви не впевнені, що не збираєтеся додавати більше змінних стану до внутрішньої структури. Неможливо додати нові змінні стану до внутрішніх структур під час оновлення без перезапису слотів зберігання змінних, оголошених після структури.
Обхідним шляхом є додавання нових змінних стану до відображеної в пам’яті структури замість розміщення «структури» безпосередньо в «структурі». Слоти зберігання змінних на карті обчислюються по-різному і не є суміжними в пам’яті.
На розмір масиву впливатиме розмір структури. Коли до структури додається нова змінна стану, вона змінює розмір і макет цієї структури.
Це може спричинити проблеми, якщо структура використовується як елемент у масиві. Якщо розмір і компонування структури змінюються, розмір і компонування масиву також зміняться, що може спричинити проблеми з індексуванням або іншими операціями, які залежать від узгодженого розміру та компонування структури.
Подібно до інших шаблонів проксі, кожна змінна повинна мати унікальний слот для зберігання. В іншому випадку дві різні структури в одному місці перезапишуть одна одну.
Функція initialize() зазвичай використовується для встановлення важливих змінних, таких як адреси привілейованих ролей. Якщо його не ініціалізовано під час розгортання контракту, зловмисник може викликати контракт і контролювати його.
Рекомендується додати відповідний контроль доступу до функції ініціалізації/налаштування або переконатися, що функція викликається під час розгортання контракту та не може бути викликана повторно.
Якщо будь-який аспект контракту може викликати функцію selfdestruct(), він може знищити весь контракт, що призведе до втрати коштів або даних. Це надзвичайно небезпечно в режимі діамантового проксі-сервера, оскільки кілька аспектів можуть отримати доступ до сховища та даних проксі-контракту.
Зараз ми бачимо все більше і більше проектів, які використовують модель алмазного проксі у своїх смарт-контрактах. Він пропонує гнучкість та інші переваги порівняно з традиційними проксі-серверами.
Однак додаткова гнучкість також може означати ширшу поверхню атаки для зловмисників. Ми сподіваємося, що ця стаття допоможе спільноті розробників зрозуміти механізми моделі алмазного проксі та її аспекти безпеки.
У той же час команда проекту повинна проводити ретельне тестування та сторонні аудити, щоб зменшити ризик уразливості, пов’язаної з виконанням контрактів з алмазним агентством.