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







