Порушення незмінності блокчейну: як модель проксі може досягти оновлень смарт-контракту

Проксі-шаблон дозволяє смарт-контрактам оновлювати свою логіку, зберігаючи свої адреси в ланцюжку та значення стану. Виклик проксі-контракту виконає код із логічного контракту через delegateCall, щоб змінити стан проксі-контракту.

У цій статті надано огляд типів проксі-контрактів, пов’язаних інцидентів безпеки та рекомендацій, а також найкращих практик щодо використання проксі-контрактів.

Знайомство з оновлюваними контрактами та режимом проксі

Ми всі знаємо, що блокчейн не піддається втручанню, і код смарт-контракту не можна змінити після розгортання в блокчейні.

Отже, коли розробники хочуть оновити код контракту для оновлень логіки, виправлення помилок або оновлень безпеки, вони повинні розгорнути новий контракт, і буде згенеровано нову адресу контракту.

Для вирішення цієї проблеми можна використовувати режим проксі.

Режим проксі реалізує можливість оновлення контракту без зміни адреси розгортання контракту, який наразі є найпоширенішим режимом оновлення контракту.

Проксі-режим — це контрактна система з можливістю оновлення, включаючи проксі-контракт і договір реалізації логіки.

Проксі-контракт керує взаємодією користувача та збереженням даних і стану контракту. Виклик користувача проксі-контракту виконає код із логічного контракту через delegatecall(), тим самим змінюючи стан проксі-контракту. Оновлення реалізується шляхом оновлення логічної адреси контракту, записаної в попередньо визначеному слоті зберігання проксі-контракту.

Три звичайні режими проксі: прозорий проксі, проксі UUPS і проксі-маяк.

Прозорий проксі

У режимі прозорого проксі функція оновлення реалізована в договорі проксі. Роль адміністратора проксі-контракту надається прямим повноваженням керувати проксі-контрактом для оновлення адреси логічної реалізації, що відповідає проксі. Абоненти без прав адміністратора делегуватимуть свої дзвінки договору реалізації.

Примітка. Адміністратор проксі-контракту не може відігравати ключову роль у логічній реалізації контракту, а також не може бути навіть звичайним користувачем, оскільки адміністратор проксі-сервера не може взаємодіяти з контрактом реалізації.

Проксі UUPS

У режимі UUPS (Universal Upgradeable Proxy Standard) функція оновлення контракту реалізована в логіці контракту. Оскільки механізм оновлення зберігається в логічному контракті, оновлена версія може видалити пов’язану з оновленням логіку, щоб заборонити майбутні оновлення. У цьому режимі всі виклики проксі-контракту перенаправляються до контракту логічної реалізації.

Проксі-маяк

Режим проксі-сервера Beacon дозволяє кільком проксі-контрактам спільно використовувати ту саму логіку, посилаючись на контракт Beacon. Контракт Beacon надає адресу контракту логічної реалізації для викликаного проксі-контракту. Під час оновлення до нової адреси реалізації логіки потрібно оновити лише адресу, записану в контракті Beacon.

Неправильне використання проксі-сервера та інциденти безпеки

Розробники можуть використовувати контракти в режимі проксі для реалізації оновлюваних контрактних систем. Однак режим проксі також має певні робочі пороги. Неналежне використання може призвести до серйозних проблем із безпекою проекту. У наступних розділах демонструються інциденти, пов’язані з неправильним використанням проксі-сервера, і ризики централізації, які становлять проксі-сервери.

Розкриття ключів, керованих проксі

Адміністратор проксі-сервера контролює механізм оновлення режиму прозорого проксі-сервера. У разі витоку приватного ключа адміністратора зловмисник може оновити логічний контракт і виконати власну шкідливу логіку в стані проксі-сервера.

5 березня 2021 року PAID Network зазнала атаки «карбування», спричиненої поганим керуванням приватним ключем. Платну мережу скористався зловмисник, який викрав закритий ключ адміністратора проксі-сервера та запустив механізм оновлення для зміни логічного контракту.

