Новая модель безопасности контрактов DeFi: упор на неизменность протокола

Краткое содержание

Не просто пишите операторы require для определенных функций, пишите операторы require для своих протоколов. Проверки соответствия функций (требования) - эффективность (эффекты) - взаимодействия (INteractions) + инвариантность протокола (Iniants) или режим FREI-PI могут помочь вашему контракту быть более безопасным, поскольку это заставляет разработчиков сосредоточиться на безопасности на уровне функций. Также будьте осторожны для инвариантов на уровне протокола.

мотивация

В марте 2023 года Euler Finance был взломан и потерял 200 миллионов долларов. Euler Finance — это рынок кредитования, на котором пользователи могут вносить залог и брать под него займы. У него есть некоторые уникальные особенности, на самом деле это рынок кредитования, сравнимый с Compound Finance и Aave.

Вы можете прочитать вскрытие об этом взломе здесь. Его основное содержание — отсутствие проверки работоспособности в конкретной функции, что позволяет пользователям нарушать фундаментальную неизменность рынка кредитования.

Фундаментальные Инициаторы

В основе большинства протоколов DeFi лежит неизменность, свойство состояния программы, которое, как ожидается, всегда будет истинным. Также возможно иметь несколько инвариантов, но в целом они строятся вокруг основной идеи. Вот некоторые примеры:

  • Как и на рынке кредитования: пользователи не могут предпринимать никаких действий, чтобы поставить какую-либо учетную запись в небезопасное или менее безопасное залоговое положение («менее безопасное» означает, что оно уже ниже минимального порога безопасности и, следовательно, не может быть снято в дальнейшем).
  • В AMM DEX: x * y == k, x + y == k и т. д.
  • При стейкинге добычи ликвидности: пользователи должны иметь возможность снимать только то количество токенов стейкинга, которое они внесли.

Что пошло не так с Euler Finance, не обязательно было то, что они добавили функции, не написали тесты или не следовали традиционным передовым методам. Они проверили обновление и провели тесты, но оно все равно проскользнуло сквозь трещины. Основная проблема заключается в том, что они забывают об основном инварианте кредитного рынка (как и аудиторы!).

*Примечание: я не пытаюсь придраться к Эйлеру, это талантливая команда, но это недавний случай. *

Суть проблемы

Вы, вероятно, думаете: «Ну, это верно. Вот почему их взломали; они забыли оператор require». Да и нет.

Но почему они забыли оператор require?

проверить - подтвердить - взаимодействие недостаточно хорошее

Распространенный шаблон, рекомендуемый для разработчиков Solidity, — это шаблон «Проверки-Эффекты-Взаимодействия». Это очень полезно для устранения ошибок, связанных с повторным входом, и часто увеличивает объем проверки входных данных, которую должен выполнять разработчик. _Но_ он склонен к тому, что не видит леса за деревьями.

Вот чему он учит разработчика: «Сначала я пишу свое требование, затем я выполняю проверку, затем, возможно, я выполняю какое-либо взаимодействие, и тогда я в безопасности». Проблема в том, что чаще всего это становится смесью проверок и эффектов — неплохо, не так ли? Взаимодействия по-прежнему являются окончательными, поэтому повторный вход не является проблемой. Но это заставляет пользователей сосредоточиться на более конкретных функциях и отдельных переходах между состояниями, а не на глобальном, более широком контексте. Это значит, что:

Простой шаблон проверки-подтверждения-взаимодействия заставляет разработчиков забыть об основных инвариантах своих протоколов.

Это по-прежнему отличный шаблон для разработчиков, но неизменность протокола всегда должна обеспечиваться (серьезно, вы все равно должны использовать CEI!).

Делайте все правильно: режим FREI-PI

Возьмем, к примеру, этот фрагмент из контракта SoloMargin dYdX (исходный код), который представляет собой кредитный рынок и торговый контракт с кредитным плечом. Это хороший пример того, что я называю шаблоном функциональные требования-эффекты-взаимодействия + протокольные инициативы, или шаблоном FREI-PI.

Таким образом, я считаю, что это единственный рынок кредитования на раннем этапе кредитования, который не имеет каких-либо рыночных уязвимостей. У Compound и Aave нет прямых проблем, но у их форков есть. А bZx много раз взламывали.

Изучите приведенный ниже код и обратите внимание на следующие абстракции:

  1. Проверьте входные параметры (_verifyInputs).
  2. Действие (преобразование данных, манипулирование состоянием)
  3. Проверьте конечное состояние (_verifyFinalState).

