При разработке мультиподписного кошелька на 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 дней |
Стоимость рассчитывается индивидуально. Свяжитесь с нами, чтобы обсудить ваш проект и получить консультацию инженера с многолетним опытом в блокчейне.