Після оновлення зловмисник може знищити PAID користувача та викарбувати для себе партію PAID, яку можна буде продати пізніше. У самому коді немає вразливості безпеки, але зловмисник отримав закритий ключ для оновлення контракту від адміністратора.

** Неініціалізована реалізація проксі UUPS **

Для проксі-режиму UUPS під час ініціалізації проксі-контракту абонент передає початкові параметри в проксі-контракт, а потім проксі-контракт викликає функцію initialize() у логічному контракті для досягнення ініціалізації.

Функція initialize() зазвичай захищена модифікатором «initializer», щоб обмежити функцію, яку можна викликати лише один раз. Після виклику функції initialize() з точки зору проксі-контракту ініціалізується логічний контракт.

Однак, з точки зору логічного контракту, логічний контракт не ініціалізується, оскільки initialize() не викликається безпосередньо в логічному контракті. Враховуючи, що сам логічний контракт не ініціалізований, будь-хто може викликати функцію initialize(), щоб ініціалізувати його, встановити для змінної стану зловмисне значення та потенційно взяти на себе логічний контракт.

Вплив логічного контракту, який приймається, залежить від коду контракту в системі. У гіршому випадку зловмисник може оновити логічний контракт у режимі проксі UUPS до зловмисного контракту та виконати виклик функції «самознищення», що може призвести до того, що весь контракт проксі стане марним, а активи в контракті будуть бути остаточно знищено втрачено.

корпус

① Parity Multisig Freeze: логічний контракт не ініціалізовано. Зловмисник запускає ініціалізацію багатьох гаманців і блокує ефір у контракті, викликаючи selfdestruct().

② Harvest Finance, Teller, KeeperDAO та Rivermen використовують неініціалізовані логічні контракти, що дозволить зловмисникам довільно встановлювати параметри ініціалізації контрактів і виконувати selfdestruct() під час delegatecall(), щоб знищити проксі-контракт.

Конфлікт зберігання

У оновлюваній контрактній системі проксі-контракт не оголошує змінні стану, але використовує псевдовипадкові слоти для зберігання важливих даних.

Проксі-контракти зберігають значення логічних змінних стану контракту відносно місця, де вони були оголошені. Якщо проксі-контракт оголошує власні змінні стану, і проксі-сервер, і логічний контракт намагаються використовувати той самий слот для зберігання, виникне конфлікт зберігання.

Проксі-контракт, наданий бібліотекою OpenZeppelin, не оголошує змінні стану в контракті, але на основі стандарту EIP 1967 зберігає значення, яке потрібно зберегти (наприклад, адресу керування) у певному слоті для зберігання, щоб запобігти конфліктам.

корпус

23 липня 2022 року за пекінським часом децентралізовану музичну платформу Audius було зламано. Інцидент стався через впровадження нової логіки в проксі-контракт, що призвело до конфліктів зберігання.

Проксі-контракт оголошує змінну стану адреси proxyAdmin, і її значення буде неправильно прочитано під час виконання коду логічного контракту.

Значення proxyAdmin, налаштоване учасником проекту, помилково вважалося значенням ініціалізованого та ініціалізованого, тому модифікатор ініціалізатора повернув неправильний результат, що дозволило зловмиснику знову викликати функцію initialize() і надати собі повноваження керувати договір. Потім зловмисники змінили параметри голосування та передали свою зловмисну пропозицію, щоб викрасти активи Audius.

Виклик delegatecall() у логічному контракті або ненадійному контракті

Припустімо, що delegatecall() існує в логічному контракті, і контракт не перевіряє належним чином ціль виклику. У цьому випадку зловмисник може використовувати цю функцію для виконання викликів зловмисних контрактів, якими він керує, для руйнування реалізацій логіки або для виконання спеціальної логіки.

Подібним чином, якщо в логічному контракті є необмежена функція address.call(), коли зловмисник зловмисно надає поля адреси та даних, її можна використовувати як проксі-контракт.

корпус