Обычные Проверки-Эффекты-Взаимодействия по-прежнему выполняются. Стоит отметить, что проверка-валидация-взаимодействие с дополнительными проверками не эквивалентны FREI-PI — они похожи, но служат принципиально разным целям. Поэтому разработчики должны думать о них по-разному: FREI-PI, как более высокая абстракция, нацелена на безопасность протокола, а CEI — на функциональную безопасность.

Структура этого контракта действительно интересна — пользователи могут выполнять действия, которые они хотят (депозит, заимствование, торговля, перевод, ликвидация и т. д.) в цепочке действий. Хотите внести 3 разных токена, вывести 4-й и ликвидировать аккаунт? Это один звонок.

В этом сила FREI-PI: пользователи могут делать в протоколе все, что захотят, при условии, что инварианты основного рынка кредитования сохраняются в конце разговора: один пользователь не может предпринять никаких действий, которые могли бы сделать любую учетную запись небезопасной. или более Ненадежные залоговые позиции. Для этого контракта это выполняется в _verifyFinalState , проверяя обеспечение каждой затронутой учетной записи, чтобы убедиться, что соглашение лучше, чем при запуске транзакции.

В эту функцию включены некоторые дополнительные инварианты, которые дополняют основные инварианты и помогают с побочными функциями, такими как закрытие рынков, но именно основные проверки действительно обеспечивают безопасность протокола.

Объектно-ориентированный FREI-PI

Еще одна проблема с FREI-PI — концепция, ориентированная на объекты. Возьмем в качестве примера кредитный рынок и предполагаемые основные инварианты:

Технически это не единственный инвариант, но он предназначен для объекта пользователя (это по-прежнему инвариант основного протокола, а обычно пользовательские инварианты являются инвариантами основного протокола). Рынки кредитования также обычно имеют 2 дополнительных субъекта:

  • Оракул
  • Менеджмент / Управление

Каждая дополнительная неизменность усложняет обеспечение протокола, поэтому чем меньше, тем лучше. На самом деле это то, что Дэн Элицер сказал в своей статье под названием «Почему DeFi сломан и как это исправить № 1 Протокол без оракула» (подсказка: в статье на самом деле не говорится, что проблема в оракулах).

Оракул

Что касается оракулов, возьмем эксплойт Cream Finance стоимостью 130 миллионов долларов. Основная неизменность сущностей оракула:

Как оказалось, проверка оракулов во время выполнения с помощью FREI-PI сложна, но выполнима при некоторой предусмотрительности. Вообще говоря, Chainlink — это хороший выбор, на который можно положиться, поскольку он обеспечивает большую часть неизменности. В редких случаях манипуляции или неожиданности может быть полезно иметь меры безопасности, которые снижают гибкость в пользу точности (например, проверка того, что последнее известное значение было на несколько процентов больше, чем текущее значение). Кроме того, система SoloMargin от dYdX отлично справляется со своим оракулом DAI, вот код (если вы не можете сказать, я думаю, что это лучшая сложная система смарт-контрактов из когда-либо написанных).

Чтобы узнать больше об оценке оракула и осветить возможности команды Эйлера, они написали хорошую статью о вычислительном манипулировании ценами оракула Uniswap V3 TWAP.

Администрация / Управление

Создание инвариантов для управляемых сущностей — самая сложная задача. В основном это связано с тем, что большая часть их роли заключается в изменении существующих других инвариантов. Тем не менее, если вы можете избежать использования административных ролей, вы должны это сделать.

По сути, основными инвариантами управляемого объекта могут быть:

Интерпретация: Администраторы могут делать вещи, которые в конечном итоге не должны нарушать инварианты, если только они не вносят радикальных изменений для защиты средств пользователей (например, перемещение активов в контракт на спасение означает удаление инвариантов). Администраторы также должны считаться пользователями, поэтому для них также должна сохраняться пользовательская инвариантность основного рынка кредитования (это означает, что они не могут атаковать других пользователей или протоколы). В настоящее время некоторые действия администратора невозможно проверить во время выполнения с помощью FREI-PI, но с достаточно сильными инвариантами в других местах можно надеяться, что большинство проблем можно смягчить. Я говорю в настоящее время, потому что можно представить себе использование системы проверки zk, которая могла бы проверять все состояние контракта (для каждого пользователя, для каждого оракула и т. д.).

