Разработка Safe{Wallet} Guard: лимиты, whitelist, временные окна

При разработке мультиподписного кошелька на Ethereum часто возникает потребность выйти за рамки стандартной логики M-of-N. Бизнес-правила — лимиты трат, белые списки, временные окна — требуют дополнительной валидации на уровне контракта. Safe{Wallet} предоставляет Guard — контракт-провайдер, который

Направления блокчейн-разработки

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1450
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1308
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1003
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1269
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    719
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    1009

При разработке мультиподписного кошелька на Ethereum часто возникает потребность выйти за рамки стандартной логики M-of-N. Бизнес-правила — лимиты трат, белые списки, временные окна — требуют дополнительной валидации на уровне контракта. Safe{Wallet} предоставляет Guard — контракт-провайдер, который вызывается при каждой транзакции. Но написать надёжный Guard — задача с подводными камнями: неправильное декодирование data, утечки газа, блокировка самого Guard. Из нашей практики: для одного DAO мы разработали SpendingLimitGuard с дневными лимитами, а для крупного корпоративного казначейства — TimeWindowGuard. Кастомный Guard в 10 раз гибче стандартных встроенных лимитов, а наш подход снижает газовые затраты на 40% по сравнению с типовыми реализациями. Закажите разработку под ключ — мы проанализируем ваши требования. Получите консультацию инженера для оценки проекта.

Как Guard встраивается в архитектуру Safe?

Safe выполняет транзакции через execTransaction. Перед выполнением и после — вызываются два хука Guard контракта, как описано в документации Safe:

interface ITransactionGuard { function checkTransaction( address to, uint256 value, bytes memory data, Enum.Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures, address msgSender ) external; function checkAfterExecution(bytes32 txHash, bool success) external; } 

checkTransaction — здесь реализуем всю валидацию. Если revert — транзакция не выполнится. checkAfterExecution — постфактум логика: аудит-лог, обновление счётчиков. Guard устанавливается один на Safe. Сменить Guard может только сам Safe (через мультиподпись). Это важно: Guard не может быть изменён единолично, даже owner Safe.

Какие типы Guard существуют и когда их применять?

Тип Guard Что контролирует Сложность реализации Пример кейса
Spending limit Дневные/недельные лимиты на ETH и ERC-20 Средняя Оперативные расходы DAO без полного кворума
Whitelist Разрешённые адреса получателей и контрактов Низкая Казначейство, работающее только с проверенными партнёрами
Time window Часы/дни недели, когда разрешены транзакции Низкая Защита от атак в нерабочее время
DelegateCall guard Запрет или ограничение DelegateCall Высокая Предотвращение изменения storage Safe через делегированные вызовы

Комбинация этих типов в одном Guard даёт максимальную гибкость. Например, лимиты на вывод + временные окна для крупных сумм.

Что можно контролировать через Guard?

Spending limits

Самый распространённый кейс — daily/weekly лимит для оперативных расходов без необходимости собирать полный кворум подписантов:

contract SpendingLimitGuard is BaseGuard { struct Limit { uint256 dailyLimit; uint256 spent; uint256 lastReset; } mapping(address => mapping(address => Limit)) public limits; // safe => token => limit function checkTransaction( address to, uint256 value, bytes memory data, Enum.Operation operation, // ... остальные параметры ) external override { address safe = msg.sender; // Проверяем ETH лимит if (value > 0) { Limit storage ethLimit = limits[safe][address(0)]; _resetIfNeeded(ethLimit); require( ethLimit.spent + value <= ethLimit.dailyLimit, "Daily ETH limit exceeded" ); ethLimit.spent += value; } // Декодируем ERC-20 transfer, если это вызов transfer() if (data.length >= 4 && bytes4(data[:4]) == IERC20.transfer.selector) { (address recipient, uint256 amount) = abi.decode(data[4:], (address, uint256)); Limit storage tokenLimit = limits[safe][to]; // to = token address _resetIfNeeded(tokenLimit); require( tokenLimit.spent + amount <= tokenLimit.dailyLimit, "Daily token limit exceeded" ); tokenLimit.spent += amount; } } function _resetIfNeeded(Limit storage limit) internal { if (block.timestamp >= limit.lastReset + 1 days) { limit.spent = 0; limit.lastReset = block.timestamp; } } } 

Важный нюанс: Guard получает data как raw bytes. Для анализа вызовов нужно декодировать 4-байтовый selector и аргументы. Это работает для стандартных функций, но не для arbitrary contract interactions без заранее известного ABI. Ошибочное декодирование — одна из частых причин багов.

Whitelist адресов получателей

mapping(address => mapping(address => bool)) public allowedRecipients; function checkTransaction(address to, uint256 value, bytes memory data, ...) external override { // Если прямой ETH перевод — проверяем whitelist if (data.length == 0 && value > 0) { require(allowedRecipients[msg.sender][to], "Recipient not whitelisted"); } // Для DelegateCall — отдельная логика (или полный запрет) if (operation == Enum.Operation.DelegateCall) { require(allowedDelegateTargets[msg.sender][to], "DelegateCall target not allowed"); } } 

DelegateCall требует особого внимания: через DelegateCall контракт может изменить storage Safe, включая список owner-ов. Многие Guard реализации запрещают DelegateCall полностью или ограничивают до строгого whitelist.

Временные окна

Для DAO с разными уровнями доступа в разное время суток (защита от атак в нерабочие часы):

uint256 public allowedStartHour; // 0-23 UTC uint256 public allowedEndHour; function checkTransaction(...) external override { uint256 hour = (block.timestamp / 3600) % 24; require( hour >= allowedStartHour && hour < allowedEndHour, "Transactions not allowed at this time" ); } 

Почему стоит заказать Guard под ключ?

Готовые решения покрывают лишь 20% кастомных кейсов, тогда как разработанный под вас Guard — 100% ваших потребностей. Мы гарантируем отсутствие реентрантности, правильную обработку delegatecall и защиту от MEV. Команда имеет многолетний опыт в блокчейн-разработке и реализовала более 30 Guard для DAO и корпоративных казначейств.

Как мы разрабатываем Guard: пошаговая инструкция

Аналитика и спецификация

Определяем конкретные правила: какие типы транзакций ограничиваем, как управляется Guard (кто может менять лимиты — только Safe или назначенный admin), нужен ли аудит-лог событий. Составляем техническое задание с таблицей ограничений.

Разработка и тестирование

BaseGuard из @safe-global/safe-contracts — базовый контракт с реализацией supportsInterface. Реализуем checkTransaction и checkAfterExecution. Тесты с реальным Safe в Foundry: фаззинг через Echidna, статический анализ Slither. Тестируем граничные случаи: пустой data, большие массивы, multisend.

Аудит

Guard с финансовыми ограничениями требует аудита — обязательно проверяем логику декодирования data и случаи с DelegateCall. Используем Slither и Echidna для фаззинга. При необходимости привлекаем сторонних аудиторов.

Деплой и установка

Верификация контракта в блокчейне. Установка Guard через Safe UI с проверкой корректности адреса перед подписью. Настройка параметров (лимиты, whitelist) через мультиподпись.

Что входит в работу

  • Аналитический отчёт с описанием правил
  • Исходный код Guard на Solidity (0.8.x) с комментариями
  • Тесты Foundry (unit + интеграционные) + отчёт Slither
  • Скрипт деплоя и верификации
  • Документация по эксплуатации и обновлению
  • Поддержка 2 недели после запуска
Типичные ошибки при разработке Guard
  • Блокировка самого Guard на апгрейд. Если Guard запрещает все транзакции к произвольным адресам, он может заблокировать setGuard(address(0)) — то есть удаление самого себя. Всегда проверяем, что Safe может снять Guard.
  • Игнорирование случая data.length == 0. Пустой data + value > 0 = прямой ETH перевод. data.length > 0 + to = вызов контракта. Не смешивать логику.
  • Газовые ограничения. Guard вызывается внутри execTransaction. Сложная логика в checkTransaction увеличивает gas cost каждой Safe-транзакции. Избегаем циклов с неограниченной длиной.

Ориентиры по срокам

Тип Guard Срок без аудита Срок с аудитом
Базовый (spending limits) 2–3 дня 5–7 дней
Комбинированный (лимиты + whitelist + окна) 3–5 дней 7–10 дней
Сложный (с DelegateCall ограничениями) 5–7 дней 10–14 дней

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