Атаки Pickle Finance, Furucombo та dYdX.

У цих інцидентах уразливий контракт було схвалено маркером користувача, і в контракті є call()/delegatecall(), наданий користувачем для виклику адреси та даних контракту, зловмисник зможе викликати договір функції transferFrom() для зняття балансів користувачів. Під час інциденту dYdX dYdX здійснили власну атаку білих капелюхів, щоб захистити кошти.

Кращі практики

загалом

(1) Використовуйте режим проксі лише за необхідності

Не кожен контракт потрібно оновлювати. Як показано вище, існує багато ризиків, пов’язаних із використанням шаблону проксі. Властивість «оновлюваний» також викликає проблеми довіри, оскільки адміністратори проксі можуть оновлювати контракти без згоди спільноти. Ми рекомендуємо інтегрувати шаблон проксі в проекти лише за необхідності.

(2) Не змінюйте бібліотеку проксі

Бібліотека проксі-контрактів є складною, особливо в частині, яка стосується керування сховищем і механізмів оновлення. Будь-які помилки в модифікації вплинуть на роботу проксі та логічних контрактів. Велика кількість серйозних помилок, пов’язаних з агентами, які ми виявили під час наших аудитів, були спричинені неправильними модифікаціями бібліотеки агентів. Інцидент з Audius є яскравим прикладом наслідків неналежної зміни агентських контрактів.

Ключові моменти роботи агентського контракту та управління ним

(1) Ініціалізація логічного контракту

Зловмисник може заволодіти неініціалізованим логічним контрактом і потенційно скомпрометувати систему проксі-контрактів. Тому, будь ласка, ініціалізуйте логічний контракт після розгортання або використовуйте _disableInitializers() у конструкторі логічного контракту, щоб автоматично вимкнути ініціалізацію.

(2) Забезпечте безпеку облікового запису керування агентом

Контрактна система з можливістю оновлення зазвичай вимагає привілейованої ролі «проксі-адміністратора» для керування оновленнями контрактів. У разі витоку ключа керування зловмисник може вільно оновити контракт до зловмисного, який може викрасти активи користувачів. Ми рекомендуємо ретельно керувати особистими ключами облікових записів адміністратора проксі-сервера, щоб уникнути потенційного ризику злому. Гаманці з кількома підписами можна використовувати, щоб запобігти збоям керування одноточковими ключами.

(3) Використовуйте окремий обліковий запис для прозорого керування проксі

Керування проксі-сервером і керування логікою мають бути окремими адресами, щоб запобігти втраті взаємодії з реалізацією логіки. Якщо керування проксі-сервером і логічне керування посилаються на ту саму адресу, виклики не переадресовуються для виконання привілейованих функцій, таким чином забороняючи зміни функцій керування.

Пов’язане зберігання контракту проксі

(1) Будьте обережні, оголошуючи змінні стану в проксі-контрактах

Як пояснюється в хаці Audius, проксі-контракти повинні бути обережними при оголошенні власних змінних стану. Змінні стану, оголошені звичайним способом у проксі-контрактах, можуть викликати конфлікти даних під час читання та запису даних. Якщо проксі-контракт вимагає змінної стану, збережіть значення в слоті для зберігання, наприклад EIP1967, щоб запобігти конфліктам під час виконання логічного коду контракту.

(2) Підтримувати порядок оголошення змінних і тип логічного контракту

Кожна версія логічного контракту повинна підтримувати той самий порядок і тип змінних стану, а нові змінні стану потрібно додавати в кінці існуючих змінних. В іншому випадку виклики делегування можуть спричинити зчитування або перезаписування неправильних збережених значень проксі-контрактами, а старі дані можуть бути пов’язані з нещодавно оголошеними змінними, що може спричинити серйозні проблеми для програм.

(3) Включіть прогалини у зберіганні в базовий договір

Логічні контракти повинні містити прогалини в коді контракту, щоб передбачити нові змінні стану під час розгортання нових реалізацій логіки. Після додавання нової змінної стану необхідно відповідним чином оновити розмір розриву.