В качестве примера того, как администратор нарушил неизменность, возьмем действие по управлению Compound, которое остановило рынок cETH в августе 2022 года. По сути, это обновление нарушает неизменность Oracle: Oracle предоставляет точную и (относительно) оперативную информацию. Из-за отсутствия функциональности Oracle может предоставить неверную информацию. Проверка FREI-PI во время выполнения, проверяющая, может ли затронутый Oracle предоставлять информацию в режиме реального времени, может предотвратить это при обновлении. Это можно включить в _setPriceOracle, чтобы проверить, все ли активы получили информацию в реальном времени. Преимущество FREI-PI для ролей администраторов заключается в том, что роли администраторов относительно нечувствительны к цене (или, по крайней мере, должны быть), поэтому большее использование газа не должно быть большой проблемой.

Сложность опасна

Таким образом, хотя наиболее важными инвариантами являются базовые инварианты протокола, также могут существовать некоторые ориентированные на объекты инварианты, которые должны поддерживаться базовыми инвариантами. Однако самый простой (и наименьший) набор инвариантов, вероятно, является самым безопасным. Простота — это хорошо Ярким примером является Uniswap…

Почему Uniswap никогда не был взломан (вероятно)

AMM могут иметь простейшую базовую инвариантность любого примитива DeFi: tokenBalanceX * tokenBalanceY == k (например, модель постоянного продукта). Каждая функция в Uniswap V2 вращается вокруг этого простого инварианта:

  1. Монетный двор: добавлено в k
  2. Сжечь: вычесть из k
  3. Поменять местами: перенести x и y, но оставить k.
  4. Обезжирить: перенастройте tokenBalanceX * tokenBalanceY, чтобы сделать его равным k, и удалите лишнюю часть.

Секрет безопасности Uniswap V2: ядро — это простая неизменность, и все функции ей служат. Единственная другая сущность, о которой можно спорить, — это управление, которое может включить переключатель платы, который не затрагивает неизменность ядра, а только распределение владения балансом токенов. Эта простота в заявлении о безопасности является причиной того, что Uniswap никогда не подвергался взлому. Простота на самом деле не является презрением к превосходным разработчикам смарт-контрактов Uniswap, напротив, чтобы найти простоту, нужны отличные инженеры.

Проблема с газом

Мой твиттер уже полон оптимистическими криками ужаса и боли, что эти проверки не нужны и неэффективны. Две вещи по этому вопросу:

  1. Знаете, что еще неэффективно? Приходилось отправлять сообщения ~~Laurence~~ северокорейским хакерам через etherscan, переводить деньги с помощью ETH и угрожать вмешательством ФБР.
  2. Вероятно, вы уже загрузили все нужные вам данные из хранилища, поэтому в конце звонка просто добавьте в горячие данные немного require check. Вы хотите, чтобы ваше соглашение стоило незначительную плату, или позволить ему умереть?

Если стоимость непомерно высока, переосмыслите основные переменные и попытайтесь упростить.

Что это значит для меня?

Как разработчик, важно определить и выразить основные инварианты на ранней стадии процесса разработки. В качестве конкретного предложения: первая функция, которую нужно написать, это _verifyAfter, чтобы проверять ваши инварианты после каждого вызова вашего контракта. Поместите это в свой контракт и разверните его там. Дополните этот инвариант (и другие инварианты, ориентированные на сущности) более широкими инвариантными тестами, которые проверяются перед развертыванием (руководство по Foundry).

Временные хранилища открывают некоторые интересные оптимизации и улучшения, с которыми Nacent будет экспериментировать — я предлагаю вам подумать, как временные хранилища можно использовать в качестве инструмента для повышения безопасности в контекстах вызовов.

В этой статье не так много времени уделено внедрению модели FREI-PI в проверку ввода, но это тоже очень важно. Определение границ ввода — сложная задача, позволяющая избежать переполнения и подобных ситуаций. Подумайте о том, чтобы проверить и следить за развитием нашего инструмента: пирометра (в настоящее время находится в стадии бета-тестирования, пожалуйста, поставьте нам звездочку). Он может развернуть и помочь найти места, где вы, возможно, не выполняете проверку ввода.

в заключение

Помимо любой броской аббревиатуры (FREI-PI) или имени схемы, действительно важным моментом является:

Найдите простоту в неизменности ядра вашего протокола. И работайте изо всех сил, чтобы убедиться, что он никогда не будет уничтожен (или пойман до того, как это произойдет).

Посмотреть Оригинал
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
  • Награда
  • комментарий
  • Поделиться
комментарий
0/400
Нет комментариев
  • Закрепить