Результаты: Всего проблем: 17 (9 решено) Критических: 0 (0 решено) · Высоких: 0 (0 решено) · Средних: 2 (1 решено) · Низких: 7 (6 решено)
Примечания и дополнительная информация: 8 примечаний (2 решено)
Область аудита
OpenZeppelin провела аудит следующих запросов на включение (pull requests):
- pull request #469 в коммите 7181245: Поток обмена на основе прокси
- pull request #470 в коммите c908caf: Universal Router v2.1.1
- pull request #516 в коммите d6d0656: Поэтапное проскальзывание для одиночных обменов и переключение на соотношение вывода/ввода
В область аудита вошли следующие файлы:
(pull request #516)v4-periphery└── src ├── V4Router.sol ├── interfaces │ └── IV4Router.sol └── libraries └── CalldataDecoder.sol(pull request #469)universal-router└── contracts ├── SwapProxy.sol └── interfaces └── ISwapProxy.sol(pull request #470)universal-router└── contracts ├── base │ └── Dispatcher.sol ├── libraries │ ├── Commands.sol │ └── Constants.sol └── modules ├── Payments.sol └── uniswap ├── v2 │ └── V2SwapRouter.sol └── v3 ├── BytesLib.sol └── V3SwapRouter.sol
Обзор системы
Universal Router служит единой точкой входа для выполнения обменов и других взаимодействий в экосистеме Uniswap. В рамках данного аудита рассматриваются несколько недавних обновлений Universal Router, разработанных для повышения гибкости маршрутизации, точности платежей и защиты от проскальзывания. Изменения внедряют новый поток выполнения на основе прокси, уточняют обработку платежей для поддержки дробных сумм и реализуют более детальный контроль проскальзывания для каждого этапа (per-hop). Также подлежат проверке обновления контрактов периферии v4, которые расширяют параметры проскальзывания и изменяют логику декодирования для повышения безопасности против MEV и неожиданных ценовых движений.
Поток обмена на основе прокси (Universal Router pull request #469)
Новый контракт SwapProxy облегчает двухэтапный путь выполнения обмена. Это позволяет пользователям делегировать выполнение обмена третьей стороне, не предоставляя прямого одобрения (approval) своих токенов маршрутизатору.
- Выполнение через прокси: Пользователи одобряют SwapProxy для расходования своих токенов. Затем прокси выполняет обмен от их имени через Universal Router.
- Защита контекста: Основная цель — гарантировать, что даже при делегированном выполнении активы пользователя используются только для запланированного обмена и не могут быть присвоены иным образом в контексте прокси.
Новое поведение платежей (Universal Router pull request #470)
Это обновление вводит новую команду оплаты, способную обрабатывать суммы с полной точностью. Она позволяет использовать более сложные сценарии обмена, где выходные данные могут быть дробными, например, в определенных структурированных продуктах или сложных путях маршрутизации.
Защита от проскальзывания на каждом этапе (Universal Router pull request #470)
Для обеспечения более детального контроля над многоэтапными обменами защита от проскальзывания теперь может быть задана для каждого этапа отдельно. Это значительное улучшение по сравнению с глобальными лимитами проскальзывания, которые могут не обеспечивать адекватную защиту от ценового воздействия на отдельных этапах сложной сделки.
- Согласованность V2/V3: Новая модель проскальзывания применяется единообразно к маршрутам, включающим как пулы Uniswap V2, так и V3.
- Простые и многоэтапные обмены: Защита проверена на надежность как для обменов в одном пуле, так и для сложных путей, проходящих через несколько источников ликвидности.
Проскальзывание периферии и обновления параметров (v4 Periphery pull request #516)
Контракты периферии v4 были обновлены для поддержки более выразительных конфигураций проскальзывания и более безопасной обработки параметров. Эти изменения направлены на снижение влияния MEV и защиту пользователей от невыгодного исполнения.
- Расширенные поля проскальзывания: Структуры одиночного обмена теперь включают дополнительные поля для более точного контроля проскальзывания.
- Улучшенная проверка и декодирование: Логика проверки и декодирования параметров обмена была усилена для предотвращения манипуляций и обеспечения строгого соблюдения ограничений, заданных пользователем.
- Корректировка точности: Внутренняя точность была обновлена в соответствии с новыми моделями платежей и проскальзывания, что снижает риск регрессий или неожиданного поведения.
Модель безопасности и предположения о доверии
Безопасность Universal Router и связанных с ним компонентов зависит от целостности базовых контрактов протокола Uniswap, корректности внесетевых (off-chain) входных данных и осмотрительности пользователя. Недавние обновления вводят новую динамику, уточняющую эти предположения о доверии, особенно в отношении делегированного выполнения и проверки параметров.
- Осмотрительность пользователя: Предполагается, что пользователи устанавливают соответствующие параметры проскальзывания для своих обменов. Система предоставляет инструменты для детального контроля, но ответственность за определение безопасных лимитов исполнения лежит на пользователе.
- Целостность базовых протоколов: Все обмены в конечном итоге выполняются в пулах Uniswap v2, v3 или v4. Таким образом, безопасность этих обменов зависит от безопасности контрактов основных пулов.
- Корректность внесетевых данных: Построение маршрутов обмена и расчет лимитов проскальзывания обычно выполняются вне сети. Система предполагает, что эти входные данные генерируются корректно и не являются вредоносными. Внесетевая логика проверки и декодирования служит последним рубежом защиты, но не может полностью компенсировать некорректно сформированные входные данные.
- Двойная комиссия через прокси: Взаимодействие через SwapProxy с токенами, которые содержат комиссии за перевод, приведет к тому, что эти комиссии будут взиматься дважды. Это связано с переводами от пользователя к маршрутизатору, а затем от маршрутизатора к соответствующему пулу.
Средняя степень серьезности
Некорректная проверка границ в toLengthOffset может отключить защиту от проскальзывания на каждом этапе
Функция toLengthOffset выполняет некорректную проверку границ при декодировании динамических массивов из calldata. Функция сравнивает _bytes.length с length + relativeOffset, где length представляет количество элементов массива, а не количество байтов.
Для массивов uint256 каждый элемент занимает 32 байта. В результате проверка может пройти успешно, даже если calldata не содержит достаточно данных для всех объявленных элементов. Когда контракт позже считывает данные за пределами фактических calldata, Solidity возвращает ноль из-за дополнения нулями в calldata. Если эта функция используется для декодирования массива maxHopSlippage, отсутствующие элементы будут интерпретированы как 0, что фактически отключает защиту от проскальзывания для этих этапов, поскольку любое ценовое соотношение, большее или равное нулю, принимается.
Злонамеренный интегратор или фронтенд может сформировать calldata, где длина массива больше фактических закодированных данных. Пользователи могут полагать, что защита от проскальзывания на каждом этапе включена, в то время как некоторые этапы выполняются без эффективных проверок.
Рассмотрите возможность изменения проверки границ с учетом размера элемента путем умножения length на 32 перед добавлением relativeOffset.
Обновление: Решено в коммите cc89977.
Деление на ноль в обменах с точным выводом вызывает панику в пулах с субсидированием через хуки
Функции _swapExactOutputSingle и _swapExactOutput вычисляют цену путем умножения amountIn на PRECISION и деления результата на amountIn, где amountIn — это сумма, возвращаемая пулом после учета дельт хуков. Проблема возникает, когда хук, использующий флаг BEFORE_SWAP_RETURNS_DELTA_FLAG или AFTER_SWAP_RETURNS_DELTA_FLAG, правомерно снижает обязательство вызывающей стороны по вводу до нуля, например в субсидируемых пулах. В этом случае amountIn становится равным 0, что приводит к делению на ноль и вызывает панику EVM.
Рекомендуется добавить проверку для amountIn на равенство 0 и в таком случае обрабатывать обмен как бесплатный обмен, который удовлетворяет любому минимальному порогу цены.
Обновление: Принято к сведению, не исправлено. Команда заявила:
«Это поведение приводит к отложению транзакции (revert) для невероятно редкого крайнего случая, и его исправление увеличит затраты на газ для всех пользователей, совершающих обмен. Кроме того, это откат, который будет обнаружен при симуляции транзакции. По нашему мнению, это не является проблемой средней степени серьезности».
Низкая степень серьезности
Функция execute в SwapProxy требует адрес ERC-20 только для вызовов ETH
Функция execute в SwapProxy всегда требует предоставления адреса ERC-20 token, даже когда пользователь намеревается отправить только ETH через msg.value. В сценариях, где пользователь не хочет отправлять какие-либо токен ERC-20, он все равно вынужден указывать адрес токена, что может сбивать с толку и быть излишним. Было бы более интуитивно и удобно проверять сумму токена и вызывать safeTransferFrom только тогда, когда сумма больше нуля.
Рекомендуется обновить логику функции, чтобы разрешить транзакции только с ETH, или, если ETH не должен приниматься, удалить модификатор payable.
Обновление: Исправлено в коммите b0162a9.
Solmate SafeTransferLib возвращает успешный результат без ошибок для адресов токенов, не являющихся контрактами
Функция execute в SwapProxy использует из Solmate для выполнения переводов токенов, но она не проверяет, содержит ли предоставленный адрес токена код контракта. Если пользователь предоставляет адрес без кода контракта в качестве токена, функция safeTransferFrom завершится успешно без каких-либо ошибок, и токены фактически не будут переведены. Это может привести к непредвиденному поведению и ввести пользователей в заблуждение, заставив их поверить в то, что перевод произошел, хотя это не так.
Рекомендуется либо задокументировать это поведение, либо добавить явную проверку, гарантирующую, что адрес токена содержит код контракта.
Обновление: Исправлено в коммите 22ee596. Поведение SafeTransferLib было задокументировано.
Потоки действий без минта разрешены в V4_POSITION_MANAGER_CALL
UniversalRouter предоставляет V4_POSITION_MANAGER_CALL в своем потоке диспетчеризации команд, который перенаправляет предоставленные пользователем calldata в modifyLiquidities. Это актуально, поскольку modifyLiquidities выполняет потоки с множеством действий, в то время как авторизация позиций применяется к контексту непосредственного вызывающего абонента (см. проверку одобрения, маппинг вызывающего абонента и контекст блокировки). Если владелец позиции одобрил UniversalRouter в качестве оператора, любой внешний аккаунт может вызвать путь маршрутизатора, который использует этот контекст одобрения.
Защитный механизм в _checkV4PositionManagerCall отклоняет только три байта действия (INCREASE_LIQUIDITY, DECREASE_LIQUIDITY, BURN_POSITION) и не требует никакого действия минта. Следовательно, потоки без минта проходят проверку валидации, включая INCREASE_LIQUIDITY_FROM_DELTAS и действия по выплате, такие как TAKE / TAKE_PAIR (набор действий: reference; обработка: , TAKE, TAKE_PAIR). Поскольку комиссии учитываются через учет дельт модификации ликвидности (путь начисления комиссий, примечание о поведении кредита комиссии), злоумышленник может предоставить минимальную дельту в INCREASE_LIQUIDITY_FROM_DELTAS, реализовать начисленные комиссии в кредит и перевести эти средства на свой счет через TAKE/TAKE_PAIR.
Рекомендуется заменить текущий черный список в строгим белым списком для процессов минта миграции и потребовать по крайней мере одно действие MINT_POSITION или MINT_POSITION_FROM_DELTAS в каждом разрешенном потоке. Кроме того, рекомендуется отклонять любые действия, которые могут ссылаться на существующие идентификаторы токенов или устанавливать произвольных получателей выплат, а также декодировать параметры там, где это необходимо для привязки владельца к получателю. Также рекомендуется добавить покрытие регрессионным тестированием, которое пытается запустить потоки без минта (например, INCREASE_LIQUIDITY_FROM_DELTAS плюс TAKE_PAIR) через V4_POSITION_MANAGER_CALL.
Обновление: Исправлено в коммите 74ff047. Действие INCREASE_LIQUIDITY_FROM_DELTAS было включено в _checkV4PositionManagerCall.
Избыточные расчеты цен, когда параметр maxHopSlippage не задан
Функции и _swapExactOutputSingle выполняют расчеты цен без предварительной проверки того, задан ли параметр params.maxHopSlippage. Каждый одноходовый обмен безусловно выполняет операции умножения и деления независимо от того, настроен ли параметр maxHopSlippage, что приводит к избыточным вычислительным затратам.
Рекомендуется добавить условную проверку, чтобы убедиться, что params.maxHopSlippage не равен нулю, перед выполнением связанных расчетов цен и проверки проскальзывания. Это позволит избежать избыточных вычислений, когда защита от проскальзывания не требуется.
Обновление: Исправлено в коммите 0296654.
Некорректное наименование параметра maxPrice в ошибках V4 Periphery
После того как формула ценообразования в V4 Periphery была изменена на деление на amountIn, порог, используемый при проверке для каждого хопа, теперь представляет собой минимально допустимую цену, но соответствующий параметр ошибки по-прежнему называется maxPrice. Это затрагивает существующие ошибки V4TooLittleReceivedPerHop и V4TooMuchRequestedPerHop, а также недавно добавленные V4TooLittleReceivedPerHopSingle и V4TooMuchRequestedPerHopSingle. Во всех четырех случаях параметр с меткой maxPrice фактически представляет собой минимальный порог цены, а документация NatSpec также неверно описывает его как максимальную цену. Это несоответствие может вызвать путаницу у инструментов и интеграций, которые декодируют ошибки по именам параметров ABI, поскольку они будут отображать maxPrice, несмотря на то, что значение представляет собой минимальный порог.
Рекомендуется переименовать параметр maxPrice в minPrice во всех четырех определениях ошибок и соответствующим образом обновить комментарии NatSpec.
Обновление: Исправлено в коммите 0f447d0.
Возможная паника из-за деления на ноль при включенном проскальзывании для каждого хопа для обменов Uniswap V2
Ошибка паники из-за деления на ноль может возникнуть, когда amountInput равен нулю и включено проскальзывание для каждого хопа. Эта ситуация возникает, когда у пары нет дополнительного баланса, а сигнал ALREADY_PAID равен нулю. В этом сценарии расчет проскальзывания для каждого хопа пытается выполнить деление на amountInput, что приводит к панике. Риска потери средств или обхода нет, но паника генерирует неинформативную ошибку вместо осмысленного сообщения, указывающего на недействительное условие обмена.
Рекомендуется добавить проверку валидации, которая гарантирует, что amountInput больше нуля, перед выполнением расчетов проскальзывания для каждого хопа.
Обновление: Принято к сведению, не исправлено.
Некорректная проверка длины параметров в decodeSwapExactInParams и decodeSwapExactOutParams
Функции decodeSwapExactInParams и decodeSwapExactOutParams декодируют байтовые параметры в структуры и ExactOutputParams. Однако эти функции некорректно проверяют минимальную длину параметров, поскольку не учитывают параметр maxHopSlippage. В результате логика проверки может принимать некорректно отформатированные входные данные.
Рекомендуется обновить логику проверки в функциях decodeSwapExactInParams и decodeSwapExactOutParams, чтобы обеспечить корректную проверку длины параметров в соответствии с обновленными структурами, включая поле maxHopSlippage.
Обновление: Исправлено в коммите d20dca1.
Примечания и дополнительная информация
Неиспользованные токены и ETH могут застрять в контракте Router
Неиспользованные токены и ETH могут остаться в контракте Universal Router после выполнения, если их явно не вывести (sweep), поскольку контракт автоматически не возвращает остатки балансов. Это может привести к непреднамеренной потере средств, если пользователи или интеграторы забудут включить необходимые шаги вывода в свой поток выполнения. Ответственность за возврат любых оставшихся токенов или ETH полностью лежит на пользователе, который должен добавить команды вывода, чтобы в контракте не оставалось средств.
Рекомендуется четко документировать это поведение и подчеркнуть необходимость интеграторов внедрять правильную логику вывода во избежание ошибок и потенциальной потери средств.
Обновление: Принято к сведению, не исправлено.
Отсутствие тестов для SwapProxy
Текущая реализация набора тестов для SwapProxy проверяет базовый успешный сценарий для точных входящих и точных исходящих свопов V2, а также простые случаи отката при недостаточных разрешениях (approval) и истекших сроках. Тем не менее, можно добавить дополнительные тесты:
- Тесты для путей свопов V3 или V4 через прокси
- Тесты для свопов только с ETH, где msg.value отправляется со значением amount, равным 0 для перевода ERC-20
- Тесты поведения при передаче адреса, не являющегося контрактом, в качестве параметра token, что в настоящее время не охвачено
Рекомендуется реализовать вышеупомянутые тесты для улучшения покрытия кода тестами.
Обновление: Принято к сведению, не исправлено.
Токены с комиссией за перевод влекут двойную комиссию через SwapProxy
SwapProxy добавляет дополнительный перевод токенов по сравнению со стандартным процессом Permit2. При использовании Permit2 токены перемещаются напрямую из кошелька пользователя в целевой пул за один перевод. При использовании SwapProxy токены сначала переводятся от пользователя к роутеру, а затем от роутера к пулу. Для токенов с комиссией за перевод это приводит к тому, что комиссия за перевод применяется дважды, что ухудшает условия выполнения для пользователя.
Рекомендуется задокументировать тот факт, что токены с комиссией за перевод в настоящее время не поддерживаются.
Обновление: Принято к сведению, не исправлено. Команда заявила:
«Мы продолжим поощрять всех интеграторов использовать Permit2 из соображений безопасности, где этого не происходит».
Вредоносные хуки токенов могут взаимодействовать с роутером во время выполнения SwapProxy
При переводе токенов от msg.sender к роутеру в функции execute контракта SwapProxy роутер остается разблокированным во время перевода. Это создает окно, в котором вредоносный токен (или любой токен, позволяющий злоумышленнику перехватить поток выполнения) может выполнить произвольную логику и взаимодействовать с роутером прямо во время перевода.
Поскольку роутер не заблокирован во время этой операции, такой токен потенциально может вызвать обратный вызов роутера или инициировать непредвиденные взаимодействия до того, как продолжится логика свопа. Такое поведение не происходит при использовании подписей Permit2, где перенос токенов происходит при заблокированном роутере, что исключает аналогичное подвергание роутера опасности.
Рекомендуется задокументировать это поведение, так как текущий риск ограничен вредоносными токенами и поэтому считается ошибкой пользователя.
Обновление: Принято к сведению, не исправлено.
Вводящая в заблуждение ошибка проверки длины пути
Функции v3SwapExactInput и v3SwapExactOutput в настоящее время проверяют длину параметра path. Если длина path меньше размера адреса (20 байт), функции завершаются с откатом (revert) и ошибкой V3InvalidHopSlippageLength. Проблема заключается в том, что имя ошибки V3InvalidHopSlippageLength ошибочно предполагает, что проблема связана с проскальзыванием на переходе (hop slippage), тогда как реальной причиной является недопустимая длина пути. Это может запутать разработчиков и пользователей при отладке или обработке ошибок.
Рекомендуется ввести новую ошибку, которая явно указывает на недопустимую длину пути, и использовать ее в логике проверки для path. Это повысит ясность и корректность отчетности об ошибках.
Обновление: Исправлено в коммите ef061d4.
Избыточное явное приведение типов в расчете цены V4Router
вычисляет соотношение цен на один переход (hop), умножая amountOut на коэффициент точности 1e36 и деля на amountIn в рамках проверки свопа. Это соотношение сравнивается с заданными пользователем лимитами для обеспечения ограничений выполнения на уровне переходов во время маршрутизации. Явное приведение amountOut к uint256 в выражении цены может быть опущено без изменения поведения. Поскольку множитель точности уже объявлен как uint256, Solidity автоматически повышает тип арифметики до uint256. Таким образом, удаление приведения не изменяет поведение переполнения для этого выражения в рамках текущих границ типов.
Рекомендуется удалить явное приведение типов и стандартизировать эквивалентные выражения цен в для улучшения читаемости при сохранении того же поведения во время выполнения и безопасности переполнения.
Обновление: Принято к сведению, не исправлено.
Дублированная логика цены для переходов и определения точности в роутерах
Проверка проскальзывания на переход реализована в нескольких модулях свопов: , и . Во всех случаях роутер вычисляет нормализованное соотношение цен и сравнивает его с заданным пользователем порогом для соблюдения ограничений на каждом переходе. Такая же базовая логика поддерживается в раздельных путях кода, а источник точности разделен между Constants.SLIPPAGE_PRECISION в universal-router и V4Router.PRECISION в v4-periphery. Это создает несколько источников истины для одного и того же поведения масштабирования и проверки, что может со временем привести к расхождению, если обновления не применяются согласованно во всех реализациях роутеров.
Рекомендуется централизовать вычисление цены за переход и проверку порогов в общую утилиту или общий внутренний паттерн, а также стандартизировать точность до единого канонического определения во всех модулях роутеров.
Обновление: Принято к сведению, не исправлено.
Вводящее в заблуждение имя maxHopSlippage скрывает защитный механизм цены за переход
В V4Router, V3SwapRouter и V2SwapRouter каждый переход вычисляет price = amountOut * 1e36 / amountIn и сравнивает его с заданным пользователем порогом. Этот механизм разработан для обеспечения минимально приемлемого обменного курса за переход (соотношение выхода к входу) и используется в путях точного входящего и точного исходящего свопов. Проблема заключается в том, что имя параметра maxHopSlippage не соответствует этому поведению.
Текущее название подразумевает ограничение проскальзывания (часто интерпретируемое как базисные пункты или проценты), в то время как код ожидает минимальный ценовой порог в единицах с фиксированной точкой 1e36. Это несоответствие может привести к тому, что интеграторы настроят значения в неверных единицах или с неверным пониманием, что может либо ослабить защиту, либо вызывать непредвиденные возвраты (revert), несмотря на то, что базовая арифметическая проверка реализована последовательно.
Рекомендуется переименовать параметр в ориентированное на значение имя, такое как minHopPriceX36 (или аналогичное), и обновить связанные структуры, ошибки и документацию в соответствии с фактической семантикой. Также рекомендуется задокументировать соглашения об единицах измерения и направленности с помощью явных числовых примеров, а если требуется совместимость интерфейсов, сохранить текущее поле в качестве устаревшего псевдонима (deprecated), одновременно выполняя его внутреннюю валидацию и сопоставление с новым смыслом.
Обновление: Решено в коммитах e7f31a6 и a262eb8.
Заключение
Данный аудит охватил недавние изменения, внесенные в контракты Uniswap Universal Router и v4 Periphery, включая новый контракт SwapProxy, обновленную логику платежей и улучшенные элементы управления проскальзыванием для каждого перехода (hop). Были выявлены две проблемы средней степени серьезности: некорректная проверка границ при декодировании динамических массивов, из-за чего некоторые переходы могли выполняться без эффективной защиты от проскальзывания, и проблема деления на ноль для пулов, хуки которых субсидируют свопы. Кроме того, было сообщено о нескольких проблемах низкой степени серьезности и информационного характера. В целом, за исключением отсутствия тестов для контракта SwapProxy, кодовая база оказалась хорошо структурированной и протестированной.
Команда Uniswap Labs своевременно предоставляла контекст и ответы на протяжении всей работы, что обеспечило эффективный анализ.
Приложение
Классификация проблем
OpenZeppelin классифицирует уязвимости смарт-контрактов по 5-уровневой шкале:
- Критическая
- Высокая
- Средняя
- Низкая
- Примечание / Информация
Критическая степень серьезности
Эта классификация применяется, когда влияние проблемы является катастрофическим, создавая угрозу серьезного ущерба репутации клиента и/или приводя к тяжелым финансовым потерям для клиента или пользователей. Вероятность эксплуатации может быть высокой, что требует быстрого реагирования. Критические проблемы обычно связаны со значительными рисками, такими как безвозвратная потеря или блокировка большого объема конфиденциальных активов пользователей, либо с отказом основных функциональных возможностей системы без жизнеспособных средств смягчения последствий. Эти проблемы требуют немедленного внимания из-за их потенциала существенно подорвать целостность системы или доверие пользователей.
Высокая степень серьезности
Эти проблемы характеризуются потенциалом существенного влияния на репутацию клиента и/или значительными финансовыми потерями. Вероятность эксплуатации высока, что требует оперативного реагирования. Такие проблемы могут включать временную потерю или блокировку значительного числа конфиденциальных активов пользователей либо нарушения критически важных функций системы, хотя и с потенциальными, но ограниченными возможностями смягчения последствий. Акцент делается на значительных, но не всегда катастрофических эффектах на работу системы или безопасность активов, что требует своевременного и эффективного устранения.
Средняя степень серьезности
Проблемы, классифицированные как имеющие среднюю степень серьезности, могут привести к заметному негативному влиянию на репутацию клиента и/или умеренным финансовым потерям. Такие проблемы, если их оставить без внимания, имеют умеренную вероятность эксплуатации или могут вызвать нежелательные побочные эффекты в системе. Эти проблемы обычно ограничиваются меньшей подгруппой конфиденциальных активов пользователей или могут включать отклонения от заданного дизайна системы, которые, хотя и не носят напрямую финансового характера, нарушают целостность системы или пользовательский опыт. Основное внимание здесь уделяется проблемам, которые представляют реальный, но ограниченный риск, требующий своевременного внимания для предотвращения эскалации.








.png)
-1.png)