(4) Не встановлюйте значення змінної стану в конструкторі або процесі оголошення

Призначення змінної стану під час оголошення або в конструкторі впливає лише на значення в логічному контракті, а не на проксі-контракті. Незмінні параметри слід призначати за допомогою функції initialize().

Спадщина за договором

(1) Контракти з можливістю оновлення можуть успадковуватись лише від інших контрактів з можливістю оновлення

Контракти з можливістю оновлення мають іншу структуру, ніж контракти без можливості оновлення. Наприклад, конструктор несумісний зі зміною стану агента, він використовує функцію initialize() для встановлення змінних стану.

Будь-який контракт, який успадковує інший контракт, повинен використовувати функцію initialize() свого успадкованого контракту, щоб призначити відповідні змінні. Під час використання бібліотеки OpenZeppelin або написання власного коду переконайтеся, що оновлювані контракти можуть успадковувати лише інші оновлювані контракти.

(2) Не створюйте нові контракти в логічних контрактах

Контракти, створені та створені через Solidity, не можна буде оновити. Контракти слід розгортати індивідуально та передавати їхню адресу як параметр до оновлюваного логічного контракту, щоб досягти оновлюваного стану.

(3) Ризик ініціалізації материнського контракту

Під час ініціалізації батьківського контракту функція __{ContractName}_init ініціалізує його батьківський контракт. Кілька викликів __{ContractName}_init можуть призвести до другої ініціалізації батьківського контракту. Зауважте, що __{ContractName}_init_unchained() лише ініціалізує параметри {ContractName} і не викликає ініціалізатор батьківського контракту.

Однак це не рекомендована практика, оскільки всі материнські контракти потрібно ініціалізувати, а відсутність ініціалізації необхідних контрактів спричинить майбутні проблеми з виконанням.

Реалізація логічного контракту

Уникайте selfdestruct() або selegatecall()/call() ненадійних контрактів

Якщо в контракті є selfdestruct() або delegatecall(), зловмисник може використати ці функції, щоб порушити реалізацію логіки або виконати спеціальну логіку. Розробники повинні перевіряти введені користувачем дані та не дозволяти контрактам виконувати виклики делегування/виклики ненадійних контрактів. Крім того, не рекомендується використовувати delegatecall() у логічних контрактах, оскільки було б громіздко керувати макетом сховища в ланцюжку делегатів кількох контрактів.

Написано в кінці

Проксі-контракти обходять незмінну природу блокчейнів, дозволяючи протоколам оновлювати свою логіку коду після розгортання. Однак розробка проксі-контрактів все ще потребує дуже обережності, а неправильна реалізація може спричинити проблеми з безпекою та логікою проекту.

Загалом найкраще використовувати авторитетні та ретельно перевірені рішення, оскільки режими Transparent, UUPS і Beacon Proxy мають перевірені механізми оновлення для відповідних випадків використання. На додаток до цього, привілейованими ролями для агентів ескалації також слід безпечно керувати, щоб зловмисники не могли змінити логіку агента.

Контракт реалізації логіки також має бути обережним, щоб не використовувати delegatecall(), який може перешкодити зловмисникам виконати якийсь шкідливий код, наприклад selfdestruct().

Хоча дотримання найкращих практик може забезпечити стабільне розгортання проксі-контракту, зберігаючи при цьому гнучкість оновлення, увесь код схильний до нових проблем безпеки або логіки, які можуть поставити під загрозу проект. Тому весь код найкраще перевіряти команді експертів із безпеки, які мають досвід аудиту та захисту протоколів проксі-контрактів.

Переглянути оригінал
Контент має виключно довідковий характер і не є запрошенням до участі або пропозицією. Інвестиційні, податкові чи юридичні консультації не надаються. Перегляньте Відмову від відповідальності , щоб дізнатися більше про ризики.
  • Нагородити
  • Прокоментувати
  • Поділіться
Прокоментувати
0/400
Немає коментарів
  • Закріпити