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







